diff --git a/CHANGELOG.md b/CHANGELOG.md index dc14695211ce..c1ba5950d0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` + `, + }, + }, + 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"`, + ) + } + }, +) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 87361804f7ae..691df2920a6e 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -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 @@ -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}\``) }, }) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index ea64fe3a1239..196115d6357e 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1223,6 +1223,7 @@ test('utilities used in @apply must be prefixed', async () => { await expect( compile( css` + @tailwind utilities; @config "./config.js"; .my-underline { @@ -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\`?]`, ) }) @@ -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]`, ) }) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 9c6df63fc3f6..e34faca42925 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -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` @@ -466,7 +548,7 @@ 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]`, ) }) @@ -474,13 +556,16 @@ describe('@apply', () => { 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.]`, ) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 6e493c3689d0..4907306eee84 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -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) { @@ -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 }) => { diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts index de426ab459fa..c60383de47e3 100644 --- a/packages/tailwindcss/src/prefix.test.ts +++ b/packages/tailwindcss/src/prefix.test.ts @@ -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\`?]`, ) }) diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index 2928afd3bd55..dc9c750a12f8 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -51,6 +51,10 @@ export class Theme { private keyframes = new Set([]), ) {} + get size() { + return this.values.size + } + add(key: string, value: string, options = ThemeOptions.NONE, src?: Declaration['src']): void { if (key.endsWith('-*')) { if (value !== 'initial') {