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.
This commit is contained in:
parent
89f0047c0d
commit
db9cbf7c08
3 changed files with 86 additions and 37 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue