Error when @layer used without matching @tailwind directive (#4335)

Also refactor to only detect `@tailwind` directives once per build to improve performance.
This commit is contained in:
Adam Wathan 2021-05-12 16:58:03 -04:00 committed by GitHub
parent 87a4516871
commit 30dc2990c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 145 additions and 23 deletions

View File

@ -30,9 +30,9 @@ export default function (configOrPath = {}) {
})
}
rewriteTailwindImports(root)
let tailwindDirectives = rewriteTailwindImports(root)
let context = setupContext(configOrPath)(result, root)
let context = setupContext(configOrPath, tailwindDirectives)(result, root)
if (!env.TAILWIND_DISABLE_TOUCH) {
if (context.configPath !== null) {
@ -41,8 +41,8 @@ export default function (configOrPath = {}) {
}
return postcss([
removeLayerAtRules(context),
expandTailwindAtRules(context, registerDependency),
removeLayerAtRules(context, tailwindDirectives),
expandTailwindAtRules(context, registerDependency, tailwindDirectives),
expandApplyAtRules(context),
evaluateTailwindFunctions(context.tailwindConfig),
substituteScreenAtRules(context.tailwindConfig),

View File

@ -111,9 +111,12 @@ function buildStylesheet(rules, context) {
return returnValue
}
export default function expandTailwindAtRules(context, registerDependency) {
export default function expandTailwindAtRules(context, registerDependency, tailwindDirectives) {
return (root) => {
let foundTailwind = false
if (tailwindDirectives.size === 0) {
return root
}
let layerNodes = {
base: null,
components: null,
@ -126,8 +129,6 @@ export default function expandTailwindAtRules(context, registerDependency) {
// file as a dependency since the output of this CSS does not depend on
// the source of any templates. Think Vue <style> blocks for example.
root.walkAtRules('tailwind', (rule) => {
foundTailwind = true
if (rule.params === 'base') {
layerNodes.base = rule
}
@ -145,10 +146,6 @@ export default function expandTailwindAtRules(context, registerDependency) {
}
})
if (!foundTailwind) {
return root
}
// ---
if (sharedState.env.TAILWIND_DISABLE_TOUCH) {

View File

@ -1,7 +1,22 @@
export default function removeLayerAtRules() {
export default function removeLayerAtRules(_context, tailwindDirectives) {
return (root) => {
root.walkAtRules((rule) => {
if (['layer', 'responsive', 'variants'].includes(rule.name)) {
if (rule.name === 'layer' && ['base', 'components', 'utilities'].includes(rule.params)) {
if (!tailwindDirectives.has(rule.params)) {
throw rule.error(
`\`@layer ${rule.params}\` is used but no matching \`@tailwind ${rule.params}\` directive is present.`
)
}
rule.remove()
} else if (rule.name === 'responsive') {
if (!tailwindDirectives.has('utilities')) {
throw rule.error('`@responsive` is used but `@tailwind utilities` is missing.')
}
rule.remove()
} else if (rule.name === 'variants') {
if (!tailwindDirectives.has('utilities')) {
throw rule.error('`@variants` is used but `@tailwind utilities` is missing.')
}
rule.remove()
}
})

View File

@ -23,4 +23,12 @@ export default function rewriteTailwindImports(root) {
atRule.params = 'screens'
}
})
let tailwindDirectives = new Set()
root.walkAtRules('tailwind', (rule) => {
tailwindDirectives.add(rule.params)
})
return tailwindDirectives
}

View File

@ -314,6 +314,7 @@ function rebootWatcher(context) {
touch(context.configPath)
} else {
context.changedFiles.add(path.resolve('.', file))
console.log(context.touchFile)
touch(context.touchFile)
}
})
@ -686,14 +687,8 @@ function cleanupContext(context) {
// Retrieve an existing context from cache if possible (since contexts are unique per
// source path), or set up a new one (including setting up watchers and registering
// plugins) then return it
export default function setupContext(configOrPath) {
export default function setupContext(configOrPath, tailwindDirectives) {
return (result, root) => {
let foundTailwind = false
root.walkAtRules('tailwind', () => {
foundTailwind = true
})
let sourcePath = result.opts.from
let [
tailwindConfig,
@ -712,7 +707,7 @@ export default function setupContext(configOrPath) {
// We may want to think about `@layer` being part of this trigger too, but it's tough
// because it's impossible for a layer in one file to end up in the actual @tailwind rule
// in another file since independent sources are effectively isolated.
if (foundTailwind) {
if (tailwindDirectives.size > 0) {
contextDependencies.add(sourcePath)
for (let message of result.messages) {
if (message.type === 'dependency') {

View File

@ -0,0 +1,5 @@
@layer components {
.foo {
color: black;
}
}

View File

@ -0,0 +1 @@
<div class="foo"></div>

View File

@ -0,0 +1,101 @@
import postcss from 'postcss'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
const { currentTestName } = expect.getState()
return postcss(tailwind(config)).process(input, {
from: `${path.resolve(__filename)}?test=${currentTestName}`,
})
}
test('using @layer without @tailwind', async () => {
let config = {
purge: [path.resolve(__dirname, './layer-without-tailwind.test.html')],
mode: 'jit',
theme: {},
plugins: [],
}
let css = `
@layer components {
.foo {
color: black;
}
}
`
await expect(run(css, config)).rejects.toThrowError(
'`@layer components` is used but no matching `@tailwind components` directive is present.'
)
})
test('using @responsive without @tailwind', async () => {
let config = {
purge: [path.resolve(__dirname, './layer-without-tailwind.test.html')],
mode: 'jit',
theme: {},
plugins: [],
}
let css = `
@responsive {
.foo {
color: black;
}
}
`
await expect(run(css, config)).rejects.toThrowError(
'`@responsive` is used but `@tailwind utilities` is missing.'
)
})
test('using @variants without @tailwind', async () => {
let config = {
purge: [path.resolve(__dirname, './layer-without-tailwind.test.html')],
mode: 'jit',
theme: {},
plugins: [],
}
let css = `
@variants hover {
.foo {
color: black;
}
}
`
await expect(run(css, config)).rejects.toThrowError(
'`@variants` is used but `@tailwind utilities` is missing.'
)
})
test('non-Tailwind @layer rules are okay', async () => {
let config = {
purge: [path.resolve(__dirname, './layer-without-tailwind.test.html')],
mode: 'jit',
theme: {},
plugins: [],
}
let css = `
@layer custom {
.foo {
color: black;
}
}
`
return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
@layer custom {
.foo {
color: black;
}
}
`)
})
})

View File

@ -66,7 +66,7 @@ test('prefix', () => {
}
}
@tailwind utilities;
@layer utilites {
@layer utilities {
.custom-utility {
foo: bar;
}