Skip to content

Commit 910b655

Browse files
Use local user css cache for apply (#7524)
* Fix context reuse test * Don't update files with at-apply when content changes * Prevent at-apply directives from creating new contexts * 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. * Don’t build local cache unless `@apply` is used * Update changelog
1 parent f84ee8b commit 910b655

9 files changed

+340
-91
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 default ring color to be a function ([#7587](https://github.com/tailwindlabs/tailwindcss/pull/7587))
1313
- Preserve source maps for generated CSS ([#7588](https://github.com/tailwindlabs/tailwindcss/pull/7588))
1414
- Split box shadows on top-level commas only ([#7479](https://github.com/tailwindlabs/tailwindcss/pull/7479))
15+
- Use local user css cache for `@apply` ([#7524](https://github.com/tailwindlabs/tailwindcss/pull/7524))
1516

1617
### Changed
1718

src/lib/expandApplyAtRules.js

+171-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,131 @@ 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+
* Only clone the node itself and not its children
50+
*
51+
* @param {*} node
52+
* @param {*} overrides
53+
* @returns
54+
*/
55+
function shallowClone(node, overrides = {}) {
56+
let children = node.nodes
57+
node.nodes = []
58+
59+
let tmp = node.clone(overrides)
60+
61+
node.nodes = children
62+
63+
return tmp
64+
}
65+
66+
/**
67+
* Clone just the nodes all the way to the top that are required to represent
68+
* this singular rule in the tree.
69+
*
70+
* For example, if we have CSS like this:
71+
* ```css
72+
* @media (min-width: 768px) {
73+
* @supports (display: grid) {
74+
* .foo {
75+
* display: grid;
76+
* grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
77+
* }
78+
* }
79+
*
80+
* @supports (backdrop-filter: blur(1px)) {
81+
* .bar {
82+
* backdrop-filter: blur(1px);
83+
* }
84+
* }
85+
*
86+
* .baz {
87+
* color: orange;
88+
* }
89+
* }
90+
* ```
91+
*
92+
* And we're cloning `.bar` it'll return a cloned version of what's required for just that single node:
93+
*
94+
* ```css
95+
* @media (min-width: 768px) {
96+
* @supports (backdrop-filter: blur(1px)) {
97+
* .bar {
98+
* backdrop-filter: blur(1px);
99+
* }
100+
* }
101+
* }
102+
* ```
103+
*
104+
* @param {import('postcss').Node} node
105+
*/
106+
function nestedClone(node) {
107+
for (let parent of pathToRoot(node)) {
108+
if (node === parent) {
109+
continue
110+
}
111+
112+
if (parent.type === 'root') {
113+
break
114+
}
115+
116+
node = shallowClone(parent, {
117+
nodes: [node],
118+
})
119+
}
120+
121+
return node
122+
}
123+
124+
/**
125+
* @param {import('postcss').Root} root
126+
*/
127+
function buildLocalApplyCache(root, context) {
128+
/** @type {ApplyCache} */
129+
let cache = new Map()
130+
131+
let highestOffset = context.layerOrder.user >> 4n
132+
133+
root.walkRules((rule, idx) => {
134+
// Ignore rules generated by Tailwind
135+
for (let node of pathToRoot(rule)) {
136+
if (node.raws.tailwind?.layer !== undefined) {
137+
return
138+
}
139+
}
140+
141+
// Clone what's required to represent this singular rule in the tree
142+
let container = nestedClone(rule)
143+
144+
for (let className of extractClasses(rule)) {
145+
let list = cache.get(className) || []
146+
cache.set(className, list)
147+
148+
list.push([
149+
{
150+
layer: 'user',
151+
sort: BigInt(idx) + highestOffset,
152+
important: false,
153+
},
154+
container,
155+
])
156+
}
157+
})
158+
159+
return cache
160+
}
161+
162+
/**
163+
* @returns {ApplyCache}
164+
*/
38165
function buildApplyCache(applyCandidates, context) {
39166
for (let candidate of applyCandidates) {
40167
if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) {
@@ -62,6 +189,43 @@ function buildApplyCache(applyCandidates, context) {
62189
return context.applyClassCache
63190
}
64191

192+
/**
193+
* Build a cache only when it's first used
194+
*
195+
* @param {() => ApplyCache} buildCacheFn
196+
* @returns {ApplyCache}
197+
*/
198+
function lazyCache(buildCacheFn) {
199+
let cache = null
200+
201+
return {
202+
get: (name) => {
203+
cache = cache || buildCacheFn()
204+
205+
return cache.get(name)
206+
},
207+
has: (name) => {
208+
cache = cache || buildCacheFn()
209+
210+
return cache.has(name)
211+
},
212+
}
213+
}
214+
215+
/**
216+
* Take a series of multiple caches and merge
217+
* them so they act like one large cache
218+
*
219+
* @param {ApplyCache[]} caches
220+
* @returns {ApplyCache}
221+
*/
222+
function combineCaches(caches) {
223+
return {
224+
get: (name) => caches.flatMap((cache) => cache.get(name) || []),
225+
has: (name) => caches.some((cache) => cache.has(name)),
226+
}
227+
}
228+
65229
function extractApplyCandidates(params) {
66230
let candidates = params.split(/[\s\t\n]+/g)
67231

@@ -72,7 +236,7 @@ function extractApplyCandidates(params) {
72236
return [candidates, false]
73237
}
74238

75-
function processApply(root, context) {
239+
function processApply(root, context, localCache) {
76240
let applyCandidates = new Set()
77241

78242
// Collect all @apply rules and candidates
@@ -90,7 +254,7 @@ function processApply(root, context) {
90254
// Start the @apply process if we have rules with @apply in them
91255
if (applies.length > 0) {
92256
// Fill up some caches!
93-
let applyClassCache = buildApplyCache(applyCandidates, context)
257+
let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
94258

95259
/**
96260
* When we have an apply like this:
@@ -302,12 +466,15 @@ function processApply(root, context) {
302466
}
303467

304468
// Do it again, in case we have other `@apply` rules
305-
processApply(root, context)
469+
processApply(root, context, localCache)
306470
}
307471
}
308472

309473
export default function expandApplyAtRules(context) {
310474
return (root) => {
311-
processApply(root, context)
475+
// Build a cache of the user's CSS so we can use it to resolve classes used by @apply
476+
let localCache = lazyCache(() => buildLocalApplyCache(root, context))
477+
478+
processApply(root, context, localCache)
312479
}
313480
}

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/lib/setupTrackingContext.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function resolveChangedFiles(candidateFiles, fileModifiedMap) {
112112
// source path), or set up a new one (including setting up watchers and registering
113113
// plugins) then return it
114114
export default function setupTrackingContext(configOrPath) {
115-
return ({ tailwindDirectives, registerDependency, applyDirectives }) => {
115+
return ({ tailwindDirectives, registerDependency }) => {
116116
return (root, result) => {
117117
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
118118
getTailwindConfig(configOrPath)
@@ -125,7 +125,7 @@ export default function setupTrackingContext(configOrPath) {
125125
// being part of this trigger too, but it's tough because it's impossible
126126
// for a layer in one file to end up in the actual @tailwind rule in
127127
// another file since independent sources are effectively isolated.
128-
if (tailwindDirectives.size > 0 || applyDirectives.size > 0) {
128+
if (tailwindDirectives.size > 0) {
129129
// Add current css file as a context dependencies.
130130
contextDependencies.add(result.opts.from)
131131

@@ -153,7 +153,7 @@ export default function setupTrackingContext(configOrPath) {
153153
// We may want to think about `@layer` being part of this trigger too, but it's tough
154154
// because it's impossible for a layer in one file to end up in the actual @tailwind rule
155155
// in another file since independent sources are effectively isolated.
156-
if (tailwindDirectives.size > 0 || applyDirectives.size > 0) {
156+
if (tailwindDirectives.size > 0) {
157157
let fileModifiedMap = getFileModifiedMap(context)
158158

159159
// Add template paths as postcss dependencies.

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
}

0 commit comments

Comments
 (0)