Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 = []
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/tailwindcss/src/apply.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down
48 changes: 42 additions & 6 deletions packages/tailwindcss/src/ast.bench.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}`)
}
}
62 changes: 62 additions & 0 deletions packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
88 changes: 81 additions & 7 deletions packages/tailwindcss/src/candidate.bench.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,94 @@
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'

// FOLDER=path/to/folder vitest bench
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')
}
}
Loading