Skip to content

Commit 8fccb99

Browse files
committed
Rework apply to use local postcss root
We were storing user CSS in the context so we could use it with apply. The problem is that this CSS does not get updated on save unless it has a tailwind directive in it resulting in stale apply caches. This could result in either stale generation or errors about missing classes.
1 parent 326bf53 commit 8fccb99

6 files changed

+181
-38
lines changed

src/lib/expandApplyAtRules.js

+96-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { resolveMatches } from './generateRules'
55
import bigSign from '../util/bigSign'
66
import escapeClassName from '../util/escapeClassName'
77

8+
/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
9+
810
function extractClasses(node) {
911
let classes = new Set()
1012
let container = postcss.root({ nodes: [node.clone()] })
@@ -35,6 +37,83 @@ function prefix(context, selector) {
3537
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
3638
}
3739

40+
function* pathToRoot(node) {
41+
yield node
42+
while (node.parent) {
43+
yield node.parent
44+
node = node.parent
45+
}
46+
}
47+
48+
/**
49+
* @param {import('postcss').Node} node
50+
*/
51+
function structuralCloneOfNode(node) {
52+
for (let parent of pathToRoot(node)) {
53+
if (node === parent) {
54+
continue
55+
}
56+
57+
if (parent.type === 'root') {
58+
break
59+
}
60+
61+
node = parent.clone({
62+
nodes: [node],
63+
})
64+
}
65+
66+
return node
67+
}
68+
69+
/**
70+
* @param {import('postcss').Root} root
71+
*/
72+
function buildLocalApplyCache(root, context) {
73+
/** @type {ApplyCache} */
74+
let cache = new Map()
75+
76+
let reservedBits = 0n
77+
let tmp = context.layerOrder.utilities >> 3n
78+
while (tmp > 1n) {
79+
tmp = tmp >> 1n
80+
reservedBits++
81+
}
82+
83+
let highestOffset = 1n << reservedBits
84+
85+
root.walkRules((rule, idx) => {
86+
// Ignore rules generated by Tailwind
87+
for (let node of pathToRoot(rule)) {
88+
if (node.raws.tailwind?.layer !== undefined) {
89+
return
90+
}
91+
}
92+
93+
// Walk to the top of the rule
94+
let container = structuralCloneOfNode(rule)
95+
96+
for (let className of extractClasses(rule)) {
97+
let list = cache.get(className) || []
98+
cache.set(className, list)
99+
100+
list.push([
101+
{
102+
layer: 'user',
103+
sort: BigInt(idx) + highestOffset,
104+
important: false,
105+
},
106+
container,
107+
])
108+
}
109+
})
110+
111+
return cache
112+
}
113+
114+
/**
115+
* @returns {ApplyCache}
116+
*/
38117
function buildApplyCache(applyCandidates, context) {
39118
for (let candidate of applyCandidates) {
40119
if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) {
@@ -62,6 +141,17 @@ function buildApplyCache(applyCandidates, context) {
62141
return context.applyClassCache
63142
}
64143

144+
/**
145+
* @param {ApplyCache[]} caches
146+
* @returns {ApplyCache}
147+
*/
148+
function combineCaches(caches) {
149+
return {
150+
get: (name) => caches.flatMap((cache) => cache.get(name) || []),
151+
has: (name) => caches.some((cache) => cache.has(name)),
152+
}
153+
}
154+
65155
function extractApplyCandidates(params) {
66156
let candidates = params.split(/[\s\t\n]+/g)
67157

@@ -72,7 +162,7 @@ function extractApplyCandidates(params) {
72162
return [candidates, false]
73163
}
74164

75-
function processApply(root, context) {
165+
function processApply(root, context, localCache) {
76166
let applyCandidates = new Set()
77167

78168
// Collect all @apply rules and candidates
@@ -90,7 +180,7 @@ function processApply(root, context) {
90180
// Start the @apply process if we have rules with @apply in them
91181
if (applies.length > 0) {
92182
// Fill up some caches!
93-
let applyClassCache = buildApplyCache(applyCandidates, context)
183+
let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
94184

95185
/**
96186
* When we have an apply like this:
@@ -302,12 +392,14 @@ function processApply(root, context) {
302392
}
303393

304394
// Do it again, in case we have other `@apply` rules
305-
processApply(root, context)
395+
processApply(root, context, localCache)
306396
}
307397
}
308398

309399
export default function expandApplyAtRules(context) {
310400
return (root) => {
311-
processApply(root, context)
401+
let localCache = buildLocalApplyCache(root, context)
402+
403+
processApply(root, context, localCache)
312404
}
313405
}

src/lib/expandTailwindAtRules.js

+25-5
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,29 @@ export default function expandTailwindAtRules(context) {
204204
// Replace any Tailwind directives with generated CSS
205205

206206
if (layerNodes.base) {
207-
layerNodes.base.before(cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source))
207+
layerNodes.base.before(
208+
cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source, {
209+
layer: 'base',
210+
})
211+
)
208212
layerNodes.base.remove()
209213
}
210214

211215
if (layerNodes.components) {
212-
layerNodes.components.before(cloneNodes([...componentNodes], layerNodes.components.source))
216+
layerNodes.components.before(
217+
cloneNodes([...componentNodes], layerNodes.components.source, {
218+
layer: 'components',
219+
})
220+
)
213221
layerNodes.components.remove()
214222
}
215223

216224
if (layerNodes.utilities) {
217-
layerNodes.utilities.before(cloneNodes([...utilityNodes], layerNodes.utilities.source))
225+
layerNodes.utilities.before(
226+
cloneNodes([...utilityNodes], layerNodes.utilities.source, {
227+
layer: 'utilities',
228+
})
229+
)
218230
layerNodes.utilities.remove()
219231
}
220232

@@ -234,10 +246,18 @@ export default function expandTailwindAtRules(context) {
234246
})
235247

236248
if (layerNodes.variants) {
237-
layerNodes.variants.before(cloneNodes(variantNodes, layerNodes.variants.source))
249+
layerNodes.variants.before(
250+
cloneNodes(variantNodes, layerNodes.variants.source, {
251+
layer: 'variants',
252+
})
253+
)
238254
layerNodes.variants.remove()
239255
} else if (variantNodes.length > 0) {
240-
root.append(cloneNodes(variantNodes, root.source))
256+
root.append(
257+
cloneNodes(variantNodes, root.source, {
258+
layer: 'variants',
259+
})
260+
)
241261
}
242262

243263
// If we've got a utility layer and no utilities are generated there's likely something wrong

src/lib/setupContextUtils.js

-20
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
230230
// Preserved for backwards compatibility but not used in v3.0+
231231
return []
232232
},
233-
addUserCss(userCss) {
234-
for (let [identifier, rule] of withIdentifiers(userCss)) {
235-
let offset = offsets.user++
236-
237-
if (!context.candidateRuleMap.has(identifier)) {
238-
context.candidateRuleMap.set(identifier, [])
239-
}
240-
241-
context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
242-
}
243-
},
244233
addBase(base) {
245234
for (let [identifier, rule] of withIdentifiers(base)) {
246235
let prefixedIdentifier = prefixIdentifier(identifier, {})
@@ -521,15 +510,6 @@ function collectLayerPlugins(root) {
521510
}
522511
})
523512

524-
root.walkRules((rule) => {
525-
// At this point it is safe to include all the left-over css from the
526-
// user's css file. This is because the `@tailwind` and `@layer` directives
527-
// will already be handled and will be removed from the css tree.
528-
layerPlugins.push(function ({ addUserCss }) {
529-
addUserCss(rule, { respectPrefix: false })
530-
})
531-
})
532-
533513
return layerPlugins
534514
}
535515

src/util/cloneNodes.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default function cloneNodes(nodes, source) {
1+
export default function cloneNodes(nodes, source = undefined, raws = undefined) {
22
return nodes.map((node) => {
33
let cloned = node.clone()
44

@@ -12,6 +12,13 @@ export default function cloneNodes(nodes, source) {
1212
}
1313
}
1414

15+
if (raws !== undefined) {
16+
cloned.raws.tailwind = {
17+
...cloned.raws.tailwind,
18+
...raws,
19+
}
20+
}
21+
1522
return cloned
1623
})
1724
}

tests/apply.test.js

+51-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
import fs from 'fs'
22
import path from 'path'
3-
import * as sharedState from '../src/lib/sharedState.js'
43

54
import { run, html, css, defaults } from './util/run'
65

7-
beforeEach(() => {
8-
sharedState.contextMap.clear()
9-
sharedState.configContextMap.clear()
10-
sharedState.contextSourcesMap.clear()
11-
})
12-
136
test('@apply', () => {
147
let config = {
158
darkMode: 'class',
@@ -1335,3 +1328,54 @@ it('should be possible to use apply in plugins', async () => {
13351328
`)
13361329
})
13371330
})
1331+
1332+
it('The apply class cache is invalidated when rules change', async () => {
1333+
let config = {
1334+
content: [{ raw: html`<div></div>` }],
1335+
plugins: [],
1336+
}
1337+
1338+
let inputBefore = css`
1339+
.foo {
1340+
color: green;
1341+
}
1342+
1343+
.bar {
1344+
@apply foo;
1345+
}
1346+
`
1347+
1348+
let inputAfter = css`
1349+
.foo {
1350+
color: red;
1351+
}
1352+
1353+
.bar {
1354+
@apply foo;
1355+
}
1356+
`
1357+
1358+
let result = await run(inputBefore, config)
1359+
1360+
expect(result.css).toMatchFormattedCss(css`
1361+
.foo {
1362+
color: green;
1363+
}
1364+
1365+
.bar {
1366+
color: green;
1367+
}
1368+
`)
1369+
1370+
result = await run(inputAfter, config)
1371+
1372+
expect(result.css).toMatchFormattedCss(css`
1373+
.foo {
1374+
color: red;
1375+
}
1376+
1377+
.bar {
1378+
color: red;
1379+
}
1380+
`)
1381+
})

tests/context-reuse.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,5 @@ it('a build re-uses the context across multiple files with the same config', asy
8383
expect(dependencies[3]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
8484

8585
// And none of this should have resulted in multiple contexts being created
86-
// expect(sharedState.contextSourcesMap.size).toBe(1)
86+
expect(sharedState.contextSourcesMap.size).toBe(1)
8787
})

0 commit comments

Comments
 (0)