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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions integrations/vite/vue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,73 @@
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(error.message) ?? []
expect(message.trim()).toMatchInlineSnapshot(`

Check failure on line 137 in integrations/vite/vue.test.ts

View workflow job for this annotation

GitHub Actions / Linux / vite

vite/vue.test.ts > error when using `@apply` without `@reference`

Error: Snapshot `error when using `@apply` without `@reference` 1` mismatched - Expected + Received - "[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: `text-red-500`. + "[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: `text-red-500`. - It looks like you are missing a `@reference "app.css"` or `@import "tailwindcss";`" + It looks like you are missing a `@reference "app.css"` or `@import "tailwindcss";`" ❯ vite/vue.test.ts:137:30 ❯ utils.ts:452:14

Check failure on line 137 in integrations/vite/vue.test.ts

View workflow job for this annotation

GitHub Actions / Linux / vite

vite/vue.test.ts > error when using `@apply` without `@reference`

Error: Snapshot `error when using `@apply` without `@reference` 2` mismatched - Expected + Received - "[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: `text-red-500`. + "[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: `text-red-500`. - It looks like you are missing a `@reference "app.css"` or `@import "tailwindcss";`" + It looks like you are missing a `@reference "app.css"` or `@import "tailwindcss";`" ❯ vite/vue.test.ts:137:30 ❯ utils.ts:452:14

Check failure on line 137 in integrations/vite/vue.test.ts

View workflow job for this annotation

GitHub Actions / Linux / vite

vite/vue.test.ts > error when using `@apply` without `@reference`

Error: Snapshot `error when using `@apply` without `@reference` 3` mismatched - Expected + Received - "[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: `text-red-500`. + "[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: `text-red-500`. - It looks like you are missing a `@reference "app.css"` or `@import "tailwindcss";`" + It looks like you are missing a `@reference "app.css"` or `@import "tailwindcss";`" ❯ vite/vue.test.ts:137:30 ❯ utils.ts:452:14
"[@tailwindcss/vite:generate:build] Cannot apply unknown utility class: \`text-red-500\`.
It looks like you are missing a \`@reference "app.css"\` or \`@import "tailwindcss";\`"
`)
}
},
)
67 changes: 57 additions & 10 deletions packages/tailwindcss/src/apply.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Features } from '.'
import { rule, toCss, walk, WalkAction, type AstNode } from './ast'
import { compileCandidates } from './compile'
import type { DesignSystem } from './design-system'
import type { SourceLocation } from './source-maps/source'
import { DefaultMap } from './utils/default-map'
import {Features} from '.'
import {rule, toCss, walk, WalkAction, type AstNode} from './ast'
import {compileCandidates} from './compile'
import type {DesignSystem} from './design-system'
import type {SourceLocation} from './source-maps/source'
import {DefaultMap} from './utils/default-map'

export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let features = Features.None
Expand All @@ -24,7 +24,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let definitions = new DefaultMap(() => new Set<AstNode>())

// Collect all new `@utility` definitions and all `@apply` rules first
walk([root], (node, { parent, path }) => {
walk([root], (node, {parent, path}) => {
if (node.kind !== 'at-rule') return

// Do not allow `@apply` rules inside `@keyframes` rules.
Expand Down Expand Up @@ -157,7 +157,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
for (let parent of sorted) {
if (!('nodes' in parent)) continue

walk(parent.nodes, (child, { replaceWith }) => {
walk(parent.nodes, (child, {replaceWith}) => {
if (child.kind !== 'at-rule' || child.name !== '@apply') return

let parts = child.params.split(/(\s+)/g)
Expand All @@ -176,7 +176,54 @@ 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 \`${candidate}\`, it seems like the utility was explicitly excluded and cannot be applied.\n\nMore info: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes`,
)
}

// Verify `@tailwind utilities` or `@reference` is used
let hasUtilitiesOrReference = false
walk(ast, (node, {context}) => {
// Find `@reference`
if (context.reference) {
hasUtilitiesOrReference = true
return WalkAction.Stop
}

// Find `@tailwind utilities`
else if (
node.kind === 'at-rule' &&
node.name === '@tailwind' &&
(node.params === 'utilities' || node.params.startsWith('utilities'))
) {
hasUtilitiesOrReference = true
return WalkAction.Stop
}
})
if (!hasUtilitiesOrReference) {
throw new Error(
`Cannot apply unknown utility class: \`${candidate}\`.\nIt looks like you are missing a \`@reference "app.css"\` or \`@import "tailwindcss";\``,
)
}

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

Expand Down Expand Up @@ -237,7 +284,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
}

function* resolveApplyDependencies(
node: Extract<AstNode, { kind: 'at-rule' }>,
node: Extract<AstNode, {kind: 'at-rule'}>,
designSystem: DesignSystem,
) {
for (let candidate of node.params.split(/\s+/g)) {
Expand Down
9 changes: 7 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,11 @@ test('blocklisted candidates cannot be used with `@apply`', async () => {
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: bg-white]`,
`
[Error: Cannot apply \`bg-white\`, it seems like the utility was explicitly excluded and cannot be applied.

More info: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes]
`,
)
})

Expand Down
89 changes: 87 additions & 2 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,91 @@ 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\`.
It looks like you are missing a \`@reference "app.css"\` or \`@import "tailwindcss";\` ]
`,
)
})

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,7 +551,7 @@ describe('@apply', () => {
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: bg-not-found]`,
`[Error: Cannot apply unknown utility class: \`bg-not-found\`]`,
)
})

Expand All @@ -480,7 +565,7 @@ describe('@apply', () => {
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: hocus:bg-red-500]`,
`[Error: Cannot apply unknown utility class: \`hocus:bg-red-500\`]`,
)
})

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
Loading