Skip to content

Commit fe08e91

Browse files
authored
Ensure @apply works consistently with or without @layer (#6938)
* partition nodes as soon as possible Time to write another story on `@apply`... When we write code like this: ```css .a { @apply b; } .b { @apply uppercase; color: red; } ``` Then we create 2 Nodes in our context to keep track of. One has identifier `a`, the other has identifier `b`. However, when we have an `@apply` and it contains multiple declarations/atrules, then we have to split up the (aka partition) node into multiple nodes so that we can guarantee the correct expected sort order. This means that the above example technically looks like this: ```css .a { @apply b; } .b { @apply uppercase; } .b { color: red; } ``` If this was your input, then we would still have 1 node for identifier 'a', but we would have 2 nodes for identifier 'b'. As mentioned earlier, this is important to guarantee the correct order, here is an example: ```css .b { @apply md:font-bold xl:font-normal; /* Here we can sort by our internal rules. This means that the `md` comes before `xl`. */ } ``` ... however ```css .b { @apply xl:font-normal; /* This now exists _before_ the example below */ } .b { @apply md:font-bold; /* Because we respect the order of the user's css */ } ``` So to guarantee the order when doing this: ```css .b { @apply xl:font-normal; @apply lg:font-normal; } ``` We also split this up into 2 nodes like this: ```css .b { @apply xl:font-normal; } .b { @apply lg:font-normal; } ``` The tricky part is that now only 1 empty `.b` node exists in our context because we partitioned the orginal node into multiple nodes and moved the children to the new nodes and because they are new nodes it means that they have a different identity. This partitioning used to happen in the expandApplyAtRules code, but this is a bit too late because the context has already been filled at this time. Instead, we move the code more to the front, as if you wrote those separated blocks yourself. Now the code to inject those nodes into the context happens in a single spot instead of multiple places. Another good part about this is that we have better consistency between each layer because it turns out that these two examples generated different results... ```css .a { @apply b; } .b { @apply uppercase; color: red; } ``` ... is different compared to: ```css @tailwind components; @layer components { .a { @apply b; } .b { @apply uppercase; color: red; } } ``` Even if both `a` and `b` are being used in one of your content paths... Yeah.. *sigh* * add more `@apply` related tests * update changelog * remove support for basic nesting (leftover) * remove leftover todo This has been fixed already
1 parent 9c72add commit fe08e91

6 files changed

+267
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Allow use of falsy values in theme config ([#6917](https://github.com/tailwindlabs/tailwindcss/pull/6917))
1313
- Ensure we can apply classes that are grouped with non-class selectors ([#6922](https://github.com/tailwindlabs/tailwindcss/pull/6922))
1414
- Improve standalone CLI compatibility on Linux by switching to the `linuxstatic` build target ([#6914](https://github.com/tailwindlabs/tailwindcss/pull/6914))
15+
- Ensure `@apply` works consistently with or without `@layer` ([#6938](https://github.com/tailwindlabs/tailwindcss/pull/6938))
1516

1617
## [3.0.11] - 2022-01-05
1718

src/lib/expandApplyAtRules.js

-42
Original file line numberDiff line numberDiff line change
@@ -72,47 +72,6 @@ function extractApplyCandidates(params) {
7272
return [candidates, false]
7373
}
7474

75-
function partitionApplyParents(root) {
76-
let applyParents = new Set()
77-
78-
root.walkAtRules('apply', (rule) => {
79-
applyParents.add(rule.parent)
80-
})
81-
82-
for (let rule of applyParents) {
83-
let nodeGroups = []
84-
let lastGroup = []
85-
86-
for (let node of rule.nodes) {
87-
if (node.type === 'atrule' && node.name === 'apply') {
88-
if (lastGroup.length > 0) {
89-
nodeGroups.push(lastGroup)
90-
lastGroup = []
91-
}
92-
nodeGroups.push([node])
93-
} else {
94-
lastGroup.push(node)
95-
}
96-
}
97-
98-
if (lastGroup.length > 0) {
99-
nodeGroups.push(lastGroup)
100-
}
101-
102-
if (nodeGroups.length === 1) {
103-
continue
104-
}
105-
106-
for (let group of [...nodeGroups].reverse()) {
107-
let newParent = rule.clone({ nodes: [] })
108-
newParent.append(group)
109-
rule.after(newParent)
110-
}
111-
112-
rule.remove()
113-
}
114-
}
115-
11675
function processApply(root, context) {
11776
let applyCandidates = new Set()
11877

@@ -343,7 +302,6 @@ function processApply(root, context) {
343302

344303
export default function expandApplyAtRules(context) {
345304
return (root) => {
346-
partitionApplyParents(root)
347305
processApply(root, context)
348306
}
349307
}

src/lib/setupContextUtils.js

+74-8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,58 @@ import log from '../util/log'
2020
import negateValue from '../util/negateValue'
2121
import isValidArbitraryValue from '../util/isValidArbitraryValue'
2222

23+
function partitionRules(root) {
24+
if (!root.walkAtRules) return [root]
25+
26+
let applyParents = new Set()
27+
let rules = []
28+
29+
root.walkAtRules('apply', (rule) => {
30+
applyParents.add(rule.parent)
31+
})
32+
33+
if (applyParents.size === 0) {
34+
rules.push(root)
35+
}
36+
37+
for (let rule of applyParents) {
38+
let nodeGroups = []
39+
let lastGroup = []
40+
41+
for (let node of rule.nodes) {
42+
if (node.type === 'atrule' && node.name === 'apply') {
43+
if (lastGroup.length > 0) {
44+
nodeGroups.push(lastGroup)
45+
lastGroup = []
46+
}
47+
nodeGroups.push([node])
48+
} else {
49+
lastGroup.push(node)
50+
}
51+
}
52+
53+
if (lastGroup.length > 0) {
54+
nodeGroups.push(lastGroup)
55+
}
56+
57+
if (nodeGroups.length === 1) {
58+
rules.push(rule)
59+
continue
60+
}
61+
62+
for (let group of [...nodeGroups].reverse()) {
63+
let clone = rule.clone({ nodes: [] })
64+
clone.append(group)
65+
rules.unshift(clone)
66+
rule.after(clone)
67+
}
68+
69+
rule.remove()
70+
}
71+
72+
return rules
73+
}
74+
2375
function parseVariantFormatString(input) {
2476
if (input.includes('{')) {
2577
if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
@@ -232,7 +284,9 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
232284
context.candidateRuleMap.set(identifier, [])
233285
}
234286

235-
context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
287+
context.candidateRuleMap
288+
.get(identifier)
289+
.push(...partitionRules(rule).map((rule) => [{ sort: offset, layer: 'user' }, rule]))
236290
}
237291
},
238292
addBase(base) {
@@ -246,7 +300,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
246300

247301
context.candidateRuleMap
248302
.get(prefixedIdentifier)
249-
.push([{ sort: offset, layer: 'base' }, rule])
303+
.push(...partitionRules(rule).map((rule) => [{ sort: offset, layer: 'base' }, rule]))
250304
}
251305
},
252306
/**
@@ -260,15 +314,19 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
260314

261315
for (let [identifier, rule] of withIdentifiers(groups)) {
262316
let prefixedIdentifier = prefixIdentifier(identifier, {})
263-
let offset = offsets.base++
264317

265318
if (!context.candidateRuleMap.has(prefixedIdentifier)) {
266319
context.candidateRuleMap.set(prefixedIdentifier, [])
267320
}
268321

269322
context.candidateRuleMap
270323
.get(prefixedIdentifier)
271-
.push([{ sort: offset, layer: 'defaults' }, rule])
324+
.push(
325+
...partitionRules(rule).map((rule) => [
326+
{ sort: offsets.base++, layer: 'defaults' },
327+
rule,
328+
])
329+
)
272330
}
273331
},
274332
addComponents(components, options) {
@@ -281,7 +339,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
281339

282340
for (let [identifier, rule] of withIdentifiers(components)) {
283341
let prefixedIdentifier = prefixIdentifier(identifier, options)
284-
let offset = offsets.components++
285342

286343
classList.add(prefixedIdentifier)
287344

@@ -291,7 +348,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
291348

292349
context.candidateRuleMap
293350
.get(prefixedIdentifier)
294-
.push([{ sort: offset, layer: 'components', options }, rule])
351+
.push(
352+
...partitionRules(rule).map((rule) => [
353+
{ sort: offsets.components++, layer: 'components', options },
354+
rule,
355+
])
356+
)
295357
}
296358
},
297359
addUtilities(utilities, options) {
@@ -304,7 +366,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
304366

305367
for (let [identifier, rule] of withIdentifiers(utilities)) {
306368
let prefixedIdentifier = prefixIdentifier(identifier, options)
307-
let offset = offsets.utilities++
308369

309370
classList.add(prefixedIdentifier)
310371

@@ -314,7 +375,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
314375

315376
context.candidateRuleMap
316377
.get(prefixedIdentifier)
317-
.push([{ sort: offset, layer: 'utilities', options }, rule])
378+
.push(
379+
...partitionRules(rule).map((rule) => [
380+
{ sort: offsets.utilities++, layer: 'utilities', options },
381+
rule,
382+
])
383+
)
318384
}
319385
},
320386
matchUtilities: function (utilities, options) {

src/processTailwindFeatures.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export default function processTailwindFeatures(setupContext) {
1414
return function (root, result) {
1515
let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)
1616

17+
detectNesting()(root, result)
18+
1719
let context = setupContext({
1820
tailwindDirectives,
1921
applyDirectives,
@@ -37,7 +39,6 @@ export default function processTailwindFeatures(setupContext) {
3739

3840
issueFlagNotices(context.tailwindConfig)
3941

40-
detectNesting(context)(root, result)
4142
expandTailwindAtRules(context)(root, result)
4243
expandApplyAtRules(context)(root, result)
4344
evaluateTailwindFunctions(context)(root, result)

tests/apply.test.css

-9
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@
122122
text-align: left;
123123
}
124124
}
125-
/* TODO: This works but the generated CSS is unnecessarily verbose. */
126125
.complex-utilities {
127126
--tw-ordinal: ordinal;
128127
--tw-numeric-spacing: tabular-nums;
@@ -144,14 +143,6 @@
144143
--tw-numeric-fraction: diagonal-fractions;
145144
font-variant-numeric: var(--tw-font-variant-numeric);
146145
}
147-
.basic-nesting-parent {
148-
.basic-nesting-child {
149-
font-weight: 700;
150-
}
151-
.basic-nesting-child:hover {
152-
font-weight: 400;
153-
}
154-
}
155146
.use-base-only-a {
156147
font-weight: 700;
157148
}

0 commit comments

Comments
 (0)