optimizeAst() — tailwindcss Function Reference
Architecture documentation for the optimizeAst() function in ast.ts from the tailwindcss codebase.
Entity Profile
Dependency Diagram
graph TD ec867cf3_916b_0d16_65ec_c715e69fee03["optimizeAst()"] cebe77e1_f0f2_aeee_417e_2192f5790344["buildDesignSystem()"] cebe77e1_f0f2_aeee_417e_2192f5790344 -->|calls| ec867cf3_916b_0d16_65ec_c715e69fee03 6a3a8ab4_d53c_7516_c736_663c060fe979["compileAst()"] 6a3a8ab4_d53c_7516_c736_663c060fe979 -->|calls| ec867cf3_916b_0d16_65ec_c715e69fee03 3ba19013_498f_3c9b_5c44_0eb24efc4394["add()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| 3ba19013_498f_3c9b_5c44_0eb24efc4394 e9e50ff6_7df6_110f_a568_3816eef96d3a["extractUsedVariables()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| e9e50ff6_7df6_110f_a568_3816eef96d3a 88749b8a_ca72_f27d_6dfa_334b2fdf8635["extractKeyframeNames()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| 88749b8a_ca72_f27d_6dfa_334b2fdf8635 85b46de2_edfa_9371_e2c6_e60f3f5346a2["decl()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| 85b46de2_edfa_9371_e2c6_e60f3f5346a2 80f64279_dc28_709a_aad4_02bea029c935["isVariableUsed()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| 80f64279_dc28_709a_aad4_02bea029c935 fd0b3d51_296f_d8dc_755c_ae2bc21124e9["prefixKey()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| fd0b3d51_296f_d8dc_755c_ae2bc21124e9 542f9fd7_747b_195e_e9c9_5ecb41125f1b["findNode()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| 542f9fd7_747b_195e_e9c9_5ecb41125f1b e9d556bc_f22d_356c_1bd2_27442c34b5c7["walk()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| e9d556bc_f22d_356c_1bd2_27442c34b5c7 fedba2ef_0b11_0dd0_fc72_1873b2b9e509["resolveValue()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| fedba2ef_0b11_0dd0_fc72_1873b2b9e509 af90c185_29a2_6c4c_ef06_b18f00f7655c["toCss()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| af90c185_29a2_6c4c_ef06_b18f00f7655c 08f33202_11d1_569a_e8df_de23eb987e2f["rule()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| 08f33202_11d1_569a_e8df_de23eb987e2f a9af385a_fd12_f1d8_7cf0_ccb9b281ca18["atRule()"] ec867cf3_916b_0d16_65ec_c715e69fee03 -->|calls| a9af385a_fd12_f1d8_7cf0_ccb9b281ca18 style ec867cf3_916b_0d16_65ec_c715e69fee03 fill:#6366f1,stroke:#818cf8,color:#fff
Relationship Graph
Source Code
packages/tailwindcss/src/ast.ts lines 220–680
export function optimizeAst(
ast: AstNode[],
designSystem: DesignSystem,
polyfills: Polyfills = Polyfills.All,
) {
let atRoots: AstNode[] = []
let seenAtProperties = new Set<string>()
let cssThemeVariables = new DefaultMap<AstNode[], Set<Declaration>>(() => new Set())
let colorMixDeclarations = new DefaultMap<AstNode[], Set<Declaration>>(() => new Set())
let keyframes = new Set<AtRule>()
let usedKeyframeNames = new Set()
let propertyFallbacksRoot: Declaration[] = []
let propertyFallbacksUniversal: Declaration[] = []
let variableDependencies = new DefaultMap<string, Set<string>>(() => new Set())
function transform(
node: AstNode,
parent: AstNode[],
context: Record<string, string | boolean> = {},
depth = 0,
) {
// Declaration
if (node.kind === 'declaration') {
if (node.property === '--tw-sort' || node.value === undefined || node.value === null) {
return
}
// Track variables defined in `@theme`
if (context.theme && node.property[0] === '-' && node.property[1] === '-') {
// Variables that resolve to `initial` should never be emitted. This can happen because of
// the `--theme(…)` being used and evaluated lazily
if (node.value === 'initial') {
node.value = undefined
return
}
if (!context.keyframes) {
cssThemeVariables.get(parent).add(node)
}
}
// Track used CSS variables
if (node.value.includes('var(')) {
// Declaring another variable does not count as usage. Instead, we mark
// the relationship
if (context.theme && node.property[0] === '-' && node.property[1] === '-') {
for (let variable of extractUsedVariables(node.value)) {
variableDependencies.get(variable).add(node.property)
}
} else {
designSystem.trackUsedVariables(node.value)
}
}
// Track used animation names
if (node.property === 'animation') {
for (let keyframeName of extractKeyframeNames(node.value)) {
usedKeyframeNames.add(keyframeName)
}
}
// Create fallback values for usages of the `color-mix(…)` function that reference variables
// found in the theme config.
if (
polyfills & Polyfills.ColorMix &&
node.value.includes('color-mix(') &&
!context.supportsColorMix &&
!context.keyframes
) {
colorMixDeclarations.get(parent).add(node)
}
parent.push(node)
}
// Rule
else if (node.kind === 'rule') {
let nodes: AstNode[] = []
for (let child of node.nodes) {
transform(child, nodes, context, depth + 1)
}
// Keep the last decl when there are exact duplicates. Keeping the *first* one might
// not be correct when given nested rules where a rule sits between declarations.
let seen: Record<string, AstNode[]> = {}
let toRemove = new Set<AstNode>()
// Keep track of all nodes that produce a given declaration
for (let child of nodes) {
if (child.kind !== 'declaration') continue
let key = `${child.property}:${child.value}:${child.important}`
seen[key] ??= []
seen[key].push(child)
}
// And remove all but the last of each
for (let key in seen) {
for (let i = 0; i < seen[key].length - 1; ++i) {
toRemove.add(seen[key][i])
}
}
if (toRemove.size > 0) {
nodes = nodes.filter((node) => !toRemove.has(node))
}
if (nodes.length === 0) return
// Rules with `&` as the selector should be flattened
if (node.selector === '&') {
parent.push(...nodes)
} else {
parent.push({ ...node, nodes })
}
}
// AtRule `@property`
else if (node.kind === 'at-rule' && node.name === '@property' && depth === 0) {
// Don't output duplicate `@property` rules
if (seenAtProperties.has(node.params)) {
return
}
// Collect fallbacks for `@property` rules for Firefox support We turn these into rules on
// `:root` or `*` and some pseudo-elements based on the value of `inherits`
if (polyfills & Polyfills.AtProperty) {
let property = node.params
let initialValue = null
let inherits = false
for (let prop of node.nodes) {
if (prop.kind !== 'declaration') continue
if (prop.property === 'initial-value') {
initialValue = prop.value
} else if (prop.property === 'inherits') {
inherits = prop.value === 'true'
}
}
let fallback = decl(property, initialValue ?? 'initial')
fallback.src = node.src
if (inherits) {
propertyFallbacksRoot.push(fallback)
} else {
propertyFallbacksUniversal.push(fallback)
}
}
seenAtProperties.add(node.params)
let copy = { ...node, nodes: [] }
for (let child of node.nodes) {
transform(child, copy.nodes, context, depth + 1)
}
parent.push(copy)
}
// AtRule
else if (node.kind === 'at-rule') {
if (node.name === '@keyframes') {
context = { ...context, keyframes: true }
} else if (node.name === '@supports' && node.params.includes('color-mix(')) {
context = { ...context, supportsColorMix: true }
}
let copy = { ...node, nodes: [] }
for (let child of node.nodes) {
transform(child, copy.nodes, context, depth + 1)
}
// Only track `@keyframes` that could be removed, when they were defined inside of a `@theme`.
if (node.name === '@keyframes' && context.theme) {
keyframes.add(copy)
}
if (
copy.nodes.length > 0 ||
copy.name === '@layer' ||
copy.name === '@charset' ||
copy.name === '@custom-media' ||
copy.name === '@namespace' ||
copy.name === '@import'
) {
parent.push(copy)
}
}
// AtRoot
else if (node.kind === 'at-root') {
for (let child of node.nodes) {
let newParent: AstNode[] = []
transform(child, newParent, context, 0)
for (let child of newParent) {
atRoots.push(child)
}
}
}
// Context
else if (node.kind === 'context') {
// Remove reference imports from printing
if (node.context.reference) {
return
} else {
for (let child of node.nodes) {
transform(child, parent, { ...context, ...node.context }, depth)
}
}
}
// Comment
else if (node.kind === 'comment') {
parent.push(node)
}
// Unknown
else {
node satisfies never
}
}
let newAst: AstNode[] = []
for (let node of ast) {
transform(node, newAst, {}, 0)
}
// Remove unused theme variables
next: for (let [parent, declarations] of cssThemeVariables) {
for (let declaration of declarations) {
// Find out if a variable is either used directly or if any of the
// variables referencing it is used.
let variableUsed = isVariableUsed(
declaration.property,
designSystem.theme,
variableDependencies,
)
if (variableUsed) {
if (declaration.property.startsWith(designSystem.theme.prefixKey('--animate-'))) {
for (let keyframeName of extractKeyframeNames(declaration.value!))
usedKeyframeNames.add(keyframeName)
}
continue
}
// Remove the declaration (from its parent)
let idx = parent.indexOf(declaration)
parent.splice(idx, 1)
// If the parent is now empty, remove it and any `@layer` rules above it
// from the AST
if (parent.length === 0) {
let path = findNode(newAst, (node) => node.kind === 'rule' && node.nodes === parent)
if (!path || path.length === 0) continue next
// Add the root of the AST so we can delete from the top-level
path.unshift({
kind: 'at-root',
nodes: newAst,
})
// Remove nodes from the parent as long as the parent is empty
// otherwise and consist of only @layer rules
do {
let nodeToRemove = path.pop()
if (!nodeToRemove) break
let removeFrom = path[path.length - 1]
if (!removeFrom) break
if (removeFrom.kind !== 'at-root' && removeFrom.kind !== 'at-rule') break
let idx = removeFrom.nodes.indexOf(nodeToRemove)
if (idx === -1) break
removeFrom.nodes.splice(idx, 1)
} while (true)
continue next
}
}
}
// Remove unused keyframes
for (let keyframe of keyframes) {
if (!usedKeyframeNames.has(keyframe.params)) {
let idx = atRoots.indexOf(keyframe)
atRoots.splice(idx, 1)
}
}
newAst = newAst.concat(atRoots)
// Fallbacks
// Create fallback values for usages of the `color-mix(…)` function that reference variables
// found in the theme config.
if (polyfills & Polyfills.ColorMix) {
for (let [parent, declarations] of colorMixDeclarations) {
for (let declaration of declarations) {
let idx = parent.indexOf(declaration)
// If the declaration is no longer present, we don't need to create a polyfill anymore
if (idx === -1 || declaration.value == null) continue
let ast = ValueParser.parse(declaration.value)
let requiresPolyfill = false
walk(ast, (node) => {
if (node.kind !== 'function' || node.value !== 'color-mix') return
let containsUnresolvableVars = false
let containsCurrentcolor = false
walk(node.nodes, (node) => {
if (node.kind == 'word' && node.value.toLowerCase() === 'currentcolor') {
containsCurrentcolor = true
requiresPolyfill = true
return
}
let varNode: ValueParser.ValueAstNode | null = node
let inlinedColor: string | null = null
let seenVariables = new Set<string>()
do {
if (varNode.kind !== 'function' || varNode.value !== 'var') return
let firstChild = varNode.nodes[0]
if (!firstChild || firstChild.kind !== 'word') return
let variableName = firstChild.value
if (seenVariables.has(variableName)) {
containsUnresolvableVars = true
return
}
seenVariables.add(variableName)
requiresPolyfill = true
inlinedColor = designSystem.theme.resolveValue(null, [firstChild.value as any])
if (!inlinedColor) {
containsUnresolvableVars = true
return
}
if (inlinedColor.toLowerCase() === 'currentcolor') {
containsCurrentcolor = true
return
}
if (inlinedColor.startsWith('var(')) {
let subAst = ValueParser.parse(inlinedColor)
varNode = subAst[0]
} else {
varNode = null
}
} while (varNode)
return WalkAction.Replace({ kind: 'word', value: inlinedColor } as const)
})
if (containsUnresolvableVars || containsCurrentcolor) {
let separatorIndex = node.nodes.findIndex(
(node) => node.kind === 'separator' && node.value.trim().includes(','),
)
if (separatorIndex === -1) return
let firstColorValue =
node.nodes.length > separatorIndex ? node.nodes[separatorIndex + 1] : null
if (!firstColorValue) return
return WalkAction.Replace(firstColorValue)
} else if (requiresPolyfill) {
// Change the colorspace to `srgb` since the fallback values should not be represented as
// `oklab(…)` functions again as their support in Safari <16 is very limited.
let colorspace = node.nodes[2]
if (
colorspace.kind === 'word' &&
(colorspace.value === 'oklab' ||
colorspace.value === 'oklch' ||
colorspace.value === 'lab' ||
colorspace.value === 'lch')
) {
colorspace.value = 'srgb'
}
}
})
if (!requiresPolyfill) continue
let fallback = {
...declaration,
value: ValueParser.toCss(ast),
}
let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [declaration])
colorMixQuery.src = declaration.src
parent.splice(idx, 1, fallback, colorMixQuery)
}
}
}
if (polyfills & Polyfills.AtProperty) {
let fallbackAst = []
if (propertyFallbacksRoot.length > 0) {
let wrapper = rule(':root, :host', propertyFallbacksRoot)
wrapper.src = propertyFallbacksRoot[0].src
fallbackAst.push(wrapper)
}
if (propertyFallbacksUniversal.length > 0) {
let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)
wrapper.src = propertyFallbacksUniversal[0].src
fallbackAst.push(wrapper)
}
if (fallbackAst.length > 0) {
// Insert an empty `@layer properties;` at the beginning of the document. We need to place it
// after eventual external imports as `lightningcss` would otherwise move the content into the
// same place.
let firstValidNodeIndex = newAst.findIndex((node) => {
// License comments
if (node.kind === 'comment') return false
if (node.kind === 'at-rule') {
// Charset
if (node.name === '@charset') return false
// External imports
if (node.name === '@import') return false
}
return true
})
let layerPropertiesStatement = atRule('@layer', 'properties', [])
layerPropertiesStatement.src = fallbackAst[0].src
newAst.splice(
firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex,
0,
layerPropertiesStatement,
)
let block = rule('@layer properties', [
atRule(
'@supports',
// We can't write a supports query for `@property` directly so we have to test for
// features that are added around the same time in Mozilla and Safari.
'((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))',
fallbackAst,
),
])
block.src = fallbackAst[0].src
block.nodes[0].src = fallbackAst[0].src
newAst.push(block)
}
}
return newAst
}
Domain
Subdomains
Calls
Called By
Source
Frequently Asked Questions
What does optimizeAst() do?
optimizeAst() is a function in the tailwindcss codebase.
What does optimizeAst() call?
optimizeAst() calls 14 function(s): add, atRule, decl, extractKeyframeNames, extractUsedVariables, findNode, get, isVariableUsed, and 6 more.
What calls optimizeAst()?
optimizeAst() is called by 2 function(s): buildDesignSystem, compileAst.
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free