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
|
changed_content
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| match (c.file, c.content) {
|
.filter_map(|c| match (c.file, c.content) {
|
||||||
(Some(file), None) => std::fs::read(file).unwrap(),
|
(Some(file), None) => std::fs::read(file).ok(),
|
||||||
(None, Some(content)) => content.into_bytes(),
|
(None, Some(content)) => Some(content.into_bytes()),
|
||||||
_ => Default::default(),
|
_ => Default::default(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
import watcher from '@parcel/watcher'
|
import watcher from '@parcel/watcher'
|
||||||
import {
|
import { IO, Parsing, scanDir, scanFiles, type ChangedContent } from '@tailwindcss/oxide'
|
||||||
IO,
|
|
||||||
Parsing,
|
|
||||||
clearCache,
|
|
||||||
scanDir,
|
|
||||||
scanFiles,
|
|
||||||
type ChangedContent,
|
|
||||||
} from '@tailwindcss/oxide'
|
|
||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
@ -99,23 +92,41 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
||||||
args['--input'] ?? base,
|
args['--input'] ?? base,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compile the input
|
let previous = {
|
||||||
let result = compile(input, candidates)
|
css: '',
|
||||||
|
optimizedCss: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function write(css: string, args: Result<ReturnType<typeof options>>) {
|
||||||
|
let output = css
|
||||||
|
|
||||||
// Optimize the output
|
// Optimize the output
|
||||||
if (args['--minify'] || args['--optimize']) {
|
if (args['--minify'] || args['--optimize']) {
|
||||||
result = optimizeCss(result, {
|
if (css !== previous.css) {
|
||||||
|
let optimizedCss = optimizeCss(css, {
|
||||||
file: args['--input'] ?? 'input.css',
|
file: args['--input'] ?? 'input.css',
|
||||||
minify: args['--minify'] ?? false,
|
minify: args['--minify'] ?? false,
|
||||||
})
|
})
|
||||||
|
previous.css = css
|
||||||
|
previous.optimizedCss = optimizedCss
|
||||||
|
output = optimizedCss
|
||||||
|
} else {
|
||||||
|
output = previous.optimizedCss
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the output
|
// Write the output
|
||||||
if (args['--output']) {
|
if (args['--output']) {
|
||||||
await outputFile(args['--output'], result)
|
await outputFile(args['--output'], output)
|
||||||
} else {
|
} else {
|
||||||
println(result)
|
println(output)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the input
|
||||||
|
let result = compile(input, candidates)
|
||||||
|
|
||||||
|
await write(result.css, args)
|
||||||
|
|
||||||
let end = process.hrtime.bigint()
|
let end = process.hrtime.bigint()
|
||||||
eprintln(header())
|
eprintln(header())
|
||||||
|
|
@ -161,26 +172,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
||||||
// Re-compile the input
|
// Re-compile the input
|
||||||
let start = process.hrtime.bigint()
|
let start = process.hrtime.bigint()
|
||||||
|
|
||||||
|
// Track the compiled CSS
|
||||||
|
let compiledCss = ''
|
||||||
|
|
||||||
// Scan the entire `base` directory for full rebuilds.
|
// Scan the entire `base` directory for full rebuilds.
|
||||||
if (rebuildStrategy === 'full') {
|
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`.
|
// Re-scan the directory to get the new `candidates`.
|
||||||
candidates = scanDir({ base }).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`.
|
// Collect the new `input` and `cssImportPaths`.
|
||||||
;[input, cssImportPaths] = await handleImports(
|
;[input, cssImportPaths] = await handleImports(
|
||||||
args['--input']
|
args['--input']
|
||||||
|
|
@ -190,25 +189,19 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
||||||
`,
|
`,
|
||||||
args['--input'] ?? base,
|
args['--input'] ?? base,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
result = compile(input, candidates)
|
||||||
|
compiledCss = result.css
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile the input
|
// Scan changed files only for incremental rebuilds.
|
||||||
let result = compile(input, candidates)
|
else if (rebuildStrategy === 'incremental') {
|
||||||
|
let newCandidates = scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)
|
||||||
|
|
||||||
// Optimize the output
|
compiledCss = result.rebuild(newCandidates)
|
||||||
if (args['--minify'] || args['--optimize']) {
|
|
||||||
result = optimizeCss(result, {
|
|
||||||
file: args['--input'] ?? 'input.css',
|
|
||||||
minify: args['--minify'] ?? false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the output
|
await write(compiledCss, args)
|
||||||
if (args['--output']) {
|
|
||||||
await outputFile(args['--output'], result)
|
|
||||||
} else {
|
|
||||||
println(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = process.hrtime.bigint()
|
let end = process.hrtime.bigint()
|
||||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||||
|
|
@ -243,7 +236,9 @@ function handleImports(
|
||||||
// Relevant specification:
|
// Relevant specification:
|
||||||
// - CSS Import Resolve: https://csstools.github.io/css-import-resolve/
|
// - 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()
|
return postcss()
|
||||||
.use(atImport())
|
.use(atImport())
|
||||||
|
|
@ -253,6 +248,8 @@ function handleImports(
|
||||||
|
|
||||||
// Use `result.messages` to get the imported files. This also includes the
|
// Use `result.messages` to get the imported files. This also includes the
|
||||||
// current file itself.
|
// 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 { scanDir } from '@tailwindcss/oxide'
|
||||||
|
import fs from 'fs'
|
||||||
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
|
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||||
import postcssImport from 'postcss-import'
|
import postcssImport from 'postcss-import'
|
||||||
import { compile, optimizeCss } from 'tailwindcss'
|
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 = {
|
type PluginOptions = {
|
||||||
// The base directory to scan for class candidates.
|
// The base directory to scan for class candidates.
|
||||||
base?: string
|
base?: string
|
||||||
|
|
@ -15,13 +37,54 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||||
let base = opts.base ?? process.cwd()
|
let base = opts.base ?? process.cwd()
|
||||||
let optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
|
let optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
let cache = new DefaultMap(() => {
|
||||||
return {
|
return {
|
||||||
postcssPlugin: 'tailwindcss-v4',
|
mtimes: new Map<string, number>(),
|
||||||
|
rebuild: null as null | ReturnType<typeof compile>['rebuild'],
|
||||||
|
previousCss: '',
|
||||||
|
previousOptimizedCss: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
postcssPlugin: '@tailwindcss/postcss',
|
||||||
plugins: [
|
plugins: [
|
||||||
// We need to run `postcss-import` first to handle `@import` rules.
|
// We need to run `postcss-import` first to handle `@import` rules.
|
||||||
postcssImport(),
|
postcssImport(),
|
||||||
|
|
||||||
(root, result) => {
|
(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 hasApply = false
|
||||||
let hasTailwind = false
|
let hasTailwind = false
|
||||||
|
|
||||||
|
|
@ -40,22 +103,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||||
// Do nothing if neither `@tailwind` nor `@apply` is used
|
// Do nothing if neither `@tailwind` nor `@apply` is used
|
||||||
if (!hasTailwind && !hasApply) return
|
if (!hasTailwind && !hasApply) return
|
||||||
|
|
||||||
function replaceCss(css: string) {
|
let css = ''
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for candidates used to generate the CSS
|
// Look for candidates used to generate the CSS
|
||||||
let { candidates, files, globs } = scanDir({ base, globs: true })
|
let { candidates, files, globs } = scanDir({ base, globs: true })
|
||||||
|
|
@ -64,7 +112,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
result.messages.push({
|
result.messages.push({
|
||||||
type: 'dependency',
|
type: 'dependency',
|
||||||
plugin: 'tailwindcss-v4',
|
plugin: '@tailwindcss/postcss',
|
||||||
file,
|
file,
|
||||||
parent: result.opts.from,
|
parent: result.opts.from,
|
||||||
})
|
})
|
||||||
|
|
@ -76,14 +124,49 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||||
for (let { base, glob } of globs) {
|
for (let { base, glob } of globs) {
|
||||||
result.messages.push({
|
result.messages.push({
|
||||||
type: 'dir-dependency',
|
type: 'dir-dependency',
|
||||||
plugin: 'tailwindcss-v4',
|
plugin: '@tailwindcss/postcss',
|
||||||
dir: base,
|
dir: base,
|
||||||
glob,
|
glob,
|
||||||
parent: result.opts.from,
|
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) {
|
function generateCss(css: string) {
|
||||||
return compile(css, Array.from(candidates))
|
return compile(css, Array.from(candidates)).css
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateOptimizedCss(css: string) {
|
function generateOptimizedCss(css: string) {
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,13 @@ export function toCss(ast: AstNode[]) {
|
||||||
return css
|
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.
|
// Print at-rules without nodes with a `;` instead of an empty block.
|
||||||
//
|
//
|
||||||
// E.g.:
|
// E.g.:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { scanDir } from '@tailwindcss/oxide'
|
import { scanDir } from '@tailwindcss/oxide'
|
||||||
import { bench } from 'vitest'
|
import { bench } from 'vitest'
|
||||||
import { parseCandidate, parseVariant } from './candidate'
|
import { parseCandidate } from './candidate'
|
||||||
import { buildDesignSystem } from './design-system'
|
import { buildDesignSystem } from './design-system'
|
||||||
import { Theme } from './theme'
|
import { Theme } from './theme'
|
||||||
import { DefaultMap } from './utils/default-map'
|
|
||||||
|
|
||||||
// FOLDER=path/to/folder vitest bench
|
// FOLDER=path/to/folder vitest bench
|
||||||
const root = process.env.FOLDER || process.cwd()
|
const root = process.env.FOLDER || process.cwd()
|
||||||
|
|
@ -15,10 +14,6 @@ const designSystem = buildDesignSystem(new Theme())
|
||||||
|
|
||||||
bench('parseCandidate', () => {
|
bench('parseCandidate', () => {
|
||||||
for (let candidate of result.candidates) {
|
for (let candidate of result.candidates) {
|
||||||
parseCandidate(
|
parseCandidate(candidate, designSystem)
|
||||||
candidate,
|
|
||||||
designSystem.utilities,
|
|
||||||
new DefaultMap((variant, map) => parseVariant(variant, designSystem.variants, map)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it } from 'vitest'
|
||||||
import { parseCandidate, parseVariant } from './candidate'
|
import { buildDesignSystem } from './design-system'
|
||||||
|
import { Theme } from './theme'
|
||||||
import { Utilities } from './utilities'
|
import { Utilities } from './utilities'
|
||||||
import { DefaultMap } from './utils/default-map'
|
|
||||||
import { Variants } from './variants'
|
import { Variants } from './variants'
|
||||||
|
|
||||||
function run(
|
function run(
|
||||||
|
|
@ -11,11 +11,12 @@ function run(
|
||||||
utilities ??= new Utilities()
|
utilities ??= new Utilities()
|
||||||
variants ??= new Variants()
|
variants ??= new Variants()
|
||||||
|
|
||||||
let parsedVariants = new DefaultMap((variant, map) => {
|
let designSystem = buildDesignSystem(new Theme())
|
||||||
return parseVariant(variant, variants!, map)
|
|
||||||
})
|
|
||||||
|
|
||||||
return parseCandidate(candidate, utilities, parsedVariants)
|
designSystem.utilities = utilities
|
||||||
|
designSystem.variants = variants
|
||||||
|
|
||||||
|
return designSystem.parseCandidate(candidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should skip unknown utilities', () => {
|
it('should skip unknown utilities', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { DesignSystem } from './design-system'
|
||||||
import { decodeArbitraryValue } from './utils/decode-arbitrary-value'
|
import { decodeArbitraryValue } from './utils/decode-arbitrary-value'
|
||||||
import { segment } from './utils/segment'
|
import { segment } from './utils/segment'
|
||||||
|
|
||||||
|
|
@ -206,14 +207,7 @@ export type Candidate =
|
||||||
important: boolean
|
important: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCandidate(
|
export function parseCandidate(input: string, designSystem: DesignSystem): Candidate | null {
|
||||||
input: string,
|
|
||||||
utilities: {
|
|
||||||
has: (value: string) => boolean
|
|
||||||
kind: (root: string) => Omit<Candidate['kind'], 'arbitrary'>
|
|
||||||
},
|
|
||||||
parsedVariants: { get: (value: string) => Variant | null },
|
|
||||||
): Candidate | null {
|
|
||||||
// hover:focus:underline
|
// hover:focus:underline
|
||||||
// ^^^^^ ^^^^^^ -> Variants
|
// ^^^^^ ^^^^^^ -> Variants
|
||||||
// ^^^^^^^^^ -> Base
|
// ^^^^^^^^^ -> Base
|
||||||
|
|
@ -228,7 +222,7 @@ export function parseCandidate(
|
||||||
let parsedCandidateVariants: Variant[] = []
|
let parsedCandidateVariants: Variant[] = []
|
||||||
|
|
||||||
for (let variant of rawVariants) {
|
for (let variant of rawVariants) {
|
||||||
let parsedVariant = parsedVariants.get(variant)
|
let parsedVariant = designSystem.parseVariant(variant)
|
||||||
if (parsedVariant === null) return null
|
if (parsedVariant === null) return null
|
||||||
|
|
||||||
// Variants are applied left-to-right meaning that any representing pseudo-
|
// Variants are applied left-to-right meaning that any representing pseudo-
|
||||||
|
|
@ -320,7 +314,7 @@ export function parseCandidate(
|
||||||
base = base.slice(1)
|
base = base.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let [root, value] = findRoot(base, utilities)
|
let [root, value] = findRoot(base, designSystem.utilities)
|
||||||
|
|
||||||
let modifierSegment: string | null = null
|
let modifierSegment: string | null = null
|
||||||
|
|
||||||
|
|
@ -335,13 +329,13 @@ export function parseCandidate(
|
||||||
modifierSegment = rootModifierSegment
|
modifierSegment = rootModifierSegment
|
||||||
|
|
||||||
// Try to find the root and value, without the modifier present
|
// 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 there's no root, the candidate isn't a valid class and can be discarded.
|
||||||
if (root === null) return null
|
if (root === null) return null
|
||||||
|
|
||||||
let kind = utilities.kind(root)
|
let kind = designSystem.utilities.kind(root)
|
||||||
|
|
||||||
if (kind === 'static') {
|
if (kind === 'static') {
|
||||||
if (value !== null) return null
|
if (value !== null) return null
|
||||||
|
|
@ -475,15 +469,7 @@ function parseModifier(modifier: string): CandidateModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseVariant(
|
export function parseVariant(variant: string, designSystem: DesignSystem): Variant | null {
|
||||||
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 {
|
|
||||||
// Arbitrary variants
|
// Arbitrary variants
|
||||||
if (variant[0] === '[' && variant[variant.length - 1] === ']') {
|
if (variant[0] === '[' && variant[variant.length - 1] === ']') {
|
||||||
/**
|
/**
|
||||||
|
|
@ -535,20 +521,20 @@ export function parseVariant(
|
||||||
// - `group-hover/foo/bar`
|
// - `group-hover/foo/bar`
|
||||||
if (additionalModifier) return null
|
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
|
// Variant is invalid, therefore the candidate is invalid and we can skip
|
||||||
// continue parsing it.
|
// continue parsing it.
|
||||||
if (root === null) return null
|
if (root === null) return null
|
||||||
|
|
||||||
switch (variants.kind(root)) {
|
switch (designSystem.variants.kind(root)) {
|
||||||
case 'static': {
|
case 'static': {
|
||||||
if (value !== null) return null
|
if (value !== null) return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 'static',
|
kind: 'static',
|
||||||
root,
|
root,
|
||||||
compounds: variants.compounds(root),
|
compounds: designSystem.variants.compounds(root),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -564,7 +550,7 @@ export function parseVariant(
|
||||||
kind: 'arbitrary',
|
kind: 'arbitrary',
|
||||||
value: decodeArbitraryValue(value.slice(1, -1)),
|
value: decodeArbitraryValue(value.slice(1, -1)),
|
||||||
},
|
},
|
||||||
compounds: variants.compounds(root),
|
compounds: designSystem.variants.compounds(root),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -573,14 +559,14 @@ export function parseVariant(
|
||||||
root,
|
root,
|
||||||
modifier: modifier === null ? null : parseModifier(modifier),
|
modifier: modifier === null ? null : parseModifier(modifier),
|
||||||
value: { kind: 'named', value },
|
value: { kind: 'named', value },
|
||||||
compounds: variants.compounds(root),
|
compounds: designSystem.variants.compounds(root),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'compound': {
|
case 'compound': {
|
||||||
if (value === null) return null
|
if (value === null) return null
|
||||||
|
|
||||||
let subVariant = parsedVariants.get(value)
|
let subVariant = designSystem.parseVariant(value)
|
||||||
if (subVariant === null) return null
|
if (subVariant === null) return null
|
||||||
if (subVariant.compounds === false) return null
|
if (subVariant.compounds === false) return null
|
||||||
|
|
||||||
|
|
@ -589,7 +575,7 @@ export function parseVariant(
|
||||||
root,
|
root,
|
||||||
modifier: modifier === null ? null : { kind: 'named', value: modifier },
|
modifier: modifier === null ? null : { kind: 'named', value: modifier },
|
||||||
variant: subVariant,
|
variant: subVariant,
|
||||||
compounds: variants.compounds(root),
|
compounds: designSystem.variants.compounds(root),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,120 +1,58 @@
|
||||||
import { rule, type AstNode, type Rule } from './ast'
|
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 { type DesignSystem } from './design-system'
|
||||||
import GLOBAL_PROPERTY_ORDER from './property-order'
|
import GLOBAL_PROPERTY_ORDER from './property-order'
|
||||||
import { DefaultMap } from './utils/default-map'
|
|
||||||
import { escape } from './utils/escape'
|
import { escape } from './utils/escape'
|
||||||
import type { Variants } from './variants'
|
import type { Variants } from './variants'
|
||||||
|
|
||||||
export function compileCandidates(
|
export function compileCandidates(
|
||||||
rawCandidates: string[],
|
rawCandidates: Iterable<string>,
|
||||||
designSystem: DesignSystem,
|
designSystem: DesignSystem,
|
||||||
{ throwOnInvalidCandidate = false } = {},
|
{ throwOnInvalidCandidate = false } = {},
|
||||||
) {
|
) {
|
||||||
// Ensure the candidates are sorted alphabetically
|
|
||||||
rawCandidates.sort()
|
|
||||||
|
|
||||||
let nodeSorting = new Map<
|
let nodeSorting = new Map<
|
||||||
AstNode,
|
AstNode,
|
||||||
{ properties: number[]; variants: bigint; candidate: string }
|
{ properties: number[]; variants: bigint; candidate: string }
|
||||||
>()
|
>()
|
||||||
let astNodes: AstNode[] = []
|
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>()
|
let candidates = new Map<Candidate, string>()
|
||||||
|
|
||||||
// Parse candidates and variants
|
// Parse candidates and variants
|
||||||
for (let rawCandidate of rawCandidates) {
|
for (let rawCandidate of rawCandidates) {
|
||||||
let candidate = parseCandidate(rawCandidate, designSystem.utilities, parsedVariants)
|
let candidate = designSystem.parseCandidate(rawCandidate)
|
||||||
if (candidate === null) {
|
if (candidate === null) {
|
||||||
if (throwOnInvalidCandidate) {
|
if (throwOnInvalidCandidate) {
|
||||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||||
}
|
}
|
||||||
|
designSystem.markInvalidCandidate(rawCandidate)
|
||||||
continue // Bail, invalid candidate
|
continue // Bail, invalid candidate
|
||||||
}
|
}
|
||||||
candidates.set(candidate, rawCandidate)
|
candidates.set(candidate, rawCandidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the variants
|
// 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)
|
return designSystem.variants.compare(a, z)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create the AST
|
// Create the AST
|
||||||
next: for (let [candidate, rawCandidate] of candidates) {
|
next: for (let [candidate, rawCandidate] of candidates) {
|
||||||
let nodes: AstNode[] = []
|
let astNode = designSystem.compileAstNodes(rawCandidate)
|
||||||
|
if (astNode === null) {
|
||||||
// Handle arbitrary properties
|
|
||||||
if (candidate.kind === 'arbitrary') {
|
|
||||||
let compileFn = designSystem.utilities.getArbitrary()
|
|
||||||
|
|
||||||
// Build the node
|
|
||||||
let compiledNodes = compileFn(candidate)
|
|
||||||
if (compiledNodes === undefined) {
|
|
||||||
if (throwOnInvalidCandidate) {
|
if (throwOnInvalidCandidate) {
|
||||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||||
}
|
}
|
||||||
|
designSystem.markInvalidCandidate(rawCandidate)
|
||||||
continue next
|
continue next
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes = compiledNodes
|
let { node, propertySort } = astNode
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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
|
// 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
|
// variant. This allows us to sort the rules based on the order of
|
||||||
// variants used.
|
// variants used.
|
||||||
|
let variantOrder = 0n
|
||||||
|
for (let variant of candidate.variants) {
|
||||||
variantOrder |= 1n << BigInt(variants.indexOf(variant))
|
variantOrder |= 1n << BigInt(variants.indexOf(variant))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +91,9 @@ export function compileCandidates(
|
||||||
// Sort by lowest property index first
|
// Sort by lowest property index first
|
||||||
(aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) ||
|
(aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) ||
|
||||||
// Sort by most properties first, then by least properties
|
// 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 {
|
export function applyVariant(node: Rule, variant: Variant, variants: Variants): null | void {
|
||||||
if (variant.kind === 'arbitrary') {
|
if (variant.kind === 'arbitrary') {
|
||||||
node.nodes = [rule(variant.selector, node.nodes)]
|
node.nodes = [rule(variant.selector, node.nodes)]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { toCss } from './ast'
|
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 { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
|
||||||
import { getClassOrder } from './sort'
|
import { getClassOrder } from './sort'
|
||||||
import type { Theme } from './theme'
|
import type { Theme } from './theme'
|
||||||
import { Utilities, createUtilities } from './utilities'
|
import { Utilities, createUtilities } from './utilities'
|
||||||
|
import { DefaultMap } from './utils/default-map'
|
||||||
import { Variants, createVariants } from './variants'
|
import { Variants, createVariants } from './variants'
|
||||||
|
|
||||||
export type DesignSystem = {
|
export type DesignSystem = {
|
||||||
|
|
@ -15,13 +17,31 @@ export type DesignSystem = {
|
||||||
getClassOrder(classes: string[]): [string, bigint | null][]
|
getClassOrder(classes: string[]): [string, bigint | null][]
|
||||||
getClassList(): ClassEntry[]
|
getClassList(): ClassEntry[]
|
||||||
getVariants(): VariantEntry[]
|
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 {
|
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,
|
theme,
|
||||||
utilities: createUtilities(theme),
|
utilities,
|
||||||
variants: createVariants(theme),
|
variants,
|
||||||
|
|
||||||
candidatesToCss(classes: string[]) {
|
candidatesToCss(classes: string[]) {
|
||||||
let result: (string | null)[] = []
|
let result: (string | null)[] = []
|
||||||
|
|
@ -47,5 +67,30 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||||
getVariants() {
|
getVariants() {
|
||||||
return getVariants(this)
|
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 {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}"
|
||||||
|
|
||||||
@tailwind utilities;"
|
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ import * as CSS from './css-parser'
|
||||||
import { buildDesignSystem } from './design-system'
|
import { buildDesignSystem } from './design-system'
|
||||||
import { Theme } from './theme'
|
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)
|
let ast = CSS.parse(css)
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
|
@ -97,11 +103,17 @@ export function compile(css: string, rawCandidates: string[]) {
|
||||||
|
|
||||||
let designSystem = buildDesignSystem(theme)
|
let designSystem = buildDesignSystem(theme)
|
||||||
|
|
||||||
|
let tailwindUtilitiesNode: Rule | null = null
|
||||||
|
|
||||||
// Find `@tailwind utilities` and replace it with the actual generated utility
|
// Find `@tailwind utilities` and replace it with the actual generated utility
|
||||||
// class CSS.
|
// class CSS.
|
||||||
walk(ast, (node, { replaceWith }) => {
|
walk(ast, (node) => {
|
||||||
if (node.kind === 'rule' && node.selector === '@tailwind utilities') {
|
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
|
// Stop walking after finding `@tailwind utilities` to avoid walking all
|
||||||
// of the generated CSS. This means `@tailwind utilities` can only appear
|
// 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.
|
// 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(
|
export function optimizeCss(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { decl, rule } from './ast'
|
import { decl, rule } from './ast'
|
||||||
import { parseVariant, type Variant } from './candidate'
|
|
||||||
import { applyVariant } from './compile'
|
import { applyVariant } from './compile'
|
||||||
import type { DesignSystem } from './design-system'
|
import type { DesignSystem } from './design-system'
|
||||||
import { DefaultMap } from './utils/default-map'
|
|
||||||
|
|
||||||
interface ClassMetadata {
|
interface ClassMetadata {
|
||||||
modifiers: string[]
|
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
|
return list
|
||||||
}
|
}
|
||||||
|
|
@ -60,9 +58,6 @@ export interface VariantEntry {
|
||||||
|
|
||||||
export function getVariants(design: DesignSystem) {
|
export function getVariants(design: DesignSystem) {
|
||||||
let list: VariantEntry[] = []
|
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()) {
|
for (let [root, variant] of design.variants.entries()) {
|
||||||
if (variant.kind === 'arbitrary') continue
|
if (variant.kind === 'arbitrary') continue
|
||||||
|
|
@ -74,7 +69,7 @@ export function getVariants(design: DesignSystem) {
|
||||||
if (value) name += `-${value}`
|
if (value) name += `-${value}`
|
||||||
if (modifier) name += `/${modifier}`
|
if (modifier) name += `/${modifier}`
|
||||||
|
|
||||||
let variant = parsedVariants.get(name)
|
let variant = design.parseVariant(name)
|
||||||
|
|
||||||
if (!variant) return []
|
if (!variant) return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { compile, optimizeCss } from '..'
|
import { compile, optimizeCss } from '..'
|
||||||
|
|
||||||
export function compileCss(css: string, candidates: string[] = []) {
|
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[]) {
|
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(
|
expect(
|
||||||
run(['supports-gap:grid', 'supports-[display:grid]:flex', 'supports-[selector(A_>_B)]:flex']),
|
run(['supports-gap:grid', 'supports-[display:grid]:flex', 'supports-[selector(A_>_B)]:flex']),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"@supports (display: grid) {
|
"@supports (gap: var(--tw)) {
|
||||||
|
.supports-gap\\:grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (display: grid) {
|
||||||
.supports-\\[display\\:grid\\]\\:flex {
|
.supports-\\[display\\:grid\\]\\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
@ -1290,12 +1296,6 @@ test('supports', () => {
|
||||||
.supports-\\[selector\\(A_\\>_B\\)\\]\\:flex {
|
.supports-\\[selector\\(A_\\>_B\\)\\]\\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@supports (gap: var(--tw)) {
|
|
||||||
.supports-gap\\:grid {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
@ -1322,11 +1322,11 @@ test('not', () => {
|
||||||
display: flex;
|
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;
|
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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1334,11 +1334,11 @@ test('not', () => {
|
||||||
display: flex;
|
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;
|
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;
|
display: flex;
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
@ -1362,11 +1362,11 @@ test('has', () => {
|
||||||
display: flex;
|
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;
|
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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1374,11 +1374,11 @@ test('has', () => {
|
||||||
display: flex;
|
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;
|
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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1405,15 +1405,7 @@ test('aria', () => {
|
||||||
'peer-aria-checked/parent-name:flex',
|
'peer-aria-checked/parent-name:flex',
|
||||||
]),
|
]),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
".group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) {
|
".group-aria-\\[modal\\]\\:flex:is(:where(.group)[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"] *) {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1421,7 +1413,11 @@ test('aria', () => {
|
||||||
display: flex;
|
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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1429,20 +1425,24 @@ test('aria', () => {
|
||||||
display: flex;
|
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"] ~ *) {
|
.peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *) {
|
||||||
display: flex;
|
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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aria-checked\\:flex[aria-checked="true"] {
|
.aria-checked\\:flex[aria-checked="true"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] {
|
||||||
|
display: flex;
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
@ -1460,15 +1460,11 @@ test('data', () => {
|
||||||
'peer-data-[disabled]/parent-name:flex',
|
'peer-data-[disabled]/parent-name:flex',
|
||||||
]),
|
]),
|
||||||
).toMatchInlineSnapshot(`
|
).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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) {
|
.group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) {
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1476,12 +1472,16 @@ test('data', () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] {
|
.peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-disabled\\:flex[data-disabled] {
|
.data-disabled\\:flex[data-disabled] {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] {
|
||||||
|
display: flex;
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
@ -1567,14 +1567,14 @@ test('container queries', () => {
|
||||||
--width-lg: 1024px;
|
--width-lg: 1024px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container name (width < 1024px) {
|
@container (width < 1024px) {
|
||||||
.\\@max-lg\\/name\\:flex {
|
.\\@max-lg\\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (width < 1024px) {
|
@container name (width < 1024px) {
|
||||||
.\\@max-lg\\:flex {
|
.\\@max-lg\\/name\\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1615,12 +1615,6 @@ test('container queries', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@container name (width >= 1024px) {
|
|
||||||
.\\@lg\\/name\\:flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (width >= 1024px) {
|
@container (width >= 1024px) {
|
||||||
.\\@lg\\:flex {
|
.\\@lg\\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1628,7 +1622,7 @@ test('container queries', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
@container name (width >= 1024px) {
|
@container name (width >= 1024px) {
|
||||||
.\\@min-lg\\/name\\:flex {
|
.\\@lg\\/name\\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1637,6 +1631,12 @@ test('container queries', () => {
|
||||||
.\\@min-lg\\:flex {
|
.\\@min-lg\\:flex {
|
||||||
display: 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 (z === null) return 1
|
||||||
|
|
||||||
if (a.kind === 'arbitrary' && z.kind === 'arbitrary') {
|
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') {
|
} else if (a.kind === 'arbitrary') {
|
||||||
return 1
|
return 1
|
||||||
} else if (z.kind === 'arbitrary') {
|
} else if (z.kind === 'arbitrary') {
|
||||||
|
|
@ -114,7 +114,7 @@ export class Variants {
|
||||||
let compareFn = this.compareFns.get(aOrder)
|
let compareFn = this.compareFns.get(aOrder)
|
||||||
if (compareFn === undefined) return 0
|
if (compareFn === undefined) return 0
|
||||||
|
|
||||||
return compareFn(a, z)
|
return compareFn(a, z) || (a.root < z.root ? -1 : 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys() {
|
keys() {
|
||||||
|
|
@ -469,7 +469,7 @@ export function createVariants(theme: Theme): Variants {
|
||||||
|
|
||||||
let order =
|
let order =
|
||||||
// Compare by bucket name
|
// Compare by bucket name
|
||||||
aBucket.localeCompare(zBucket) ||
|
(aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) ||
|
||||||
// If bucket names are the same, compare by value
|
// If bucket names are the same, compare by value
|
||||||
(direction === 'asc'
|
(direction === 'asc'
|
||||||
? parseInt(aValue) - parseInt(zValue)
|
? 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
|
// In this scenario, we want to alphabetically sort `calc(100%-1rem)` and
|
||||||
// `calc(100%-2rem)` to make it deterministic.
|
// `calc(100%-2rem)` to make it deterministic.
|
||||||
if (Number.isNaN(order)) {
|
if (Number.isNaN(order)) {
|
||||||
return aValue.localeCompare(zValue)
|
return aValue < zValue ? -1 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ async function render(page: Page, content: string) {
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential),
|
scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential),
|
||||||
),
|
).css,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue