Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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,
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
})
}
Loading