Improve performance of cloning AST nodes (#19067)
This PR improves the performance of when we need to clone some AST
nodes. We have a few places where we clone `Candidate`, `Variant` and
CSS `AST` nodes.
Right now we use `structuredClone`, which works, but it is a generic
solution. However, we do know the exact structure of these AST nodes, so
we can write specialized clone functions that are much faster.
## Test plan
1. All the tests still pass with this change
2. The performance is better:
```
cloneCandidate - src/candidate.bench.ts > Candidate cloning
1.72x faster than cloneCandidate (spread)
74.03x faster than structuredClone
cloneAstNode() - src/ast.bench.ts > Cloning AST nodes
1.15x faster than cloneAstNode (with spread)
33.54x faster than structuredClone()
```
Ready for review, but should be merged after #19059
This commit is contained in:
parent
b77971f754
commit
efe084b7e7
18 changed files with 312 additions and 37 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import type { Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
|
||||
export async function extractRawCandidates(
|
||||
|
|
@ -18,7 +18,7 @@ export async function extractRawCandidates(
|
|||
|
||||
// Create a basic stripped candidate without variants or important flag
|
||||
export function baseCandidate<T extends Candidate>(candidate: T) {
|
||||
let base = structuredClone(candidate)
|
||||
let base = cloneCandidate(candidate)
|
||||
|
||||
base.important = false
|
||||
base.variants = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { cloneCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import {
|
||||
|
|
@ -22,7 +23,7 @@ export function migrateArbitraryVariants(
|
|||
|
||||
// The below logic makes use of mutation. Since candidates in the
|
||||
// DesignSystem are cached, we can't mutate them directly.
|
||||
let candidate = structuredClone(readonlyCandidate) as Writable<typeof readonlyCandidate>
|
||||
let candidate = cloneCandidate(readonlyCandidate) as Writable<typeof readonlyCandidate>
|
||||
|
||||
for (let [variant] of walkVariants(candidate)) {
|
||||
if (variant.kind === 'compound') continue
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { walk, WalkAction } from '../../../../tailwindcss/src/ast'
|
||||
import { type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
|
||||
import { cloneCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import type { Writable } from '../../../../tailwindcss/src/types'
|
||||
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
|
||||
|
||||
export function migrateAutomaticVarInjection(
|
||||
|
|
@ -12,7 +13,7 @@ export function migrateAutomaticVarInjection(
|
|||
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
|
||||
// The below logic makes extended use of mutation. Since candidates in the
|
||||
// DesignSystem are cached, we can't mutate them directly.
|
||||
let candidate = structuredClone(readonlyCandidate) as Candidate
|
||||
let candidate = cloneCandidate(readonlyCandidate) as Writable<Candidate>
|
||||
|
||||
let didChange = false
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { cloneCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import * as version from '../../utils/version'
|
||||
|
|
@ -15,7 +16,7 @@ export function migrateCamelcaseInNamedValue(
|
|||
|
||||
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
|
||||
if (candidate.kind !== 'functional') continue
|
||||
let clone = structuredClone(candidate)
|
||||
let clone = cloneCandidate(candidate)
|
||||
let didChange = false
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import { cloneCandidate, parseCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { segment } from '../../../../tailwindcss/src/utils/segment'
|
||||
|
|
@ -9,7 +9,7 @@ export function migrateLegacyArbitraryValues(
|
|||
rawCandidate: string,
|
||||
): string {
|
||||
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
|
||||
let clone = structuredClone(candidate)
|
||||
let clone = cloneCandidate(candidate)
|
||||
let changed = false
|
||||
|
||||
// Convert commas to spaces. E.g.: [auto,1fr] to [auto_1fr]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
|
||||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
import type { Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
|
|
@ -100,7 +100,7 @@ export async function migrateLegacyClasses(
|
|||
|
||||
// Re-apply the variants and important flag from the original candidate.
|
||||
// E.g.: `hover:blur!` -> `blur` -> `blur-sm` -> `hover:blur-sm!`
|
||||
let newCandidate = structuredClone(newBaseCandidate) as Candidate
|
||||
let newCandidate = cloneCandidate(newBaseCandidate) as Candidate
|
||||
newCandidate.variants = candidate.variants
|
||||
newCandidate.important = candidate.important
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import { cloneCandidate, parseCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { replaceObject } from '../../../../tailwindcss/src/utils/replace-object'
|
||||
|
|
@ -10,7 +10,7 @@ export function migrateModernizeArbitraryValues(
|
|||
rawCandidate: string,
|
||||
): string {
|
||||
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
|
||||
let clone = structuredClone(candidate)
|
||||
let clone = cloneCandidate(candidate)
|
||||
let changed = false
|
||||
|
||||
for (let [variant] of walkVariants(clone)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Features } from '.'
|
||||
import { rule, toCss, walk, WalkAction, type AstNode } from './ast'
|
||||
import { cloneAstNode, rule, toCss, walk, WalkAction, type AstNode } from './ast'
|
||||
import { compileCandidates } from './compile'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import type { SourceLocation } from './source-maps/source'
|
||||
|
|
@ -249,7 +249,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
|
|||
let candidate = compiled.nodeSorting.get(node)?.candidate
|
||||
let candidateOffset = candidate ? candidateOffsets[candidate] : undefined
|
||||
|
||||
node = structuredClone(node)
|
||||
node = cloneAstNode(node)
|
||||
|
||||
if (!src || !candidate || candidateOffset === undefined) {
|
||||
// While the original nodes may have come from an `@utility` we still
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { bench } from 'vitest'
|
||||
import { toCss } from './ast'
|
||||
import { bench, describe } from 'vitest'
|
||||
import { cloneAstNode, toCss, type AstNode } from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
|
||||
const css = String.raw
|
||||
|
|
@ -19,10 +19,46 @@ const input = css`
|
|||
`
|
||||
const ast = CSS.parse(input)
|
||||
|
||||
bench('toCss', () => {
|
||||
toCss(ast)
|
||||
describe('AST to CSS', () => {
|
||||
bench('toCss', () => {
|
||||
toCss(ast)
|
||||
})
|
||||
|
||||
bench('toCss with source maps', () => {
|
||||
toCss(ast, true)
|
||||
})
|
||||
})
|
||||
|
||||
bench('toCss with source maps', () => {
|
||||
toCss(ast, true)
|
||||
describe('Cloning AST nodes', () => {
|
||||
bench('cloneAstNode()', () => {
|
||||
ast.map(cloneAstNode)
|
||||
})
|
||||
|
||||
bench('cloneAstNode (with spread)', () => {
|
||||
ast.map(cloneAstNodeSpread)
|
||||
})
|
||||
|
||||
bench('structuredClone()', () => {
|
||||
structuredClone(ast)
|
||||
})
|
||||
})
|
||||
|
||||
function cloneAstNodeSpread<T extends AstNode>(node: T): T {
|
||||
switch (node.kind) {
|
||||
case 'rule':
|
||||
case 'at-rule':
|
||||
case 'at-root':
|
||||
return { ...node, nodes: node.nodes.map(cloneAstNodeSpread) }
|
||||
|
||||
case 'context':
|
||||
return { ...node, context: { ...node.context }, nodes: node.nodes.map(cloneAstNodeSpread) }
|
||||
|
||||
case 'declaration':
|
||||
case 'comment':
|
||||
return { ...node }
|
||||
|
||||
default:
|
||||
node satisfies never
|
||||
throw new Error(`Unknown node kind: ${(node as any).kind}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,68 @@ export function atRoot(nodes: AstNode[]): AtRoot {
|
|||
}
|
||||
}
|
||||
|
||||
export function cloneAstNode<T extends AstNode>(node: T): T {
|
||||
switch (node.kind) {
|
||||
case 'rule':
|
||||
return {
|
||||
kind: node.kind,
|
||||
selector: node.selector,
|
||||
nodes: node.nodes.map(cloneAstNode),
|
||||
src: node.src,
|
||||
dst: node.dst,
|
||||
} satisfies StyleRule as T
|
||||
|
||||
case 'at-rule':
|
||||
return {
|
||||
kind: node.kind,
|
||||
name: node.name,
|
||||
params: node.params,
|
||||
nodes: node.nodes.map(cloneAstNode),
|
||||
src: node.src,
|
||||
dst: node.dst,
|
||||
} satisfies AtRule as T
|
||||
|
||||
case 'at-root':
|
||||
return {
|
||||
kind: node.kind,
|
||||
nodes: node.nodes.map(cloneAstNode),
|
||||
src: node.src,
|
||||
dst: node.dst,
|
||||
} satisfies AtRoot as T
|
||||
|
||||
case 'context':
|
||||
return {
|
||||
kind: node.kind,
|
||||
context: { ...node.context },
|
||||
nodes: node.nodes.map(cloneAstNode),
|
||||
src: node.src,
|
||||
dst: node.dst,
|
||||
} satisfies Context as T
|
||||
|
||||
case 'declaration':
|
||||
return {
|
||||
kind: node.kind,
|
||||
property: node.property,
|
||||
value: node.value,
|
||||
important: node.important,
|
||||
src: node.src,
|
||||
dst: node.dst,
|
||||
} satisfies Declaration as T
|
||||
|
||||
case 'comment':
|
||||
return {
|
||||
kind: node.kind,
|
||||
value: node.value,
|
||||
src: node.src,
|
||||
dst: node.dst,
|
||||
} satisfies Comment as T
|
||||
|
||||
default:
|
||||
node satisfies never
|
||||
throw new Error(`Unknown node kind: ${(node as any).kind}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const enum WalkAction {
|
||||
/** Continue walking, which is the default */
|
||||
Continue,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import { bench } from 'vitest'
|
||||
import { parseCandidate } from './candidate'
|
||||
import { bench, describe } from 'vitest'
|
||||
import { cloneCandidate, parseCandidate, type Candidate, type Variant } from './candidate'
|
||||
import { buildDesignSystem } from './design-system'
|
||||
import { Theme } from './theme'
|
||||
|
||||
|
|
@ -8,13 +8,87 @@ import { Theme } from './theme'
|
|||
const root = process.env.FOLDER || process.cwd()
|
||||
|
||||
// Auto content detection
|
||||
const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*' }] })
|
||||
const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*', negated: false }] })
|
||||
|
||||
const candidates = scanner.scan()
|
||||
const designSystem = buildDesignSystem(new Theme())
|
||||
|
||||
bench('parseCandidate', () => {
|
||||
for (let candidate of candidates) {
|
||||
Array.from(parseCandidate(candidate, designSystem))
|
||||
}
|
||||
describe('parsing', () => {
|
||||
bench('parseCandidate', () => {
|
||||
for (let candidate of candidates) {
|
||||
Array.from(parseCandidate(candidate, designSystem))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Candidate cloning', async () => {
|
||||
let parsedCanddiates = candidates.flatMap((candidate) =>
|
||||
Array.from(parseCandidate(candidate, designSystem)),
|
||||
)
|
||||
|
||||
bench('structuredClone', () => {
|
||||
for (let candidate of parsedCanddiates) {
|
||||
structuredClone(candidate)
|
||||
}
|
||||
})
|
||||
|
||||
bench('cloneCandidate', () => {
|
||||
for (let candidate of parsedCanddiates) {
|
||||
cloneCandidate(candidate)
|
||||
}
|
||||
})
|
||||
|
||||
bench('cloneCandidate (spread)', () => {
|
||||
for (let candidate of parsedCanddiates) {
|
||||
cloneCandidateSpread(candidate)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function cloneCandidateSpread(candidate: Candidate): Candidate {
|
||||
switch (candidate.kind) {
|
||||
case 'arbitrary':
|
||||
return {
|
||||
...candidate,
|
||||
modifier: candidate.modifier ? { ...candidate.modifier } : null,
|
||||
variants: candidate.variants.map(cloneVariantSpread),
|
||||
}
|
||||
|
||||
case 'static':
|
||||
return { ...candidate, variants: candidate.variants.map(cloneVariantSpread) }
|
||||
|
||||
case 'functional':
|
||||
return {
|
||||
...candidate,
|
||||
value: candidate.value ? { ...candidate.value } : null,
|
||||
modifier: candidate.modifier ? { ...candidate.modifier } : null,
|
||||
variants: candidate.variants.map(cloneVariantSpread),
|
||||
}
|
||||
|
||||
default:
|
||||
candidate satisfies never
|
||||
throw new Error('Unknown candidate kind')
|
||||
}
|
||||
}
|
||||
|
||||
function cloneVariantSpread(variant: Variant): Variant {
|
||||
switch (variant.kind) {
|
||||
case 'arbitrary':
|
||||
case 'static':
|
||||
return { ...variant }
|
||||
|
||||
case 'functional':
|
||||
return { ...variant, modifier: variant.modifier ? { ...variant.modifier } : null }
|
||||
|
||||
case 'compound':
|
||||
return {
|
||||
...variant,
|
||||
variant: cloneVariantSpread(variant.variant),
|
||||
modifier: variant.modifier ? { ...variant.modifier } : null,
|
||||
}
|
||||
|
||||
default:
|
||||
variant satisfies never
|
||||
throw new Error('Unknown variant kind')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,102 @@ export type Candidate =
|
|||
raw: string
|
||||
}
|
||||
|
||||
export function cloneCandidate<T extends Candidate>(candidate: T): T {
|
||||
switch (candidate.kind) {
|
||||
case 'arbitrary':
|
||||
return {
|
||||
kind: candidate.kind,
|
||||
property: candidate.property,
|
||||
value: candidate.value,
|
||||
modifier: candidate.modifier
|
||||
? { kind: candidate.modifier.kind, value: candidate.modifier.value }
|
||||
: null,
|
||||
variants: candidate.variants.map(cloneVariant),
|
||||
important: candidate.important,
|
||||
raw: candidate.raw,
|
||||
} satisfies Extract<Candidate, { kind: 'arbitrary' }> as T
|
||||
|
||||
case 'static':
|
||||
return {
|
||||
kind: candidate.kind,
|
||||
root: candidate.root,
|
||||
variants: candidate.variants.map(cloneVariant),
|
||||
important: candidate.important,
|
||||
raw: candidate.raw,
|
||||
} satisfies Extract<Candidate, { kind: 'static' }> as T
|
||||
|
||||
case 'functional':
|
||||
return {
|
||||
kind: candidate.kind,
|
||||
root: candidate.root,
|
||||
value: candidate.value
|
||||
? candidate.value.kind === 'arbitrary'
|
||||
? {
|
||||
kind: candidate.value.kind,
|
||||
dataType: candidate.value.dataType,
|
||||
value: candidate.value.value,
|
||||
}
|
||||
: {
|
||||
kind: candidate.value.kind,
|
||||
value: candidate.value.value,
|
||||
fraction: candidate.value.fraction,
|
||||
}
|
||||
: null,
|
||||
modifier: candidate.modifier
|
||||
? { kind: candidate.modifier.kind, value: candidate.modifier.value }
|
||||
: null,
|
||||
variants: candidate.variants.map(cloneVariant),
|
||||
important: candidate.important,
|
||||
raw: candidate.raw,
|
||||
} satisfies Extract<Candidate, { kind: 'functional' }> as T
|
||||
|
||||
default:
|
||||
candidate satisfies never
|
||||
throw new Error('Unknown candidate kind')
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneVariant<T extends Variant>(variant: T): T {
|
||||
switch (variant.kind) {
|
||||
case 'arbitrary':
|
||||
return {
|
||||
kind: variant.kind,
|
||||
selector: variant.selector,
|
||||
relative: variant.relative,
|
||||
} satisfies Extract<Variant, { kind: 'arbitrary' }> as T
|
||||
|
||||
case 'static':
|
||||
return { kind: variant.kind, root: variant.root } satisfies Extract<
|
||||
Variant,
|
||||
{ kind: 'static' }
|
||||
> as T
|
||||
|
||||
case 'functional':
|
||||
return {
|
||||
kind: variant.kind,
|
||||
root: variant.root,
|
||||
value: variant.value ? { kind: variant.value.kind, value: variant.value.value } : null,
|
||||
modifier: variant.modifier
|
||||
? { kind: variant.modifier.kind, value: variant.modifier.value }
|
||||
: null,
|
||||
} satisfies Extract<Variant, { kind: 'functional' }> as T
|
||||
|
||||
case 'compound':
|
||||
return {
|
||||
kind: variant.kind,
|
||||
root: variant.root,
|
||||
variant: cloneVariant(variant.variant),
|
||||
modifier: variant.modifier
|
||||
? { kind: variant.modifier.kind, value: variant.modifier.value }
|
||||
: null,
|
||||
} satisfies Extract<Variant, { kind: 'compound' }> as T
|
||||
|
||||
default:
|
||||
variant satisfies never
|
||||
throw new Error('Unknown variant kind')
|
||||
}
|
||||
}
|
||||
|
||||
export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable<Candidate> {
|
||||
// hover:focus:underline
|
||||
// ^^^^^ ^^^^^^ -> Variants
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as AttributeSelectorParser from './attribute-selector-parser'
|
||||
import {
|
||||
cloneCandidate,
|
||||
cloneVariant,
|
||||
printModifier,
|
||||
type Candidate,
|
||||
type CandidateModifier,
|
||||
|
|
@ -107,7 +109,7 @@ const canonicalizeVariantCache = new DefaultMap((ds: DesignSystem) => {
|
|||
for (let current of replacement.splice(0)) {
|
||||
// A single variant can result in multiple variants, e.g.:
|
||||
// `[&>[data-selected]]:flex` → `*:data-selected:flex`
|
||||
let result = fn(ds, structuredClone(current))
|
||||
let result = fn(ds, cloneVariant(current))
|
||||
if (Array.isArray(result)) {
|
||||
replacement.push(...result)
|
||||
continue
|
||||
|
|
@ -134,7 +136,7 @@ const UTILITY_CANONICALIZATIONS = [
|
|||
const canonicalizeUtilityCache = new DefaultMap((ds: DesignSystem) => {
|
||||
return new DefaultMap((rawCandidate: string): string => {
|
||||
for (let readonlyCandidate of ds.parseCandidate(rawCandidate)) {
|
||||
let replacement = structuredClone(readonlyCandidate) as Writable<typeof readonlyCandidate>
|
||||
let replacement = cloneCandidate(readonlyCandidate) as Writable<typeof readonlyCandidate>
|
||||
|
||||
for (let fn of UTILITY_CANONICALIZATIONS) {
|
||||
replacement = fn(ds, replacement)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ export function keyPathToCssProperty(path: string[]) {
|
|||
// The legacy container component config should not be included in the Theme
|
||||
if (path[0] === 'container') return null
|
||||
|
||||
path = structuredClone(path)
|
||||
path = path.slice()
|
||||
|
||||
if (path[0] === 'animation') path[0] = 'animate'
|
||||
if (path[0] === 'aspectRatio') path[0] = 'aspect'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { atRule, decl, type AstNode, type AtRule } from '../ast'
|
||||
import { atRule, cloneAstNode, decl, type AstNode, type AtRule } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import { compareBreakpoints } from '../utils/compare-breakpoints'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
|
|
@ -16,7 +16,7 @@ export function registerContainerCompat(userConfig: ResolvedConfig, designSystem
|
|||
return
|
||||
}
|
||||
|
||||
designSystem.utilities.static('container', () => structuredClone(rules))
|
||||
designSystem.utilities.static('container', () => rules.map(cloneAstNode))
|
||||
}
|
||||
|
||||
export function buildCustomContainerUtilityRules(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Features } from '..'
|
||||
import { substituteAtApply } from '../apply'
|
||||
import { atRule, decl, rule, walk, type AstNode } from '../ast'
|
||||
import { atRule, cloneAstNode, decl, rule, walk, type AstNode } from '../ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate'
|
||||
import { substituteFunctions } from '../css-functions'
|
||||
import * as CSS from '../css-parser'
|
||||
|
|
@ -329,7 +329,7 @@ export function buildPluginApi({
|
|||
}
|
||||
|
||||
designSystem.utilities.static(className, (candidate) => {
|
||||
let clonedAst = structuredClone(ast)
|
||||
let clonedAst = ast.map(cloneAstNode)
|
||||
replaceNestedClassNameReferences(clonedAst, className, candidate.raw)
|
||||
featuresRef.current |= substituteAtApply(clonedAst, designSystem)
|
||||
return clonedAst
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
atRoot,
|
||||
atRule,
|
||||
cloneAstNode,
|
||||
decl,
|
||||
rule,
|
||||
styleRule,
|
||||
|
|
@ -6008,7 +6009,7 @@ export function createCssUtility(node: AtRule) {
|
|||
})
|
||||
|
||||
designSystem.utilities.functional(name.slice(0, -2), (candidate) => {
|
||||
let atRule = structuredClone(node)
|
||||
let atRule = cloneAstNode(node)
|
||||
|
||||
let value = candidate.value
|
||||
let modifier = candidate.modifier
|
||||
|
|
@ -6176,7 +6177,7 @@ export function createCssUtility(node: AtRule) {
|
|||
|
||||
if (IS_VALID_STATIC_UTILITY_NAME.test(name)) {
|
||||
return (designSystem: DesignSystem) => {
|
||||
designSystem.utilities.static(name, () => structuredClone(node.nodes))
|
||||
designSystem.utilities.static(name, () => node.nodes.map(cloneAstNode))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
WalkAction,
|
||||
atRoot,
|
||||
atRule,
|
||||
cloneAstNode,
|
||||
decl,
|
||||
rule,
|
||||
styleRule,
|
||||
|
|
@ -100,7 +101,7 @@ export class Variants {
|
|||
this.static(
|
||||
name,
|
||||
(r) => {
|
||||
let body = structuredClone(ast)
|
||||
let body = ast.map(cloneAstNode)
|
||||
if (usesAtVariant) substituteAtVariant(body, designSystem)
|
||||
substituteAtSlot(body, r.nodes)
|
||||
r.nodes = body
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue