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)) { 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..034faee2797e 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, 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(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 1ca7b7d6233b..e57e1c2a3133 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -122,6 +122,68 @@ 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 { + 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, diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index 5b7171432e17..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 } 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') + } +} diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index d169ba758cc9..a9ba2d6e200f 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -215,6 +215,102 @@ export type Candidate = raw: string } +export function cloneCandidate(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 as T + + case 'static': + return { + kind: candidate.kind, + root: candidate.root, + variants: candidate.variants.map(cloneVariant), + important: candidate.important, + raw: candidate.raw, + } satisfies Extract 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 as T + + default: + candidate satisfies never + throw new Error('Unknown candidate kind') + } +} + +export function cloneVariant(variant: T): T { + switch (variant.kind) { + case 'arbitrary': + return { + kind: variant.kind, + selector: variant.selector, + relative: variant.relative, + } satisfies Extract 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 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 as T + + 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) 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' 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