Merge pull request #516 from tailwindcss/shadow-table
[Experiment] Allow `@apply`-ing utility classes that aren't explicitly defined but would be generated
This commit is contained in:
commit
dd6fa7c4eb
5 changed files with 92 additions and 46 deletions
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
])
|
||||
}
|
||||
|
|
|
|||
24
src/util/generateUtilities.js
Normal file
24
src/util/generateUtilities.js
Normal file
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue