Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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