Skip to content

Improve error messages when @apply fails #18059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 27, 2025
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Improve error messages when `@apply` fails ([#18059](https://github.com/tailwindlabs/tailwindcss/pull/18059))

### Fixed

- 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))
Expand Down
73 changes: 73 additions & 0 deletions integrations/vite/vue.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { stripVTControlCharacters } from 'node:util'
import { candidate, html, json, test, ts } from '../utils'

test(
Expand Down Expand Up @@ -71,3 +72,75 @@ test(
await fs.expectFileToContain(files[0][0], ['.bar{'])
},
)

test(
'error when using `@apply` without `@reference`',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"vue": "^3.4.37",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
"@tailwindcss/vite": "workspace:^",
"vite": "^6"
}
}
`,
'vite.config.ts': ts`
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
plugins: [vue(), tailwindcss()],
})
`,
'index.html': html`
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
`,
'src/main.ts': ts`
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
`,
'src/App.vue': html`
<template>
<div class="foo">Hello Vue!</div>
</template>

<style>
.foo {
@apply text-red-500;
}
</style>
`,
},
},
async ({ exec, expect }) => {
expect.assertions(1)

try {
await exec('pnpm vite build')
} catch (error) {
let [, message] =
/error during build:([\s\S]*?)file:/g.exec(
stripVTControlCharacters(error.message.replace(/\r?\n/g, '\n')),
) ?? []
expect(message.trim()).toMatchInlineSnapshot(
`"[@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"`,
)
}
},
)
64 changes: 63 additions & 1 deletion packages/tailwindcss/src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { compileCandidates } from './compile'
import type { DesignSystem } from './design-system'
import type { SourceLocation } from './source-maps/source'
import { DefaultMap } from './utils/default-map'
import { segment } from './utils/segment'

export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let features = Features.None
Expand Down Expand Up @@ -176,7 +177,68 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let candidates = Object.keys(candidateOffsets)
let compiled = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
// When using prefix, make sure prefix is used in candidate
if (designSystem.theme.prefix && !candidate.startsWith(designSystem.theme.prefix)) {
throw new Error(
`Cannot apply unprefixed utility class \`${candidate}\`. Did you mean \`${designSystem.theme.prefix}:${candidate}\`?`,
)
}

// When the utility is blocklisted, let the user know
//
// Note: `@apply` is processed before handling incoming classes from
// template files. This means that the `invalidCandidates` set will
// only contain explicit classes via:
//
// - `blocklist` from a JS config
// - `@source not inline(…)`
if (designSystem.invalidCandidates.has(candidate)) {
throw new Error(
`Cannot apply utility class \`${candidate}\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes`,
)
}

// Verify if variants exist
let parts = segment(candidate, ':')
if (parts.length > 1) {
let utility = parts.pop()!

// Ensure utility on its own compiles, if not, we will fallback to
// the next error
if (designSystem.candidatesToCss([utility])[0]) {
let compiledVariants = designSystem.candidatesToCss(
parts.map((variant) => `${variant}:[--tw-variant-check:1]`),
)
let unknownVariants = parts.filter((_, idx) => compiledVariants[idx] === null)
if (unknownVariants.length > 0) {
if (unknownVariants.length === 1) {
throw new Error(
`Cannot apply utility class \`${candidate}\` because the ${unknownVariants.map((variant) => `\`${variant}\``)} variant does not exist.`,
)
} else {
let formatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
})
throw new Error(
`Cannot apply utility class \`${candidate}\` because the ${formatter.format(unknownVariants.map((variant) => `\`${variant}\``))} variants do not exist.`,
)
}
}
}
}

// When the theme is empty, it means that no theme was loaded and
// `@import "tailwindcss"`, `@reference "app.css"` or similar is
// very likely missing.
if (designSystem.theme.size === 0) {
throw new Error(
`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`,
)
}

// Fallback to most generic error message
throw new Error(`Cannot apply unknown utility class \`${candidate}\``)
},
})

Expand Down
5 changes: 3 additions & 2 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ test('utilities used in @apply must be prefixed', async () => {
await expect(
compile(
css`
@tailwind utilities;
@config "./config.js";

.my-underline {
Expand All @@ -1238,7 +1239,7 @@ test('utilities used in @apply must be prefixed', async () => {
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: underline]`,
`[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`,
)
})

Expand Down Expand Up @@ -1440,7 +1441,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => {
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: bg-white]`,
`[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]`,
)
})

Expand Down
91 changes: 88 additions & 3 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,88 @@ describe('@apply', () => {
)
})

it('@apply referencing theme values without `@tailwind utilities` or `@reference` should error', () => {
return expect(() =>
compileCss(css`
.foo {
@apply p-2;
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[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]`,
)
})

it('@apply referencing theme values with `@tailwind utilities` should work', async () => {
return expect(
await compileCss(
css`
@import 'tailwindcss';

.foo {
@apply p-2;
}
`,
[],
{
async loadStylesheet() {
return {
path: '',
base: '/',
content: css`
@theme {
--spacing: 0.25rem;
}
@tailwind utilities;
`,
}
},
},
),
).toMatchInlineSnapshot(`
":root, :host {
--spacing: .25rem;
}

.foo {
padding: calc(var(--spacing) * 2);
}"
`)
})

it('@apply referencing theme values with `@reference` should work', async () => {
return expect(
await compileCss(
css`
@reference "style.css";

.foo {
@apply p-2;
}
`,
[],
{
async loadStylesheet() {
return {
path: '',
base: '/',
content: css`
@theme {
--spacing: 0.25rem;
}
@tailwind utilities;
`,
}
},
},
),
).toMatchInlineSnapshot(`
".foo {
padding: calc(var(--spacing, .25rem) * 2);
}"
`)
})

it('should replace @apply with the correct result', async () => {
expect(
await compileCss(css`
Expand Down Expand Up @@ -466,21 +548,24 @@ describe('@apply', () => {
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: bg-not-found]`,
`[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]`,
)
})

it('should error when using @apply with a variant that does not exist', async () => {
await expect(
compile(css`
@tailwind utilities;
@theme {
--color-red-500: red;
}

.foo {
@apply hocus:bg-red-500;
@apply hocus:hover:pocus:bg-red-500;
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: hocus:bg-red-500]`,
`[Error: Cannot apply utility class \`hocus:hover:pocus:bg-red-500\` because the \`hocus\` and \`pocus\` variants do not exist.]`,
)
})

Expand Down
16 changes: 8 additions & 8 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,14 +624,6 @@ async function parseCss(
firstThemeRule.nodes = [context({ theme: true }, nodes)]
}

// Replace the `@tailwind utilities` node with a context since it prints
// children directly.
if (utilitiesNode) {
let node = utilitiesNode as AstNode as Context
node.kind = 'context'
node.context = {}
}

// Replace the `@variant` at-rules with the actual variant rules.
if (variantNodes.length > 0) {
for (let variantNode of variantNodes) {
Expand Down Expand Up @@ -659,6 +651,14 @@ async function parseCss(
features |= substituteFunctions(ast, designSystem)
features |= substituteAtApply(ast, designSystem)

// Replace the `@tailwind utilities` node with a context since it prints
// children directly.
if (utilitiesNode) {
let node = utilitiesNode as AstNode as Context
node.kind = 'context'
node.context = {}
}

// Remove `@utility`, we couldn't replace it before yet because we had to
// handle the nested `@apply` at-rules first.
walk(ast, (node, { replaceWith }) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ test('utilities used in @apply must be prefixed', async () => {
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: underline]`,
`[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`,
)
})

Expand Down
4 changes: 4 additions & 0 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export class Theme {
private keyframes = new Set<AtRule>([]),
) {}

get size() {
return this.values.size
}

add(key: string, value: string, options = ThemeOptions.NONE, src?: Declaration['src']): void {
if (key.endsWith('-*')) {
if (value !== 'initial') {
Expand Down