Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use local user css cache for apply #7524

Merged
merged 13 commits into from Feb 25, 2022
132 changes: 128 additions & 4 deletions src/lib/expandApplyAtRules.js
Expand Up @@ -5,6 +5,8 @@ import { resolveMatches } from './generateRules'
import bigSign from '../util/bigSign'
import escapeClassName from '../util/escapeClassName'

/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */

function extractClasses(node) {
let classes = new Set()
let container = postcss.root({ nodes: [node.clone()] })
Expand Down Expand Up @@ -35,6 +37,94 @@ function prefix(context, selector) {
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
}

function* pathToRoot(node) {
yield node
while (node.parent) {
yield node.parent
node = node.parent
}
}

function shallowClone(node, overrides = {}) {
let children = node.nodes
node.nodes = []

const tmp = node.clone(overrides)
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

node.nodes = children

return tmp
}

/**
* @param {import('postcss').Node} node
*/
function structuralCloneOfNode(node) {
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
for (let parent of pathToRoot(node)) {
if (node === parent) {
continue
}

if (parent.type === 'root') {
break
}

node = shallowClone(parent, {
nodes: [node],
})
}

return node
}

/**
* @param {import('postcss').Root} root
*/
function buildLocalApplyCache(root, context) {
/** @type {ApplyCache} */
let cache = new Map()

let reservedBits = 0n
let tmp = context.layerOrder.utilities >> 3n
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
while (tmp > 1n) {
tmp = tmp >> 1n
reservedBits++
}

let highestOffset = 1n << reservedBits
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

root.walkRules((rule, idx) => {
// Ignore rules generated by Tailwind
for (let node of pathToRoot(rule)) {
if (node.raws.tailwind?.layer !== undefined) {
return
}
}

// Walk to the top of the rule
let container = structuralCloneOfNode(rule)

for (let className of extractClasses(rule)) {
let list = cache.get(className) || []
cache.set(className, list)

list.push([
{
layer: 'user',
sort: BigInt(idx) + highestOffset,
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
important: false,
},
container,
])
}
})

return cache
}

/**
* @returns {ApplyCache}
*/
function buildApplyCache(applyCandidates, context) {
for (let candidate of applyCandidates) {
if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) {
Expand Down Expand Up @@ -62,6 +152,38 @@ function buildApplyCache(applyCandidates, context) {
return context.applyClassCache
}

/**
* @param {() => ApplyCache} buildCacheFn
* @returns {ApplyCache}
*/
function lazyCache(buildCacheFn) {
let cache = null

return {
get: (name) => {
cache = cache || buildCacheFn()

return cache.get(name)
},
has: (name) => {
cache = cache || buildCacheFn()

return cache.has(name)
},
}
}

/**
* @param {ApplyCache[]} caches
* @returns {ApplyCache}
*/
function combineCaches(caches) {
return {
get: (name) => caches.flatMap((cache) => cache.get(name) || []),
has: (name) => caches.some((cache) => cache.has(name)),
}
}

function extractApplyCandidates(params) {
let candidates = params.split(/[\s\t\n]+/g)

Expand All @@ -72,7 +194,7 @@ function extractApplyCandidates(params) {
return [candidates, false]
}

function processApply(root, context) {
function processApply(root, context, localCache) {
let applyCandidates = new Set()

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

/**
* When we have an apply like this:
Expand Down Expand Up @@ -302,12 +424,14 @@ function processApply(root, context) {
}

// Do it again, in case we have other `@apply` rules
processApply(root, context)
processApply(root, context, localCache)
}
}

export default function expandApplyAtRules(context) {
return (root) => {
processApply(root, context)
let localCache = lazyCache(() => buildLocalApplyCache(root, context))

processApply(root, context, localCache)
}
}
30 changes: 25 additions & 5 deletions src/lib/expandTailwindAtRules.js
Expand Up @@ -204,17 +204,29 @@ export default function expandTailwindAtRules(context) {
// Replace any Tailwind directives with generated CSS

if (layerNodes.base) {
layerNodes.base.before(cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source))
layerNodes.base.before(
cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source, {
layer: 'base',
})
)
layerNodes.base.remove()
}

if (layerNodes.components) {
layerNodes.components.before(cloneNodes([...componentNodes], layerNodes.components.source))
layerNodes.components.before(
cloneNodes([...componentNodes], layerNodes.components.source, {
layer: 'components',
})
)
layerNodes.components.remove()
}

if (layerNodes.utilities) {
layerNodes.utilities.before(cloneNodes([...utilityNodes], layerNodes.utilities.source))
layerNodes.utilities.before(
cloneNodes([...utilityNodes], layerNodes.utilities.source, {
layer: 'utilities',
})
)
layerNodes.utilities.remove()
}

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

if (layerNodes.variants) {
layerNodes.variants.before(cloneNodes(variantNodes, layerNodes.variants.source))
layerNodes.variants.before(
cloneNodes(variantNodes, layerNodes.variants.source, {
layer: 'variants',
})
)
layerNodes.variants.remove()
} else if (variantNodes.length > 0) {
root.append(cloneNodes(variantNodes, root.source))
root.append(
cloneNodes(variantNodes, root.source, {
layer: 'variants',
})
)
}

// If we've got a utility layer and no utilities are generated there's likely something wrong
Expand Down
20 changes: 0 additions & 20 deletions src/lib/setupContextUtils.js
Expand Up @@ -230,17 +230,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
// Preserved for backwards compatibility but not used in v3.0+
return []
},
addUserCss(userCss) {
for (let [identifier, rule] of withIdentifiers(userCss)) {
let offset = offsets.user++

if (!context.candidateRuleMap.has(identifier)) {
context.candidateRuleMap.set(identifier, [])
}

context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
}
},
addBase(base) {
for (let [identifier, rule] of withIdentifiers(base)) {
let prefixedIdentifier = prefixIdentifier(identifier, {})
Expand Down Expand Up @@ -521,15 +510,6 @@ function collectLayerPlugins(root) {
}
})

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

return layerPlugins
}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/setupTrackingContext.js
Expand Up @@ -112,7 +112,7 @@ function resolveChangedFiles(candidateFiles, fileModifiedMap) {
// source path), or set up a new one (including setting up watchers and registering
// plugins) then return it
export default function setupTrackingContext(configOrPath) {
return ({ tailwindDirectives, registerDependency, applyDirectives }) => {
return ({ tailwindDirectives, registerDependency }) => {
return (root, result) => {
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
getTailwindConfig(configOrPath)
Expand All @@ -125,7 +125,7 @@ export default function setupTrackingContext(configOrPath) {
// being part of this trigger too, but it's tough because it's impossible
// for a layer in one file to end up in the actual @tailwind rule in
// another file since independent sources are effectively isolated.
if (tailwindDirectives.size > 0 || applyDirectives.size > 0) {
if (tailwindDirectives.size > 0) {
// Add current css file as a context dependencies.
contextDependencies.add(result.opts.from)

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

// Add template paths as postcss dependencies.
Expand Down
9 changes: 8 additions & 1 deletion src/util/cloneNodes.js
@@ -1,4 +1,4 @@
export default function cloneNodes(nodes, source) {
export default function cloneNodes(nodes, source = undefined, raws = undefined) {
return nodes.map((node) => {
let cloned = node.clone()

Expand All @@ -12,6 +12,13 @@ export default function cloneNodes(nodes, source) {
}
}

if (raws !== undefined) {
cloned.raws.tailwind = {
...cloned.raws.tailwind,
...raws,
}
}

return cloned
})
}
51 changes: 51 additions & 0 deletions tests/apply.test.js
Expand Up @@ -1328,3 +1328,54 @@ it('should be possible to use apply in plugins', async () => {
`)
})
})

it('The apply class cache is invalidated when rules change', async () => {
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
let config = {
content: [{ raw: html`<div></div>` }],
plugins: [],
}

let inputBefore = css`
.foo {
color: green;
}

.bar {
@apply foo;
}
`

let inputAfter = css`
.foo {
color: red;
}

.bar {
@apply foo;
}
`

let result = await run(inputBefore, config)

expect(result.css).toMatchFormattedCss(css`
.foo {
color: green;
}

.bar {
color: green;
}
`)

result = await run(inputAfter, config)

expect(result.css).toMatchFormattedCss(css`
.foo {
color: red;
}

.bar {
color: red;
}
`)
})