Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that @utility is top-level and cannot be nested #14525

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
118 changes: 81 additions & 37 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -79,7 +79,8 @@ describe('compiling CSS', () => {
> }
}
]
`))
`)
})

test('`@tailwind utilities` is only processed once', async () => {
expect(
Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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 '';
Expand All @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -1694,7 +1699,7 @@ describe('plugins', () => {
})

test('@plugin options can only be simple key/value pairs', () => {
expect(
return expect(
compile(
css`
@plugin "my-plugin" {
Expand Down Expand Up @@ -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" {
Expand All @@ -1762,7 +1767,7 @@ describe('plugins', () => {
})

test('@plugin errors on array-like syntax', () => {
expect(
return expect(
compile(
css`
@plugin "my-plugin" {
Expand All @@ -1779,7 +1784,7 @@ describe('plugins', () => {
})

test('@plugin errors on object-like syntax', () => {
expect(
return expect(
compile(
css`
@plugin "my-plugin" {
Expand All @@ -1794,17 +1799,15 @@ describe('plugins', () => {
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`
).rejects.toThrowErrorMatchingInlineSnapshot(`
[Error: Unexpected \`@plugin\` option: Value of declaration \`--color: {
red: 100;
green: 200;
blue: 300;
};\` is not supported.

Using an object as a plugin option is currently only supported in JavaScript configuration files.]
`,
)
`)
})

test('addVariant with string selector', async () => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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`
Expand Down
4 changes: 4 additions & 0 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading