Home / Function/ optimizeAst() — tailwindcss Function Reference

optimizeAst() — tailwindcss Function Reference

Architecture documentation for the optimizeAst() function in ast.ts from the tailwindcss codebase.

Entity Profile

Dependency Diagram

graph TD
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0["optimizeAst()"]
  c58d3214_88d6_f4fc_257f_8e84def5b24f["buildDesignSystem()"]
  c58d3214_88d6_f4fc_257f_8e84def5b24f -->|calls| a6e11c3d_c962_0a65_d91f_6fbe955cf4f0
  f5e443d2_a934_36af_23f2_b1c002aaa585["compileAst()"]
  f5e443d2_a934_36af_23f2_b1c002aaa585 -->|calls| a6e11c3d_c962_0a65_d91f_6fbe955cf4f0
  24d95be4_356f_a1f9_9702_2a4f413db0f5["add()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| 24d95be4_356f_a1f9_9702_2a4f413db0f5
  6dbfaefd_8d1b_d78e_592a_db6ff37aec55["extractUsedVariables()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| 6dbfaefd_8d1b_d78e_592a_db6ff37aec55
  a9926afb_c973_eef8_a5d9_c99c6e47d643["extractKeyframeNames()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| a9926afb_c973_eef8_a5d9_c99c6e47d643
  47ef7bd7_b959_59c4_dbd1_328f35d7cd89["decl()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| 47ef7bd7_b959_59c4_dbd1_328f35d7cd89
  eebd49be_6bf6_0e23_60fa_4e12a1151233["isVariableUsed()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| eebd49be_6bf6_0e23_60fa_4e12a1151233
  67d7a4a7_43e7_6396_2f54_c4a22ca2291b["prefixKey()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| 67d7a4a7_43e7_6396_2f54_c4a22ca2291b
  a9d656ee_4882_d7f1_c98a_c18a84b72f3f["findNode()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| a9d656ee_4882_d7f1_c98a_c18a84b72f3f
  a32bba76_f60d_883f_1ff1_276a0bb9db9f["walk()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| a32bba76_f60d_883f_1ff1_276a0bb9db9f
  f8828bcc_a9b4_73c4_863d_9b59d159d819["resolveValue()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| f8828bcc_a9b4_73c4_863d_9b59d159d819
  e7a34553_0273_6202_4792_07409e33d8f0["toCss()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| e7a34553_0273_6202_4792_07409e33d8f0
  f4f92a3d_c13e_a751_8402_451ffa4c772f["rule()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| f4f92a3d_c13e_a751_8402_451ffa4c772f
  c35acfc6_964d_737e_6ecc_275e6f10293a["atRule()"]
  a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 -->|calls| c35acfc6_964d_737e_6ecc_275e6f10293a
  style a6e11c3d_c962_0a65_d91f_6fbe955cf4f0 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

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