Skip to content

Commit 4bfacb3

Browse files
Improve error messages when @apply fails (#18059)
This PR improves error messages when `@apply` fails. Right now it gives you a generic error message that you cannot apply a certain utility. ```css .foo { @apply bg-red-500; } ``` Would result in: ``` Cannot apply unknown utility class: bg-red-500 ``` However, there are some situations where we can give you more context about what's happening. ### Missing `@import "tailwindcss"` or `@reference` If you are in a Vue file for example, and you have the following code: ```vue <template> <div class="foo"></div> </template> <style> .foo { @apply bg-red-500; } </style> ``` Then this will now result in: ``` Cannot apply unknown utility class `bg-white`. Are you using CSS modules or similar and missing `@reference`? https://tailwindcss.com/docs/functions-and-directives#reference-directive ``` We do this by checking if we found a `@tailwind utilities` or `@reference`. If not, we throw this more specific error. ### Explicitly excluded classes via `@source not inline('…')` Or via the legacy `blocklist` from a JS config. If you then have the following file: ```css @import "tailwindcss"; @source not inline('bg-white'); .foo { @apply bg-white; } ``` Then this will now result in: ``` Cannot apply utility class `bg-white` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes ``` We do this by checking if the class was marked as invalid. ### Applying unprefixed class in prefix mode If you have the prefix option configured, but you are applying a non-prefixed class, then we will show the following error: Given this input: ```css @import "tailwindcss" prefix(tw); .foo { @apply underline; } ``` The following error is thrown: ``` Cannot apply unprefixed utility class `underline`. Did you mean `tw:underline`? ``` ### Applying known utilities with unknown variants If you have unknown variants, then we will list them as well if the base utility does compile correctly. Given this input: ```css @import "tailwindcss"; .foo { @apply hocus:hover:pocus:bg-red-500; } ``` The following error is thrown: ``` Cannot apply utility class `hocus:hover:pocus:bg-red-500` because the `hocus` and `pocus` variants do not exist. ``` ## Test plan 1. Everything behaves the same, but the error messages give more details. 2. Updated tests with new error messages 3. Added new unit tests to verify the various scenarios 4. Added a Vue specific integration test with a `<style>…</style>` block using `@apply` [ci-all] There are some newlines here and there, let's verify that they work identically on all platforms. --------- Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
1 parent 58a6ad0 commit 4bfacb3

File tree

8 files changed

+244
-15
lines changed

8 files changed

+244
-15
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Improve error messages when `@apply` fails ([#18059](https://github.com/tailwindlabs/tailwindcss/pull/18059))
13+
1014
### Fixed
1115

1216
- Upgrade: Do not migrate declarations that look like candidates in `<style>` blocks ([#18057](https://github.com/tailwindlabs/tailwindcss/pull/18057), [18068](https://github.com/tailwindlabs/tailwindcss/pull/18068))

integrations/vite/vue.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { stripVTControlCharacters } from 'node:util'
12
import { candidate, html, json, test, ts } from '../utils'
23

34
test(
@@ -71,3 +72,75 @@ test(
7172
await fs.expectFileToContain(files[0][0], ['.bar{'])
7273
},
7374
)
75+
76+
test(
77+
'error when using `@apply` without `@reference`',
78+
{
79+
fs: {
80+
'package.json': json`
81+
{
82+
"type": "module",
83+
"dependencies": {
84+
"vue": "^3.4.37",
85+
"tailwindcss": "workspace:^"
86+
},
87+
"devDependencies": {
88+
"@vitejs/plugin-vue": "^5.1.2",
89+
"@tailwindcss/vite": "workspace:^",
90+
"vite": "^6"
91+
}
92+
}
93+
`,
94+
'vite.config.ts': ts`
95+
import { defineConfig } from 'vite'
96+
import vue from '@vitejs/plugin-vue'
97+
import tailwindcss from '@tailwindcss/vite'
98+
99+
export default defineConfig({
100+
plugins: [vue(), tailwindcss()],
101+
})
102+
`,
103+
'index.html': html`
104+
<!doctype html>
105+
<html>
106+
<body>
107+
<div id="app"></div>
108+
<script type="module" src="./src/main.ts"></script>
109+
</body>
110+
</html>
111+
`,
112+
'src/main.ts': ts`
113+
import { createApp } from 'vue'
114+
import App from './App.vue'
115+
116+
createApp(App).mount('#app')
117+
`,
118+
'src/App.vue': html`
119+
<template>
120+
<div class="foo">Hello Vue!</div>
121+
</template>
122+
123+
<style>
124+
.foo {
125+
@apply text-red-500;
126+
}
127+
</style>
128+
`,
129+
},
130+
},
131+
async ({ exec, expect }) => {
132+
expect.assertions(1)
133+
134+
try {
135+
await exec('pnpm vite build')
136+
} catch (error) {
137+
let [, message] =
138+
/error during build:([\s\S]*?)file:/g.exec(
139+
stripVTControlCharacters(error.message.replace(/\r?\n/g, '\n')),
140+
) ?? []
141+
expect(message.trim()).toMatchInlineSnapshot(
142+
`"[@tailwindcss/vite:generate:build] Cannot apply unknown utility class \`text-red-500\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive"`,
143+
)
144+
}
145+
},
146+
)

packages/tailwindcss/src/apply.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { compileCandidates } from './compile'
44
import type { DesignSystem } from './design-system'
55
import type { SourceLocation } from './source-maps/source'
66
import { DefaultMap } from './utils/default-map'
7+
import { segment } from './utils/segment'
78

89
export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
910
let features = Features.None
@@ -176,7 +177,68 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
176177
let candidates = Object.keys(candidateOffsets)
177178
let compiled = compileCandidates(candidates, designSystem, {
178179
onInvalidCandidate: (candidate) => {
179-
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
180+
// When using prefix, make sure prefix is used in candidate
181+
if (designSystem.theme.prefix && !candidate.startsWith(designSystem.theme.prefix)) {
182+
throw new Error(
183+
`Cannot apply unprefixed utility class \`${candidate}\`. Did you mean \`${designSystem.theme.prefix}:${candidate}\`?`,
184+
)
185+
}
186+
187+
// When the utility is blocklisted, let the user know
188+
//
189+
// Note: `@apply` is processed before handling incoming classes from
190+
// template files. This means that the `invalidCandidates` set will
191+
// only contain explicit classes via:
192+
//
193+
// - `blocklist` from a JS config
194+
// - `@source not inline(…)`
195+
if (designSystem.invalidCandidates.has(candidate)) {
196+
throw new Error(
197+
`Cannot apply utility class \`${candidate}\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes`,
198+
)
199+
}
200+
201+
// Verify if variants exist
202+
let parts = segment(candidate, ':')
203+
if (parts.length > 1) {
204+
let utility = parts.pop()!
205+
206+
// Ensure utility on its own compiles, if not, we will fallback to
207+
// the next error
208+
if (designSystem.candidatesToCss([utility])[0]) {
209+
let compiledVariants = designSystem.candidatesToCss(
210+
parts.map((variant) => `${variant}:[--tw-variant-check:1]`),
211+
)
212+
let unknownVariants = parts.filter((_, idx) => compiledVariants[idx] === null)
213+
if (unknownVariants.length > 0) {
214+
if (unknownVariants.length === 1) {
215+
throw new Error(
216+
`Cannot apply utility class \`${candidate}\` because the ${unknownVariants.map((variant) => `\`${variant}\``)} variant does not exist.`,
217+
)
218+
} else {
219+
let formatter = new Intl.ListFormat('en', {
220+
style: 'long',
221+
type: 'conjunction',
222+
})
223+
throw new Error(
224+
`Cannot apply utility class \`${candidate}\` because the ${formatter.format(unknownVariants.map((variant) => `\`${variant}\``))} variants do not exist.`,
225+
)
226+
}
227+
}
228+
}
229+
}
230+
231+
// When the theme is empty, it means that no theme was loaded and
232+
// `@import "tailwindcss"`, `@reference "app.css"` or similar is
233+
// very likely missing.
234+
if (designSystem.theme.size === 0) {
235+
throw new Error(
236+
`Cannot apply unknown utility class \`${candidate}\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive`,
237+
)
238+
}
239+
240+
// Fallback to most generic error message
241+
throw new Error(`Cannot apply unknown utility class \`${candidate}\``)
180242
},
181243
})
182244

packages/tailwindcss/src/compat/config.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,7 @@ test('utilities used in @apply must be prefixed', async () => {
12231223
await expect(
12241224
compile(
12251225
css`
1226+
@tailwind utilities;
12261227
@config "./config.js";
12271228
12281229
.my-underline {
@@ -1238,7 +1239,7 @@ test('utilities used in @apply must be prefixed', async () => {
12381239
},
12391240
),
12401241
).rejects.toThrowErrorMatchingInlineSnapshot(
1241-
`[Error: Cannot apply unknown utility class: underline]`,
1242+
`[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`,
12421243
)
12431244
})
12441245

@@ -1440,7 +1441,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => {
14401441
},
14411442
),
14421443
).rejects.toThrowErrorMatchingInlineSnapshot(
1443-
`[Error: Cannot apply unknown utility class: bg-white]`,
1444+
`[Error: Cannot apply utility class \`bg-white\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes]`,
14441445
)
14451446
})
14461447

packages/tailwindcss/src/index.test.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,88 @@ describe('@apply', () => {
286286
)
287287
})
288288

289+
it('@apply referencing theme values without `@tailwind utilities` or `@reference` should error', () => {
290+
return expect(() =>
291+
compileCss(css`
292+
.foo {
293+
@apply p-2;
294+
}
295+
`),
296+
).rejects.toThrowErrorMatchingInlineSnapshot(
297+
`[Error: Cannot apply unknown utility class \`p-2\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive]`,
298+
)
299+
})
300+
301+
it('@apply referencing theme values with `@tailwind utilities` should work', async () => {
302+
return expect(
303+
await compileCss(
304+
css`
305+
@import 'tailwindcss';
306+
307+
.foo {
308+
@apply p-2;
309+
}
310+
`,
311+
[],
312+
{
313+
async loadStylesheet() {
314+
return {
315+
path: '',
316+
base: '/',
317+
content: css`
318+
@theme {
319+
--spacing: 0.25rem;
320+
}
321+
@tailwind utilities;
322+
`,
323+
}
324+
},
325+
},
326+
),
327+
).toMatchInlineSnapshot(`
328+
":root, :host {
329+
--spacing: .25rem;
330+
}
331+
332+
.foo {
333+
padding: calc(var(--spacing) * 2);
334+
}"
335+
`)
336+
})
337+
338+
it('@apply referencing theme values with `@reference` should work', async () => {
339+
return expect(
340+
await compileCss(
341+
css`
342+
@reference "style.css";
343+
344+
.foo {
345+
@apply p-2;
346+
}
347+
`,
348+
[],
349+
{
350+
async loadStylesheet() {
351+
return {
352+
path: '',
353+
base: '/',
354+
content: css`
355+
@theme {
356+
--spacing: 0.25rem;
357+
}
358+
@tailwind utilities;
359+
`,
360+
}
361+
},
362+
},
363+
),
364+
).toMatchInlineSnapshot(`
365+
".foo {
366+
padding: calc(var(--spacing, .25rem) * 2);
367+
}"
368+
`)
369+
})
370+
289371
it('should replace @apply with the correct result', async () => {
290372
expect(
291373
await compileCss(css`
@@ -466,21 +548,24 @@ describe('@apply', () => {
466548
}
467549
`),
468550
).rejects.toThrowErrorMatchingInlineSnapshot(
469-
`[Error: Cannot apply unknown utility class: bg-not-found]`,
551+
`[Error: Cannot apply unknown utility class \`bg-not-found\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive]`,
470552
)
471553
})
472554

473555
it('should error when using @apply with a variant that does not exist', async () => {
474556
await expect(
475557
compile(css`
476558
@tailwind utilities;
559+
@theme {
560+
--color-red-500: red;
561+
}
477562
478563
.foo {
479-
@apply hocus:bg-red-500;
564+
@apply hocus:hover:pocus:bg-red-500;
480565
}
481566
`),
482567
).rejects.toThrowErrorMatchingInlineSnapshot(
483-
`[Error: Cannot apply unknown utility class: hocus:bg-red-500]`,
568+
`[Error: Cannot apply utility class \`hocus:hover:pocus:bg-red-500\` because the \`hocus\` and \`pocus\` variants do not exist.]`,
484569
)
485570
})
486571

packages/tailwindcss/src/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -624,14 +624,6 @@ async function parseCss(
624624
firstThemeRule.nodes = [context({ theme: true }, nodes)]
625625
}
626626

627-
// Replace the `@tailwind utilities` node with a context since it prints
628-
// children directly.
629-
if (utilitiesNode) {
630-
let node = utilitiesNode as AstNode as Context
631-
node.kind = 'context'
632-
node.context = {}
633-
}
634-
635627
// Replace the `@variant` at-rules with the actual variant rules.
636628
if (variantNodes.length > 0) {
637629
for (let variantNode of variantNodes) {
@@ -659,6 +651,14 @@ async function parseCss(
659651
features |= substituteFunctions(ast, designSystem)
660652
features |= substituteAtApply(ast, designSystem)
661653

654+
// Replace the `@tailwind utilities` node with a context since it prints
655+
// children directly.
656+
if (utilitiesNode) {
657+
let node = utilitiesNode as AstNode as Context
658+
node.kind = 'context'
659+
node.context = {}
660+
}
661+
662662
// Remove `@utility`, we couldn't replace it before yet because we had to
663663
// handle the nested `@apply` at-rules first.
664664
walk(ast, (node, { replaceWith }) => {

packages/tailwindcss/src/prefix.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ test('utilities used in @apply must be prefixed', async () => {
8989
}
9090
`),
9191
).rejects.toThrowErrorMatchingInlineSnapshot(
92-
`[Error: Cannot apply unknown utility class: underline]`,
92+
`[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`,
9393
)
9494
})
9595

packages/tailwindcss/src/theme.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export class Theme {
5151
private keyframes = new Set<AtRule>([]),
5252
) {}
5353

54+
get size() {
55+
return this.values.size
56+
}
57+
5458
add(key: string, value: string, options = ThemeOptions.NONE, src?: Declaration['src']): void {
5559
if (key.endsWith('-*')) {
5660
if (value !== 'initial') {

0 commit comments

Comments
 (0)