Skip to content

Commit

Permalink
Ensure that @utility is top-level and cannot be nested (#14525)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
RobinMalfait authored Sep 26, 2024
1 parent 89f0047 commit db9cbf7
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 37 deletions.
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

0 comments on commit db9cbf7

Please sign in to comment.