UNPKG

twin.macro

Version:

Twin blends the magic of Tailwind with the flexibility of css-in-js

1,327 lines (1,117 loc) 118 kB
var babelPluginMacros = require('babel-plugin-macros'); var path = require('path'); var fs = require('fs'); var toPath$1 = require('tailwindcss/lib/util/toPath'); var generateRules = require('tailwindcss/lib/lib/generateRules'); var setupContextUtils = require('tailwindcss/lib/lib/setupContextUtils'); require('tailwindcss/stubs/config.full'); var transformThemeValueRaw = require('tailwindcss/lib/util/transformThemeValue'); var resolveTailwindConfigRaw = require('tailwindcss/lib/util/resolveConfig'); var getAllConfigsRaw = require('tailwindcss/lib/util/getAllConfigs'); var splitAtTopLevelOnly$1 = require('tailwindcss/lib/util/splitAtTopLevelOnly'); var unescapeRaw = require('postcss-selector-parser/dist/util/unesc'); var chalk = require('chalk'); var loadConfig = require('tailwindcss/loadConfig'); var deepMerge = require('lodash.merge'); var get = require('lodash.get'); var template = require('@babel/template'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var transformThemeValueRaw__default = /*#__PURE__*/_interopDefaultLegacy(transformThemeValueRaw); var resolveTailwindConfigRaw__default = /*#__PURE__*/_interopDefaultLegacy(resolveTailwindConfigRaw); var getAllConfigsRaw__default = /*#__PURE__*/_interopDefaultLegacy(getAllConfigsRaw); var unescapeRaw__default = /*#__PURE__*/_interopDefaultLegacy(unescapeRaw); var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk); var loadConfig__default = /*#__PURE__*/_interopDefaultLegacy(loadConfig); var deepMerge__default = /*#__PURE__*/_interopDefaultLegacy(deepMerge); var get__default = /*#__PURE__*/_interopDefaultLegacy(get); var template__default = /*#__PURE__*/_interopDefaultLegacy(template); function escalade (start, callback) { let dir = path.resolve('.', start); let tmp, stats = fs.statSync(dir); if (!stats.isDirectory()) { dir = path.dirname(dir); } while (true) { tmp = callback(dir, fs.readdirSync(dir)); if (tmp) return path.resolve(dir, tmp); dir = path.dirname(tmp = dir); if (tmp === dir) break; } } const TWIN_CONFIG_DEFAULTS = { allowStyleProp: false, autoCssProp: false, config: undefined, convertHtmlElementToStyled: false, convertStyledDotToParam: false, convertStyledDotToFunction: false, css: { import: '', from: '' }, dataCsProp: false, dataTwProp: false, debug: false, disableCsProp: true, disableShortCss: true, global: { import: '', from: '' }, hasLogColors: true, includeClassNames: false, moveTwPropToStyled: false, moveKeyframesToGlobalStyles: false, preset: undefined, sassyPseudo: false, stitchesConfig: undefined, styled: { import: '', from: '' } }; // Defaults for different css-in-js libraries const configDefaultsStyledComponents = { sassyPseudo: true // Sets selectors like hover to &:hover }; const configDefaultsGoober = { sassyPseudo: true // Sets selectors like hover to &:hover }; const configDefaultsSolid = { sassyPseudo: true, moveTwPropToStyled: true, convertHtmlElementToStyled: true, convertStyledDotToFunction: true // Convert styled.[element] to a default syntax }; const configDefaultsStitches = { sassyPseudo: true, convertStyledDotToParam: true, moveTwPropToStyled: true, convertHtmlElementToStyled: true, stitchesConfig: undefined, moveKeyframesToGlobalStyles: true // Stitches doesn't support inline @keyframes }; function configDefaultsTwin({ isSolid, isStyledComponents, isGoober, isStitches, isDev }) { return { ...TWIN_CONFIG_DEFAULTS, ...(isSolid && configDefaultsSolid), ...(isStyledComponents && configDefaultsStyledComponents), ...(isGoober && configDefaultsGoober), ...(isStitches && configDefaultsStitches), dataTwProp: isDev, dataCsProp: isDev }; } function isBoolean(value) { return typeof value === 'boolean'; } const allowedPresets = ['styled-components', 'emotion', 'goober', 'stitches', 'solid']; const configTwinValidators = { preset: [value => value === undefined || typeof value === 'string' && allowedPresets.includes(value), `The config “preset” can only be:\n${allowedPresets.map(p => `'${p}'`).join(', ')}`], allowStyleProp: [isBoolean, 'The config “allowStyleProp” can only be a boolean'], autoCssProp: [value => !value, 'The “autoCssProp” feature has been removed from twin.macro@2.8.2+\nThis means the css prop must be added by styled-components instead.\nSetup info at https://twinredirect.page.link/auto-css-prop\n\nRemove the “autoCssProp” item from your config to avoid this message.'], convertStyledDot: [value => !value, 'The “convertStyledDot” feature was changed to “convertStyledDotParam”.'], disableColorVariables: [value => !value, 'The disableColorVariables feature has been removed from twin.macro@3+\n\nRemove the disableColorVariables item from your config to avoid this message.'], sassyPseudo: [isBoolean, 'The config “sassyPseudo” can only be a boolean'], dataTwProp: [value => isBoolean(value) || value === 'all', 'The config “dataTwProp” can only be true, false or "all"'], dataCsProp: [value => isBoolean(value) || value === 'all', 'The config “dataCsProp” can only be true, false or "all"'], includeClassNames: [isBoolean, 'The config “includeClassNames” can only be a boolean'], disableCsProp: [isBoolean, 'The config “disableCsProp” can only be a boolean'], convertStyledDotToParam: [isBoolean, 'The config “convertStyledDotToParam” can only be a boolean'], convertStyledDotToFunction: [isBoolean, 'The config “convertStyledDotToFunction” can only be a boolean'], moveTwPropToStyled: [isBoolean, 'The config “moveTwPropToStyled” can only be a boolean'], convertHtmlElementToStyled: [isBoolean, 'The config “convertHtmlElementToStyled” can only be a boolean'] }; function toArray(array) { if (Array.isArray(array)) return array; return [array]; } const AMPERSAND_AFTER = /&(.+)/g; const AMPERSAND = /&/g; function stripAmpersands(string) { return typeof string === 'string' ? string.replace(AMPERSAND, '').trim() : string; } const EXTRA_VARIANTS = [['all', '& *'], ['all-child', '& > *'], ['sibling', '& ~ *'], ['hocus', ['&:hover', '&:focus']], 'link', 'read-write', ['svg', '& svg'], ['even-of-type', '&:nth-of-type(even)'], ['odd-of-type', '&:nth-of-type(odd)']]; const EXTRA_NOT_VARIANTS = [// Positional ['first', '&:first-child'], ['last', '&:last-child'], ['only', '&:only-child'], ['odd', '&:nth-child(odd)'], ['even', '&:nth-child(even)'], 'first-of-type', 'last-of-type', 'only-of-type', // State 'target', ['open', '&[open]'], // Forms 'default', 'checked', 'indeterminate', 'placeholder-shown', 'autofill', 'optional', 'required', 'valid', 'invalid', 'in-range', 'out-of-range', 'read-only', // Content 'empty', // Interactive 'focus-within', 'hover', 'focus', 'focus-visible', 'active', 'enabled', 'disabled']; function defaultVariants({ config, addVariant }) { const extraVariants = EXTRA_VARIANTS.flatMap(v => { let [name, selector] = toArray(v); selector = selector || `&:${String(name)}`; const variant = [name, selector]; // Create a :not() version of the selectors above const notVariant = [`not-${String(name)}`, toArray(selector).map(s => `&:not(${stripAmpersands(s)})`)]; return [variant, notVariant]; }); // Create :not() versions of these selectors const notPseudoVariants = EXTRA_NOT_VARIANTS.map(v => { const [name, selector] = toArray(v); const notConfig = [`not-${name}`, toArray(selector || `&:${name}`).map(s => `&:not(${stripAmpersands(s)})`)]; return notConfig; }); const variants = [...extraVariants, ...notPseudoVariants]; for (const [name, selector] of variants) { addVariant(name, toArray(selector)); } for (const [name, selector] of variants) { const groupSelector = toArray(selector).map(s => s.replace(AMPERSAND_AFTER, ':merge(.group)$1 &')); addVariant(`group-${name}`, groupSelector); } for (const [name, selector] of variants) { const peerSelector = toArray(selector).map(s => s.replace(AMPERSAND_AFTER, ':merge(.peer)$1 ~ &')); addVariant(`peer-${name}`, peerSelector); } // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/any-pointer addVariant('any-pointer-none', '@media (any-pointer: none)'); addVariant('any-pointer-fine', '@media (any-pointer: fine)'); addVariant('any-pointer-coarse', '@media (any-pointer: coarse)'); // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer addVariant('pointer-none', '@media (pointer: none)'); addVariant('pointer-fine', '@media (pointer: fine)'); addVariant('pointer-coarse', '@media (pointer: coarse)'); // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/any-hover addVariant('any-hover-none', '@media (any-hover: none)'); addVariant('any-hover', '@media (any-hover: hover)'); // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover addVariant('can-hover', '@media (hover: hover)'); addVariant('cant-hover', '@media (hover: none)'); addVariant('screen', '@media screen'); // Light mode // eslint-disable-next-line unicorn/prefer-spread let [mode, className = '.light'] = [].concat(config('lightMode', 'media')); if (mode === false) mode = 'media'; if (mode === 'class') { addVariant('light', `${String(className)} &`); } else if (mode === 'media') { addVariant('light', '@media (prefers-color-scheme: light)'); } // eslint-disable-next-line unicorn/prefer-spread [mode, className = '.light'] = [].concat(config('lightMode', 'media')); if (mode === 'class') { addVariant('light', `${className} &`); } else if (mode === 'media') { addVariant('light', '@media (prefers-color-scheme: light)'); } } const defaultTailwindConfig = { presets: [{ content: [''], theme: { extend: { content: { DEFAULT: '' }, zIndex: { 1: '1' } // Add a handy small zIndex (`z-1` / `-z-1`) } }, plugins: [defaultVariants] // Add extra variants }] }; // @ts-expect-error Types added below const toPath = toPath$1.toPath; const createContext = setupContextUtils.createContext; const resolveMatches = generateRules.resolveMatches; const transformThemeValue = transformThemeValueRaw__default["default"]; const resolveTailwindConfig = resolveTailwindConfigRaw__default["default"]; const getAllConfigs = getAllConfigsRaw__default["default"]; const splitAtTopLevelOnly = splitAtTopLevelOnly$1.splitAtTopLevelOnly; const unescape = unescapeRaw__default["default"]; function isObject(value) { // eslint-disable-next-line eqeqeq, no-eq-null return value != null && typeof value === 'object' && !Array.isArray(value); } const colors$1 = { error: chalk__default["default"].hex('#ff8383'), errorLight: chalk__default["default"].hex('#ffd3d3'), warn: chalk__default["default"].yellowBright, success: chalk__default["default"].greenBright, highlight: chalk__default["default"].yellowBright, subdued: chalk__default["default"].hex('#999') }; function makeColor$1(hasColor) { return (message, type = 'error') => { if (!hasColor) return message; return colors$1[type](message); }; } function spaced(string) { return `\n\n${string}\n`; } function warning(string) { return colors$1.error(`✕ ${string}`); } function logGeneralError(error) { return Array.isArray(error) ? spaced(`${warning(typeof error[0] === 'function' ? error[0](colors$1) : error[0])}\n\n${error[1]}`) : spaced(warning(error)); } function createDebug(isDev, twinConfig) { return (reference, data, type = 'subdued') => { if (!isDev) return; if (!twinConfig.debug) return; const log = `${String(colors$1[type]('-'))} ${reference} ${String(colors$1[type](JSON.stringify(data)))}`; // eslint-disable-next-line no-console console.log(log); }; } function getTailwindConfig({ sourceRoot, filename, config, assert }) { var _sourceRoot, _escalade; sourceRoot = (_sourceRoot = sourceRoot) != null ? _sourceRoot : '.'; const baseDirectory = filename ? path.dirname(filename) : process.cwd(); const userTailwindConfig = config == null ? void 0 : config.config; if (isObject(userTailwindConfig)) return resolveTailwindConfig([// User config ...getAllConfigs(userTailwindConfig), // Default config ...getAllConfigs(defaultTailwindConfig)]); const configPath = userTailwindConfig ? path.resolve(sourceRoot, userTailwindConfig) : (_escalade = escalade(baseDirectory, (_, names) => { if (names.includes('tailwind.config.js')) return 'tailwind.config.js'; if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'; if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'; })) != null ? _escalade : ''; const configExists = Boolean(configPath && fs.existsSync(configPath)); if (userTailwindConfig) assert(configExists, ({ color }) => [`${String(color(`✕ The tailwind config ${color(String(userTailwindConfig), 'errorLight')} wasn’t found`))}`, `Update the \`config\` option in your twin config`].join('\n\n')); const configs = [// User config ...(configExists ? getAllConfigs(loadConfig__default["default"](configPath)) : []), // Default config ...getAllConfigs(defaultTailwindConfig)]; const tailwindConfig = resolveTailwindConfig(configs); return tailwindConfig; } function runConfigValidator([item, value]) { const validatorConfig = configTwinValidators[item]; if (!validatorConfig) return true; const [validator, errorMessage] = validatorConfig; if (typeof validator !== 'function') return false; if (!validator(value)) { throw new Error(logGeneralError(String(errorMessage))); } return true; } function getConfigTwin(config, params) { const output = { ...configDefaultsTwin(params), ...config }; return output; } function getConfigTwinValidated(config, params) { const twinConfig = getConfigTwin(config, params); // eslint-disable-next-line unicorn/no-array-reduce return Object.entries(twinConfig).reduce((result, item) => { const validatedItem = item; return { ...result, ...(runConfigValidator(validatedItem) && { [validatedItem[0]]: validatedItem[1] }) }; }, {}); } function getFirstValue(list, getValue) { let firstValue; const listLength = list.length - 1; const listItem = list.find((listItem, index) => { const isLast = index === listLength; firstValue = getValue(listItem, { index, isLast }); return Boolean(firstValue); }); return [firstValue, listItem]; } function checkExists(fileName, sourceRoot) { const [, value] = getFirstValue(toArray(fileName), existingFileName => fs.existsSync(path.resolve(sourceRoot, `./${existingFileName}`))); return value; } function getRelativePath(comparePath, filename) { const pathName = path.parse(filename).dir; return path.relative(pathName, comparePath); } function getStitchesPath({ sourceRoot, filename, config }) { var _sourceRoot, _config$stitchesConfi; sourceRoot = (_sourceRoot = sourceRoot) != null ? _sourceRoot : '.'; const configPathCheck = (_config$stitchesConfi = config.stitchesConfig) != null ? _config$stitchesConfi : ['stitches.config.ts', 'stitches.config.js']; const configPath = checkExists(configPathCheck, sourceRoot); if (!configPath) throw new Error(logGeneralError(`Couldn’t find the Stitches config at ${config.stitchesConfig ? `“${String(config.stitchesConfig)}”` : 'the project root'}.\nUse the twin config: stitchesConfig="PATH_FROM_PROJECT_ROOT" to set the location.`)); return getRelativePath(configPath, filename); } /** * Config presets * * To change the preset, add the following in `package.json`: * `{ "babelMacros": { "twin": { "preset": "styled-components" } } }` * * Or in `babel-plugin-macros.config.js`: * `module.exports = { twin: { preset: "styled-components" } }` */ const userPresets = { 'styled-components': { styled: { import: 'default', from: 'styled-components' }, css: { import: 'css', from: 'styled-components' }, global: { import: 'createGlobalStyle', from: 'styled-components' } }, emotion: { styled: { import: 'default', from: '@emotion/styled' }, css: { import: 'css', from: '@emotion/react' }, global: { import: 'Global', from: '@emotion/react' } }, goober: { styled: { import: 'styled', from: 'goober' }, css: { import: 'css', from: 'goober' }, global: { import: 'createGlobalStyles', from: 'goober/global' } }, stitches: { styled: { import: 'styled', from: 'stitches.config' }, css: { import: 'css', from: 'stitches.config' }, global: { import: 'global', from: 'stitches.config' } }, solid: { styled: { import: 'styled', from: 'solid-styled-components' }, css: { import: 'css', from: 'solid-styled-components' }, global: { import: 'createGlobalStyles', from: 'solid-styled-components' } } }; function dlv(t,e,l,n,r){for(e=e.split?e.split("."):e,n=0;n<e.length;n++)t=t?t[e[n]]:r;return t===r?l:t} function createTheme(tailwindConfig) { function getConfigValue(path, defaultValue) { return dlv(tailwindConfig, path, defaultValue); } function resolveThemeValue(path, defaultValue, options = {}) { let [pathRoot, ...subPaths] = toPath(path); // Retain dots in spacing values, eg: `ml-[theme(spacing.0.5)]` if (pathRoot === 'spacing' && subPaths.length === 2 && subPaths.every(x => !Number.isNaN(Number(x)))) { subPaths = [subPaths.join('.')]; } const value = getConfigValue(path ? ['theme', pathRoot, ...subPaths] : ['theme'], defaultValue); return sassifyValues(transformThemeValue(pathRoot)(value, options)); } const out = Object.assign((path, defaultValue) => resolveThemeValue(path, defaultValue), { withAlpha: (path, opacityValue) => resolveThemeValue(path, undefined, { opacityValue }) }); return out; } function sassifyValues(values) { if (!isObject(values)) return values; const transformed = Object.entries(values).map(([k, v]) => [k, isObject(v) && sassifyValues(v) || typeof v === 'number' && String(v) || v]); return Object.fromEntries(transformed); } function createAssert(CustomError = Error, isSilent = false, hasLogColors = true) { return (expression, message) => { if (isSilent) return; if (typeof expression === 'string') { throw new CustomError(`\n\n${expression}\n`); } const messageContext = { color: makeColor$1(hasLogColors) }; if (typeof expression === 'function') { throw new CustomError(`\n\n${expression(messageContext)}\n`); } if (expression) return; if (typeof message === 'string') { throw new CustomError(`\n\n${message}\n`); } if (typeof message === 'function') { throw new CustomError(`\n\n${message(messageContext)}\n`); } }; } /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ function packageCheck(packageToCheck, params, hasNoFallback) { if (params.config && params.config.preset === packageToCheck) return true; if (hasNoFallback) return false; return params.styledImport.from.includes(packageToCheck) || params.cssImport.from.includes(packageToCheck); } function getPackageUsed(params) { return { isEmotion: packageCheck('emotion', params), isStyledComponents: packageCheck('styled-components', params, true), isGoober: packageCheck('goober', params), isSolid: packageCheck('solid', params), isStitches: packageCheck('stitches', params) }; } function getStyledConfig({ sourceRoot, filename, config }) { const usedConfig = (config == null ? void 0 : config.styled) && config || (config == null ? void 0 : config.preset) && userPresets[config.preset] || userPresets.emotion; if (typeof usedConfig.styled === 'string') { return { import: 'default', from: usedConfig.styled }; } if (config && config.preset === 'stitches') { const stitchesPath = getStitchesPath({ sourceRoot, filename, config }); if (stitchesPath && usedConfig.styled) { // Overwrite the stitches import data with the path from the current file usedConfig.styled.from = stitchesPath; } } return usedConfig.styled; } function getCssConfig({ sourceRoot, filename, config }) { const usedConfig = (config == null ? void 0 : config.css) && config || (config == null ? void 0 : config.preset) && userPresets[config.preset] || userPresets.emotion; if (typeof usedConfig.css === 'string') { return { import: 'css', from: usedConfig.css }; } if (config && config.preset === 'stitches') { const stitchesPath = getStitchesPath({ sourceRoot, filename, config }); if (stitchesPath && usedConfig.css) { // Overwrite the stitches import data with the path from the current file usedConfig.css.from = stitchesPath; } } return usedConfig.css; } function getGlobalConfig(config) { const usedConfig = config.global && config || config.preset && userPresets[config.preset] || userPresets.emotion; return usedConfig.global; } function createCoreContext(params) { var _params$tailwindConfi; const { sourceRoot, filename, config, isDev = false, CustomError } = params; const assert = createAssert(CustomError, false, config == null ? void 0 : config.hasLogColors); const configParameters = { sourceRoot, assert, filename: filename != null ? filename : '', config }; const styledImport = getStyledConfig(configParameters); const cssImport = getCssConfig(configParameters); const tailwindConfig = (_params$tailwindConfi = params.tailwindConfig) != null ? _params$tailwindConfi : getTailwindConfig(configParameters); const packageUsed = getPackageUsed({ config, cssImport, styledImport }); const twinConfig = getConfigTwinValidated(config, { ...packageUsed, isDev }); const importConfig = { styled: styledImport, css: cssImport, global: getGlobalConfig(config != null ? config : {}) }; return { isDev, assert, debug: createDebug(isDev, twinConfig), theme: createTheme(tailwindConfig), tailwindContext: createContext(tailwindConfig), packageUsed, tailwindConfig, twinConfig, CustomError, importConfig }; } const CAMEL_FIND = /\W+(.)/g; function camelize(string) { return string == null ? void 0 : string.replace(CAMEL_FIND, (_, chr) => chr.toUpperCase()); } const MATCH_THEME = /theme\((.+?)\)/; const MATCH_QUOTES = /["'`]/g; function replaceThemeValue(value, { assert, theme }) { const match = MATCH_THEME.exec(value); if (!match) return value; const themeFunction = match[0]; const themeParameters = match[1].replace(MATCH_QUOTES, '').trim(); const [main, second] = themeParameters.split(','); let themeValue = theme(main, second); assert(Boolean(themeValue), ({ color }) => color(`✕ ${color(themeParameters, 'errorLight')} doesn’t match a theme value from the config`)); // Account for the 'DEFAULT' key if (typeof themeValue === 'object' && 'DEFAULT' in themeValue) { themeValue = themeValue.DEFAULT; } // Escape spaces in the value - without this we get an incorrect order // in class groups like this: // tw`w-[calc(100%-theme('spacing.1'))] w-[calc(100%-theme('spacing[0.5]'))]` // theme: { spacing: { 0.5: "calc(.5 * .25rem)", 1: "calc(1 * .25rem)" } } const stringValue = String(themeValue).replace(/\./g, '\\.'); const replacedValue = value.replace(themeFunction, stringValue); return replacedValue; } const SELECTOR_PARENT_CANDIDATE = /^[ #.[]/; const SELECTOR_SPECIAL_STARTS = /^ [>@]/; const SELECTOR_ROOT = /(^| ):root(?!\w)/g; const UNDERSCORE_ESCAPING$1 = /\\+(_)/g; const WRAPPED_PARENT_SELECTORS = /(\({3}&(.*?)\){3})/g; const sassifySelectorTasks = [selector => selector.trim(), // Prefix with the parent selector when sassyPseudo is enabled, // otherwise just replace the class with the parent selector (selector, { selectorMatchReg, sassyPseudo, original }) => { const out = selector.replace(selectorMatchReg, (match, __, offset) => { if (selector === match) return ''; if (/\w/.test(selector[offset - 1]) && selector[offset + match.length] === ':') { if (sassyPseudo && selector[offset - 1] === undefined) return '&'; return ''; // Cover [section&]:hover:block / .btn.loading&:before } return offset === 0 ? '' : '&'; }); // Fix certain matches not covered by the previous task, eg: `first:[section]:m-1` // (Arbitrary variants targeting html elements) if (original && out === selector && selector.includes(`.${original}`)) return selector.replace(`.${original}`, ''); return out; }, // Unwrap the pre-wrapped parent selectors (pre-wrapping avoids matching issues against word characters, eg: `[&section]:block`) selector => selector.replace(WRAPPED_PARENT_SELECTORS, '&$2'), // Remove unneeded escaping from the selector selector => selector.replace(UNDERSCORE_ESCAPING$1, '$1'), // Prefix classes/ids/attribute selectors with a parent selector so styles // are applied to the current element rather than its children selector => { if (selector.includes('&')) return selector; const addParentSelector = SELECTOR_PARENT_CANDIDATE.test(selector); if (!addParentSelector) return selector; // Fix: ` > :not([hidden]) ~ :not([hidden])` / ` > *` // Fix: `[@page]:x` if (SELECTOR_SPECIAL_STARTS.test(selector)) return selector; return `&${selector}`; }, // Fix the spotty `:root` support in emotion/styled-components selector => selector.replace(SELECTOR_ROOT, '*:root'), // Escape selectors containing forward slashes, eg: group-hover/link:bg-black selector => selector.replace(/\//g, '\\/'), selector => selector.trim()]; function sassifySelector(selector, params) { // Remove the selector if it only contains the parent selector if (selector === '&') { params.debug('selector not required', selector); return ''; } for (const task of sassifySelectorTasks) { selector = task(selector, params); } return selector; } const DEFAULTS_UNIVERSAL = '*, ::before, ::after'; const EMPTY_CSS_VARIABLE_VALUE = 'var(--tw-empty,/*!*/ /*!*/)'; const PRESERVED_ATRULE_TYPES = new Set(['charset', 'counter-style', 'document', 'font-face', 'font-feature-values', 'import', 'keyframes', 'namespace']); const LAYER_DEFAULTS = 'defaults'; const LINEFEED$1 = /\n/g; const WORD_CHARACTER = /\w/; const SPACE_ID$1 = '_'; const SPACES = /\s+/g; const BRACKETED = /^\(.*?\)$/; const BRACKETED_MAYBE_IMPORTANT = /\)!?$/; const ESCAPE_CHARACTERS$1 = /\n|\t/g; function spreadVariantGroups(classes, context) { var _context$tailwindConf, _context$beforeImport, _context$afterImporta; const pieces = [...splitAtTopLevelOnly(classes.trim(), (_context$tailwindConf = context.tailwindConfig.separator) != null ? _context$tailwindConf : ':')]; let groupedClasses = pieces.pop(); if (!groupedClasses) return []; // type guard // Check for too many dividers used // Added here instead of "validateClasses" as it's less error prone to check here context.assert(!pieces.includes(''), ({ color }) => { var _context$tailwindConf2; return `${color(`✕ ${String(color(classes, 'errorLight'))} has too many dividers`)}\n\nUpdate to ${String(color(`${pieces.filter(Boolean).join((_context$tailwindConf2 = context.tailwindConfig.separator) != null ? _context$tailwindConf2 : ':')}`, 'success'))}`; }); let beforeImportant = (_context$beforeImport = context == null ? void 0 : context.beforeImportant) != null ? _context$beforeImport : ''; let afterImportant = (_context$afterImporta = context == null ? void 0 : context.afterImportant) != null ? _context$afterImporta : ''; if (!beforeImportant && groupedClasses.startsWith('!')) { groupedClasses = groupedClasses.slice(1); beforeImportant = '!'; } if (!afterImportant && groupedClasses.endsWith('!')) { groupedClasses = groupedClasses.slice(0, -1); afterImportant = '!'; } // Remove () brackets and split const unwrapped = BRACKETED.test(groupedClasses) ? groupedClasses.slice(1, -1) : groupedClasses; const classList = [...splitAtTopLevelOnly(unwrapped, ' ')].filter(Boolean); const group = classList.map(className => { var _context$tailwindConf4; if (BRACKETED_MAYBE_IMPORTANT.test(className) && // Avoid infinite loop due to lack of separator, eg: `[em](block)` !className.includes('](')) { var _context$tailwindConf3; const ctx = { ...context, beforeImportant, afterImportant }; return expandVariantGroups([...pieces, className].join((_context$tailwindConf3 = context.tailwindConfig.separator) != null ? _context$tailwindConf3 : ':'), ctx); } return [...pieces, [beforeImportant, className, afterImportant].join('')].filter(Boolean).join((_context$tailwindConf4 = context.tailwindConfig.separator) != null ? _context$tailwindConf4 : ':'); }).filter(Boolean); return group; } function expandVariantGroups(classes, context) { const classList = [...splitAtTopLevelOnly(classes.replace(ESCAPE_CHARACTERS$1, ' ').trim(), ' ')]; if (classList.length === 1 && ['', '()'].includes(classList[0])) return ''; const expandedClasses = classList.flatMap(item => spreadVariantGroups(item, context)); return expandedClasses.join(' '); } const REGEX_SPECIAL_CHARACTERS = /[$()*+./?[\\\]^{|}-]/g; function escapeRegex(string) { return string.replace(REGEX_SPECIAL_CHARACTERS, '\\$&'); } function isShortCss(fullClassName, tailwindConfig) { var _tailwindConfig$separ; const classPieces = [...splitAtTopLevelOnly(fullClassName, (_tailwindConfig$separ = tailwindConfig.separator) != null ? _tailwindConfig$separ : ':')]; const className = classPieces.slice(-1)[0]; if (!className.includes('[')) return false; // Replace brackets before splitting on them as the split function already // reads brackets to determine where the top level is const splitAtArbitrary = [...splitAtTopLevelOnly(className.replace(/\[/g, '∀'), '∀')]; // Normal class if (splitAtArbitrary[0].endsWith('-')) return false; // Important suffix if (splitAtArbitrary[0].endsWith('!')) return false; // Arbitrary property if (splitAtArbitrary[0] === '') return false; // Slash opacity, eg: bg-red-500/fromConfig/[.555] if (splitAtArbitrary[0].endsWith('/')) return false; return true; } // Split a string at a value and return an array of the two parts function splitOnFirst(input, delim) { return (([first, ...rest]) => [first, rest.join(delim)])(input.split(delim)); } const ALL_COMMAS = /,/g; const ALL_AMPERSANDS = /&/g; const ENDING_AMP_THEN_WHITESPACE = /&[\s_]*$/; const ALL_CLASS_DOTS = /(?<!\\)(\.)(?=\w)/g; const ALL_CLASS_ATS = /(?<!\\)(@)(?=\w)(?!media)/g; const ALL_WRAPPABLE_PARENT_SELECTORS = /&(?=([^ $)*+,.:>[_~])[\w-])/g; const BASIC_SELECTOR_TYPES = /^#|^\\.|[^\W_]/; function convertShortCssToArbitraryProperty(className, { tailwindConfig, assert, disableShortCss, isShortCssOnly, origClassName }) { var _tailwindConfig$separ, _tailwindConfig$separ2, _tailwindConfig$separ3; const splitArray = [...splitAtTopLevelOnly(className, (_tailwindConfig$separ = tailwindConfig.separator) != null ? _tailwindConfig$separ : ':')]; const lastValue = splitArray.slice(-1)[0]; let [property, value] = splitOnFirst(lastValue, '['); value = value.slice(0, -1).trim(); let preSelector = ''; if (property.startsWith('!')) { property = property.slice(1); preSelector = '!'; } const template = `${preSelector}[${[property, value === '' ? "''" : value].join((_tailwindConfig$separ2 = tailwindConfig.separator) != null ? _tailwindConfig$separ2 : ':')}]`; splitArray.splice(-1, 1, template); const arbitraryProperty = splitArray.join((_tailwindConfig$separ3 = tailwindConfig.separator) != null ? _tailwindConfig$separ3 : ':'); const isShortCssDisabled = disableShortCss && !isShortCssOnly; assert(!isShortCssDisabled, ({ color }) => [`${String(color(`✕ ${String(color(origClassName, 'errorLight'))} uses twin’s deprecated short-css syntax`))}`, `Update to ${String(color(arbitraryProperty, 'success'))}`, `To ignore this notice, add this to your twin config:\n{ "disableShortCss": false }`, `Read more at https://twinredirect.page.link/short-css`].join('\n\n')); return arbitraryProperty; } function checkForVariantSupport({ className, tailwindConfig, assert }) { var _tailwindConfig$separ4; const pieces = [...splitAtTopLevelOnly(className, (_tailwindConfig$separ4 = tailwindConfig.separator) != null ? _tailwindConfig$separ4 : ':')]; const hasMultipleVariants = pieces.length > 2; // One is the class name const hasACommaInVariants = pieces.some(p => splitAtTopLevelOnly(p.slice(1, -1), ',').length > 1); const hasIssue = hasMultipleVariants && hasACommaInVariants; assert(!hasIssue, ({ color }) => `${color(`✕ The variants on ${String(color(className, 'errorLight'))} are invalid tailwind and twin classes`)}\n\n${color(`To fix, either reduce all variants into a single arbitrary variant:`, 'success')}\nFrom: \`[.this, .that]:first:block\`\nTo: \`[.this:first, .that:first]:block\`\n\n${color(`Or split the class into separate classes instead of using commas:`, 'success')}\nFrom: \`[.this, .that]:first:block\`\nTo: \`[.this]:first:block [.that]:first:block\`\n\nRead more at https://twinredirect.page.link/arbitrary-variants-with-commas`); } // Convert a twin class to a tailwindcss friendly class function convertClassName(className, { tailwindConfig, theme, isShortCssOnly, disableShortCss, assert, debug }) { checkForVariantSupport({ className, tailwindConfig, assert }); const origClassName = className; // Convert spaces to class friendly underscores className = className.replace(SPACES, SPACE_ID$1); // Move the bang to the front of the class if (className.endsWith('!')) { var _tailwindConfig$separ5, _tailwindConfig$separ6; debug('trailing bang found', className); const splitArray = [...splitAtTopLevelOnly(className.slice(0, -1), (_tailwindConfig$separ5 = tailwindConfig.separator) != null ? _tailwindConfig$separ5 : ':')]; // Place a ! before the class splitArray.splice(-1, 1, `!${splitArray[splitArray.length - 1]}`); className = splitArray.join((_tailwindConfig$separ6 = tailwindConfig.separator) != null ? _tailwindConfig$separ6 : ':'); } // Convert short css to an arbitrary property, eg: `[display:block]` // (Short css is deprecated) if (isShortCss(className, tailwindConfig)) { debug('short css found', className); className = convertShortCssToArbitraryProperty(className, { tailwindConfig, assert, disableShortCss, isShortCssOnly, origClassName }); } // Replace theme values throughout the class className = replaceThemeValue(className, { assert, theme }); // Add missing parent selectors and collapse arbitrary variants className = sassifyArbitraryVariants(className, { tailwindConfig }); debug('class after format', className); return className; } function isArbitraryVariant(variant) { return variant.startsWith('[') && variant.endsWith(']'); } function unbracket(variant) { return variant.slice(1, -1); } function sassifyArbitraryVariants(fullClassName, { tailwindConfig }) { var _tailwindConfig$separ7, _tailwindConfig$separ8; const splitArray = [...splitAtTopLevelOnly(fullClassName, (_tailwindConfig$separ7 = tailwindConfig.separator) != null ? _tailwindConfig$separ7 : ':')]; const variants = splitArray.slice(0, -1); const className = splitArray.slice(-1)[0]; if (variants.length === 0) return fullClassName; // Collapse arbitrary variants when they don't contain `&`. // `[> div]:[.nav]:(flex block)` -> `[> div_.nav]:flex [> div_.nav]:block` const collapsed = []; variants.forEach((variant, index) => { // We can’t match the selector if there's a character right next to the parent selector (eg: `[&section]:block`) otherwise we'd accidentally replace `.step` in classes like this: // Bad: `.steps-primary .steps` -> `&-primary &` // Good: `.steps-primary .steps` -> `.steps-primary &` // So here we replace it with crazy brackets to identify and unwrap it later if (isArbitraryVariant(variant)) variant = variant.replace(ALL_WRAPPABLE_PARENT_SELECTORS, '(((&)))'); if (index === 0 || !isArbitraryVariant(variant) || !isArbitraryVariant(variants[index - 1])) return collapsed.push(variant); const prev = collapsed[collapsed.length - 1]; if (variant.includes('&')) { const prevHasParent = prev.includes('&'); // Merge with current if (prevHasParent) { const mergedWithCurrent = variant.replace(ALL_AMPERSANDS, unbracket(prev)); const isLast = index === variants.length - 1; collapsed[index - 1] = isLast ? mergedWithCurrent.replace(ALL_AMPERSANDS, '') : mergedWithCurrent; return; } // Merge with previous if (!prevHasParent) { const mergedWithPrev = `[${unbracket(variant).replace(ALL_AMPERSANDS, unbracket(prev))}]`; collapsed[collapsed.length - 1] = mergedWithPrev; return; } } // Parentless variants are merged into the previous arbitrary variant const mergedWithPrev = `[${[unbracket(prev).replace(ENDING_AMP_THEN_WHITESPACE, ''), unbracket(variant)].join('_')}]`; collapsed[collapsed.length - 1] = mergedWithPrev; }); // The supplied class requires the reversal of it's variants as resolveMatches adds them in reverse order const reversedVariantList = [...collapsed].slice().reverse(); const allVariants = reversedVariantList.map((v, idx) => { if (!isArbitraryVariant(v)) return v; const unwrappedVariant = unbracket(v) // Unescaped dots incorrectly add the prefix within arbitrary variants (only when`prefix` is set in tailwind config) // eg: tw`[.a]:first:tw-block` -> `.tw-a &:first-child` .replace(ALL_CLASS_DOTS, '\\.') // Unescaped ats will throw a conversion error .replace(ALL_CLASS_ATS, '\\@'); const variantList = unwrappedVariant.startsWith('@') ? [unwrappedVariant] : // Arbitrary variants with commas are split, handled as separate selectors then joined [...splitAtTopLevelOnly(unwrappedVariant, ',')]; const out = variantList.map(variant => { var _collapsed; return addParentSelector(variant, collapsed[idx - 1], (_collapsed = collapsed[idx + 1]) != null ? _collapsed : ''); }) // Tailwindcss removes everything from a comma onwards in arbitrary variants, so we need to encode to preserve them // Underscore is needed to distance the code from another possible number // Eg: [path[fill='rgb(51,100,51)']]:[fill:white] .join('\\2c_').replace(ALL_COMMAS, '\\2c_'); return `[${out}]`; }); return [...allVariants, className].join((_tailwindConfig$separ8 = tailwindConfig.separator) != null ? _tailwindConfig$separ8 : ':'); } function addParentSelector(selector, prev, next) { // Preserve selectors with a parent selector and media queries if (selector.includes('&') || selector.startsWith('@')) return selector; // Arbitrary variants // Pseudo elements get an auto parent selector prefixed if (selector.startsWith(':')) return `&${selector}`; // Variants that start with a class/id get treated as a child if (BASIC_SELECTOR_TYPES.test(selector) && !prev) return `& ${selector}`; // When there's more than one variant and it's at the end then prefix it if (!next && prev) return `&${selector}`; return `& ${selector}`; } const IMPORTANT_OUTSIDE_BRACKETS = /(:!|^!)(?=(?:(?:(?!\)).)*\()|[^()]*$)(?=(?:(?:(?!]).)*\[)|[^[\]]*$)/; const COMMENTS_MULTI_LINE = /(?<!\/)\/(?!\/)\*[\S\s]*?\*\//g; const COMMENTS_SINGLE_LINE = /(?<!:)\/\/.*/g; const CLASS_DIVIDER_PIPE = / \| /g; const ALL_BRACKET_SQUARE_LEFT = /\[/g; const ALL_BRACKET_SQUARE_RIGHT = /]/g; const ALL_BRACKET_ROUND_LEFT = /\(/g; const ALL_BRACKET_ROUND_RIGHT = /\)/g; const ESCAPE_CHARACTERS = /\n|\t/g; function getStylesFromMatches(matches, params) { if (matches.length === 0) { params.debug('no matches supplied', {}, 'error'); return; } const rulesets = matches.map(([data, rule]) => extractRuleStyles([rule], { ...params, options: data.options })).filter(Boolean); if (rulesets.length === 0) { params.debug('no node rulesets found', {}, 'error'); return; } // @ts-expect-error Avoid tuple type error return deepMerge__default["default"](...rulesets); } // When removing a multiline comment, determine if a space is left or not // eg: You'd want a space left in this situation: tw`class1/* comment */class2` function multilineReplaceWith(match, index, input) { const charBefore = input[index - 1]; const directPrefixMatch = charBefore && WORD_CHARACTER.exec(charBefore); const charAfter = input[Number(index) + Number(match.length)]; const directSuffixMatch = charAfter && WORD_CHARACTER.exec(charAfter); return directPrefixMatch != null && directPrefixMatch[0] && directSuffixMatch && directSuffixMatch[0] ? ' ' : ''; } function validateClasses(classes, { assert, tailwindConfig }) { var _classes$match, _classes$match2, _classes$match3, _classes$match4; // TOFIX: Avoid counting brackets within arbitrary values assert(((_classes$match = classes.match(ALL_BRACKET_SQUARE_LEFT)) != null ? _classes$match : []).length === ((_classes$match2 = classes.match(ALL_BRACKET_SQUARE_RIGHT)) != null ? _classes$match2 : []).length, ({ color }) => `${color(`✕ Unbalanced square brackets found in classes:\n\n${color(classes, 'errorLight')}`)}`); // TOFIX: Avoid counting brackets within arbitrary values assert(((_classes$match3 = classes.match(ALL_BRACKET_ROUND_LEFT)) != null ? _classes$match3 : []).length === ((_classes$match4 = classes.match(ALL_BRACKET_ROUND_RIGHT)) != null ? _classes$match4 : []).length, ({ color }) => `${color(`✕ Unbalanced round brackets found in classes:\n\n${color(classes, 'errorLight')}`)}`); for (const className of splitAtTopLevelOnly(classes, ' ')) { var _tailwindConfig$separ; // Check for missing class attached to a variant const classCheck = className.replace(ESCAPE_CHARACTERS, ' ').trim(); assert(!classCheck.endsWith((_tailwindConfig$separ = tailwindConfig.separator) != null ? _tailwindConfig$separ : ':'), ({ color }) => `${color(`✕ The variant ${String(color(classCheck, 'errorLight'))} doesn’t look right`)}\n\nUpdate to ${String(color(`${classCheck}block`, 'success'))} or ${String(color(`${classCheck}(block mt-4)`, 'success'))}`); } return true; } const tasks = [classes => classes.replace(CLASS_DIVIDER_PIPE, ' '), classes => classes.replace(COMMENTS_MULTI_LINE, multilineReplaceWith), classes => classes.replace(COMMENTS_SINGLE_LINE, ''), (classes, tailwindConfig, assert) => expandVariantGroups(classes, { assert, tailwindConfig }) // Expand grouped variants to individual classes ]; function sortBigSign(bigIntValue) { return Number(bigIntValue > 0n) - Number(bigIntValue < 0n); } function getOrderedClassList(tailwindContext, convertedClassList, classList, assert) { assert(typeof (tailwindContext == null ? void 0 : tailwindContext.getClassOrder) === 'function', ({ color }) => color('Twin requires a newer version of tailwindcss, please update')); // `getClassOrder` was added in tailwindcss@3.0.23 let orderedClassList; try { orderedClassList = tailwindContext.getClassOrder(convertedClassList).map(([className, order], index) => [order || 0n, className, classList[index]]).sort(([a], [z]) => sortBigSign(a - z)); } catch (error) { assert(false, ({ color }) => `${color(String(error).replace('with \\ may', 'with a single \\ may') // Improve error )}\n\n${color('Found in:')} ${color(convertedClassList.join(' '), 'errorLight')}`); } return orderedClassList; } function getStyles(classes, params) { const assert = createAssert(params.CustomError, params.isSilent, params.twinConfig.hasLogColors); params.debug('string in', classes); assert(![null, 'null', undefined].includes(classes), ({ color }) => `${color(`✕ Your classes need to be complete strings for Twin to detect them correctly`)}\n\nRead more at https://twinredirect.page.link/template-literals`); validateClasses(classes, { tailwindConfig: params.tailwindConfig, assert }); for (const task of tasks) { classes = task(classes, params.tailwindConfig, assert); } params.debug('classes after format', classes); const matched = []; const unmatched = []; const styles = []; const commonContext = { assert, theme: params.theme, debug: params.debug }; const convertedClassNameContext = { ...commonContext, tailwindConfig: params.tailwindConfig, isShortCssOnly: params.isShortCssOnly, disableShortCss: params.twinConfig.disableShortCss }; const classList = [...splitAtTopLevelOnly(classes, ' ')]; const convertedClassList = classList.map(c => convertClassName(c, convertedClassNameContext)); const orderedClassList = getOrderedClassList(params.tailwindContext, convertedClassList, classList, assert); const commonMatchContext = { ...commonContext, includeUniversalStyles: false, coreContext: params, twinConfig: params.twinConfig, tailwindConfig: params.tailwindConfig, tailwindContext: params.tailwindContext, sassyPseudo: params.twinConfig.sassyPseudo }; for (const [, convertedClassName, className] of orderedClassList) { const matches = [...resolveMatches(convertedClassName, params.tailwindContext)]; const results = getStylesFromMatches(matches, { ...commonMatchContext, hasImportant: IMPORTANT_OUTSIDE_BRACKETS.test(escapeRegex(convertedClassName)), selectorMatchReg: new RegExp( // This regex specifies a list of characters allowed for the character // immediately after the class ends - this avoids matching other classes // eg: Input 'btn' will avoid matching '.btn-primary' in `.btn + .btn-primary` `(${escapeRegex(`.${convertedClassName}`)})(?=[\\[.# >~+*:$\\)]|$)`), original: convertedClassName }); if (!results) { params.debug('🔥 No matching rules found', className, 'error'); // Allow tw``/tw="" to pass through if (className !== '') unmatched.push(className); // If non-match and is on silent mode: Continue next iteration if (params.isSilent) continue; // If non-match: Stop iteration and return // (This "for of" loop returns to the parent function) return { styles: undefined, matched, unmatched }; } matched.push(className); params.debug('✨ ruleset out', results, 'success'); styles.push(results); } if (styles.length === 0) return { styles: undefined, matched, unmatched }; // @ts-expect-error Avoid tuple type error const mergedStyles = deepMerge__default["default"](...styles); return { styles: mergedStyles, matched, unmatched }; } const ESC_COMMA = /\\2c/g; const ESC_DIGIT = /\\3(\d)/g; const UNDERSCORE_ESCAPING = /\\+(_)/g; const SLASH_DOT_ESCAPING = /\\\./g; const BACKSLASH_ESCAPING = /\\\\/g; function transformImportant(value, params) { var _params$options; if (params.passChecks === true) return value; if (!params.hasImportant) return value; // Avoid adding important if the rule doesn't respect it if (params.hasImportant && ((_params$options = params.options) == null ? void 0 : _params$options.respectImportant) === false) return value; return `${value} !important`; } function transformEscaping(value) { return value.replace(UNDERSCORE_ESCAPING, '$1') // Remove slash dot encoding in values // eg: calc(\\.5 * .25rem) .replace(SLASH_DOT_ESCAPING, '.') // Fix the duplicate escaping babel delivers .replace(BACKSLASH_ESCAPING, '\\'); } const transformValueTasks = [replaceThemeValue, transformImportant, transformEscaping]; function transformDeclValue(value, params) { const valueOriginal = value; for (const task of transformValueTasks) { value = task(value, params); } if (value !== valueOriginal) params.debug('converted theme/important', { old: valueOriginal, new: value }); return value; } function extractFromRule(rule, params) { const selectorForUnescape = rule.selector.replace(ESC_DIGIT, '$1'); // Remove digit escaping const selector = unescape(selectorForUnescape).replace(LINEFEED$1, ' '); return [selector, extractRuleStyles(rule.nodes, params)]; } function extractSelectorFromAtRule(name, value, params) { if (name === LAYER_DEFAULTS) { if (params.includeUniversalStyles === false) return; return DEFAULTS_UNIVERSAL; } const val = value.replace(ESC_COMMA, ','); // Handle @screen usage in plugins, eg: `@screen md` if (name === 'screen') { const screenConfig = get__default["default"](params, 'tailwindConfig.theme.screens'); return `@media (min-width: ${screenConfig[val]})`; } return `@${name} ${val}`.trim(); } const ruleTypes = { decl(decl, params) { const property = decl.prop.startsWith('--') ? decl.prop : camelize(decl.prop); const value = decl.prop.startsWith('--') && decl.value === ' ' ? EMPTY_CSS_VARIABLE_VALUE // valid empty value in js, unlike ` ` : transformDeclValue(decl.value, { ...params, decl, property }); if (value === null) return; // `background-clip: text` is still in "unofficial" phase and needs a // prefix in Firefox, Chrome and Safari. // https://caniuse.com/background-img-opts if (property === 'backgroundClip' && (value === 'text' || value === 'text !important')) return { WebkitBackgroundClip: value, [property]: value }; return { [property]: value