Compare commits
24 commits
main
...
feat/cachi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a99bcfba60 | ||
|
|
316190fe5a | ||
|
|
ac3f03a07e | ||
|
|
a45ae05fca | ||
|
|
145df6ca53 | ||
|
|
d7ec348412 | ||
|
|
e05e5ece43 | ||
|
|
ce38fcde3e | ||
|
|
a0f23d3306 | ||
|
|
115417fed4 | ||
|
|
d04a17c102 | ||
|
|
a19bc73e70 | ||
|
|
50da0a1be2 | ||
|
|
02b1794684 | ||
|
|
48b56017cd | ||
|
|
c65598e12c | ||
|
|
62ee42c30c | ||
|
|
27e06c76d2 | ||
|
|
6282e2b22e | ||
|
|
1b3ad0db65 | ||
|
|
b0353871f7 | ||
|
|
43eecb981a | ||
|
|
1e0b35d836 | ||
|
|
d6e5858e56 |
17 changed files with 435 additions and 269 deletions
|
|
@ -433,9 +433,9 @@ fn read_all_files_sync(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
|||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import watcher from '@parcel/watcher'
|
||||
import {
|
||||
IO,
|
||||
Parsing,
|
||||
clearCache,
|
||||
scanDir,
|
||||
scanFiles,
|
||||
type ChangedContent,
|
||||
} from '@tailwindcss/oxide'
|
||||
import { IO, Parsing, scanDir, scanFiles, type ChangedContent } from '@tailwindcss/oxide'
|
||||
import { existsSync } from 'node:fs'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
|
@ -99,23 +92,41 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
args['--input'] ?? base,
|
||||
)
|
||||
|
||||
let previous = {
|
||||
css: '',
|
||||
optimizedCss: '',
|
||||
}
|
||||
|
||||
async function write(css: string, args: Result<ReturnType<typeof options>>) {
|
||||
let output = css
|
||||
|
||||
// Optimize the output
|
||||
if (args['--minify'] || args['--optimize']) {
|
||||
if (css !== previous.css) {
|
||||
let optimizedCss = optimizeCss(css, {
|
||||
file: args['--input'] ?? 'input.css',
|
||||
minify: args['--minify'] ?? false,
|
||||
})
|
||||
previous.css = css
|
||||
previous.optimizedCss = optimizedCss
|
||||
output = optimizedCss
|
||||
} else {
|
||||
output = previous.optimizedCss
|
||||
}
|
||||
}
|
||||
|
||||
// Write the output
|
||||
if (args['--output']) {
|
||||
await outputFile(args['--output'], output)
|
||||
} else {
|
||||
println(output)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile the input
|
||||
let result = compile(input, candidates)
|
||||
|
||||
// Optimize the output
|
||||
if (args['--minify'] || args['--optimize']) {
|
||||
result = optimizeCss(result, {
|
||||
file: args['--input'] ?? 'input.css',
|
||||
minify: args['--minify'] ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
// Write the output
|
||||
if (args['--output']) {
|
||||
await outputFile(args['--output'], result)
|
||||
} else {
|
||||
println(result)
|
||||
}
|
||||
await write(result.css, args)
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
eprintln(header())
|
||||
|
|
@ -161,26 +172,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// Re-compile the input
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
// Track the compiled CSS
|
||||
let compiledCss = ''
|
||||
|
||||
// Scan the entire `base` directory for full rebuilds.
|
||||
if (rebuildStrategy === 'full') {
|
||||
// Clear the cache because we need to re-scan the entire directory.
|
||||
clearCache()
|
||||
|
||||
// Re-scan the directory to get the new `candidates`.
|
||||
candidates = scanDir({ base }).candidates
|
||||
}
|
||||
|
||||
// Scan changed files only for incremental rebuilds.
|
||||
else if (rebuildStrategy === 'incremental') {
|
||||
let uniqueCandidates = new Set(candidates)
|
||||
for (let candidate of scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)) {
|
||||
uniqueCandidates.add(candidate)
|
||||
}
|
||||
candidates = Array.from(uniqueCandidates)
|
||||
}
|
||||
|
||||
// Resolve the input
|
||||
if (rebuildStrategy === 'full') {
|
||||
// Collect the new `input` and `cssImportPaths`.
|
||||
;[input, cssImportPaths] = await handleImports(
|
||||
args['--input']
|
||||
|
|
@ -190,25 +189,19 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
`,
|
||||
args['--input'] ?? base,
|
||||
)
|
||||
|
||||
result = compile(input, candidates)
|
||||
compiledCss = result.css
|
||||
}
|
||||
|
||||
// Compile the input
|
||||
let result = compile(input, candidates)
|
||||
// Scan changed files only for incremental rebuilds.
|
||||
else if (rebuildStrategy === 'incremental') {
|
||||
let newCandidates = scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)
|
||||
|
||||
// Optimize the output
|
||||
if (args['--minify'] || args['--optimize']) {
|
||||
result = optimizeCss(result, {
|
||||
file: args['--input'] ?? 'input.css',
|
||||
minify: args['--minify'] ?? false,
|
||||
})
|
||||
compiledCss = result.rebuild(newCandidates)
|
||||
}
|
||||
|
||||
// Write the output
|
||||
if (args['--output']) {
|
||||
await outputFile(args['--output'], result)
|
||||
} else {
|
||||
println(result)
|
||||
}
|
||||
await write(compiledCss, args)
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
|
|
@ -243,7 +236,9 @@ function handleImports(
|
|||
// Relevant specification:
|
||||
// - CSS Import Resolve: https://csstools.github.io/css-import-resolve/
|
||||
|
||||
if (!input.includes('@import')) return [input, []]
|
||||
if (!input.includes('@import')) {
|
||||
return [input, [file]]
|
||||
}
|
||||
|
||||
return postcss()
|
||||
.use(atImport())
|
||||
|
|
@ -253,6 +248,8 @@ function handleImports(
|
|||
|
||||
// Use `result.messages` to get the imported files. This also includes the
|
||||
// current file itself.
|
||||
result.messages.filter((msg) => msg.type === 'postcss-import').map((msg) => msg.file),
|
||||
[file].concat(
|
||||
result.messages.filter((msg) => msg.type === 'dependency').map((msg) => msg.file),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,30 @@
|
|||
import { scanDir } from '@tailwindcss/oxide'
|
||||
import fs from 'fs'
|
||||
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
import { compile, optimizeCss } from 'tailwindcss'
|
||||
|
||||
/**
|
||||
* A Map that can generate default values for keys that don't exist.
|
||||
* Generated default values are added to the map to avoid recomputation.
|
||||
*/
|
||||
class DefaultMap<T = string, V = any> extends Map<T, V> {
|
||||
constructor(private factory: (key: T, self: DefaultMap<T, V>) => V) {
|
||||
super()
|
||||
}
|
||||
|
||||
get(key: T): V {
|
||||
let value = super.get(key)
|
||||
|
||||
if (value === undefined) {
|
||||
value = this.factory(key, this)
|
||||
this.set(key, value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
type PluginOptions = {
|
||||
// The base directory to scan for class candidates.
|
||||
base?: string
|
||||
|
|
@ -15,13 +37,54 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
let base = opts.base ?? process.cwd()
|
||||
let optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
|
||||
|
||||
let cache = new DefaultMap(() => {
|
||||
return {
|
||||
mtimes: new Map<string, number>(),
|
||||
rebuild: null as null | ReturnType<typeof compile>['rebuild'],
|
||||
previousCss: '',
|
||||
previousOptimizedCss: '',
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss-v4',
|
||||
postcssPlugin: '@tailwindcss/postcss',
|
||||
plugins: [
|
||||
// We need to run `postcss-import` first to handle `@import` rules.
|
||||
postcssImport(),
|
||||
|
||||
(root, result) => {
|
||||
let from = result.opts.from ?? ''
|
||||
let context = cache.get(from)
|
||||
|
||||
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
|
||||
|
||||
// Bookkeeping — track file modification times to CSS files
|
||||
{
|
||||
let changedTime = fs.statSync(from, { throwIfNoEntry: false })?.mtimeMs ?? null
|
||||
if (changedTime !== null) {
|
||||
let prevTime = context.mtimes.get(from)
|
||||
if (prevTime !== changedTime) {
|
||||
rebuildStrategy = 'full'
|
||||
context.mtimes.set(from, changedTime)
|
||||
}
|
||||
} else {
|
||||
rebuildStrategy = 'full'
|
||||
}
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
let file = message.file as string
|
||||
let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
|
||||
if (changedTime !== null) {
|
||||
let prevTime = context.mtimes.get(file)
|
||||
if (prevTime !== changedTime) {
|
||||
rebuildStrategy = 'full'
|
||||
context.mtimes.set(file, changedTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hasApply = false
|
||||
let hasTailwind = false
|
||||
|
||||
|
|
@ -40,22 +103,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
// Do nothing if neither `@tailwind` nor `@apply` is used
|
||||
if (!hasTailwind && !hasApply) return
|
||||
|
||||
function replaceCss(css: string) {
|
||||
root.removeAll()
|
||||
let output = css
|
||||
if (optimize) {
|
||||
output = optimizeCss(output, {
|
||||
minify: typeof optimize === 'object' ? optimize.minify : false,
|
||||
})
|
||||
}
|
||||
root.append(postcss.parse(output, result.opts))
|
||||
}
|
||||
|
||||
// No `@tailwind` means we don't have to look for candidates
|
||||
if (!hasTailwind) {
|
||||
replaceCss(compile(root.toString(), []))
|
||||
return
|
||||
}
|
||||
let css = ''
|
||||
|
||||
// Look for candidates used to generate the CSS
|
||||
let { candidates, files, globs } = scanDir({ base, globs: true })
|
||||
|
|
@ -64,7 +112,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
for (let file of files) {
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
plugin: 'tailwindcss-v4',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
file,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
|
|
@ -76,14 +124,49 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
for (let { base, glob } of globs) {
|
||||
result.messages.push({
|
||||
type: 'dir-dependency',
|
||||
plugin: 'tailwindcss-v4',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
dir: base,
|
||||
glob,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
replaceCss(compile(root.toString(), candidates))
|
||||
if (rebuildStrategy === 'full') {
|
||||
if (hasTailwind) {
|
||||
let compileResult = compile(root.toString(), candidates)
|
||||
css = compileResult.css
|
||||
context.rebuild = compileResult.rebuild.bind(compileResult)
|
||||
} else {
|
||||
css = compile(root.toString(), []).css
|
||||
}
|
||||
} else if (rebuildStrategy === 'incremental') {
|
||||
css = context.rebuild!(candidates)
|
||||
}
|
||||
|
||||
function replaceCss(css: string) {
|
||||
root.removeAll()
|
||||
root.append(postcss.parse(css, result.opts))
|
||||
}
|
||||
|
||||
// Replace CSS
|
||||
if (css === context.previousCss) {
|
||||
if (optimize) {
|
||||
replaceCss(context.previousOptimizedCss)
|
||||
} else {
|
||||
replaceCss(css)
|
||||
}
|
||||
} else {
|
||||
if (optimize) {
|
||||
let optimizedCss = optimizeCss(css, {
|
||||
minify: typeof optimize === 'object' ? optimize.minify : false,
|
||||
})
|
||||
replaceCss(optimizedCss)
|
||||
context.previousOptimizedCss = optimizedCss
|
||||
} else {
|
||||
replaceCss(css)
|
||||
}
|
||||
context.previousCss = css
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default function tailwindcss(): Plugin[] {
|
|||
}
|
||||
|
||||
function generateCss(css: string) {
|
||||
return compile(css, Array.from(candidates))
|
||||
return compile(css, Array.from(candidates)).css
|
||||
}
|
||||
|
||||
function generateOptimizedCss(css: string) {
|
||||
|
|
|
|||
|
|
@ -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.:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,120 +1,58 @@
|
|||
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<string>,
|
||||
designSystem: DesignSystem,
|
||||
{ throwOnInvalidCandidate = false } = {},
|
||||
) {
|
||||
// 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<string, Variant | null> = new DefaultMap((variant, map) => {
|
||||
return parseVariant(variant, designSystem.variants, map)
|
||||
})
|
||||
|
||||
let candidates = new Map<Candidate, string>()
|
||||
|
||||
// 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}`)
|
||||
}
|
||||
designSystem.markInvalidCandidate(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
|
||||
let astNode = designSystem.compileAstNodes(rawCandidate)
|
||||
if (astNode === null) {
|
||||
if (throwOnInvalidCandidate) {
|
||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||
}
|
||||
|
||||
nodes = compiledNodes
|
||||
designSystem.markInvalidCandidate(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 +91,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 +103,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)]
|
||||
|
|
|
|||
|
|
@ -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,13 +17,31 @@ 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>
|
||||
compileAstNodes(candidate: string): ReturnType<typeof compileAstNodes>
|
||||
|
||||
getUsedVariants(): ReturnType<typeof parseVariant>[]
|
||||
getAstNodeSize(): number
|
||||
|
||||
markInvalidCandidate(candidate: string): void
|
||||
isInvalidCandidate(candidate: string): boolean
|
||||
}
|
||||
|
||||
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 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)[] = []
|
||||
|
|
@ -47,5 +67,30 @@ 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())
|
||||
},
|
||||
getAstNodeSize() {
|
||||
return compiledAstNodes.size
|
||||
},
|
||||
|
||||
markInvalidCandidate(candidate: string) {
|
||||
invalidRawCandidates.add(candidate)
|
||||
},
|
||||
isInvalidCandidate(candidate: string) {
|
||||
return invalidRawCandidates.has(candidate)
|
||||
},
|
||||
}
|
||||
|
||||
return designSystem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,9 +93,7 @@ describe('compiling CSS', () => {
|
|||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@tailwind utilities;"
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ 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,
|
||||
rawCandidates: string[],
|
||||
): {
|
||||
rebuild(candidates: string[]): string
|
||||
css: string
|
||||
} {
|
||||
let ast = CSS.parse(css)
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
|
|
@ -97,11 +103,17 @@ 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
|
||||
|
||||
// Set the `@tailwind utilities` nodes, to the actual generated CSS
|
||||
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
|
||||
// once per file but that's the intended usage at this point in time.
|
||||
|
|
@ -162,7 +174,55 @@ 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<string>()
|
||||
for (let rawCandidate of rawCandidates) {
|
||||
if (!designSystem.isInvalidCandidate(rawCandidate)) {
|
||||
allValidCandidates.add(rawCandidate)
|
||||
}
|
||||
}
|
||||
let compiledCss = toCss(ast)
|
||||
|
||||
return {
|
||||
rebuild(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 (!designSystem.isInvalidCandidate(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 previousAstNodeCount = designSystem.getAstNodeSize()
|
||||
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 === designSystem.getAstNodeSize()) {
|
||||
return compiledCss
|
||||
}
|
||||
|
||||
tailwindUtilitiesNode.nodes = newNodes
|
||||
compiledCss = toCss(ast)
|
||||
}
|
||||
|
||||
return compiledCss
|
||||
},
|
||||
css: compiledCss,
|
||||
}
|
||||
}
|
||||
|
||||
export function optimizeCss(
|
||||
|
|
|
|||
|
|
@ -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<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 []
|
||||
|
||||
|
|
|
|||
|
|
@ -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, candidates).css).trim()
|
||||
}
|
||||
|
||||
export function run(candidates: string[]) {
|
||||
return optimizeCss(compile('@tailwind utilities;', candidates)).trim()
|
||||
return optimizeCss(compile('@tailwind utilities;', candidates).css).trim()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ async function render(page: Page, content: string) {
|
|||
}
|
||||
`,
|
||||
scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential),
|
||||
),
|
||||
).css,
|
||||
),
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue