From 1e95291b96e64c9deb27c5529c1dd8f9cfaa5191 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 18:16:18 +0200 Subject: [PATCH 1/7] add `cloneAstNode` helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our AST nodes are simple plain objects with a `kind` as the discriminant instead of of an actual class instance. Since we're dealing with simple plain objects, there is no need for a heavy handed `structuredClone` and we can use this function instead. Also added some benchmarks which you can run to see how much the difference is. On my M1 Pro Max while using battery, these are the results: ``` ✓ Cloning AST nodes (2) 2462ms name hz min max mean p75 p99 p995 p999 rme samples · cloneAstNode() 5,769,838.55 0.0000 0.0467 0.0002 0.0002 0.0004 0.0004 0.0005 ±0.09% 2884920 fastest · structuredClone() 222,598.38 0.0042 4.5958 0.0045 0.0045 0.0048 0.0049 0.0100 ±1.81% 111300 BENCH Summary cloneAstNode() - ast.bench.ts > Cloning AST nodes 25.92x faster than structuredClone() ``` --- packages/tailwindcss/src/apply.ts | 4 ++-- packages/tailwindcss/src/ast.bench.ts | 24 ++++++++++++++----- packages/tailwindcss/src/ast.ts | 20 ++++++++++++++++ packages/tailwindcss/src/compat/container.ts | 4 ++-- packages/tailwindcss/src/compat/plugin-api.ts | 4 ++-- packages/tailwindcss/src/utilities.ts | 5 ++-- packages/tailwindcss/src/variants.ts | 3 ++- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 526a5e000900..1c4223e0fbec 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -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 diff --git a/packages/tailwindcss/src/ast.bench.ts b/packages/tailwindcss/src/ast.bench.ts index a2d8b28920f2..0091561d7c1a 100644 --- a/packages/tailwindcss/src/ast.bench.ts +++ b/packages/tailwindcss/src/ast.bench.ts @@ -1,5 +1,5 @@ -import { bench } from 'vitest' -import { toCss } from './ast' +import { bench, describe } from 'vitest' +import { cloneAstNode, toCss } from './ast' import * as CSS from './css-parser' const css = String.raw @@ -19,10 +19,22 @@ 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('structuredClone()', () => { + structuredClone(ast) + }) }) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 1ca7b7d6233b..15a3a43da926 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -122,6 +122,26 @@ export function atRoot(nodes: AstNode[]): AtRoot { } } +export function cloneAstNode(node: T): T { + switch (node.kind) { + case 'rule': + case 'at-rule': + case 'at-root': + return { ...node, nodes: node.nodes.map(cloneAstNode) } + + case 'context': + return { ...node, context: { ...node.context }, nodes: node.nodes.map(cloneAstNode) } + + case 'declaration': + case 'comment': + return { ...node } + + 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, diff --git a/packages/tailwindcss/src/compat/container.ts b/packages/tailwindcss/src/compat/container.ts index 266d101b0409..98f23ce7a1c6 100644 --- a/packages/tailwindcss/src/compat/container.ts +++ b/packages/tailwindcss/src/compat/container.ts @@ -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( diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index f5444cf5686c..4aca5feeccf1 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -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 diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index efe7b3dc65ad..0ef9907bb31c 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -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)) } } diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 4e5c46894913..0008e1c77c2e 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -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 From b76a57133cd3733020405bcde1fdbfd46197b9a6 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 19:53:08 +0200 Subject: [PATCH 2/7] add `cloneCandidate` and `cloneVariant` helpers These are much faster compared to `structuredClone`. This is again because we're dealing with simple plain objects. --- packages/tailwindcss/src/candidate.bench.ts | 34 +++++++++--- packages/tailwindcss/src/candidate.ts | 54 +++++++++++++++++++ .../src/canonicalize-candidates.ts | 6 ++- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index 5b7171432e17..e8866ecb05e9 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -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 } from './candidate' import { buildDesignSystem } from './design-system' import { Theme } from './theme' @@ -8,13 +8,33 @@ 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) + } + }) }) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index d169ba758cc9..534dd6b0fdb0 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -215,6 +215,60 @@ export type Candidate = raw: string } +export function cloneCandidate(candidate: Candidate): Candidate { + switch (candidate.kind) { + case 'arbitrary': + return { + ...candidate, + variants: candidate.variants.map(cloneVariant), + modifier: candidate.modifier ? { ...candidate.modifier } : null, + } + + case 'static': + return { ...candidate, variants: candidate.variants.map(cloneVariant) } + + case 'functional': + return { + ...candidate, + variants: candidate.variants.map(cloneVariant), + value: candidate.value ? { ...candidate.value } : null, + modifier: candidate.modifier ? { ...candidate.modifier } : null, + } + + default: + candidate satisfies never + throw new Error('Unknown candidate kind') + } +} + +export function cloneVariant(variant: Variant): Variant { + switch (variant.kind) { + case 'arbitrary': + return { ...variant } + + case 'static': + return { ...variant } + + case 'functional': + return { + ...variant, + value: variant.value ? { ...variant.value } : null, + modifier: variant.modifier ? { ...variant.modifier } : null, + } + + case 'compound': + return { + ...variant, + variant: cloneVariant(variant.variant), + modifier: variant.modifier ? { ...variant.modifier } : null, + } + + default: + variant satisfies never + throw new Error('Unknown variant kind') + } +} + export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable { // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index ba6910552ba4..ec07b9511f14 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -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 + let replacement = cloneCandidate(readonlyCandidate) as Writable for (let fn of UTILITY_CANONICALIZATIONS) { replacement = fn(ds, replacement) From 53ca38ec82af3aa540bfa6d9608a5add001eaf11 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 21:54:32 +0200 Subject: [PATCH 3/7] make `cloneAstNode` faster Turns out that if you keep the exact same structure instead of a spread, it's even faster! ``` cloneAstNode() - src/ast.bench.ts > Cloning AST nodes 1.15x faster than cloneAstNode (with spread) 33.54x faster than structuredClone() ``` --- packages/tailwindcss/src/ast.bench.ts | 26 ++++++++++++++- packages/tailwindcss/src/ast.ts | 48 +++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/ast.bench.ts b/packages/tailwindcss/src/ast.bench.ts index 0091561d7c1a..034faee2797e 100644 --- a/packages/tailwindcss/src/ast.bench.ts +++ b/packages/tailwindcss/src/ast.bench.ts @@ -1,5 +1,5 @@ import { bench, describe } from 'vitest' -import { cloneAstNode, toCss } from './ast' +import { cloneAstNode, toCss, type AstNode } from './ast' import * as CSS from './css-parser' const css = String.raw @@ -34,7 +34,31 @@ describe('Cloning AST nodes', () => { ast.map(cloneAstNode) }) + bench('cloneAstNode (with spread)', () => { + ast.map(cloneAstNodeSpread) + }) + bench('structuredClone()', () => { structuredClone(ast) }) }) + +function cloneAstNodeSpread(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}`) + } +} diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 15a3a43da926..e57e1c2a3133 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -125,16 +125,58 @@ export function atRoot(nodes: AstNode[]): AtRoot { export function cloneAstNode(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 { ...node, nodes: node.nodes.map(cloneAstNode) } + return { + kind: node.kind, + nodes: node.nodes.map(cloneAstNode), + src: node.src, + dst: node.dst, + } satisfies AtRoot as T case 'context': - return { ...node, context: { ...node.context }, nodes: node.nodes.map(cloneAstNode) } + 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 { ...node } + return { + kind: node.kind, + value: node.value, + src: node.src, + dst: node.dst, + } satisfies Comment as T default: node satisfies never From 37389638e2cbc14b8fc51503d2228666e15f535d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 21:55:19 +0200 Subject: [PATCH 4/7] make `cloneCandidate` and `cloneVariant` faster ``` cloneCandidate - src/candidate.bench.ts > Candidate cloning 1.72x faster than cloneCandidate (spread) 74.03x faster than structuredClone ``` --- packages/tailwindcss/src/candidate.bench.ts | 56 ++++++++++++++++++- packages/tailwindcss/src/candidate.ts | 61 ++++++++++++++++----- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index e8866ecb05e9..7ac0dbb0a001 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -1,6 +1,6 @@ import { Scanner } from '@tailwindcss/oxide' import { bench, describe } from 'vitest' -import { cloneCandidate, parseCandidate } from './candidate' +import { cloneCandidate, parseCandidate, type Candidate, type Variant } from './candidate' import { buildDesignSystem } from './design-system' import { Theme } from './theme' @@ -37,4 +37,58 @@ describe('Candidate cloning', async () => { 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') + } +} diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 534dd6b0fdb0..c8ce7bc42f87 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -219,20 +219,49 @@ export function cloneCandidate(candidate: Candidate): Candidate { switch (candidate.kind) { case 'arbitrary': return { - ...candidate, + 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), - modifier: candidate.modifier ? { ...candidate.modifier } : null, + important: candidate.important, + raw: candidate.raw, } case 'static': - return { ...candidate, variants: candidate.variants.map(cloneVariant) } + return { + kind: candidate.kind, + root: candidate.root, + variants: candidate.variants.map(cloneVariant), + important: candidate.important, + raw: candidate.raw, + } case 'functional': return { - ...candidate, + 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), - value: candidate.value ? { ...candidate.value } : null, - modifier: candidate.modifier ? { ...candidate.modifier } : null, + important: candidate.important, + raw: candidate.raw, } default: @@ -244,23 +273,29 @@ export function cloneCandidate(candidate: Candidate): Candidate { export function cloneVariant(variant: Variant): Variant { switch (variant.kind) { case 'arbitrary': - return { ...variant } + return { kind: variant.kind, selector: variant.selector, relative: variant.relative } case 'static': - return { ...variant } + return { kind: variant.kind, root: variant.root } case 'functional': return { - ...variant, - value: variant.value ? { ...variant.value } : null, - modifier: variant.modifier ? { ...variant.modifier } : null, + 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, } case 'compound': return { - ...variant, + kind: variant.kind, + root: variant.root, variant: cloneVariant(variant.variant), - modifier: variant.modifier ? { ...variant.modifier } : null, + modifier: variant.modifier + ? { kind: variant.modifier.kind, value: variant.modifier.value } + : null, } default: From 096bf0497a997078d4785f1a65529abb12bac06f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 18:18:11 +0200 Subject: [PATCH 5/7] clone the `string[]` by using a simple slice Strings are immutable, so the only thing we're cloning here is the array itself. --- packages/tailwindcss/src/compat/apply-config-to-theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index c957d320975e..a430f9faa332 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -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' From 74ae2a9d3570a17305f028bd7388b712bf3a2746 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 22:15:11 +0200 Subject: [PATCH 6/7] make `cloneCandidate` and `cloneVariant` generic over `T` This just means that if you pass in an static candidate, that a static candidate comes out. Otherwise an static candidate results in the generic `Candidate` type which is too wide. --- packages/tailwindcss/src/candidate.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index c8ce7bc42f87..a9ba2d6e200f 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -215,7 +215,7 @@ export type Candidate = raw: string } -export function cloneCandidate(candidate: Candidate): Candidate { +export function cloneCandidate(candidate: T): T { switch (candidate.kind) { case 'arbitrary': return { @@ -228,7 +228,7 @@ export function cloneCandidate(candidate: Candidate): Candidate { variants: candidate.variants.map(cloneVariant), important: candidate.important, raw: candidate.raw, - } + } satisfies Extract as T case 'static': return { @@ -237,7 +237,7 @@ export function cloneCandidate(candidate: Candidate): Candidate { variants: candidate.variants.map(cloneVariant), important: candidate.important, raw: candidate.raw, - } + } satisfies Extract as T case 'functional': return { @@ -262,7 +262,7 @@ export function cloneCandidate(candidate: Candidate): Candidate { variants: candidate.variants.map(cloneVariant), important: candidate.important, raw: candidate.raw, - } + } satisfies Extract as T default: candidate satisfies never @@ -270,13 +270,20 @@ export function cloneCandidate(candidate: Candidate): Candidate { } } -export function cloneVariant(variant: Variant): Variant { +export function cloneVariant(variant: T): T { switch (variant.kind) { case 'arbitrary': - return { kind: variant.kind, selector: variant.selector, relative: variant.relative } + return { + kind: variant.kind, + selector: variant.selector, + relative: variant.relative, + } satisfies Extract as T case 'static': - return { kind: variant.kind, root: variant.root } + return { kind: variant.kind, root: variant.root } satisfies Extract< + Variant, + { kind: 'static' } + > as T case 'functional': return { @@ -286,7 +293,7 @@ export function cloneVariant(variant: Variant): Variant { modifier: variant.modifier ? { kind: variant.modifier.kind, value: variant.modifier.value } : null, - } + } satisfies Extract as T case 'compound': return { @@ -296,7 +303,7 @@ export function cloneVariant(variant: Variant): Variant { modifier: variant.modifier ? { kind: variant.modifier.kind, value: variant.modifier.value } : null, - } + } satisfies Extract as T default: variant satisfies never From 7e26cfcb6e4e64ffb45119d0855b5a4e50c642ba Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 4 Oct 2025 22:16:18 +0200 Subject: [PATCH 7/7] use `cloneCandidate` instead of `structuredClone` --- .../@tailwindcss-upgrade/src/codemods/template/candidates.ts | 4 ++-- .../src/codemods/template/migrate-arbitrary-variants.ts | 3 ++- .../src/codemods/template/migrate-automatic-var-injection.ts | 5 +++-- .../codemods/template/migrate-camelcase-in-named-value.ts | 3 ++- .../src/codemods/template/migrate-legacy-arbitrary-values.ts | 4 ++-- .../src/codemods/template/migrate-legacy-classes.ts | 4 ++-- .../codemods/template/migrate-modernize-arbitrary-values.ts | 4 ++-- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts index f873c1e10a9c..88b1e4e0307f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts @@ -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(candidate: T) { - let base = structuredClone(candidate) + let base = cloneCandidate(candidate) base.important = false base.variants = [] diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts index fb1e9948e963..9d8b176bd858 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts @@ -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 + let candidate = cloneCandidate(readonlyCandidate) as Writable for (let [variant] of walkVariants(candidate)) { if (variant.kind === 'compound') continue diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts index 586d8091daf8..13b2c04499b9 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts @@ -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 let didChange = false diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts index 2214a8366cef..9cfbc7de081f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts @@ -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 ( diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts index 89afbb21ea79..2bd7d2fd55c5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts @@ -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] diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts index fa425574c882..614b4017b42b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index cafae7aedb3a..91718b323242 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -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)) {