move cached parsers to designSystem

This commit is contained in:
Robin Malfait 2024-03-08 00:25:06 +01:00
parent d04a17c102
commit 115417fed4
No known key found for this signature in database
7 changed files with 137 additions and 176 deletions

View file

@ -1,9 +1,8 @@
import { scanDir } from '@tailwindcss/oxide'
import { bench } from 'vitest'
import { parseCandidate, parseVariant } from './candidate'
import { parseCandidate } from './candidate'
import { buildDesignSystem } from './design-system'
import { Theme } from './theme'
import { DefaultMap } from './utils/default-map'
// FOLDER=path/to/folder vitest bench
const root = process.env.FOLDER || process.cwd()
@ -15,10 +14,6 @@ const designSystem = buildDesignSystem(new Theme())
bench('parseCandidate', () => {
for (let candidate of result.candidates) {
parseCandidate(
candidate,
designSystem.utilities,
new DefaultMap((variant, map) => parseVariant(variant, designSystem.variants, map)),
)
parseCandidate(candidate, designSystem)
}
})

View file

@ -1,7 +1,7 @@
import { expect, it } from 'vitest'
import { parseCandidate, parseVariant } from './candidate'
import { buildDesignSystem } from './design-system'
import { Theme } from './theme'
import { Utilities } from './utilities'
import { DefaultMap } from './utils/default-map'
import { Variants } from './variants'
function run(
@ -11,11 +11,12 @@ function run(
utilities ??= new Utilities()
variants ??= new Variants()
let parsedVariants = new DefaultMap((variant, map) => {
return parseVariant(variant, variants!, map)
})
let designSystem = buildDesignSystem(new Theme())
return parseCandidate(candidate, utilities, parsedVariants)
designSystem.utilities = utilities
designSystem.variants = variants
return designSystem.parseCandidate(candidate)
}
it('should skip unknown utilities', () => {

View file

@ -1,3 +1,4 @@
import type { DesignSystem } from './design-system'
import { decodeArbitraryValue } from './utils/decode-arbitrary-value'
import { segment } from './utils/segment'
@ -206,14 +207,7 @@ export type Candidate =
important: boolean
}
export function parseCandidate(
input: string,
utilities: {
has: (value: string) => boolean
kind: (root: string) => Omit<Candidate['kind'], 'arbitrary'>
},
parsedVariants: { get: (value: string) => Variant | null },
): Candidate | null {
export function parseCandidate(input: string, designSystem: DesignSystem): Candidate | null {
// hover:focus:underline
// ^^^^^ ^^^^^^ -> Variants
// ^^^^^^^^^ -> Base
@ -228,7 +222,7 @@ export function parseCandidate(
let parsedCandidateVariants: Variant[] = []
for (let variant of rawVariants) {
let parsedVariant = parsedVariants.get(variant)
let parsedVariant = designSystem.parseVariant(variant)
if (parsedVariant === null) return null
// Variants are applied left-to-right meaning that any representing pseudo-
@ -320,7 +314,7 @@ export function parseCandidate(
base = base.slice(1)
}
let [root, value] = findRoot(base, utilities)
let [root, value] = findRoot(base, designSystem.utilities)
let modifierSegment: string | null = null
@ -335,13 +329,13 @@ export function parseCandidate(
modifierSegment = rootModifierSegment
// Try to find the root and value, without the modifier present
;[root, value] = findRoot(rootWithoutModifier, utilities)
;[root, value] = findRoot(rootWithoutModifier, designSystem.utilities)
}
// If there's no root, the candidate isn't a valid class and can be discarded.
if (root === null) return null
let kind = utilities.kind(root)
let kind = designSystem.utilities.kind(root)
if (kind === 'static') {
if (value !== null) return null
@ -475,15 +469,7 @@ function parseModifier(modifier: string): CandidateModifier {
}
}
export function parseVariant(
variant: string,
variants: {
has: (value: string) => boolean
kind: (root: string) => Omit<Variant['kind'], 'arbitrary'>
compounds: (root: string) => boolean
},
parsedVariants: { get: (value: string) => Variant | null },
): Variant | null {
export function parseVariant(variant: string, designSystem: DesignSystem): Variant | null {
// Arbitrary variants
if (variant[0] === '[' && variant[variant.length - 1] === ']') {
/**
@ -535,20 +521,20 @@ export function parseVariant(
// - `group-hover/foo/bar`
if (additionalModifier) return null
let [root, value] = findRoot(variantWithoutModifier, variants)
let [root, value] = findRoot(variantWithoutModifier, designSystem.variants)
// Variant is invalid, therefore the candidate is invalid and we can skip
// continue parsing it.
if (root === null) return null
switch (variants.kind(root)) {
switch (designSystem.variants.kind(root)) {
case 'static': {
if (value !== null) return null
return {
kind: 'static',
root,
compounds: variants.compounds(root),
compounds: designSystem.variants.compounds(root),
}
}
@ -564,7 +550,7 @@ export function parseVariant(
kind: 'arbitrary',
value: decodeArbitraryValue(value.slice(1, -1)),
},
compounds: variants.compounds(root),
compounds: designSystem.variants.compounds(root),
}
}
@ -573,14 +559,14 @@ export function parseVariant(
root,
modifier: modifier === null ? null : parseModifier(modifier),
value: { kind: 'named', value },
compounds: variants.compounds(root),
compounds: designSystem.variants.compounds(root),
}
}
case 'compound': {
if (value === null) return null
let subVariant = parsedVariants.get(value)
let subVariant = designSystem.parseVariant(value)
if (subVariant === null) return null
if (subVariant.compounds === false) return null
@ -589,7 +575,7 @@ export function parseVariant(
root,
modifier: modifier === null ? null : { kind: 'named', value: modifier },
variant: subVariant,
compounds: variants.compounds(root),
compounds: designSystem.variants.compounds(root),
}
}
}

View file

@ -1,102 +1,14 @@
import { rule, type AstNode, type Rule } from './ast'
import { parseCandidate, parseVariant, type Candidate, type Variant } from './candidate'
import { type Candidate, type Variant } from './candidate'
import { type DesignSystem } from './design-system'
import GLOBAL_PROPERTY_ORDER from './property-order'
import { DefaultMap } from './utils/default-map'
import { escape } from './utils/escape'
import type { Variants } from './variants'
export function createParsedVariants(
designSystem: DesignSystem,
): DefaultMap<string, Variant | null> {
return new DefaultMap((variant, map) => {
return parseVariant(variant, designSystem.variants, map)
})
}
export function createParsedCandidates(
designSystem: DesignSystem,
parsedVariants: DefaultMap<string, Variant | null>,
) {
return new DefaultMap((candidate) => {
return parseCandidate(candidate, designSystem.utilities, parsedVariants)
})
}
export function createParsedAstNodes(
designSystem: DesignSystem,
parsedCandidates: DefaultMap<string, Candidate | null>,
) {
return new DefaultMap((rawCandidate: string) => {
let candidate = parsedCandidates.get(rawCandidate)
if (candidate === null) return null
let nodes: AstNode[] = []
// Handle arbitrary properties
if (candidate.kind === 'arbitrary') {
let compileFn = designSystem.utilities.getArbitrary()
// Build the node
let compiledNodes = compileFn(candidate)
if (compiledNodes === undefined) return null
nodes = compiledNodes
}
// Handle named utilities
else if (candidate.kind === 'static' || candidate.kind === 'functional') {
// Safety: At this point it is safe to use TypeScript's non-null assertion
// operator because if the `candidate.root` didn't exist, `parseCandidate`
// would have returned `null` and we would have returned early resulting
// in not hitting this code path.
let { compileFn } = designSystem.utilities.get(candidate.root)!
// Build the node
let compiledNodes = compileFn(candidate)
if (compiledNodes === undefined) return null
nodes = compiledNodes
}
let propertySort = getPropertySort(nodes)
if (candidate.important) {
applyImportant(nodes)
}
let node: Rule = {
kind: 'rule',
selector: `.${escape(rawCandidate)}`,
nodes,
}
for (let variant of candidate.variants) {
let result = applyVariant(node, variant, designSystem.variants)
// When the variant results in `null`, it means that the variant cannot be
// applied to the rule. Discard the candidate and continue to the next
// one.
if (result === null) return null
}
return {
node,
propertySort,
}
})
}
export function compileCandidates(
rawCandidates: Iterable<string>,
designSystem: DesignSystem,
{
parsedVariants = createParsedVariants(designSystem),
parsedCandidates = createParsedCandidates(designSystem, parsedVariants),
parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates),
invalidRawCandidates = new Set<string>(),
throwOnInvalidCandidate = false,
} = {},
{ throwOnInvalidCandidate = false } = {},
) {
let nodeSorting = new Map<
AstNode,
@ -107,30 +19,30 @@ export function compileCandidates(
// Parse candidates and variants
for (let rawCandidate of rawCandidates) {
let candidate = parsedCandidates.get(rawCandidate)
let candidate = designSystem.parseCandidate(rawCandidate)
if (candidate === null) {
if (throwOnInvalidCandidate) {
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
}
invalidRawCandidates.add(rawCandidate)
designSystem.invalidRawCandidates.add(rawCandidate)
continue // Bail, invalid candidate
}
candidates.set(candidate, rawCandidate)
}
// Sort the variants
let variants = Array.from(parsedVariants.values()).sort((a, z) => {
let variants = Array.from(designSystem.parsedVariants.values()).sort((a, z) => {
return designSystem.variants.compare(a, z)
})
// Create the AST
next: for (let [candidate, rawCandidate] of candidates) {
let astNode = parsedAstNodes.get(rawCandidate)
let astNode = designSystem.parseAstNode(rawCandidate)
if (astNode === null) {
if (throwOnInvalidCandidate) {
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
}
invalidRawCandidates.add(rawCandidate)
designSystem.invalidRawCandidates.add(rawCandidate)
continue next
}
@ -191,6 +103,65 @@ export function compileCandidates(
}
}
export function parseAstNodes(rawCandidate: string, designSystem: DesignSystem) {
let candidate = designSystem.parseCandidate(rawCandidate)
if (candidate === null) return null
let nodes: AstNode[] = []
// Handle arbitrary properties
if (candidate.kind === 'arbitrary') {
let compileFn = designSystem.utilities.getArbitrary()
// Build the node
let compiledNodes = compileFn(candidate)
if (compiledNodes === undefined) return null
nodes = compiledNodes
}
// Handle named utilities
else if (candidate.kind === 'static' || candidate.kind === 'functional') {
// Safety: At this point it is safe to use TypeScript's non-null assertion
// operator because if the `candidate.root` didn't exist, `parseCandidate`
// would have returned `null` and we would have returned early resulting
// in not hitting this code path.
let { compileFn } = designSystem.utilities.get(candidate.root)!
// Build the node
let compiledNodes = compileFn(candidate)
if (compiledNodes === undefined) return null
nodes = compiledNodes
}
let propertySort = getPropertySort(nodes)
if (candidate.important) {
applyImportant(nodes)
}
let node: Rule = {
kind: 'rule',
selector: `.${escape(rawCandidate)}`,
nodes,
}
for (let variant of candidate.variants) {
let result = applyVariant(node, variant, designSystem.variants)
// When the variant results in `null`, it means that the variant cannot be
// applied to the rule. Discard the candidate and continue to the next
// one.
if (result === null) return null
}
return {
node,
propertySort,
}
}
export function applyVariant(node: Rule, variant: Variant, variants: Variants): null | void {
if (variant.kind === 'arbitrary') {
node.nodes = [rule(variant.selector, node.nodes)]

View file

@ -1,9 +1,11 @@
import { toCss } from './ast'
import { compileCandidates } from './compile'
import { parseCandidate, parseVariant } from './candidate'
import { compileCandidates, parseAstNodes as parseAstNode } from './compile'
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
import { getClassOrder } from './sort'
import type { Theme } from './theme'
import { Utilities, createUtilities } from './utilities'
import { DefaultMap } from './utils/default-map'
import { Variants, createVariants } from './variants'
export type DesignSystem = {
@ -15,13 +17,35 @@ export type DesignSystem = {
getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]
parseCandidate(candidate: string): ReturnType<typeof parseCandidate>
parseVariant(variant: string): ReturnType<typeof parseVariant>
parseAstNode(candidate: string): ReturnType<typeof parseAstNode>
parsedCandidates: DefaultMap<string, ReturnType<typeof parseCandidate>>
parsedVariants: DefaultMap<string, ReturnType<typeof parseVariant>>
parsedAstNodes: DefaultMap<string, ReturnType<typeof parseAstNode>>
invalidRawCandidates: Set<string>
}
export function buildDesignSystem(theme: Theme): DesignSystem {
return {
let utilities = createUtilities(theme)
let variants = createVariants(theme)
let invalidRawCandidates = new Set<string>()
let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem))
let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem))
let parsedAstNodes = new DefaultMap((candidate) => parseAstNode(candidate, designSystem))
let designSystem: DesignSystem = {
theme,
utilities: createUtilities(theme),
variants: createVariants(theme),
utilities,
variants,
parsedCandidates,
parsedVariants,
parsedAstNodes,
invalidRawCandidates,
candidatesToCss(classes: string[]) {
let result: (string | null)[] = []
@ -47,5 +71,17 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
getVariants() {
return getVariants(this)
},
parseCandidate(candidate: string) {
return parsedCandidates.get(candidate)
},
parseVariant(variant: string) {
return parsedVariants.get(variant)
},
parseAstNode(candidate: string) {
return parsedAstNodes.get(candidate)
},
}
return designSystem
}

View file

@ -1,12 +1,7 @@
import { Features, transform } from 'lightningcss'
import { version } from '../package.json'
import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast'
import {
compileCandidates,
createParsedAstNodes,
createParsedCandidates,
createParsedVariants,
} from './compile'
import { compileCandidates } from './compile'
import * as CSS from './css-parser'
import { buildDesignSystem } from './design-system'
import { Theme } from './theme'
@ -107,10 +102,6 @@ export function compile(
}
let designSystem = buildDesignSystem(theme)
let invalidRawCandidates = new Set<string>()
let parsedVariants = createParsedVariants(designSystem)
let parsedCandidates = createParsedCandidates(designSystem, parsedVariants)
let parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates)
let tailwindUtilitiesNode: Rule | null = null
@ -121,12 +112,7 @@ export function compile(
tailwindUtilitiesNode = node
// Set the `@tailwind utilities` nodes, to the actual generated CSS
node.nodes = compileCandidates(rawCandidates, designSystem, {
parsedVariants,
parsedCandidates,
parsedAstNodes,
invalidRawCandidates,
}).astNodes
node.nodes = compileCandidates(rawCandidates, designSystem).astNodes
// Stop walking after finding `@tailwind utilities` to avoid walking all
// of the generated CSS. This means `@tailwind utilities` can only appear
@ -148,10 +134,6 @@ export function compile(
{
// Parse the candidates to an AST that we can replace the `@apply` rule with.
let candidateAst = compileCandidates(candidates, designSystem, {
parsedVariants,
parsedCandidates,
parsedAstNodes,
invalidRawCandidates,
throwOnInvalidCandidate: true,
}).astNodes
@ -194,7 +176,7 @@ export function compile(
let allValidCandidates = new Set<string>()
for (let rawCandidate of rawCandidates) {
if (!invalidRawCandidates.has(rawCandidate)) {
if (!designSystem.invalidRawCandidates.has(rawCandidate)) {
allValidCandidates.add(rawCandidate)
}
}
@ -205,7 +187,7 @@ export function compile(
// Add all new candidates unless we know that they are invalid.
let previousSize = allValidCandidates.size
for (let candidate of newRawCandidates) {
if (!invalidRawCandidates.has(candidate)) {
if (!designSystem.invalidRawCandidates.has(candidate)) {
allValidCandidates.add(candidate)
}
}
@ -217,18 +199,13 @@ export function compile(
}
if (tailwindUtilitiesNode) {
let previousAstNodeCount = parsedAstNodes.size
let newNodes = compileCandidates(allValidCandidates, designSystem, {
parsedVariants,
parsedCandidates,
parsedAstNodes,
invalidRawCandidates,
}).astNodes
let previousAstNodeCount = designSystem.parsedAstNodes.size
let newNodes = compileCandidates(allValidCandidates, designSystem).astNodes
// If no new ast nodes were generated, then we can return the original
// CSS. This currently assumes that we only add new ast nodes and never
// remove any.
if (previousAstNodeCount === parsedAstNodes.size) {
if (previousAstNodeCount === designSystem.parsedAstNodes.size) {
return compiledCss
}

View file

@ -1,8 +1,6 @@
import { decl, rule } from './ast'
import { parseVariant, type Variant } from './candidate'
import { applyVariant } from './compile'
import type { DesignSystem } from './design-system'
import { DefaultMap } from './utils/default-map'
interface ClassMetadata {
modifiers: string[]
@ -60,9 +58,6 @@ export interface VariantEntry {
export function getVariants(design: DesignSystem) {
let list: VariantEntry[] = []
let parsedVariants = new DefaultMap<string, Variant | null>((variant, map) =>
parseVariant(variant, design.variants, map),
)
for (let [root, variant] of design.variants.entries()) {
if (variant.kind === 'arbitrary') continue
@ -74,7 +69,7 @@ export function getVariants(design: DesignSystem) {
if (value) name += `-${value}`
if (modifier) name += `/${modifier}`
let variant = parsedVariants.get(name)
let variant = design.parseVariant(name)
if (!variant) return []