Switch to new apply implementation

This commit is contained in:
Adam Wathan 2020-10-17 21:00:42 -04:00
parent 998f3ffc03
commit 782a828c81
4 changed files with 4 additions and 410 deletions

View file

@ -1,252 +0,0 @@
import postcss from 'postcss'
import tailwind from '../src/index'
function run(input, config = {}) {
return postcss([tailwind({ ...config })]).process(input, { from: undefined })
}
test('it copies the declarations from a class into itself', () => {
const output = '.a { color: red; } .b { color: red; }'
return run('.a { color: red; } .b { @apply .a; }').then((result) => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('selectors with invalid characters do not need to be manually escaped', () => {
const input = `
.a\\:1\\/2 { color: red; }
.b { @apply .a:1/2; }
`
const expected = `
.a\\:1\\/2 { color: red; }
.b { color: red; }
`
return run(input).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('it removes important from applied classes by default', () => {
const input = `
.a { color: red !important; }
.b { @apply .a; }
`
const expected = `
.a { color: red !important; }
.b { color: red; }
`
return run(input).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('applied rules can be made !important', () => {
const input = `
.a { color: red; }
.b { @apply .a !important; }
`
const expected = `
.a { color: red; }
.b { color: red !important; }
`
return run(input).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('cssnext custom property sets are preserved', () => {
const input = `
.a {
color: red;
}
.b {
@apply .a --custom-property-set;
}
`
const expected = `
.a {
color: red;
}
.b {
color: red;
@apply --custom-property-set;
}
`
return run(input).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('it fails if the class does not exist', () => {
return run('.b { @apply .a; }').catch((e) => {
expect(e).toMatchObject({ name: 'CssSyntaxError' })
})
})
test('applying classes that are defined in a media query is not supported', () => {
const input = `
@media (min-width: 300px) {
.a { color: blue; }
}
.b {
@apply .a;
}
`
expect.assertions(1)
return run(input).catch((e) => {
expect(e).toMatchObject({ name: 'CssSyntaxError' })
})
})
test('applying classes that are ever used in a media query is not supported', () => {
const input = `
.a {
color: red;
}
@media (min-width: 300px) {
.a { color: blue; }
}
.b {
@apply .a;
}
`
expect.assertions(1)
return run(input).catch((e) => {
expect(e).toMatchObject({ name: 'CssSyntaxError' })
})
})
test('it does not match classes that include pseudo-selectors', () => {
const input = `
.a:hover {
color: red;
}
.b {
@apply .a;
}
`
expect.assertions(1)
return run(input).catch((e) => {
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).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('you can apply utility classes without using the given prefix', () => {
const input = `
.foo { @apply .tw-mt-4 .mb-4; }
`
const expected = `
.foo { margin-top: 1rem; margin-bottom: 1rem; }
`
return run(input, { prefix: 'tw-' }).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('you can apply utility classes without using the given prefix when using a function for the prefix', () => {
const input = `
.foo { @apply .tw-mt-4 .mb-4; }
`
const expected = `
.foo { margin-top: 1rem; margin-bottom: 1rem; }
`
return run(input, {
prefix: () => {
return 'tw-'
},
}).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('you can apply utility classes without specificity prefix even if important (selector) is used', () => {
const input = `
.foo { @apply .mt-8 .mb-8; }
`
const expected = `
.foo { margin-top: 2rem; margin-bottom: 2rem; }
`
return run(input, { important: '#app' }).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('you can apply utility classes without using the given prefix even if important (selector) is used', () => {
const input = `
.foo { @apply .tw-mt-4 .mb-4; }
`
const expected = `
.foo { margin-top: 1rem; margin-bottom: 1rem; }
`
return run(input, {
prefix: 'tw-',
important: '#app',
}).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

View file

@ -1,12 +1,12 @@
@import "tailwindcss/base";
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import "tailwindcss/utilities";
@import 'tailwindcss/utilities';
@responsive {
.example {
@apply .font-bold;
@apply font-bold;
color: theme('colors.red.500');
}
}

View file

@ -8,7 +8,6 @@ const featureFlags = {
'uniformColorPalette',
'extendedSpacingScale',
'extendedFontSizeScale',
'applyComplexClasses',
'darkModeVariant',
'additionalBreakpoint',
],

View file

@ -1,158 +1,5 @@
import _ from 'lodash'
import postcss from 'postcss'
import escapeClassName from '../util/escapeClassName'
import prefixSelector from '../util/prefixSelector'
import increaseSpecificity from '../util/increaseSpecificity'
import { flagEnabled } from '../featureFlags'
import applyComplexClasses from '../flagged/applyComplexClasses'
function buildClassTable(css) {
const classTable = {}
css.walkRules((rule) => {
if (!_.has(classTable, rule.selector)) {
classTable[rule.selector] = []
}
classTable[rule.selector].push(rule)
})
return classTable
}
function buildShadowTable(generatedUtilities) {
const utilities = postcss.root()
postcss.root({ nodes: generatedUtilities }).walkAtRules('variants', (atRule) => {
utilities.append(atRule.clone().nodes)
})
return buildClassTable(utilities)
}
function normalizeClassName(className) {
return `.${escapeClassName(_.trimStart(className, '.'))}`
}
function findClass(classToApply, classTable, onError) {
const matches = _.get(classTable, classToApply, [])
if (_.isEmpty(matches)) {
return []
}
if (matches.length > 1) {
// prettier-ignore
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
throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is nested inside of an at-rule (@${match.parent.name}).`)
}
return match.clone().nodes
}
let shadowLookup = null
export default function (config, getProcessedPlugins, configChanged) {
if (flagEnabled(config, 'applyComplexClasses')) {
return applyComplexClasses(config, getProcessedPlugins, configChanged)
}
return function (css) {
const classLookup = buildClassTable(css)
shadowLookup =
configChanged || !shadowLookup
? buildShadowTable(getProcessedPlugins().utilities)
: shadowLookup
css.walkRules((rule) => {
rule.walkAtRules('apply', (atRule) => {
const classesAndProperties = postcss.list.space(atRule.params)
/*
* Don't wreck CSSNext-style @apply rules:
* http://cssnext.io/features/#custom-properties-set-apply
*
* These are deprecated in CSSNext but still playing it safe for now.
* We might consider renaming this at-rule.
*/
const [customProperties, classes] = _.partition(classesAndProperties, (classOrProperty) => {
return _.startsWith(classOrProperty, '--')
})
const decls = _(classes)
.reject((cssClass) => cssClass === '!important')
.flatMap((cssClass) => {
const classToApply = normalizeClassName(cssClass)
const onError = (message) => {
return atRule.error(message)
}
return _.reduce(
[
// Find exact class match in user's CSS
() => {
return findClass(classToApply, classLookup, onError)
},
// Find exact class match in shadow lookup
() => {
return findClass(classToApply, shadowLookup, onError)
},
// Find prefixed version of class in shadow lookup
() => {
return findClass(
prefixSelector(config.prefix, classToApply),
shadowLookup,
onError
)
},
// Find important-scoped version of class in shadow lookup
() => {
return findClass(
increaseSpecificity(config.important, classToApply),
shadowLookup,
onError
)
},
// Find important-scoped and prefixed version of class in shadow lookup
() => {
return findClass(
increaseSpecificity(
config.important,
prefixSelector(config.prefix, classToApply)
),
shadowLookup,
onError
)
},
() => {
// prettier-ignore
throw onError(`\`@apply\` cannot be used with \`${classToApply}\` because \`${classToApply}\` either cannot be found, or its 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.`)
},
],
(classDecls, candidate) => (!_.isEmpty(classDecls) ? classDecls : candidate()),
[]
)
})
.value()
_.tap(_.last(classesAndProperties) === '!important', (important) => {
decls.forEach((decl) => (decl.important = important))
})
atRule.before(decls)
atRule.params = customProperties.join(' ')
if (_.isEmpty(customProperties)) {
atRule.remove()
}
})
})
}
return applyComplexClasses(config, getProcessedPlugins, configChanged)
}