Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use AST transformations in @tailwindcss/postcss #15297

Merged
merged 25 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a5407e3
allow to pass in the `!important` flag
RobinMalfait Dec 3, 2024
29cc49d
implement `postCssAstToCssAst` and `cssAstToPostCssAst`
RobinMalfait Dec 3, 2024
94aeeab
optimize AST before printing
RobinMalfait Dec 3, 2024
7eefb19
implement new `compileAst(…)` and update `compile(…)`
RobinMalfait Dec 3, 2024
531670b
expose `compileAst` from `@tailwindcss/node`
RobinMalfait Dec 3, 2024
6cabeea
WIP: PostCSS implementation
RobinMalfait Dec 3, 2024
42401b4
Tweak code
thecrypticace Dec 3, 2024
0e954b2
Convert our AST to PostCSS’s AST
thecrypticace Dec 3, 2024
79cadb8
Pass through source information
thecrypticace Dec 3, 2024
2adaf42
print CSS from our own AST
RobinMalfait Dec 4, 2024
ef17c45
only convert our AST into a PostCSS AST when not optimizing
RobinMalfait Dec 4, 2024
f006e36
remove unused variable
RobinMalfait Dec 4, 2024
709947c
rename variables to be more explicit
RobinMalfait Dec 4, 2024
a4df476
add timing info around `cssAstToPostCssAst(…)`
RobinMalfait Dec 4, 2024
71c1c0d
restructure `build(…)`
RobinMalfait Dec 4, 2024
a24e4c4
track the `tailwindCssAst` for subsequent cache hits
RobinMalfait Dec 4, 2024
260beed
ensure PostCSS AST nodes have semicolons
RobinMalfait Dec 4, 2024
87065a8
handle comments correctly
RobinMalfait Dec 4, 2024
3de1864
compute current_mtimes in parallel per file/directory
RobinMalfait Dec 4, 2024
98f66cb
remove instrumentation from `read_dir`
RobinMalfait Dec 4, 2024
84bde5e
trick PostCSS into using 2 spaces instead of 4
RobinMalfait Dec 4, 2024
7aeea03
use `??=` shorthand
RobinMalfait Dec 4, 2024
1a04957
ensure `compile` and `compileAst` re-use the same logic
RobinMalfait Dec 4, 2024
5f22986
run format
RobinMalfait Dec 4, 2024
c5c7c9f
clone the cached PostCSS tree before appending it to the root
RobinMalfait Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions crates/oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion crates/oxide/src/scanner/allowed_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 38 additions & 22 deletions packages/@tailwindcss-node/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,40 @@ 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'

export { Features }

export type Resolver = (id: string, base: string) => Promise<string | false | undefined>

export async function compile(
css: string,
{
base,
onDependency,
shouldRewriteUrls,

customCssResolver,
customJsResolver,
}: {
base: string
onDependency: (path: string) => void
shouldRewriteUrls?: boolean

customCssResolver?: Resolver
customJsResolver?: Resolver
},
) {
let compiler = await _compile(css, {
export interface CompileOptions {
base: string
onDependency: (path: string) => void
shouldRewriteUrls?: boolean

customCssResolver?: Resolver
customJsResolver?: Resolver
}

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) {
Expand All @@ -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 = /[*{]/
Expand All @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 }

Expand Down
107 changes: 107 additions & 0 deletions packages/@tailwindcss-postcss/src/ast.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 117 additions & 0 deletions packages/@tailwindcss-postcss/src/ast.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading