Don't consider the global important state in @apply (#18404)

Fixes #18400

In v3 when you used `important: true` it did not affect `@apply`.
However, in v4 it does and there's no way to make it *not*. This is
definitely a bug and would be unexpected for users coming from v3 who
use `@apply` and `important` together.

Basically, the following code, along with the detected utility `flex` in
source files…

```css
@import 'tailwindcss/utilities' important;
.flex-explicitly-important {
  @apply flex!;
}
.flex-not-important {
  @apply flex;
}
```

… would output this:
```css
.flex {
  display: flex !important;
}
.flex-explicitly-important  {
  display: flex !important;
}
.flex-not-important {
  display: flex !important;
}
```

But it's expected that `@apply` doesn't consider the "global" important
state. This PR addresss this problem and now the output is this:

```css
.flex {
  display: flex !important;
}
.flex-explicitly-important  {
  display: flex !important;
}
.flex-not-important {
  display: flex; /* this line changed */
}
```

If you want to mark a utility as important in `@apply` you can still use
`!` after the utility to do so as shown above.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Jordan Pittman 2025-06-27 11:55:15 -04:00 • committed by GitHub
parent b716d10129
commit 2030e942bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 30 deletions

View file

@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Nothing yet!
### Fixed
- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404))
## [4.1.11] - 2025-06-26

View file

@ -2,7 +2,7 @@ import { substituteAtApply } from '../../../../tailwindcss/src/apply'
import { atRule, styleRule, toCss, walk, type AstNode } from '../../../../tailwindcss/src/ast'
import { printArbitraryValue } from '../../../../tailwindcss/src/candidate'
import * as SelectorParser from '../../../../tailwindcss/src/compat/selector-parser'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { CompileAstFlags, type DesignSystem } from '../../../../tailwindcss/src/design-system'
import { ThemeOptions } from '../../../../tailwindcss/src/theme'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { isValidSpacingMultiplier } from '../../../../tailwindcss/src/utils/infer-data-type'
@ -40,7 +40,16 @@ export const computeUtilitySignature = new DefaultMap<
// Use `@apply` to normalize the selector to `.x`
let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])]
temporarilyDisableThemeInline(designSystem, () => substituteAtApply(ast, designSystem))
temporarilyDisableThemeInline(designSystem, () => {
// There's separate utility caches for respect important vs not
// so we want to compile them both with `@theme inline` disabled
for (let candidate of designSystem.parseCandidate(utility)) {
designSystem.compileAstNodes(candidate, CompileAstFlags.None)
designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant)
}
substituteAtApply(ast, designSystem)
})
// We will be mutating the AST, so we need to clone it first to not affect
// the original AST

View file

