diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 4e35afa5a..17cf8c724 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -4444,6 +4444,43 @@ describe('addComponents()', () => { }" `) }) + + test('throws on custom static utilities with an invalid name', async () => { + await expect(() => { + return compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + + @theme reference { + --breakpoint-lg: 1024px; + } + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ addComponents }: PluginAPI) => { + addComponents({ + ':hover > *': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, + } + }, + }, + ) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: \`addComponents({ ':hover > *': … })\` defines an invalid utility selector. Components must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`. + + Note: in Tailwind CSS v4 \`addComponents\` is an alias for \`addUtilities\`.] + `) + }) }) describe('matchComponents()', () => { @@ -4490,6 +4527,37 @@ describe('matchComponents()', () => { }" `) }) + + test('throws on custom utilities with an invalid name', async () => { + await expect(() => { + return compile( + css` + @plugin "my-plugin"; + @tailwind utilities; + `, + { + async loadModule(id, base) { + return { + path: '', + base, + module: ({ matchComponents }: PluginAPI) => { + matchComponents({ + '.text-trim > *': () => ({ + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }), + }) + }, + } + }, + }, + ) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: \`matchComponents({ '.text-trim > *': … })\` defines an invalid utility name. Components should be alphanumeric and start with a lowercase letter, eg. \`scrollbar\`. + + Note: in Tailwind CSS v4 \`matchComponents\` is an alias for \`matchUtilities\`.] + `) + }) }) describe('prefix()', () => { diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index c52693ef0..b526260e6 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -307,7 +307,7 @@ export function buildPluginApi({ if (!foundValidUtility) { throw new Error( - `\`addUtilities({ '${name}' : … })\` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`.`, + `\`addUtilities({ '${name}': … })\` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`.`, ) } } @@ -347,7 +347,7 @@ export function buildPluginApi({ for (let [name, fn] of Object.entries(utilities)) { if (!IS_VALID_UTILITY_NAME.test(name)) { throw new Error( - `\`matchUtilities({ '${name}' : … })\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter, eg. \`scrollbar\`.`, + `\`matchUtilities({ '${name}': … })\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter, eg. \`scrollbar\`.`, ) } @@ -493,11 +493,31 @@ export function buildPluginApi({ }, addComponents(components, options) { - this.addUtilities(components, options) + try { + this.addUtilities(components, options) + } catch (e) { + if (e instanceof Error) { + throw new Error( + `${e.message.replaceAll('addUtilities', 'addComponents').replaceAll('Utilities', 'Components')}\n\nNote: in Tailwind CSS v4 \`addComponents\` is an alias for \`addUtilities\`.`, + ) + } else { + throw e + } + } }, matchComponents(components, options) { - this.matchUtilities(components, options) + try { + this.matchUtilities(components, options) + } catch (e) { + if (e instanceof Error) { + throw new Error( + `${e.message.replaceAll('matchUtilities', 'matchComponents').replaceAll('Utilities', 'Components')}\n\nNote: in Tailwind CSS v4 \`matchComponents\` is an alias for \`matchUtilities\`.`, + ) + } else { + throw e + } + } }, theme: createThemeFn(