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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow default ring color to be a function ([#7587](https://github.com/tailwindlabs/tailwindcss/pull/7587))
- Preserve source maps for generated CSS ([#7588](https://github.com/tailwindlabs/tailwindcss/pull/7588))
- Split box shadows on top-level commas only ([#7479](https://github.com/tailwindlabs/tailwindcss/pull/7479))
- Use local user css cache for `@apply` ([#7524](https://github.com/tailwindlabs/tailwindcss/pull/7524))

## [3.0.23] - 2022-02-16

Expand Down
175 changes: 171 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,131 @@ 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
}
}

/**
* Only clone the node itself and not its children
*
* @param {*} node
* @param {*} overrides
* @returns
*/
function shallowClone(node, overrides = {}) {
let children = node.nodes
node.nodes = []

let tmp = node.clone(overrides)

node.nodes = children

return tmp
}

/**
* Clone just the nodes all the way to the top that are required to represent
* this singular rule in the tree.
*
* For example, if we have CSS like this:
* ```css
* @media (min-width: 768px) {
* @supports (display: grid) {
* .foo {
* display: grid;
* grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
* }
* }
*
* @supports (backdrop-filter: blur(1px)) {
* .bar {
* backdrop-filter: blur(1px);
* }
* }
*
* .baz {
* color: orange;
* }
* }
* ```
*
* And we're cloning `.bar` it'll return a cloned version of what's required for just that single node:
*
* ```css
* @media (min-width: 768px) {
* @supports (backdrop-filter: blur(1px)) {
* .bar {
* backdrop-filter: blur(1px);
* }
* }
* }
* ```
*
* @param {import('postcss').Node} node
*/
function nestedClone(node) {
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 highestOffset = context.layerOrder.user >> 4n

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

// Clone what's required to represent this singular rule in the tree
let container = nestedClone(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 +189,43 @@ function buildApplyCache(applyCandidates, context) {
return context.applyClassCache
}

/**
* Build a cache only when it's first used
*
* @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)
},
}
}

/**
* Take a series of multiple caches and merge
* them so they act like one large cache
*
* @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 +236,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 +254,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 +466,15 @@ 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)
// Build a cache of the user's CSS so we can use it to resolve classes used by @apply
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
})
}