Skip to content

Commit 895e521

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 95c7ca0 commit 895e521

File tree

4 files changed

+160
-32
lines changed

4 files changed

+160
-32
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:
@@ -296,12 +386,14 @@ function processApply(root, context) {
296386
}
297387

298388
// Do it again, in case we have other `@apply` rules
299-
processApply(root, context)
389+
processApply(root, context, localCache)
300390
}
301391
}
302392

303393
export default function expandApplyAtRules(context) {
304394
return (root) => {
305-
processApply(root, context)
395+
let localCache = buildLocalApplyCache(root, context)
396+
397+
processApply(root, context, localCache)
306398
}
307399
}

src/lib/setupContextUtils.js

+12-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, {})
@@ -500,36 +489,39 @@ function collectLayerPlugins(root) {
500489
if (layerRule.params === 'base') {
501490
for (let node of layerRule.nodes) {
502491
layerPlugins.push(function ({ addBase }) {
492+
node.raws.tailwind = {
493+
...node.raws.tailwind,
494+
layer: layerRule.params,
495+
}
503496
addBase(node, { respectPrefix: false })
504497
})
505498
}
506499
layerRule.remove()
507500
} else if (layerRule.params === 'components') {
508501
for (let node of layerRule.nodes) {
509502
layerPlugins.push(function ({ addComponents }) {
503+
node.raws.tailwind = {
504+
...node.raws.tailwind,
505+
layer: layerRule.params,
506+
}
510507
addComponents(node, { respectPrefix: false })
511508
})
512509
}
513510
layerRule.remove()
514511
} else if (layerRule.params === 'utilities') {
515512
for (let node of layerRule.nodes) {
516513
layerPlugins.push(function ({ addUtilities }) {
514+
node.raws.tailwind = {
515+
...node.raws.tailwind,
516+
layer: layerRule.params,
517+
}
517518
addUtilities(node, { respectPrefix: false })
518519
})
519520
}
520521
layerRule.remove()
521522
}
522523
})
523524

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-
533525
return layerPlugins
534526
}
535527

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',
@@ -1338,3 +1331,54 @@ it('should be possible to use apply in plugins', async () => {
13381331
`)
13391332
})
13401333
})
1334+
1335+
it('The apply class cache is invalidated when rules change', async () => {
1336+
let config = {
1337+
content: [{ raw: html`<div></div>` }],
1338+
plugins: [],
1339+
}
1340+
1341+
let inputBefore = css`
1342+
.foo {
1343+
color: green;
1344+
}
1345+
1346+
.bar {
1347+
@apply foo;
1348+
}
1349+
`
1350+
1351+
let inputAfter = css`
1352+
.foo {
1353+
color: red;
1354+
}
1355+
1356+
.bar {
1357+
@apply foo;
1358+
}
1359+
`
1360+
1361+
let result = await run(inputBefore, config)
1362+
1363+
expect(result.css).toMatchFormattedCss(css`
1364+
.foo {
1365+
color: green;
1366+
}
1367+
1368+
.bar {
1369+
color: green;
1370+
}
1371+
`)
1372+
1373+
result = await run(inputAfter, config)
1374+
1375+
expect(result.css).toMatchFormattedCss(css`
1376+
.foo {
1377+
color: red;
1378+
}
1379+
1380+
.bar {
1381+
color: red;
1382+
}
1383+
`)
1384+
})

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)