diff --git a/oxide/crates/core/src/lib.rs b/oxide/crates/core/src/lib.rs index 87327f39a..6f04adae0 100644 --- a/oxide/crates/core/src/lib.rs +++ b/oxide/crates/core/src/lib.rs @@ -433,9 +433,9 @@ fn read_all_files_sync(changed_content: Vec) -> Vec> { changed_content .into_iter() - .map(|c| match (c.file, c.content) { - (Some(file), None) => std::fs::read(file).unwrap(), - (None, Some(content)) => content.into_bytes(), + .filter_map(|c| match (c.file, c.content) { + (Some(file), None) => std::fs::read(file).ok(), + (None, Some(content)) => Some(content.into_bytes()), _ => Default::default(), }) .collect() diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index f3627ff91..d92984851 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -100,7 +100,8 @@ export async function handle(args: Result>) { ) // Compile the input - let result = compile(input, candidates) + let { build } = compile(input) + let result = build(candidates) // Optimize the output if (args['--minify'] || args['--optimize']) { @@ -193,7 +194,7 @@ export async function handle(args: Result>) { } // Compile the input - let result = compile(input, candidates) + result = compile(input).build(candidates) // Optimize the output if (args['--minify'] || args['--optimize']) { diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 73621836d..303ffb924 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -53,7 +53,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // No `@tailwind` means we don't have to look for candidates if (!hasTailwind) { - replaceCss(compile(root.toString(), [])) + replaceCss(compile(root.toString()).build([])) return } @@ -83,7 +83,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } - replaceCss(compile(root.toString(), candidates)) + replaceCss(compile(root.toString()).build(candidates)) }, ], } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 6842aa7de..64cfa3036 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -63,7 +63,7 @@ export default function tailwindcss(): Plugin[] { } function generateCss(css: string) { - return compile(css, Array.from(candidates)) + return compile(css).build(Array.from(candidates)) } function generateOptimizedCss(css: string) { diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index d316c5411..dae7cf0e8 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -105,6 +105,13 @@ export function toCss(ast: AstNode[]) { return css } + if (node.selector === '@tailwind utilities') { + for (let child of node.nodes) { + css += stringify(child, depth) + } + return css + } + // Print at-rules without nodes with a `;` instead of an empty block. // // E.g.: diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index f10e95d09..f46dec7ec 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -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) } }) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 672e8cbb4..17d1371b6 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -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', () => { diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index e3f6b92f2..03420e4d8 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -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 - }, - 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 - 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), } } } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index b91c19aca..ebbe2a74c 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,120 +1,52 @@ 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 compileCandidates( - rawCandidates: string[], + rawCandidates: Iterable, designSystem: DesignSystem, - { throwOnInvalidCandidate = false } = {}, + { onInvalidCandidate }: { onInvalidCandidate?: (candidate: string) => void } = {}, ) { - // Ensure the candidates are sorted alphabetically - rawCandidates.sort() - let nodeSorting = new Map< AstNode, { properties: number[]; variants: bigint; candidate: string } >() let astNodes: AstNode[] = [] - - // A lazy map implementation that will return the variant if it exists. If it - // doesn't exist yet, the raw string variant will be parsed and added to the - // map. - let parsedVariants: DefaultMap = new DefaultMap((variant, map) => { - return parseVariant(variant, designSystem.variants, map) - }) - let candidates = new Map() // Parse candidates and variants for (let rawCandidate of rawCandidates) { - let candidate = parseCandidate(rawCandidate, designSystem.utilities, parsedVariants) + let candidate = designSystem.parseCandidate(rawCandidate) if (candidate === null) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } + onInvalidCandidate?.(rawCandidate) continue // Bail, invalid candidate } candidates.set(candidate, rawCandidate) } // Sort the variants - let variants = Array.from(parsedVariants.values()).sort((a, z) => { + let variants = designSystem.getUsedVariants().sort((a, z) => { return designSystem.variants.compare(a, z) }) // Create the AST next: for (let [candidate, rawCandidate] of candidates) { - 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) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } - - nodes = compiledNodes + let astNode = designSystem.compileAstNodes(rawCandidate) + if (astNode === null) { + onInvalidCandidate?.(rawCandidate) + continue next } - // 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) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } - - nodes = compiledNodes - } - - let propertySort = getPropertySort(nodes) - - if (candidate.important) { - applyImportant(nodes) - } - - let node: Rule = { - kind: 'rule', - selector: `.${escape(rawCandidate)}`, - nodes, - } + let { node, propertySort } = astNode + // Track the variant order which is a number with each bit representing a + // variant. This allows us to sort the rules based on the order of + // variants used. let variantOrder = 0n 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) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } - - // Track the variant order which is a number with each bit representing a - // variant. This allows us to sort the rules based on the order of - // variants used. variantOrder |= 1n << BigInt(variants.indexOf(variant)) } @@ -153,7 +85,9 @@ export function compileCandidates( // Sort by lowest property index first (aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) || // Sort by most properties first, then by least properties - zSorting.properties.length - aSorting.properties.length + zSorting.properties.length - aSorting.properties.length || + // Sort alphabetically + (aSorting.candidate < zSorting.candidate ? -1 : 1) ) }) @@ -163,6 +97,65 @@ export function compileCandidates( } } +export function compileAstNodes(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)] diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 190c5e7e2..bebd82a33 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -1,9 +1,11 @@ import { toCss } from './ast' -import { compileCandidates } from './compile' +import { parseCandidate, parseVariant } from './candidate' +import { compileAstNodes, compileCandidates } 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,19 +17,32 @@ export type DesignSystem = { getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] + + parseCandidate(candidate: string): ReturnType + parseVariant(variant: string): ReturnType + compileAstNodes(candidate: string): ReturnType + + getUsedVariants(): ReturnType[] } export function buildDesignSystem(theme: Theme): DesignSystem { - return { + let utilities = createUtilities(theme) + let variants = createVariants(theme) + + let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) + let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem)) + let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem)) + + let designSystem: DesignSystem = { theme, - utilities: createUtilities(theme), - variants: createVariants(theme), + utilities, + variants, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] for (let className of classes) { - let { astNodes } = compileCandidates([className], this, { throwOnInvalidCandidate: false }) + let { astNodes } = compileCandidates([className], this) if (astNodes.length === 0) { result.push(null) } else { @@ -47,5 +62,20 @@ export function buildDesignSystem(theme: Theme): DesignSystem { getVariants() { return getVariants(this) }, + + parseCandidate(candidate: string) { + return parsedCandidates.get(candidate) + }, + parseVariant(variant: string) { + return parsedVariants.get(variant) + }, + compileAstNodes(candidate: string) { + return compiledAstNodes.get(candidate) + }, + getUsedVariants() { + return Array.from(parsedVariants.values()) + }, } + + return designSystem } diff --git a/packages/tailwindcss/src/index.bench.ts b/packages/tailwindcss/src/index.bench.ts index 1953aa195..6ec69f2f6 100644 --- a/packages/tailwindcss/src/index.bench.ts +++ b/packages/tailwindcss/src/index.bench.ts @@ -9,10 +9,7 @@ const css = String.raw bench('compile', async () => { let { candidates } = scanDir({ base: root, globs: true }) - compile( - css` - @tailwind utilities; - `, - candidates, - ) + compile(css` + @tailwind utilities; + `).build(candidates) }) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index cf7025102..1488b4817 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -93,9 +93,7 @@ describe('compiling CSS', () => { .grid { display: grid; - } - - @tailwind utilities;" + }" `) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d066a5a1f..9678028f0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -6,13 +6,21 @@ import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' import { Theme } from './theme' -export function compile(css: string, rawCandidates: string[]) { +export function compile(css: string): { + build(candidates: string[]): string +} { let ast = CSS.parse(css) if (process.env.NODE_ENV !== 'test') { ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `)) } + // Track all invalid candidates + let invalidCandidates = new Set() + function onInvalidCandidate(candidate: string) { + invalidCandidates.add(candidate) + } + // Find all `@theme` declarations let theme = new Theme() let firstThemeRule: Rule | null = null @@ -97,11 +105,14 @@ export function compile(css: string, rawCandidates: string[]) { let designSystem = buildDesignSystem(theme) + let tailwindUtilitiesNode: Rule | null = null + // Find `@tailwind utilities` and replace it with the actual generated utility // class CSS. - walk(ast, (node, { replaceWith }) => { + walk(ast, (node) => { if (node.kind === 'rule' && node.selector === '@tailwind utilities') { - replaceWith(compileCandidates(rawCandidates, designSystem).astNodes) + tailwindUtilitiesNode = node + // Stop walking after finding `@tailwind utilities` to avoid walking all // of the generated CSS. This means `@tailwind utilities` can only appear // once per file but that's the intended usage at this point in time. @@ -122,7 +133,9 @@ export function compile(css: string, rawCandidates: string[]) { { // Parse the candidates to an AST that we can replace the `@apply` rule with. let candidateAst = compileCandidates(candidates, designSystem, { - throwOnInvalidCandidate: true, + onInvalidCandidate: (candidate) => { + throw new Error(`Cannot apply unknown utility class: ${candidate}`) + }, }).astNodes // Collect the nodes to insert in place of the `@apply` rule. When a @@ -162,7 +175,53 @@ export function compile(css: string, rawCandidates: string[]) { }) } - return toCss(ast) + // Track all valid candidates, these are the incoming `rawCandidate` that + // resulted in a generated AST Node. All the other `rawCandidates` are invalid + // and should be ignored. + let allValidCandidates = new Set() + let compiledCss = toCss(ast) + let previousAstNodeCount = 0 + + return { + build(newRawCandidates: string[]) { + let didChange = false + + // Add all new candidates unless we know that they are invalid. + let prevSize = allValidCandidates.size + for (let candidate of newRawCandidates) { + if (!invalidCandidates.has(candidate)) { + allValidCandidates.add(candidate) + didChange ||= allValidCandidates.size !== prevSize + } + } + + // If no new candidates were added, we can return the original CSS. This + // currently assumes that we only add new candidates and never remove any. + if (!didChange) { + return compiledCss + } + + if (tailwindUtilitiesNode) { + let newNodes = compileCandidates(allValidCandidates, designSystem, { + onInvalidCandidate, + }).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 === newNodes.length) { + return compiledCss + } + + previousAstNodeCount = newNodes.length + + tailwindUtilitiesNode.nodes = newNodes + compiledCss = toCss(ast) + } + + return compiledCss + }, + } } export function optimizeCss( diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index 8b67641c6..4b1d814fb 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -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[] @@ -40,7 +38,7 @@ export function getClassList(design: DesignSystem): ClassEntry[] { } } - list.sort((a, b) => a[0].localeCompare(b[0])) + list.sort((a, b) => (a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1)) return list } @@ -60,9 +58,6 @@ export interface VariantEntry { export function getVariants(design: DesignSystem) { let list: VariantEntry[] = [] - let parsedVariants = new DefaultMap((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 [] diff --git a/packages/tailwindcss/src/sort.ts b/packages/tailwindcss/src/sort.ts index b3434ed09..e15c4f02e 100644 --- a/packages/tailwindcss/src/sort.ts +++ b/packages/tailwindcss/src/sort.ts @@ -3,9 +3,7 @@ import type { DesignSystem } from './design-system' export function getClassOrder(design: DesignSystem, classes: string[]): [string, bigint | null][] { // Generate a sorted AST - let { astNodes, nodeSorting } = compileCandidates(Array.from(classes), design, { - throwOnInvalidCandidate: false, - }) + let { astNodes, nodeSorting } = compileCandidates(Array.from(classes), design) // Map class names to their order in the AST // `null` indicates a non-Tailwind class diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index 8df6feab2..039560482 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -1,9 +1,9 @@ import { compile, optimizeCss } from '..' export function compileCss(css: string, candidates: string[] = []) { - return optimizeCss(compile(css, candidates)).trim() + return optimizeCss(compile(css).build(candidates)).trim() } export function run(candidates: string[]) { - return optimizeCss(compile('@tailwind utilities;', candidates)).trim() + return optimizeCss(compile('@tailwind utilities;').build(candidates)).trim() } diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 675fa6931..3df052b5a 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1280,7 +1280,13 @@ test('supports', () => { expect( run(['supports-gap:grid', 'supports-[display:grid]:flex', 'supports-[selector(A_>_B)]:flex']), ).toMatchInlineSnapshot(` - "@supports (display: grid) { + "@supports (gap: var(--tw)) { + .supports-gap\\:grid { + display: grid; + } + } + + @supports (display: grid) { .supports-\\[display\\:grid\\]\\:flex { display: flex; } @@ -1290,12 +1296,6 @@ test('supports', () => { .supports-\\[selector\\(A_\\>_B\\)\\]\\:flex { display: flex; } - } - - @supports (gap: var(--tw)) { - .supports-gap\\:grid { - display: grid; - } }" `) }) @@ -1322,11 +1322,11 @@ test('not', () => { display: flex; } - .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *) { + .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { display: flex; } - .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { + .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *) { display: flex; } @@ -1334,11 +1334,11 @@ test('not', () => { display: flex; } - .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { display: flex; } - .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { display: flex; }" `) @@ -1362,11 +1362,11 @@ test('has', () => { display: flex; } - .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) { + .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { display: flex; } - .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { + .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) { display: flex; } @@ -1374,11 +1374,11 @@ test('has', () => { display: flex; } - .peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) { + .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { display: flex; } - .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { + .peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) { display: flex; } @@ -1405,15 +1405,7 @@ test('aria', () => { 'peer-aria-checked/parent-name:flex', ]), ).toMatchInlineSnapshot(` - ".group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) { - display: flex; - } - - .group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) { - display: flex; - } - - .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *) { + ".group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) { display: flex; } @@ -1421,7 +1413,11 @@ test('aria', () => { display: flex; } - .peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) { + .group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) { + display: flex; + } + + .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *) { display: flex; } @@ -1429,20 +1425,24 @@ test('aria', () => { display: flex; } - .peer-aria-checked\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-checked="true"] ~ *) { - display: flex; - } - .peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *) { display: flex; } - .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] { + .peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) { + display: flex; + } + + .peer-aria-checked\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-checked="true"] ~ *) { display: flex; } .aria-checked\\:flex[aria-checked="true"] { display: flex; + } + + .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] { + display: flex; }" `) }) @@ -1460,15 +1460,11 @@ test('data', () => { 'peer-data-[disabled]/parent-name:flex', ]), ).toMatchInlineSnapshot(` - ".group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) { + ".group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) { display: flex; } - .group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) { - display: flex; - } - - .peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) { + .group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) { display: flex; } @@ -1476,12 +1472,16 @@ test('data', () => { display: flex; } - .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { + .peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) { display: flex; } .data-disabled\\:flex[data-disabled] { display: flex; + } + + .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { + display: flex; }" `) }) @@ -1567,14 +1567,14 @@ test('container queries', () => { --width-lg: 1024px; } - @container name (width < 1024px) { - .\\@max-lg\\/name\\:flex { + @container (width < 1024px) { + .\\@max-lg\\:flex { display: flex; } } - @container (width < 1024px) { - .\\@max-lg\\:flex { + @container name (width < 1024px) { + .\\@max-lg\\/name\\:flex { display: flex; } } @@ -1615,12 +1615,6 @@ test('container queries', () => { } } - @container name (width >= 1024px) { - .\\@lg\\/name\\:flex { - display: flex; - } - } - @container (width >= 1024px) { .\\@lg\\:flex { display: flex; @@ -1628,7 +1622,7 @@ test('container queries', () => { } @container name (width >= 1024px) { - .\\@min-lg\\/name\\:flex { + .\\@lg\\/name\\:flex { display: flex; } } @@ -1637,6 +1631,12 @@ test('container queries', () => { .\\@min-lg\\:flex { display: flex; } + } + + @container name (width >= 1024px) { + .\\@min-lg\\/name\\:flex { + display: flex; + } }" `) }) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 3e5138c62..3cc0919ad 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -94,7 +94,7 @@ export class Variants { if (z === null) return 1 if (a.kind === 'arbitrary' && z.kind === 'arbitrary') { - return a.selector.localeCompare(z.selector) + return a.selector < z.selector ? -1 : 1 } else if (a.kind === 'arbitrary') { return 1 } else if (z.kind === 'arbitrary') { @@ -114,7 +114,7 @@ export class Variants { let compareFn = this.compareFns.get(aOrder) if (compareFn === undefined) return 0 - return compareFn(a, z) + return compareFn(a, z) || (a.root < z.root ? -1 : 1) } keys() { @@ -469,7 +469,7 @@ export function createVariants(theme: Theme): Variants { let order = // Compare by bucket name - aBucket.localeCompare(zBucket) || + (aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) || // If bucket names are the same, compare by value (direction === 'asc' ? parseInt(aValue) - parseInt(zValue) @@ -489,7 +489,7 @@ export function createVariants(theme: Theme): Variants { // In this scenario, we want to alphabetically sort `calc(100%-1rem)` and // `calc(100%-2rem)` to make it deterministic. if (Number.isNaN(order)) { - return aValue.localeCompare(zValue) + return aValue < zValue ? -1 : 1 } return order diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index 7a7f357b0..f16736f66 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -255,21 +255,18 @@ async function render(page: Page, content: string) { await page.setContent(content) await page.addStyleTag({ content: optimizeCss( - compile( - css` - @layer theme, base, components, utilities; - @layer theme { - ${defaultTheme} - } - @layer base { - ${preflight} - } - @layer utilities { - @tailwind utilities; - } - `, - scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential), - ), + compile(css` + @layer theme, base, components, utilities; + @layer theme { + ${defaultTheme} + } + @layer base { + ${preflight} + } + @layer utilities { + @tailwind utilities; + } + `).build(scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential)), ), })