From db9cbf7c08365d63488b33f7b74d8fbb48cfffc9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 26 Sep 2024 17:53:41 +0200 Subject: [PATCH] Ensure that `@utility` is top-level and cannot be nested (#14525) This PR fixes an issue where we expect the `@utility` to be top-level, but we didn't enforce it. This PR enforces that the `@utility` is top-level. --- CHANGELOG.md | 1 + packages/tailwindcss/src/index.test.ts | 118 +++++++++++++++++-------- packages/tailwindcss/src/index.ts | 4 + 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 472341631f71..f3d1627c9bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514)) - _Experimental_: Do not wrap comment nodes in `@layer` when running codemods ([#14517](https://github.com/tailwindlabs/tailwindcss/pull/14517)) - _Experimental_: Ensure we don't lose selectors when running codemods ([#14518](https://github.com/tailwindlabs/tailwindcss/pull/14518)) +- Ensure that `@utility` is top-level and cannot be nested ([#14525](https://github.com/tailwindlabs/tailwindcss/pull/14525)) ## [4.0.0-alpha.25] - 2024-09-24 diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 1305b9415fe6..06a81b2774d2 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -56,8 +56,8 @@ describe('compiling CSS', () => { `) }) - test('that only CSS variables are allowed', () => - expect( + test('that only CSS variables are allowed', () => { + return expect( compileCss( css` @theme { @@ -79,7 +79,8 @@ describe('compiling CSS', () => { > } } ] - `)) + `) + }) test('`@tailwind utilities` is only processed once', async () => { expect( @@ -290,7 +291,7 @@ describe('@apply', () => { }) it('should error when using @apply with a utility that does not exist', () => { - expect( + return expect( compileCss(css` @tailwind utilities; @@ -304,7 +305,7 @@ describe('@apply', () => { }) it('should error when using @apply with a variant that does not exist', () => { - expect( + return expect( compileCss(css` @tailwind utilities; @@ -1184,8 +1185,8 @@ describe('Parsing themes values from CSS', () => { `) }) - test('`@media theme(…)` can only contain `@theme` rules', () => - expect( + test('`@media theme(…)` can only contain `@theme` rules', () => { + return expect( compileCss( css` @media theme(reference) { @@ -1199,7 +1200,8 @@ describe('Parsing themes values from CSS', () => { ), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Files imported with \`@import "…" theme(…)\` must only contain \`@theme\` blocks.]`, - )) + ) + }) test('theme values added as `inline` are not wrapped in `var(…)` when used as utility values', async () => { expect( @@ -1550,8 +1552,8 @@ describe('Parsing themes values from CSS', () => { }) describe('plugins', () => { - test('@plugin need a path', () => - expect( + test('@plugin need a path', () => { + return expect( compile( css` @plugin; @@ -1565,10 +1567,11 @@ describe('plugins', () => { }), }, ), - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`)) + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`) + }) - test('@plugin can not have an empty path', () => - expect( + test('@plugin can not have an empty path', () => { + return expect( compile( css` @plugin ''; @@ -1582,10 +1585,11 @@ describe('plugins', () => { }), }, ), - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`)) + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`) + }) - test('@plugin cannot be nested.', () => - expect( + test('@plugin cannot be nested.', () => { + return expect( compile( css` div { @@ -1601,7 +1605,8 @@ describe('plugins', () => { }), }, ), - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` cannot be nested.]`)) + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` cannot be nested.]`) + }) test('@plugin can accept options', async () => { expect.hasAssertions() @@ -1694,7 +1699,7 @@ describe('plugins', () => { }) test('@plugin options can only be simple key/value pairs', () => { - expect( + return expect( compile( css` @plugin "my-plugin" { @@ -1736,7 +1741,7 @@ describe('plugins', () => { }) test('@plugin options can only be provided to plugins using withOptions', () => { - expect( + return expect( compile( css` @plugin "my-plugin" { @@ -1762,7 +1767,7 @@ describe('plugins', () => { }) test('@plugin errors on array-like syntax', () => { - expect( + return expect( compile( css` @plugin "my-plugin" { @@ -1779,7 +1784,7 @@ describe('plugins', () => { }) test('@plugin errors on object-like syntax', () => { - expect( + return expect( compile( css` @plugin "my-plugin" { @@ -1794,8 +1799,7 @@ describe('plugins', () => { loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, ), - ).rejects.toThrowErrorMatchingInlineSnapshot( - ` + ).rejects.toThrowErrorMatchingInlineSnapshot(` [Error: Unexpected \`@plugin\` option: Value of declaration \`--color: { red: 100; green: 200; @@ -1803,8 +1807,7 @@ describe('plugins', () => { };\` is not supported. Using an object as a plugin option is currently only supported in JavaScript configuration files.] - `, - ) + `) }) test('addVariant with string selector', async () => { @@ -2066,36 +2069,39 @@ describe('@source', () => { }) describe('@variant', () => { - test('@variant must be top-level and cannot be nested', () => - expect( + test('@variant must be top-level and cannot be nested', () => { + return expect( compileCss(css` .foo { @variant hocus (&:hover, &:focus); } `), - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@variant\` cannot be nested.]`)) + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@variant\` cannot be nested.]`) + }) - test('@variant with no body must include a selector', () => - expect( + test('@variant with no body must include a selector', () => { + return expect( compileCss(css` @variant hocus; `), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Error: `@variant hocus` has no selector or body.]', - )) + ) + }) - test('@variant with selector must include a body', () => - expect( + test('@variant with selector must include a body', () => { + return expect( compileCss(css` @variant hocus { } `), ).rejects.toThrowErrorMatchingInlineSnapshot( '[Error: `@variant hocus` has no selector or body.]', - )) + ) + }) - test('@variant cannot have both a selector and a body', () => - expect( + test('@variant cannot have both a selector and a body', () => { + return expect( compileCss(css` @variant hocus (&:hover, &:focus) { &:is(.potato) { @@ -2105,7 +2111,8 @@ describe('@variant', () => { `), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: \`@variant hocus\` cannot have both a selector and a body.]`, - )) + ) + }) describe('body-less syntax', () => { test('selector variant', async () => { @@ -2573,6 +2580,43 @@ describe('@variant', () => { }) }) +describe('@utility', () => { + test('@utility must be top-level and cannot be nested', () => { + return expect( + compileCss(css` + .foo { + @utility foo { + color: red; + } + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@utility\` cannot be nested.]`) + }) + + test('@utility must include a body', () => { + return expect( + compileCss(css` + @utility foo { + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: \`@utility foo\` is empty. Utilities should include at least one property.]`, + ) + }) + + test('@utility cannot contain any special characters', () => { + return expect( + compileCss(css` + @utility 💨 { + color: red; + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: \`@utility 💨\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.]`, + ) + }) +}) + test('addBase', async () => { let { build } = await compile( css` diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f3c7ee674370..79c0d46e62a6 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -88,6 +88,10 @@ async function parseCss( // Collect custom `@utility` at-rules if (node.selector.startsWith('@utility ')) { + if (parent !== null) { + throw new Error('`@utility` cannot be nested.') + } + let name = node.selector.slice(9).trim() if (!IS_VALID_UTILITY_NAME.test(name)) {