From 0e789f3f83f78e52b795080d0a0d783e32768a4f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 16 May 2025 13:41:48 +0200 Subject: [PATCH 01/11] improve `@apply` error messages --- packages/tailwindcss/src/apply.ts | 49 ++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 87361804f7ae..b587ff5de82c 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -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}\``) }, }) From f9d13b7a589eb15e0f1148cbe5fef193ebe3d866 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 16 May 2025 13:42:17 +0200 Subject: [PATCH 02/11] add and update tests --- .../tailwindcss/src/compat/config.test.ts | 9 +- packages/tailwindcss/src/index.test.ts | 89 ++++++++++++++++++- packages/tailwindcss/src/prefix.test.ts | 2 +- 3 files changed, 95 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index ea64fe3a1239..b28950dc28bd 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,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] + `, ) }) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 9c6df63fc3f6..0b9a5d5433e3 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -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` @@ -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\`]`, ) }) @@ -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\`]`, ) }) diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts index de426ab459fa..32725f21074e 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\`?]`, ) }) From 16d4ea5d011ca7e94922345cc565e00eb4124faa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 16 May 2025 13:42:26 +0200 Subject: [PATCH 03/11] delay making `@tailwind utilities` a context node If we do this _before_ handling `@apply`, then we won't be able to find a `@tailwind utilities` at-rule. --- packages/tailwindcss/src/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 }) => { From b35f713366a98f7b11f9a999f870060d68cdde1a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 16 May 2025 14:40:41 +0200 Subject: [PATCH 04/11] add `@apply` integration tests for Vue ` + `, + }, + }, + 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(` + "[@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";\`" + `) + } + }, +) From 6fbb4318432599a71ebd7df8aa37cdf0483c278e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 26 May 2025 15:36:59 +0200 Subject: [PATCH 05/11] normalize whitespace --- integrations/vite/vue.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts index d2766e1cd9de..2c3d56f4dd6b 100644 --- a/integrations/vite/vue.test.ts +++ b/integrations/vite/vue.test.ts @@ -133,7 +133,8 @@ test( try { await exec('pnpm vite build') } catch (error) { - let [, message] = /error during build:([\s\S]*?)file:/g.exec(error.message) ?? [] + let [, message] = + /error during build:([\s\S]*?)file:/g.exec(error.message.replace(/\r?\n/g, '\n')) ?? [] expect(message.trim()).toMatchInlineSnapshot(` "[@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";\`" From 11adcbe0579c0046ad28cdd53cf7369f3c886431 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 26 May 2025 15:45:27 +0200 Subject: [PATCH 06/11] are there VT control characters? Who knows! --- integrations/vite/vue.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts index 2c3d56f4dd6b..cec0181bfb47 100644 --- a/integrations/vite/vue.test.ts +++ b/integrations/vite/vue.test.ts @@ -1,3 +1,4 @@ +import { stripVTControlCharacters } from 'node:util' import { candidate, html, json, test, ts } from '../utils' test( @@ -134,7 +135,9 @@ test( await exec('pnpm vite build') } catch (error) { let [, message] = - /error during build:([\s\S]*?)file:/g.exec(error.message.replace(/\r?\n/g, '\n')) ?? [] + /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\`. It looks like you are missing a \`@reference "app.css"\` or \`@import "tailwindcss";\`" From 75bdb44cbde06d91f3a10b2ae81b257d761e9d7f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 26 May 2025 16:30:58 +0200 Subject: [PATCH 07/11] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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 `