Skip to content
Open
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 @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Correctly handle duplicate CLI arguments ([#19416](https://github.com/tailwindlabs/tailwindcss/pull/19416))
- Don’t emit color-mix fallback rules inside `@keyframes` ([#19419](https://github.com/tailwindlabs/tailwindcss/pull/19419))
- CLI: Don't hang when output is `/dev/stdout` ([#19421](https://github.com/tailwindlabs/tailwindcss/pull/19421))
- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427))

### Added

Expand Down
41 changes: 40 additions & 1 deletion packages/tailwindcss/src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,52 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {

let parts = child.params.split(/(\s+)/g)
let candidateOffsets: Record<string, number> = {}
let normalIdents: string[] = []
let dashedIdents: string[] = []

let offset = 0
for (let [idx, part] of parts.entries()) {
if (idx % 2 === 0) candidateOffsets[part] = offset
if (idx % 2 === 0) {
if (part[0] === '-' && part[1] === '-') {
dashedIdents.push(part)
} else {
normalIdents.push(part)
}

candidateOffsets[part] = offset
}

offset += part.length
}

if (dashedIdents.length) {
// If we have an `@apply` that only consists of dashed idents then the
// user is intending to use a CSS mixin:
// https://drafts.csswg.org/css-mixins-1/#apply-rule
//
// These are not considered utilities and need to be emitted literally.
if (normalIdents.length === 0) return WalkAction.Skip

// If we find a dashed ident *here* it means that someone is trying
// to use mixins and our `@apply` behavior together.
//
// This is invalid and the rules must be written separately. Let the
// user know they need to move them into a separate rule.
let list = dashedIdents.join(' ')

throw new Error(
`You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply ${list}\` into a separate rule.`,
)
}

let hasBody = child.nodes.length > 0

if (hasBody && normalIdents.length) {
let list = normalIdents.join(' ')

throw new Error(`The rule \`@apply ${list}\` must not have a body.`)
}

// Replace the `@apply` rule with the actual utility classes
{
// Parse the candidates to an AST that we can replace the `@apply` rule
Expand Down
3 changes: 2 additions & 1 deletion packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@ export function optimizeAst(
copy.name === '@charset' ||
copy.name === '@custom-media' ||
copy.name === '@namespace' ||
copy.name === '@import'
copy.name === '@import' ||
copy.name === '@apply'
) {
parent.push(copy)
}
Expand Down
68 changes: 68 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,74 @@ describe('@apply', () => {
}"
`)
})

it('should be usable with CSS mixins', async () => {
let compiler = await compile(css`
.foo {
/* Utility usage */
@apply underline;

/* CSS mixin usage */
@apply --my-mixin-1;
@apply --my-mixin-1();
@apply --my-mixin-1 --my-mixin-2;
@apply --my-mixin-1() --my-mixin-2();
@apply --my-mixin-3 {
color: red;
}
}
`)

expect(compiler.build([])).toMatchInlineSnapshot(`
".foo {
text-decoration-line: underline;
@apply --my-mixin-1;
@apply --my-mixin-1();
@apply --my-mixin-1 --my-mixin-2;
@apply --my-mixin-1() --my-mixin-2();
@apply --my-mixin-3 {
color: red;
}
}
"
`)
})

it('should error when trying to use mixins and utilities together', async () => {
await expect(
compile(css`
.foo {
@apply underline --my-mixin-1;
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`,
)

await expect(
compile(css`
.foo {
@apply --my-mixin-1 underline;
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`,
)
})

it('should error when used with a body', async () => {
await expect(
compile(css`
.foo {
@apply underline {
color: red;
}
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The rule \`@apply underline\` must not have a body.]`,
)
})
})

describe('arbitrary variants', () => {
Expand Down