diff --git a/scripts/create-plugin-list.js b/scripts/create-plugin-list.js index 23f3cb485b65..f38cf5036e39 100644 --- a/scripts/create-plugin-list.js +++ b/scripts/create-plugin-list.js @@ -1,8 +1,8 @@ -import corePlugins from '../src/corePlugins' +import { corePlugins } from '../src/corePlugins' import fs from 'fs' import path from 'path' -let corePluginList = Object.keys(corePlugins).filter((plugin) => !plugin.includes('Variants')) +let corePluginList = Object.keys(corePlugins) fs.writeFileSync( path.join(process.cwd(), 'src', 'corePluginList.js'), diff --git a/src/corePlugins.js b/src/corePlugins.js index 79ff993a1db3..efd389447b0b 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -20,8 +20,7 @@ import { import { version as tailwindVersion } from '../package.json' import log from './util/log' -export default { - // Variant plugins +export let variantPlugins = { pseudoElementVariants: ({ config, addVariant }) => { addVariant( 'first-letter', @@ -360,8 +359,9 @@ export default { ) } }, +} - // Actual plugins +export let corePlugins = { preflight: ({ addBase }) => { let preflightStyles = postcss.parse(fs.readFileSync(`${__dirname}/css/preflight.css`, 'utf8')) @@ -753,31 +753,28 @@ export default { let prefixName = (name) => prefix(`.${name}`).slice(1) let keyframes = Object.fromEntries( Object.entries(theme('keyframes') ?? {}).map(([key, value]) => { - return [key, [{ [`@keyframes ${prefixName(key)}`]: value }]] + return [key, { [`@keyframes ${prefixName(key)}`]: value }] }) ) matchUtilities( { - animate: (value, { includeRules }) => { + animate: (value) => { let animations = parseAnimationValue(value) - for (let { name } of animations) { - if (keyframes[name] !== undefined) { - includeRules(keyframes[name], { respectImportant: false }) - } - } - - return { - animation: animations - .map(({ name, value }) => { - if (name === undefined || keyframes[name] === undefined) { - return value - } - return value.replace(name, prefixName(name)) - }) - .join(', '), - } + return [ + ...animations.flatMap((animation) => keyframes[animation.name]), + { + animation: animations + .map(({ name, value }) => { + if (name === undefined || keyframes[name] === undefined) { + return value + } + return value.replace(name, prefixName(name)) + }) + .join(', '), + }, + ] }, }, { values: theme('animation') } diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 13ec474803cb..b8d411c7414f 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -108,11 +108,6 @@ function applyVariant(variant, matches, context) { let result = [] for (let [meta, rule] of matches) { - if (meta.options.respectVariants === false) { - result.push([meta, rule]) - continue - } - let container = postcss.root({ nodes: [rule.clone()] }) for (let [variantSort, variantFunction] of variantFunctionTuples) { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 5826c4340d2c..52e3936fa963 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -12,7 +12,7 @@ import escapeClassName from '../util/escapeClassName' import nameClass, { formatClass } from '../util/nameClass' import { coerceValue } from '../util/pluginUtils' import bigSign from '../util/bigSign' -import corePlugins from '../corePlugins' +import { variantPlugins, corePlugins } from '../corePlugins' import * as sharedState from './sharedState' import { env } from './sharedState' import { toPath } from '../util/toPath' @@ -237,17 +237,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs }, addComponents(components, options) { let defaultOptions = { - variants: [], respectPrefix: true, respectImportant: false, - respectVariants: true, } - options = Object.assign( - {}, - defaultOptions, - Array.isArray(options) ? { variants: options } : options - ) + options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) for (let [identifier, rule] of withIdentifiers(components)) { let prefixedIdentifier = prefixIdentifier(identifier, options) @@ -266,17 +260,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs }, addUtilities(utilities, options) { let defaultOptions = { - variants: [], respectPrefix: true, respectImportant: true, - respectVariants: true, } - options = Object.assign( - {}, - defaultOptions, - Array.isArray(options) ? { variants: options } : options - ) + options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) for (let [identifier, rule] of withIdentifiers(utilities)) { let prefixedIdentifier = prefixIdentifier(identifier, options) @@ -295,10 +283,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs }, matchUtilities: function (utilities, options) { let defaultOptions = { - variants: [], respectPrefix: true, respectImportant: true, - respectVariants: true, } options = { ...defaultOptions, ...options } @@ -338,21 +324,14 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return [] } - let includedRules = [] let ruleSets = [] - .concat( - rule(value, { - includeRules(rules) { - includedRules.push(...rules) - }, - }) - ) + .concat(rule(value)) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, })) - return [...includedRules, ...ruleSets] + return ruleSets } let withOffsets = [{ sort: offset, layer: 'utilities', options }, wrapped] @@ -361,6 +340,68 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs context.candidateRuleMap.set(prefixedIdentifier, []) } + context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) + } + }, + matchComponents: function (components, options) { + let defaultOptions = { + respectPrefix: true, + respectImportant: false, + } + + options = { ...defaultOptions, ...options } + + let offset = offsets.components++ + + for (let identifier in components) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + let rule = components[identifier] + + classList.add([prefixedIdentifier, options]) + + function wrapped(modifier, { isOnlyPlugin }) { + let { type = 'any' } = options + type = [].concat(type) + let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig) + + if (value === undefined) { + return [] + } + + if (!type.includes(coercedType)) { + if (isOnlyPlugin) { + log.warn([ + `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`, + `You can safely update it to \`${identifier}-${modifier.replace( + coercedType + ':', + '' + )}\`.`, + ]) + } else { + return [] + } + } + + if (!isValidArbitraryValue(value)) { + return [] + } + + let ruleSets = [] + .concat(rule(value)) + .filter(Boolean) + .map((declaration) => ({ + [nameClass(identifier, modifier)]: declaration, + })) + + return ruleSets + } + + let withOffsets = [{ sort: offset, layer: 'components', options }, wrapped] + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) } }, @@ -457,7 +498,7 @@ function collectLayerPlugins(root) { } function resolvePlugins(context, root) { - let corePluginList = Object.entries(corePlugins) + let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins }) .map(([name, plugin]) => { if (!context.tailwindConfig.corePlugins.includes(name)) { return null @@ -479,12 +520,15 @@ function resolvePlugins(context, root) { // TODO: This is a workaround for backwards compatibility, since custom variants // were historically sorted before screen/stackable variants. - let beforeVariants = [corePlugins['pseudoElementVariants'], corePlugins['pseudoClassVariants']] + let beforeVariants = [ + variantPlugins['pseudoElementVariants'], + variantPlugins['pseudoClassVariants'], + ] let afterVariants = [ - corePlugins['directionVariants'], - corePlugins['reducedMotionVariants'], - corePlugins['darkVariants'], - corePlugins['screenVariants'], + variantPlugins['directionVariants'], + variantPlugins['reducedMotionVariants'], + variantPlugins['darkVariants'], + variantPlugins['screenVariants'], ] return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins] diff --git a/src/util/normalizeConfig.js b/src/util/normalizeConfig.js index 3a9acb18cb56..3c297f41c825 100644 --- a/src/util/normalizeConfig.js +++ b/src/util/normalizeConfig.js @@ -18,14 +18,18 @@ export function normalizeConfig(config) { */ let valid = (() => { // `config.purge` should not exist anymore - if (config.purge) return false + if (config.purge) { + return false + } // `config.content` should exist - if (!config.content) return false + if (!config.content) { + return false + } // `config.content` should be an object or an array if ( - !Array.isArray(config.content) || + !Array.isArray(config.content) && !(typeof config.content === 'object' && config.content !== null) ) { return false @@ -38,17 +42,15 @@ export function normalizeConfig(config) { if (typeof path === 'string') return true // `path` can be an object { raw: string, extension?: string } - if ( - // `raw` must be a string - typeof path?.raw === 'string' && - // `extension` (if provided) should also be a string - path?.extension !== undefined && - typeof path?.extension === 'string' - ) { - return true + // `raw` must be a string + if (typeof path?.raw !== 'string') return false + + // `extension` (if provided) should also be a string + if (path?.extension && typeof path?.extension !== 'string') { + return false } - return false + return true }) } @@ -69,17 +71,15 @@ export function normalizeConfig(config) { if (typeof path === 'string') return true // `path` can be an object { raw: string, extension?: string } - if ( - // `raw` must be a string - typeof path?.raw === 'string' && - // `extension` (if provided) should also be a string - path?.extension !== undefined && - typeof path?.extension === 'string' - ) { - return true + // `raw` must be a string + if (typeof path?.raw !== 'string') return false + + // `extension` (if provided) should also be a string + if (path?.extension && typeof path?.extension !== 'string') { + return false } - return false + return true }) ) { return false @@ -92,7 +92,6 @@ export function normalizeConfig(config) { return false } } - return false } else if ( !(config.content.extract === undefined || typeof config.content.extract === 'function') ) { @@ -106,7 +105,6 @@ export function normalizeConfig(config) { return false } } - return false } else if ( !( config.content.transform === undefined || typeof config.content.transform === 'function' diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js index 7609a0fd8bf2..78ad7ce01dd5 100644 --- a/tests/custom-plugins.test.js +++ b/tests/custom-plugins.test.js @@ -1500,25 +1500,18 @@ test('the configFunction parameter is optional when using the `createPlugin.with test('keyframes are not escaped', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`` }], + corePlugins: { preflight: false }, plugins: [ function ({ matchUtilities }) { matchUtilities({ - foo: (value, { includeRules }) => { - includeRules( - [ - { - [`@keyframes ${value}`]: { - '25.001%': { - color: 'black', - }, - }, - }, - ], - { respectImportant: false } - ) - + foo: (value) => { return { + [`@keyframes ${value}`]: { + '25.001%': { + color: 'black', + }, + }, animation: `${value} 1s infinite`, } }, @@ -1528,15 +1521,27 @@ test('keyframes are not escaped', () => { } return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(css` + expect(result.css).toMatchFormattedCss(` @keyframes abc { 25.001% { color: black; } } + .foo-\\[abc\\] { animation: abc 1s infinite; } + + @media (min-width: 768px) { + @keyframes def { + 25.md\\:001\\% { + color: black; + } + } + .md\\:foo-\\[def\\] { + animation: def 1s infinite; + } + } `) }) }) diff --git a/tests/match-components.test.js b/tests/match-components.test.js new file mode 100644 index 000000000000..f32c8f6620be --- /dev/null +++ b/tests/match-components.test.js @@ -0,0 +1,95 @@ +import { run, html, css } from './util/run' + +it('should be possible to matchComponents', () => { + let config = { + content: [ + { + raw: html`