Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
46 changes: 45 additions & 1 deletion packages/tailwindcss/src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,57 @@ 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.`,
)
}

// If we find a dashed ident *here* it means that someone is trying
// to use mixins and our `@apply` behavior together.
//
// This is considered invalid usage and we want to inform the user
// as such.
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