diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js index 4de99ffdb..29c0054d7 100644 --- a/__tests__/applyAtRule.test.js +++ b/__tests__/applyAtRule.test.js @@ -1,8 +1,12 @@ import postcss from 'postcss' import plugin from '../src/lib/substituteClassApplyAtRules' +import generateUtilities from '../src/util/generateUtilities' +import defaultConfig from '../defaultConfig.stub.js' -function run(input, opts = {}) { - return postcss([plugin(opts)]).process(input, { from: undefined }) +const defaultUtilities = generateUtilities(defaultConfig, []) + +function run(input, config = defaultConfig, utilities = defaultUtilities) { + return postcss([plugin(config, utilities)]).process(input, { from: undefined }) } test("it copies a class's declarations into itself", () => { @@ -168,3 +172,23 @@ test('it does not match classes that have multiple rules', () => { 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; } + ` + + const config = { + ...defaultConfig, + experiments: { shadowLookup: true }, + } + + return run(input, config).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..01dfb3c28 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -15,40 +15,57 @@ function buildClassTable(css) { return classTable } +function buildShadowTable(generatedUtilities) { + const utilities = postcss.root() + + generatedUtilities.walkAtRules('variants', atRule => { + utilities.append(atRule.clone().nodes) + }) + + return buildClassTable(utilities) +} + function normalizeClassName(className) { return `.${escapeClassName(_.trimStart(className, '.'))}` } -function findMixin(classTable, mixin, onError) { - const matches = _.get(classTable, mixin, []) +function findClass(classToApply, classTable, shadowLookup, onError) { + const matches = _.get(classTable, classToApply, []) 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 + throw onError(`\`@apply\` cannot be used with \`${classToApply}\` because \`${classToApply}\` either cannot be found, or it's actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that \`${classToApply}\` 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 findClass(classToApply, shadowLookup, {}, onError) } if (matches.length > 1) { // prettier-ignore - onError(`\`@apply\` cannot be used with ${mixin} because ${mixin} is included in multiple rulesets.`) + throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is included in multiple rulesets.`) } const [match] = matches 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}).`) + throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is nested inside of an at-rule (@${match.parent.name}).`) } return match.clone().nodes } -export default function() { +export default function(config, generatedUtilities) { return function(css) { const classLookup = buildClassTable(css) + const shadowLookup = _.get(config, 'experiments.shadowLookup', false) + ? buildShadowTable(generatedUtilities) + : {} css.walkRules(rule => { rule.walkAtRules('apply', atRule => { - const mixins = postcss.list.space(atRule.params) + const classesAndProperties = postcss.list.space(atRule.params) /* * Don't wreck CSSNext-style @apply rules: @@ -57,20 +74,20 @@ export default function() { * These are deprecated in CSSNext but still playing it safe for now. * We might consider renaming this at-rule. */ - const [customProperties, classes] = _.partition(mixins, mixin => { - return _.startsWith(mixin, '--') + const [customProperties, classes] = _.partition(classesAndProperties, classOrProperty => { + return _.startsWith(classOrProperty, '--') }) const decls = _(classes) - .reject(mixin => mixin === '!important') - .flatMap(mixin => { - return findMixin(classLookup, normalizeClassName(mixin), message => { - throw atRule.error(message) + .reject(cssClass => cssClass === '!important') + .flatMap(cssClass => { + return findClass(normalizeClassName(cssClass), classLookup, shadowLookup, message => { + return atRule.error(message) }) }) .value() - _.tap(_.last(mixins) === '!important', important => { + _.tap(_.last(classesAndProperties) === '!important', important => { decls.forEach(decl => (decl.important = important)) }) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 0d7365426..05b7c938c 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -1,10 +1,7 @@ import fs from 'fs' import postcss from 'postcss' -import utilityModules from '../utilityModules' -import prefixTree from '../util/prefixTree' -import generateModules from '../util/generateModules' -export default function(config, { components: pluginComponents, utilities: pluginUtilities }) { +export default function(config, { components: pluginComponents }, generatedUtilities) { return function(css) { css.walkAtRules('tailwind', atRule => { if (atRule.params === 'preflight') { @@ -30,27 +27,8 @@ export default function(config, { components: pluginComponents, utilities: plugi } if (atRule.params === 'utilities') { - const utilities = generateModules(utilityModules, config.modules, config) - - if (config.options.important) { - utilities.walkDecls(decl => (decl.important = true)) - } - - const tailwindUtilityTree = postcss.root({ - nodes: utilities.nodes, - }) - - const pluginUtilityTree = postcss.root({ - nodes: pluginUtilities, - }) - - prefixTree(tailwindUtilityTree, config.options.prefix) - - tailwindUtilityTree.walk(node => (node.source = atRule.source)) - pluginUtilityTree.walk(node => (node.source = atRule.source)) - - atRule.before(tailwindUtilityTree) - atRule.before(pluginUtilityTree) + generatedUtilities.walk(node => (node.source = atRule.source)) + atRule.before(generatedUtilities) atRule.remove() } }) diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 49f097a7b..f67e60603 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -6,18 +6,21 @@ import substituteVariantsAtRules from './lib/substituteVariantsAtRules' import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' import substituteScreenAtRules from './lib/substituteScreenAtRules' import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' + +import generateUtilities from './util/generateUtilities' import processPlugins from './util/processPlugins' export default function(lazyConfig) { const config = lazyConfig() - const plugins = processPlugins(config) + const processedPlugins = processPlugins(config) + const utilities = generateUtilities(config, processedPlugins.utilities) return postcss([ - substituteTailwindAtRules(config, plugins), + substituteTailwindAtRules(config, processedPlugins, utilities.clone()), evaluateTailwindFunctions(config), - substituteVariantsAtRules(config, plugins), + substituteVariantsAtRules(config, processedPlugins), substituteResponsiveAtRules(config), substituteScreenAtRules(config), - substituteClassApplyAtRules(config), + substituteClassApplyAtRules(config, utilities.clone()), ]) } diff --git a/src/util/generateUtilities.js b/src/util/generateUtilities.js new file mode 100644 index 000000000..b2843ddc6 --- /dev/null +++ b/src/util/generateUtilities.js @@ -0,0 +1,24 @@ +import _ from 'lodash' +import postcss from 'postcss' +import utilityModules from '../utilityModules' +import prefixTree from '../util/prefixTree' +import generateModules from '../util/generateModules' + +export default function(config, pluginUtilities) { + const utilities = generateModules(utilityModules, config.modules, config) + + if (config.options.important) { + utilities.walkDecls(decl => (decl.important = true)) + } + + const tailwindUtilityTree = postcss.root({ + nodes: utilities.nodes, + }) + + prefixTree(tailwindUtilityTree, config.options.prefix) + + return _.tap(postcss.root(), root => { + root.append(tailwindUtilityTree.nodes) + root.append(pluginUtilities) + }) +}