From e95e6e239f0cfa7aa69ef29592e5e4a32d1089ef Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:36:57 -0400 Subject: [PATCH] Track locations when parsing --- packages/@tailwindcss-node/src/compile.ts | 3 + packages/tailwindcss/src/css-parser.bench.ts | 4 ++ packages/tailwindcss/src/css-parser.ts | 58 +++++++++++++++++++- packages/tailwindcss/src/index.ts | 4 +- 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 02c488afd..5ce6299af 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -21,6 +21,7 @@ export type Resolver = (id: string, base: string) => Promise void shouldRewriteUrls?: boolean polyfills?: Polyfills @@ -31,6 +32,7 @@ export interface CompileOptions { function createCompileOptions({ base, + from, polyfills, onDependency, shouldRewriteUrls, @@ -41,6 +43,7 @@ function createCompileOptions({ return { base, polyfills, + from, async loadModule(id: string, base: string) { return loadModule(id, base, onDependency, customJsResolver) }, diff --git a/packages/tailwindcss/src/css-parser.bench.ts b/packages/tailwindcss/src/css-parser.bench.ts index ab490e103..d48f88ab3 100644 --- a/packages/tailwindcss/src/css-parser.bench.ts +++ b/packages/tailwindcss/src/css-parser.bench.ts @@ -10,3 +10,7 @@ const cssFile = readFileSync(currentFolder + './preflight.css', 'utf-8') bench('css-parser on preflight.css', () => { CSS.parse(cssFile) }) + +bench('CSS with sourcemaps', () => { + CSS.parse(cssFile, { from: 'input.css' }) +}) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 89b06da5c..6a5ce8320 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -9,6 +9,7 @@ import { type Declaration, type Rule, } from './ast' +import type { Source } from './source-maps/source' const BACKSLASH = 0x5c const SLASH = 0x2f @@ -31,7 +32,13 @@ const DASH = 0x2d const AT_SIGN = 0x40 const EXCLAMATION_MARK = 0x21 -export function parse(input: string) { +export interface ParseOptions { + from?: string +} + +export function parse(input: string, opts?: ParseOptions) { + let source: Source | null = opts?.from ? { file: opts.from, code: input } : null + // Note: it is important that any transformations of the input string // *before* processing do NOT change the length of the string. This // would invalidate the mechanism used to track source locations. @@ -48,6 +55,9 @@ export function parse(input: string) { let buffer = '' let closingBracketStack = '' + // The start of the first non-whitespace character in the buffer + let bufferStart = 0 + let peekChar for (let i = 0; i < input.length; i++) { @@ -72,6 +82,7 @@ export function parse(input: string) { // ``` // if (currentChar === BACKSLASH) { + if (buffer === '') bufferStart = i buffer += input.slice(i, i + 2) i += 1 } @@ -117,6 +128,11 @@ export function parse(input: string) { if (commentString.charCodeAt(2) === EXCLAMATION_MARK) { let node = comment(commentString.slice(2, -2)) licenseComments.push(node) + + if (source) { + node.src = [source, start, i + 1] + node.dst = [source, start, i + 1] + } } } @@ -313,6 +329,11 @@ export function parse(input: string) { let declaration = parseDeclaration(buffer, colonIdx) if (!declaration) throw new Error(`Invalid custom property, expected a value`) + if (source) { + declaration.src = [source, start, i] + declaration.dst = [source, start, i] + } + if (parent) { parent.nodes.push(declaration) } else { @@ -333,6 +354,11 @@ export function parse(input: string) { else if (currentChar === SEMICOLON && buffer.charCodeAt(0) === AT_SIGN) { node = parseAtRule(buffer) + if (source) { + node.src = [source, bufferStart, i] + node.dst = [source, bufferStart, i] + } + // At-rule is nested inside of a rule, attach it to the parent. if (parent) { parent.nodes.push(node) @@ -369,6 +395,11 @@ export function parse(input: string) { throw new Error(`Invalid declaration: \`${buffer.trim()}\``) } + if (source) { + declaration.src = [source, bufferStart, i] + declaration.dst = [source, bufferStart, i] + } + if (parent) { parent.nodes.push(declaration) } else { @@ -388,6 +419,12 @@ export function parse(input: string) { // At this point `buffer` should resemble a selector or an at-rule. node = rule(buffer.trim()) + // Track the source location for source maps + if (source) { + node.src = [source, bufferStart, i] + node.dst = [source, bufferStart, i] + } + // Attach the rule to the parent in case it's nested. if (parent) { parent.nodes.push(node) @@ -434,6 +471,12 @@ export function parse(input: string) { if (buffer.charCodeAt(0) === AT_SIGN) { node = parseAtRule(buffer) + // Track the source location for source maps + if (source) { + node.src = [source, bufferStart, i] + node.dst = [source, bufferStart, i] + } + // At-rule is nested inside of a rule, attach it to the parent. if (parent) { parent.nodes.push(node) @@ -470,6 +513,11 @@ export function parse(input: string) { let node = parseDeclaration(buffer, colonIdx) if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``) + if (source) { + node.src = [source, bufferStart, i] + node.dst = [source, bufferStart, i] + } + parent.nodes.push(node) } } @@ -519,6 +567,8 @@ export function parse(input: string) { continue } + if (buffer === '') bufferStart = i + buffer += String.fromCharCode(currentChar) } } @@ -529,6 +579,12 @@ export function parse(input: string) { if (buffer.charCodeAt(0) === AT_SIGN) { let node = parseAtRule(buffer) + // Track the source location for source maps + if (source) { + node.src = [source, bufferStart, input.length] + node.dst = [source, bufferStart, input.length] + } + ast.push(node) } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 541ec11a0..10dba2386 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -51,6 +51,7 @@ export const enum Polyfills { type CompileOptions = { base?: string + from?: string polyfills?: Polyfills loadModule?: ( id: string, @@ -136,6 +137,7 @@ async function parseCss( ast: AstNode[], { base = '', + from, loadModule = throwOnLoadModule, loadStylesheet = throwOnLoadStylesheet, }: CompileOptions = {}, @@ -786,7 +788,7 @@ export async function compile( features: Features build(candidates: string[]): string }> { - let ast = CSS.parse(css) + let ast = CSS.parse(css, { from: opts.from }) let api = await compileAst(ast, opts) let compiledAst = ast let compiledCss = css