mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Use AST transformations in @tailwindcss/postcss (#15297)
This PR improves the `@tailwindcss/postcss` integration by using direct AST transformations between our own AST and PostCSS's AST. This allows us to skip a step where we convert our AST into a string, then parse it back into a PostCSS AST. The only downside is that we still have to print the AST into a string if we want to optimize the CSS using Lightning CSS. Luckily this only happens in production (`NODE_ENV=production`). This also introduces a new private `compileAst` API, that allows us to accept an AST as the input. This allows us to skip the PostCSS AST -> string -> parse into our own AST step. To summarize: Instead of: - Input: `PostCSS AST` -> `.toString()` -> `CSS.parse(…)` -> `Tailwind CSS AST` - Output: `Tailwind CSS AST` -> `toCSS(ast)` -> `postcss.parse(…)` -> `PostCSS AST` We will now do this instead: - Input: `PostCSS AST` -> `transform(…)` -> `Tailwind CSS AST` - Output: `Tailwind CSS AST` -> `transform(…)` -> `PostCSS AST` --- Running this on Catalyst, the time spent in the `@tailwindcss/postcss` looks like this: - Before: median time per run: 19.407687 ms - After: median time per run: 11.8796455 ms This is tested on Catalyst which roughly generates ~208kb worth of CSS in dev mode. While it's not a lot, skipping the stringification and parsing seems to improve this step by ~40%. Note: these times exclude scanning the actual candidates and only time the work needed for parsing/stringifying the CSS from and into ASTs. The actual numbers are a bit higher because of the Oxide scanner reading files from disk. But since that part is going to be there no matter what, it's not fair to include it in this benchmark. --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
536e11895e
commit
408fa99849
@ -171,11 +171,18 @@ impl Scanner {
|
||||
fn compute_candidates(&mut self) {
|
||||
let mut changed_content = vec![];
|
||||
|
||||
for path in &self.files {
|
||||
let current_time = fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now());
|
||||
let current_mtimes = self
|
||||
.files
|
||||
.par_iter()
|
||||
.map(|path| {
|
||||
fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (idx, path) in self.files.iter().enumerate() {
|
||||
let current_time = current_mtimes[idx];
|
||||
let previous_time = self.mtimes.insert(path.clone(), current_time);
|
||||
|
||||
let should_scan_file = match previous_time {
|
||||
@ -218,14 +225,21 @@ impl Scanner {
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn check_for_new_files(&mut self) {
|
||||
let current_mtimes = self
|
||||
.dirs
|
||||
.par_iter()
|
||||
.map(|path| {
|
||||
fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut modified_dirs: Vec<PathBuf> = vec![];
|
||||
|
||||
// Check all directories to see if they were modified
|
||||
for path in &self.dirs {
|
||||
let current_time = fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now());
|
||||
|
||||
for (idx, path) in self.dirs.iter().enumerate() {
|
||||
let current_time = current_mtimes[idx];
|
||||
let previous_time = self.mtimes.insert(path.clone(), current_time);
|
||||
|
||||
let should_scan = match previous_time {
|
||||
|
||||
@ -40,7 +40,6 @@ pub fn resolve_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
.filter_map(Result::ok)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn read_dir(root: &Path, depth: Option<usize>) -> impl Iterator<Item = DirEntry> {
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
|
||||
@ -7,8 +7,10 @@ import { pathToFileURL } from 'node:url'
|
||||
import {
|
||||
__unstable__loadDesignSystem as ___unstable__loadDesignSystem,
|
||||
compile as _compile,
|
||||
compileAst as _compileAst,
|
||||
Features,
|
||||
} from 'tailwindcss'
|
||||
import type { AstNode } from '../../tailwindcss/src/ast'
|
||||
import { getModuleDependencies } from './get-module-dependencies'
|
||||
import { rewriteUrls } from './urls'
|
||||
|
||||
@ -16,30 +18,29 @@ export { Features }
|
||||
|
||||
export type Resolver = (id: string, base: string) => Promise<string | false | undefined>
|
||||
|
||||
export async function compile(
|
||||
css: string,
|
||||
{
|
||||
base,
|
||||
onDependency,
|
||||
shouldRewriteUrls,
|
||||
export interface CompileOptions {
|
||||
base: string
|
||||
onDependency: (path: string) => void
|
||||
shouldRewriteUrls?: boolean
|
||||
|
||||
customCssResolver,
|
||||
customJsResolver,
|
||||
}: {
|
||||
base: string
|
||||
onDependency: (path: string) => void
|
||||
shouldRewriteUrls?: boolean
|
||||
customCssResolver?: Resolver
|
||||
customJsResolver?: Resolver
|
||||
}
|
||||
|
||||
customCssResolver?: Resolver
|
||||
customJsResolver?: Resolver
|
||||
},
|
||||
) {
|
||||
let compiler = await _compile(css, {
|
||||
function createCompileOptions({
|
||||
base,
|
||||
onDependency,
|
||||
shouldRewriteUrls,
|
||||
|
||||
customCssResolver,
|
||||
customJsResolver,
|
||||
}: CompileOptions) {
|
||||
return {
|
||||
base,
|
||||
async loadModule(id, base) {
|
||||
async loadModule(id: string, base: string) {
|
||||
return loadModule(id, base, onDependency, customJsResolver)
|
||||
},
|
||||
async loadStylesheet(id, base) {
|
||||
async loadStylesheet(id: string, base: string) {
|
||||
let sheet = await loadStylesheet(id, base, onDependency, customCssResolver)
|
||||
|
||||
if (shouldRewriteUrls) {
|
||||
@ -52,8 +53,13 @@ export async function compile(
|
||||
|
||||
return sheet
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureSourceDetectionRootExists(
|
||||
compiler: { root: Awaited<ReturnType<typeof compile>>['root'] },
|
||||
base: string,
|
||||
) {
|
||||
// Verify if the `source(…)` path exists (until the glob pattern starts)
|
||||
if (compiler.root && compiler.root !== 'none') {
|
||||
let globSymbols = /[*{]/
|
||||
@ -75,7 +81,17 @@ export async function compile(
|
||||
throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function compileAst(ast: AstNode[], options: CompileOptions) {
|
||||
let compiler = await _compileAst(ast, createCompileOptions(options))
|
||||
await ensureSourceDetectionRootExists(compiler, options.base)
|
||||
return compiler
|
||||
}
|
||||
|
||||
export async function compile(css: string, options: CompileOptions) {
|
||||
let compiler = await _compile(css, createCompileOptions(options))
|
||||
await ensureSourceDetectionRootExists(compiler, options.base)
|
||||
return compiler
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as Module from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import * as env from './env'
|
||||
export { __unstable__loadDesignSystem, compile, Features } from './compile'
|
||||
export { __unstable__loadDesignSystem, compile, compileAst, Features } from './compile'
|
||||
export * from './normalize-path'
|
||||
export { env }
|
||||
|
||||
|
||||
107
packages/@tailwindcss-postcss/src/ast.test.ts
Normal file
107
packages/@tailwindcss-postcss/src/ast.test.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import dedent from 'dedent'
|
||||
import postcss from 'postcss'
|
||||
import { expect, it } from 'vitest'
|
||||
import { toCss } from '../../tailwindcss/src/ast'
|
||||
import { parse } from '../../tailwindcss/src/css-parser'
|
||||
import { cssAstToPostCssAst, postCssAstToCssAst } from './ast'
|
||||
|
||||
let css = dedent
|
||||
|
||||
it('should convert a PostCSS AST into a Tailwind CSS AST', () => {
|
||||
let input = css`
|
||||
@charset "UTF-8";
|
||||
|
||||
@layer foo, bar, baz;
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
.foo {
|
||||
color: red;
|
||||
|
||||
&:hover {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.bar {
|
||||
color: green !important;
|
||||
background-color: yellow;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let ast = postcss.parse(input)
|
||||
let transformedAst = postCssAstToCssAst(ast)
|
||||
|
||||
expect(toCss(transformedAst)).toMatchInlineSnapshot(`
|
||||
"@charset "UTF-8";
|
||||
@layer foo, bar, baz;
|
||||
@import 'tailwindcss';
|
||||
.foo {
|
||||
color: red;
|
||||
&:hover {
|
||||
color: blue;
|
||||
}
|
||||
.bar {
|
||||
color: green !important;
|
||||
background-color: yellow;
|
||||
@media (min-width: 640px) {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should convert a Tailwind CSS AST into a PostCSS AST', () => {
|
||||
let input = css`
|
||||
@charset "UTF-8";
|
||||
|
||||
@layer foo, bar, baz;
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
.foo {
|
||||
color: red;
|
||||
|
||||
&:hover {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.bar {
|
||||
color: green !important;
|
||||
background-color: yellow;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let ast = parse(input)
|
||||
let transformedAst = cssAstToPostCssAst(ast)
|
||||
|
||||
expect(transformedAst.toString()).toMatchInlineSnapshot(`
|
||||
"@charset "UTF-8";
|
||||
@layer foo, bar, baz;
|
||||
@import 'tailwindcss';
|
||||
.foo {
|
||||
color: red;
|
||||
&:hover {
|
||||
color: blue;
|
||||
}
|
||||
.bar {
|
||||
color: green !important;
|
||||
background-color: yellow;
|
||||
@media (min-width: 640px) {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
117
packages/@tailwindcss-postcss/src/ast.ts
Normal file
117
packages/@tailwindcss-postcss/src/ast.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import postcss, {
|
||||
type ChildNode as PostCssChildNode,
|
||||
type Container as PostCssContainerNode,
|
||||
type Root as PostCssRoot,
|
||||
type Source as PostcssSource,
|
||||
} from 'postcss'
|
||||
import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast'
|
||||
|
||||
const EXCLAMATION_MARK = 0x21
|
||||
|
||||
export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot {
|
||||
let root = postcss.root()
|
||||
root.source = source
|
||||
|
||||
function transform(node: AstNode, parent: PostCssContainerNode) {
|
||||
// Declaration
|
||||
if (node.kind === 'declaration') {
|
||||
let astNode = postcss.decl({
|
||||
prop: node.property,
|
||||
value: node.value ?? '',
|
||||
important: node.important,
|
||||
})
|
||||
astNode.source = source
|
||||
parent.append(astNode)
|
||||
}
|
||||
|
||||
// Rule
|
||||
else if (node.kind === 'rule') {
|
||||
let astNode = postcss.rule({ selector: node.selector })
|
||||
astNode.source = source
|
||||
astNode.raws.semicolon = true
|
||||
parent.append(astNode)
|
||||
for (let child of node.nodes) {
|
||||
transform(child, astNode)
|
||||
}
|
||||
}
|
||||
|
||||
// AtRule
|
||||
else if (node.kind === 'at-rule') {
|
||||
let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params })
|
||||
astNode.source = source
|
||||
astNode.raws.semicolon = true
|
||||
parent.append(astNode)
|
||||
for (let child of node.nodes) {
|
||||
transform(child, astNode)
|
||||
}
|
||||
}
|
||||
|
||||
// Comment
|
||||
else if (node.kind === 'comment') {
|
||||
let astNode = postcss.comment({ text: node.value })
|
||||
// Spaces are encoded in our node.value already, no need to add additional
|
||||
// spaces.
|
||||
astNode.raws.left = ''
|
||||
astNode.raws.right = ''
|
||||
astNode.source = source
|
||||
parent.append(astNode)
|
||||
}
|
||||
|
||||
// AtRoot & Context should not happen
|
||||
else if (node.kind === 'at-root' || node.kind === 'context') {
|
||||
}
|
||||
|
||||
// Unknown
|
||||
else {
|
||||
node satisfies never
|
||||
}
|
||||
}
|
||||
|
||||
for (let node of ast) {
|
||||
transform(node, root)
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
|
||||
function transform(
|
||||
node: PostCssChildNode,
|
||||
parent: Extract<AstNode, { nodes: AstNode[] }>['nodes'],
|
||||
) {
|
||||
// Declaration
|
||||
if (node.type === 'decl') {
|
||||
parent.push(decl(node.prop, node.value, node.important))
|
||||
}
|
||||
|
||||
// Rule
|
||||
else if (node.type === 'rule') {
|
||||
let astNode = rule(node.selector)
|
||||
node.each((child) => transform(child, astNode.nodes))
|
||||
parent.push(astNode)
|
||||
}
|
||||
|
||||
// AtRule
|
||||
else if (node.type === 'atrule') {
|
||||
let astNode = atRule(`@${node.name}`, node.params)
|
||||
node.each((child) => transform(child, astNode.nodes))
|
||||
parent.push(astNode)
|
||||
}
|
||||
|
||||
// Comment
|
||||
else if (node.type === 'comment') {
|
||||
if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return
|
||||
parent.push(comment(node.text))
|
||||
}
|
||||
|
||||
// Unknown
|
||||
else {
|
||||
node satisfies never
|
||||
}
|
||||
}
|
||||
|
||||
let ast: AstNode[] = []
|
||||
root.each((node) => transform(node, ast))
|
||||
|
||||
return ast
|
||||
}
|
||||
@ -1,32 +1,38 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
import { compile, env, Features } from '@tailwindcss/node'
|
||||
import { compileAst, env, Features } from '@tailwindcss/node'
|
||||
import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
||||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import { Features as LightningCssFeatures, transform } from 'lightningcss'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
|
||||
import { cssAstToPostCssAst, postCssAstToCssAst } from './ast'
|
||||
import fixRelativePathsPlugin from './postcss-fix-relative-paths'
|
||||
|
||||
interface CacheEntry {
|
||||
mtimes: Map<string, number>
|
||||
compiler: null | Awaited<ReturnType<typeof compile>>
|
||||
compiler: null | Awaited<ReturnType<typeof compileAst>>
|
||||
scanner: null | Scanner
|
||||
css: string
|
||||
optimizedCss: string
|
||||
tailwindCssAst: AstNode[]
|
||||
cachedPostCssAst: postcss.Root
|
||||
optimizedPostCssAst: postcss.Root
|
||||
fullRebuildPaths: string[]
|
||||
}
|
||||
let cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
|
||||
|
||||
function getContextFromCache(inputFile: string, opts: PluginOptions): CacheEntry {
|
||||
let key = `${inputFile}:${opts.base ?? ''}:${opts.optimize ?? ''}`
|
||||
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
|
||||
if (cache.has(key)) return cache.get(key)!
|
||||
let entry = {
|
||||
mtimes: new Map<string, number>(),
|
||||
compiler: null,
|
||||
scanner: null,
|
||||
css: '',
|
||||
optimizedCss: '',
|
||||
|
||||
tailwindCssAst: [],
|
||||
cachedPostCssAst: postcss.root(),
|
||||
optimizedPostCssAst: postcss.root(),
|
||||
|
||||
fullRebuildPaths: [] as string[],
|
||||
}
|
||||
cache.set(key, entry)
|
||||
@ -69,7 +75,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||
|
||||
context.fullRebuildPaths = []
|
||||
|
||||
let compiler = await compile(root.toString(), {
|
||||
let compiler = await compileAst(postCssAstToCssAst(root), {
|
||||
base: inputBasePath,
|
||||
onDependency: (path) => {
|
||||
context.fullRebuildPaths.push(path)
|
||||
@ -128,8 +134,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
let css = ''
|
||||
|
||||
if (
|
||||
rebuildStrategy === 'full' &&
|
||||
// We can re-use the compiler if it was created during the
|
||||
@ -205,23 +209,43 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS')
|
||||
css = context.compiler.build(candidates)
|
||||
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build CSS')
|
||||
env.DEBUG && console.time('[@tailwindcss/postcss] Build AST')
|
||||
let tailwindCssAst = context.compiler.build(candidates)
|
||||
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build AST')
|
||||
|
||||
// Replace CSS
|
||||
if (css !== context.css && optimize) {
|
||||
env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS')
|
||||
context.optimizedCss = optimizeCss(css, {
|
||||
minify: typeof optimize === 'object' ? optimize.minify : true,
|
||||
})
|
||||
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS')
|
||||
if (context.tailwindCssAst !== tailwindCssAst) {
|
||||
if (optimize) {
|
||||
env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS')
|
||||
context.optimizedPostCssAst = postcss.parse(
|
||||
optimizeCss(toCss(tailwindCssAst), {
|
||||
minify: typeof optimize === 'object' ? optimize.minify : true,
|
||||
}),
|
||||
result.opts,
|
||||
)
|
||||
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS')
|
||||
} else {
|
||||
// Convert our AST to a PostCSS AST
|
||||
env.DEBUG && console.time('[@tailwindcss/postcss] Transform CSS AST into PostCSS AST')
|
||||
context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source)
|
||||
env.DEBUG &&
|
||||
console.timeEnd('[@tailwindcss/postcss] Transform CSS AST into PostCSS AST')
|
||||
}
|
||||
}
|
||||
context.css = css
|
||||
|
||||
context.tailwindCssAst = tailwindCssAst
|
||||
|
||||
env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST')
|
||||
root.removeAll()
|
||||
root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts))
|
||||
root.append(
|
||||
optimize
|
||||
? context.optimizedPostCssAst.clone().nodes
|
||||
: context.cachedPostCssAst.clone().nodes,
|
||||
)
|
||||
|
||||
// Trick PostCSS into thinking the indent is 2 spaces, so it uses that
|
||||
// as the default instead of 4.
|
||||
root.raws.indent = ' '
|
||||
|
||||
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST')
|
||||
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss')
|
||||
},
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { context, decl, styleRule, toCss, walk, WalkAction } from './ast'
|
||||
import { context, decl, optimizeAst, styleRule, toCss, walk, WalkAction } from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
|
||||
it('should pretty print an AST', () => {
|
||||
expect(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}'))).toMatchInlineSnapshot(`
|
||||
expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}'))))
|
||||
.toMatchInlineSnapshot(`
|
||||
".foo {
|
||||
color: red;
|
||||
&:hover {
|
||||
@ -51,7 +52,7 @@ it('allows the placement of context nodes', () => {
|
||||
expect(blueContext).toEqual({ context: 'a' })
|
||||
expect(greenContext).toEqual({ context: 'b' })
|
||||
|
||||
expect(toCss(ast)).toMatchInlineSnapshot(`
|
||||
expect(toCss(optimizeAst(ast))).toMatchInlineSnapshot(`
|
||||
".foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@ -66,12 +66,12 @@ export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRul
|
||||
return styleRule(selector, nodes)
|
||||
}
|
||||
|
||||
export function decl(property: string, value: string | undefined): Declaration {
|
||||
export function decl(property: string, value: string | undefined, important = false): Declaration {
|
||||
return {
|
||||
kind: 'declaration',
|
||||
property,
|
||||
value,
|
||||
important: false,
|
||||
important,
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,18 +208,151 @@ export function walkDepth(
|
||||
}
|
||||
}
|
||||
|
||||
export function toCss(ast: AstNode[]) {
|
||||
let atRoots: string = ''
|
||||
// Optimize the AST for printing where all the special nodes that require custom
|
||||
// handling are handled such that the printing is a 1-to-1 transformation.
|
||||
export function optimizeAst(ast: AstNode[]) {
|
||||
let atRoots: AstNode[] = []
|
||||
let seenAtProperties = new Set<string>()
|
||||
let propertyFallbacksRoot: Declaration[] = []
|
||||
let propertyFallbacksUniversal: Declaration[] = []
|
||||
|
||||
function transform(
|
||||
node: AstNode,
|
||||
parent: Extract<AstNode, { nodes: AstNode[] }>['nodes'],
|
||||
depth = 0,
|
||||
) {
|
||||
// Declaration
|
||||
if (node.kind === 'declaration') {
|
||||
if (node.property === '--tw-sort' || node.value === undefined || node.value === null) {
|
||||
return
|
||||
}
|
||||
parent.push(node)
|
||||
}
|
||||
|
||||
// Rule
|
||||
else if (node.kind === 'rule') {
|
||||
let copy = { ...node, nodes: [] }
|
||||
for (let child of node.nodes) {
|
||||
transform(child, copy.nodes, depth + 1)
|
||||
}
|
||||
parent.push(copy)
|
||||
}
|
||||
|
||||
// AtRule `@property`
|
||||
else if (node.kind === 'at-rule' && node.name === '@property' && depth === 0) {
|
||||
// Don't output duplicate `@property` rules
|
||||
if (seenAtProperties.has(node.params)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect fallbacks for `@property` rules for Firefox support
|
||||
// We turn these into rules on `:root` or `*` and some pseudo-elements
|
||||
// based on the value of `inherits``
|
||||
let property = node.params
|
||||
let initialValue = null
|
||||
let inherits = false
|
||||
|
||||
for (let prop of node.nodes) {
|
||||
if (prop.kind !== 'declaration') continue
|
||||
if (prop.property === 'initial-value') {
|
||||
initialValue = prop.value
|
||||
} else if (prop.property === 'inherits') {
|
||||
inherits = prop.value === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
if (inherits) {
|
||||
propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial'))
|
||||
} else {
|
||||
propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
|
||||
}
|
||||
|
||||
seenAtProperties.add(node.params)
|
||||
|
||||
let copy = { ...node, nodes: [] }
|
||||
for (let child of node.nodes) {
|
||||
transform(child, copy.nodes, depth + 1)
|
||||
}
|
||||
parent.push(copy)
|
||||
}
|
||||
|
||||
// AtRule
|
||||
else if (node.kind === 'at-rule') {
|
||||
let copy = { ...node, nodes: [] }
|
||||
for (let child of node.nodes) {
|
||||
transform(child, copy.nodes, depth + 1)
|
||||
}
|
||||
parent.push(copy)
|
||||
}
|
||||
|
||||
// AtRoot
|
||||
else if (node.kind === 'at-root') {
|
||||
for (let child of node.nodes) {
|
||||
let newParent: AstNode[] = []
|
||||
transform(child, newParent, 0)
|
||||
for (let child of newParent) {
|
||||
atRoots.push(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context
|
||||
else if (node.kind === 'context') {
|
||||
for (let child of node.nodes) {
|
||||
transform(child, parent, depth)
|
||||
}
|
||||
}
|
||||
|
||||
// Comment
|
||||
else if (node.kind === 'comment') {
|
||||
parent.push(node)
|
||||
}
|
||||
|
||||
// Unknown
|
||||
else {
|
||||
node satisfies never
|
||||
}
|
||||
}
|
||||
|
||||
let newAst: AstNode[] = []
|
||||
for (let node of ast) {
|
||||
transform(node, newAst, 0)
|
||||
}
|
||||
|
||||
// Fallbacks
|
||||
{
|
||||
let fallbackAst = []
|
||||
|
||||
if (propertyFallbacksRoot.length > 0) {
|
||||
fallbackAst.push(rule(':root', propertyFallbacksRoot))
|
||||
}
|
||||
|
||||
if (propertyFallbacksUniversal.length > 0) {
|
||||
fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal))
|
||||
}
|
||||
|
||||
if (fallbackAst.length > 0) {
|
||||
newAst.push(
|
||||
atRule('@supports', '(-moz-orient: inline)', [atRule('@layer', 'base', fallbackAst)]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newAst.concat(atRoots)
|
||||
}
|
||||
|
||||
export function toCss(ast: AstNode[]) {
|
||||
function stringify(node: AstNode, depth = 0): string {
|
||||
let css = ''
|
||||
let indent = ' '.repeat(depth)
|
||||
|
||||
// Declaration
|
||||
if (node.kind === 'declaration') {
|
||||
css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n`
|
||||
}
|
||||
|
||||
// Rule
|
||||
if (node.kind === 'rule') {
|
||||
else if (node.kind === 'rule') {
|
||||
css += `${indent}${node.selector} {\n`
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child, depth + 1)
|
||||
@ -240,38 +373,6 @@ export function toCss(ast: AstNode[]) {
|
||||
return `${indent}${node.name} ${node.params};\n`
|
||||
}
|
||||
|
||||
//
|
||||
else if (node.name === '@property' && depth === 0) {
|
||||
// Don't output duplicate `@property` rules
|
||||
if (seenAtProperties.has(node.params)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Collect fallbacks for `@property` rules for Firefox support
|
||||
// We turn these into rules on `:root` or `*` and some pseudo-elements
|
||||
// based on the value of `inherits``
|
||||
let property = node.params
|
||||
let initialValue = null
|
||||
let inherits = false
|
||||
|
||||
for (let prop of node.nodes) {
|
||||
if (prop.kind !== 'declaration') continue
|
||||
if (prop.property === 'initial-value') {
|
||||
initialValue = prop.value
|
||||
} else if (prop.property === 'inherits') {
|
||||
inherits = prop.value === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
if (inherits) {
|
||||
propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial'))
|
||||
} else {
|
||||
propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
|
||||
}
|
||||
|
||||
seenAtProperties.add(node.params)
|
||||
}
|
||||
|
||||
css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n`
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child, depth + 1)
|
||||
@ -284,24 +385,16 @@ export function toCss(ast: AstNode[]) {
|
||||
css += `${indent}/*${node.value}*/\n`
|
||||
}
|
||||
|
||||
// Context Node
|
||||
else if (node.kind === 'context') {
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child, depth)
|
||||
}
|
||||
// These should've been handled already by `prepareAstForPrinting` which
|
||||
// means we can safely ignore them here. We return an empty string
|
||||
// immediately to signal that something went wrong.
|
||||
else if (node.kind === 'context' || node.kind === 'at-root') {
|
||||
return ''
|
||||
}
|
||||
|
||||
// AtRoot Node
|
||||
else if (node.kind === 'at-root') {
|
||||
for (let child of node.nodes) {
|
||||
atRoots += stringify(child, 0)
|
||||
}
|
||||
return css
|
||||
}
|
||||
|
||||
// Declaration
|
||||
else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) {
|
||||
css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n`
|
||||
// Unknown
|
||||
else {
|
||||
node satisfies never
|
||||
}
|
||||
|
||||
return css
|
||||
@ -316,23 +409,5 @@ export function toCss(ast: AstNode[]) {
|
||||
}
|
||||
}
|
||||
|
||||
let fallbackAst = []
|
||||
|
||||
if (propertyFallbacksRoot.length) {
|
||||
fallbackAst.push(rule(':root', propertyFallbacksRoot))
|
||||
}
|
||||
|
||||
if (propertyFallbacksUniversal.length) {
|
||||
fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal))
|
||||
}
|
||||
|
||||
let fallback = ''
|
||||
|
||||
if (fallbackAst.length) {
|
||||
fallback = stringify(
|
||||
atRule('@supports', '(-moz-orient: inline)', [atRule('@layer', 'base', fallbackAst)]),
|
||||
)
|
||||
}
|
||||
|
||||
return `${css}${fallback}${atRoots}`
|
||||
return css
|
||||
}
|
||||
|
||||
@ -17,10 +17,10 @@ import * as SelectorParser from './selector-parser'
|
||||
|
||||
export type Config = UserConfig
|
||||
export type PluginFn = (api: PluginAPI) => void
|
||||
export type PluginWithConfig = {
|
||||
handler: PluginFn;
|
||||
config?: UserConfig;
|
||||
|
||||
export type PluginWithConfig = {
|
||||
handler: PluginFn
|
||||
config?: UserConfig
|
||||
|
||||
/** @internal */
|
||||
reference?: boolean
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
atRule,
|
||||
comment,
|
||||
decl,
|
||||
rule,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
@ -434,15 +435,7 @@ export function parse(input: string) {
|
||||
|
||||
// Attach the declaration to the parent.
|
||||
if (parent) {
|
||||
let importantIdx = buffer.indexOf('!important', colonIdx + 1)
|
||||
parent.nodes.push({
|
||||
kind: 'declaration',
|
||||
property: buffer.slice(0, colonIdx).trim(),
|
||||
value: buffer
|
||||
.slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx)
|
||||
.trim(),
|
||||
important: importantIdx !== -1,
|
||||
} satisfies Declaration)
|
||||
parent.nodes.push(parseDeclaration(buffer, colonIdx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -552,10 +545,9 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
|
||||
|
||||
function parseDeclaration(buffer: string, colonIdx: number = buffer.indexOf(':')): Declaration {
|
||||
let importantIdx = buffer.indexOf('!important', colonIdx + 1)
|
||||
return {
|
||||
kind: 'declaration',
|
||||
property: buffer.slice(0, colonIdx).trim(),
|
||||
value: buffer.slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx).trim(),
|
||||
important: importantIdx !== -1,
|
||||
}
|
||||
return decl(
|
||||
buffer.slice(0, colonIdx).trim(),
|
||||
buffer.slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx).trim(),
|
||||
importantIdx !== -1,
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
comment,
|
||||
context as contextNode,
|
||||
decl,
|
||||
optimizeAst,
|
||||
rule,
|
||||
styleRule,
|
||||
toCss,
|
||||
@ -99,7 +100,7 @@ export const enum Features {
|
||||
}
|
||||
|
||||
async function parseCss(
|
||||
css: string,
|
||||
ast: AstNode[],
|
||||
{
|
||||
base = '',
|
||||
loadModule = throwOnLoadModule,
|
||||
@ -107,7 +108,7 @@ async function parseCss(
|
||||
}: CompileOptions = {},
|
||||
) {
|
||||
let features = Features.None
|
||||
let ast = [contextNode({ base }, CSS.parse(css))] as AstNode[]
|
||||
ast = [contextNode({ base }, ast)] as AstNode[]
|
||||
|
||||
features |= await substituteAtImports(ast, base, loadStylesheet)
|
||||
|
||||
@ -556,16 +557,16 @@ async function parseCss(
|
||||
}
|
||||
}
|
||||
|
||||
export async function compile(
|
||||
css: string,
|
||||
export async function compileAst(
|
||||
input: AstNode[],
|
||||
opts: CompileOptions = {},
|
||||
): Promise<{
|
||||
globs: { base: string; pattern: string }[]
|
||||
root: Root
|
||||
features: Features
|
||||
build(candidates: string[]): string
|
||||
build(candidates: string[]): AstNode[]
|
||||
}> {
|
||||
let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(css, opts)
|
||||
let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(input, opts)
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
|
||||
@ -580,7 +581,7 @@ export async function compile(
|
||||
// resulted in a generated AST Node. All the other `rawCandidates` are invalid
|
||||
// and should be ignored.
|
||||
let allValidCandidates = new Set<string>()
|
||||
let compiledCss = features !== Features.None ? toCss(ast) : css
|
||||
let compiled = null as AstNode[] | null
|
||||
let previousAstNodeCount = 0
|
||||
|
||||
return {
|
||||
@ -588,6 +589,15 @@ export async function compile(
|
||||
root,
|
||||
features,
|
||||
build(newRawCandidates: string[]) {
|
||||
if (features === Features.None) {
|
||||
return input
|
||||
}
|
||||
|
||||
if (!utilitiesNode) {
|
||||
compiled ??= optimizeAst(ast)
|
||||
return compiled
|
||||
}
|
||||
|
||||
let didChange = false
|
||||
|
||||
// Add all new candidates unless we know that they are invalid.
|
||||
@ -602,26 +612,57 @@ export async function compile(
|
||||
// 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) {
|
||||
compiled ??= optimizeAst(ast)
|
||||
return compiled
|
||||
}
|
||||
|
||||
let newNodes = compileCandidates(allValidCandidates, designSystem, {
|
||||
onInvalidCandidate,
|
||||
}).astNodes
|
||||
|
||||
// If no new ast nodes were generated, then we can return the original
|
||||
// CSS. This currently assumes that we only add new ast nodes and never
|
||||
// remove any.
|
||||
if (previousAstNodeCount === newNodes.length) {
|
||||
compiled ??= optimizeAst(ast)
|
||||
return compiled
|
||||
}
|
||||
|
||||
previousAstNodeCount = newNodes.length
|
||||
|
||||
utilitiesNode.nodes = newNodes
|
||||
|
||||
compiled = optimizeAst(ast)
|
||||
return compiled
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function compile(
|
||||
css: string,
|
||||
opts: CompileOptions = {},
|
||||
): Promise<{
|
||||
globs: { base: string; pattern: string }[]
|
||||
root: Root
|
||||
features: Features
|
||||
build(candidates: string[]): string
|
||||
}> {
|
||||
let ast = CSS.parse(css)
|
||||
let api = await compileAst(ast, opts)
|
||||
let compiledAst = ast
|
||||
let compiledCss = css
|
||||
|
||||
return {
|
||||
...api,
|
||||
build(newCandidates) {
|
||||
let newAst = api.build(newCandidates)
|
||||
|
||||
if (newAst === compiledAst) {
|
||||
return compiledCss
|
||||
}
|
||||
|
||||
if (utilitiesNode) {
|
||||
let newNodes = compileCandidates(allValidCandidates, designSystem, {
|
||||
onInvalidCandidate,
|
||||
}).astNodes
|
||||
|
||||
// If no new ast nodes were generated, then we can return the original
|
||||
// CSS. This currently assumes that we only add new ast nodes and never
|
||||
// remove any.
|
||||
if (previousAstNodeCount === newNodes.length) {
|
||||
return compiledCss
|
||||
}
|
||||
|
||||
previousAstNodeCount = newNodes.length
|
||||
|
||||
utilitiesNode.nodes = newNodes
|
||||
compiledCss = toCss(ast)
|
||||
}
|
||||
compiledCss = toCss(newAst)
|
||||
compiledAst = newAst
|
||||
|
||||
return compiledCss
|
||||
},
|
||||
@ -629,7 +670,7 @@ export async function compile(
|
||||
}
|
||||
|
||||
export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) {
|
||||
let result = await parseCss(css, opts)
|
||||
let result = await parseCss(CSS.parse(css), opts)
|
||||
return result.designSystem
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user