@ -176,6 +176,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
// with.
let candidates = Object.keys(candidateOffsets)
let compiled = compileCandidates(candidates, designSystem, {
respectImportant: false,
onInvalidCandidate: (candidate) => {
// When using prefix, make sure prefix is used in candidate
if (designSystem.theme.prefix && !candidate.startsWith(designSystem.theme.prefix)) {

View file

@ -9,7 +9,7 @@ import {
type StyleRule,
} from './ast'
import { type Candidate, type Variant } from './candidate'
import { type DesignSystem } from './design-system'
import { CompileAstFlags, type DesignSystem } from './design-system'
import GLOBAL_PROPERTY_ORDER from './property-order'
import { asColor, type Utility } from './utilities'
import { compare } from './utils/compare'
@ -19,7 +19,10 @@ import type { Variants } from './variants'
export function compileCandidates(
rawCandidates: Iterable<string>,
designSystem: DesignSystem,
{ onInvalidCandidate }: { onInvalidCandidate?: (candidate: string) => void } = {},
{
onInvalidCandidate,
respectImportant,
}: { onInvalidCandidate?: (candidate: string) => void; respectImportant?: boolean } = {},
) {
let nodeSorting = new Map<
AstNode,
@ -44,6 +47,12 @@ export function compileCandidates(
matches.set(rawCandidate, candidates)
}
let flags = CompileAstFlags.None
if (respectImportant ?? true) {
flags |= CompileAstFlags.RespectImportant
}
let variantOrderMap = designSystem.getVariantOrder()
// Create the AST
@ -51,7 +60,7 @@ export function compileCandidates(
let found = false
for (let candidate of candidates) {
let rules = designSystem.compileAstNodes(candidate)
let rules = designSystem.compileAstNodes(candidate, flags)
if (rules.length === 0) continue
found = true
@ -119,10 +128,16 @@ export function compileCandidates(
}
}
export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem) {
export function compileAstNodes(
candidate: Candidate,
designSystem: DesignSystem,
flags: CompileAstFlags,
) {
let asts = compileBaseUtility(candidate, designSystem)
if (asts.length === 0) return []
let respectImportant = designSystem.important && Boolean(flags & CompileAstFlags.RespectImportant)
let rules: {
node: AstNode
propertySort: {
@ -136,7 +151,10 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem
for (let nodes of asts) {
let propertySort = getPropertySort(nodes)
if (candidate.important || designSystem.important) {
// If the candidate itself is important then we want to always mark
// the utility as important. However, at a design system level we want
// to be able to opt-out when using things like `@apply`
if (candidate.important || respectImportant) {
applyImportant(nodes)
}

View file

@ -18,6 +18,11 @@ import { DefaultMap } from './utils/default-map'
import { extractUsedVariables } from './utils/variables'
import { Variants, createVariants } from './variants'
export const enum CompileAstFlags {
None = 0,
RespectImportant = 1 << 0,
}
export type DesignSystem = {
theme: Theme
utilities: Utilities
@ -34,7 +39,7 @@ export type DesignSystem = {
parseCandidate(candidate: string): Readonly<Candidate>[]
parseVariant(variant: string): Readonly<Variant> | null
compileAstNodes(candidate: Candidate): ReturnType<typeof compileAstNodes>
compileAstNodes(candidate: Candidate, flags?: CompileAstFlags): ReturnType<typeof compileAstNodes>
printCandidate(candidate: Candidate): string
printVariant(variant: Variant): string
@ -57,26 +62,28 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
Array.from(parseCandidate(candidate, designSystem)),
)
let compiledAstNodes = new DefaultMap<Candidate>((candidate) => {
let ast = compileAstNodes(candidate, designSystem)
let compiledAstNodes = new DefaultMap<number>((flags) => {
return new DefaultMap<Candidate>((candidate) => {
let ast = compileAstNodes(candidate, designSystem, flags)
// Arbitrary values (`text-[theme(--color-red-500)]`) and arbitrary
// properties (`[--my-var:theme(--color-red-500)]`) can contain function
// calls so we need evaluate any functions we find there that weren't in
// the source CSS.
try {
substituteFunctions(
ast.map(({ node }) => node),
designSystem,
)
} catch (err) {
// If substitution fails then the candidate likely contains a call to
// `theme()` that is invalid which may be because of incorrect usage,
// invalid arguments, or a theme key that does not exist.
return []
}
// Arbitrary values (`text-[theme(--color-red-500)]`) and arbitrary
// properties (`[--my-var:theme(--color-red-500)]`) can contain function
// calls so we need evaluate any functions we find there that weren't in
// the source CSS.
try {
substituteFunctions(
ast.map(({ node }) => node),
designSystem,
)
} catch (err) {
// If substitution fails then the candidate likely contains a call to
// `theme()` that is invalid which may be because of incorrect usage,
// invalid arguments, or a theme key that does not exist.
return []
}
return ast
return ast
})
})
let trackUsedVariables = new DefaultMap((raw) => {
@ -134,8 +141,8 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
parseVariant(variant: string) {
return parsedVariants.get(variant)
},
compileAstNodes(candidate: Candidate) {
return compiledAstNodes.get(candidate)
compileAstNodes(candidate: Candidate, flags = CompileAstFlags.RespectImportant) {
return compiledAstNodes.get(flags).get(candidate)
},
printCandidate(candidate: Candidate) {

View file

@ -666,7 +666,7 @@ describe('@apply', () => {
})
// https://github.com/tailwindlabs/tailwindcss/issues/16935
it('should now swallow @utility declarations when @apply is used in nested rules', async () => {
it('should not swallow @utility declarations when @apply is used in nested rules', async () => {
expect(
await compileCss(
css`
@ -777,6 +777,41 @@ describe('@apply', () => {
}"
`)
})
// https://github.com/tailwindlabs/tailwindcss/issues/18400
it('should ignore the design systems `important` flag when using @apply', async () => {
expect(
await compileCss(
css`
@import 'tailwindcss/utilities' important;
.flex-explicitly-important {
@apply flex!;
}
.flex-not-important {
@apply flex;
}
`,
['flex'],
{
async loadStylesheet(_, base) {
return {
content: '@tailwind utilities;',
base,
path: '',
}
},
},
),
).toMatchInlineSnapshot(`
".flex, .flex-explicitly-important {
display: flex !important;
}
.flex-not-important {
display: flex;
}"
`)
})
})
describe('arbitrary variants', () => {