Switch to new apply implementation
This commit is contained in:
parent
998f3ffc03
commit
782a828c81
4 changed files with 4 additions and 410 deletions
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
6
__tests__/fixtures/tailwind-input-import.css
vendored
6
__tests__/fixtures/tailwind-input-import.css
vendored
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ const featureFlags = {
|
|||
'uniformColorPalette',
|
||||
'extendedSpacingScale',
|
||||
'extendedFontSizeScale',
|
||||
'applyComplexClasses',
|
||||
'darkModeVariant',
|
||||
'additionalBreakpoint',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue