diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js index 4de99ffdb..4d7933ca0 100644 --- a/__tests__/applyAtRule.test.js +++ b/__tests__/applyAtRule.test.js @@ -1,7 +1,8 @@ import postcss from 'postcss' import plugin from '../src/lib/substituteClassApplyAtRules' +import defaultConfig from '../defaultCOnfig.stub.js' -function run(input, opts = {}) { +function run(input, opts = defaultConfig) { return postcss([plugin(opts)]).process(input, { from: undefined }) } @@ -168,3 +169,38 @@ test('it does not match classes that have multiple rules', () => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) + +test('it does not match classes that have multiple rules', () => { + const input = ` + .a { + color: red; + } + + .b { + @apply .a; + } + + .a { + color: blue; + } + ` + expect.assertions(1) + return run(input).catch(e => { + expect(e).toMatchObject({ name: 'CssSyntaxError' }) + }) +}) + +test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { + const input = ` + .foo { @apply .mt-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; } + ` + + return run(input, defaultConfig).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index cea7167fb..3abb63381 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -1,6 +1,47 @@ import _ from 'lodash' import postcss from 'postcss' import escapeClassName from '../util/escapeClassName' +import utilityModules from '../utilityModules' +import prefixTree from '../util/prefixTree' +import generateModules from '../util/generateModules' + + +function buildShadowTable(config, pluginUtilities) { + const utilities = postcss.root() + const generatedUtilities = generateModules(utilityModules, _.fromPairs(Object.keys(config.modules).map((k) => [k, []])), config) + + generatedUtilities.walkAtRules('variants', atRule => { + utilities.append(atRule.clone().nodes) + }) + + const tailwindUtilityTree = postcss.root({ + nodes: utilities.nodes, + }) + + const pluginUtilityTree = postcss.root({ + nodes: pluginUtilities, + }) + + prefixTree(tailwindUtilityTree, config.options.prefix) + + const shadowTable = {} + + tailwindUtilityTree.walkRules(rule => { + if (!_.has(shadowTable, rule.selector)) { + shadowTable[rule.selector] = [] + } + shadowTable[rule.selector].push(rule) + }) + + pluginUtilityTree.walkRules(rule => { + if (!_.has(shadowTable, rule.selector)) { + shadowTable[rule.selector] = [] + } + shadowTable[rule.selector].push(rule) + }) + + return shadowTable +} function buildClassTable(css) { const classTable = {} @@ -19,17 +60,23 @@ function normalizeClassName(className) { return `.${escapeClassName(_.trimStart(className, '.'))}` } -function findMixin(classTable, mixin, onError) { +function findMixin(classTable, shadowLookup, mixin, onError) { const matches = _.get(classTable, mixin, []) if (_.isEmpty(matches)) { - // prettier-ignore - onError(`\`@apply\` cannot be used with \`${mixin}\` because \`${mixin}\` either cannot be found, or it's actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that \`${mixin}\` exists, make sure that any \`@import\` statements are being properly processed *before* Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`) + if (_.isEmpty(shadowLookup)) { + // prettier-ignore + onError(`\`@apply\` cannot be used with \`${mixin}\` because \`${mixin}\` either cannot be found, or it's actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that \`${mixin}\` exists, make sure that any \`@import\` statements are being properly processed *before* Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`) + return + } + + return findMixin(shadowLookup, {}, mixin, onError) } if (matches.length > 1) { // prettier-ignore onError(`\`@apply\` cannot be used with ${mixin} because ${mixin} is included in multiple rulesets.`) + return } const [match] = matches @@ -37,14 +84,16 @@ function findMixin(classTable, mixin, onError) { if (match.parent.type !== 'root') { // prettier-ignore onError(`\`@apply\` cannot be used with ${mixin} because ${mixin} is nested inside of an at-rule (@${match.parent.name}).`) + return } return match.clone().nodes } -export default function() { +export default function(config, { components: pluginComponents = {}, utilities: pluginUtilities = {} } = {}) { return function(css) { const classLookup = buildClassTable(css) + const shadowLookup = buildShadowTable(config, pluginUtilities) css.walkRules(rule => { rule.walkAtRules('apply', atRule => { @@ -64,7 +113,7 @@ export default function() { const decls = _(classes) .reject(mixin => mixin === '!important') .flatMap(mixin => { - return findMixin(classLookup, normalizeClassName(mixin), message => { + return findMixin(classLookup, shadowLookup, normalizeClassName(mixin), message => { throw atRule.error(message) }) }) diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 49f097a7b..fb8f30e20 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -18,6 +18,6 @@ export default function(lazyConfig) { substituteVariantsAtRules(config, plugins), substituteResponsiveAtRules(config), substituteScreenAtRules(config), - substituteClassApplyAtRules(config), + substituteClassApplyAtRules(config, plugins), ]) }