From a4cd44d426016b83377a5771bdb1e3d12c5b8990 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Apr 2025 14:26:02 -0400 Subject: [PATCH 01/41] Cleanup code a bit --- packages/@tailwindcss-node/package.json | 4 +- packages/@tailwindcss-node/src/compile.ts | 6 +- packages/@tailwindcss-vite/src/index.ts | 34 +++++++--- packages/tailwindcss/src/apply.ts | 6 +- packages/tailwindcss/src/ast.ts | 48 +++++++------ packages/tailwindcss/src/at-import.ts | 17 +++-- .../src/compat/apply-compat-hooks.ts | 5 +- .../tailwindcss/src/compat/plugin-api.test.ts | 4 +- .../tailwindcss/src/css-functions.test.ts | 11 +-- packages/tailwindcss/src/css-parser.ts | 25 ++++--- packages/tailwindcss/src/index.test.ts | 68 +++++++++++-------- packages/tailwindcss/src/index.ts | 17 +++-- packages/tailwindcss/src/variants.test.ts | 2 +- 13 files changed, 153 insertions(+), 94 deletions(-) diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index d3631b758b24..0844c232416f 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -39,7 +39,7 @@ "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "tailwindcss": "workspace:*", - "lightningcss": "catalog:" + "lightningcss": "catalog:", + "tailwindcss": "workspace:*" } } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index ef9dfbcba57d..85c4bd42f948 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' import fs from 'node:fs' import fsPromises from 'node:fs/promises' -import path, { dirname } from 'node:path' +import path from 'node:path' import { pathToFileURL } from 'node:url' import { __unstable__loadDesignSystem as ___unstable__loadDesignSystem, @@ -125,7 +125,7 @@ export async function loadModule( let module = await importModule(pathToFileURL(resolvedPath).href) return { - base: dirname(resolvedPath), + base: path.dirname(resolvedPath), module: module.default ?? module, } } @@ -144,7 +144,7 @@ export async function loadModule( onDependency(file) } return { - base: dirname(resolvedPath), + base: path.dirname(resolvedPath), module: module.default ?? module, } } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index a484583a3ec3..a62929069b13 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -34,7 +34,12 @@ export default function tailwindcss(): Plugin[] { function customJsResolver(id: string, base: string) { return jsResolver(id, base, true, isSSR) } - return new Root(id, config!.root, customCssResolver, customJsResolver) + return new Root( + id, + config!.root, + customCssResolver, + customJsResolver, + ) }) return [ @@ -68,14 +73,14 @@ export default function tailwindcss(): Plugin[] { let root = roots.get(id) - let generated = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!generated) { + let result = await root.generate(src, (file) => this.addWatchFile(file), I) + if (!result) { roots.delete(id) return src } DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') - return { code: generated } + return result }, }, @@ -93,18 +98,18 @@ export default function tailwindcss(): Plugin[] { let root = roots.get(id) - let generated = await root.generate(src, (file) => this.addWatchFile(file), I) - if (!generated) { + let result = await root.generate(src, (file) => this.addWatchFile(file), I) + if (!result) { roots.delete(id) return src } DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') - generated = optimize(generated, { minify }) + result.code = optimize(result.code, { minify }) DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') - return { code: generated } + return result }, }, ] satisfies Plugin[] @@ -183,7 +188,12 @@ class Root { content: string, _addWatchFile: (file: string) => void, I: Instrumentation, - ): Promise { + ): Promise< + | { + code: string + } + | false + > { let inputPath = idToPath(this.id) function addWatchFile(file: string) { @@ -313,10 +323,12 @@ class Root { } DEBUG && I.start('Build CSS') - let result = this.compiler.build([...this.candidates]) + let code = this.compiler.build([...this.candidates]) DEBUG && I.end('Build CSS') - return result + return { + code, + } } private async addBuildDependency(path: string) { diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 676b4f2eee19..6038c4b8dfbb 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -161,11 +161,13 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { { // Parse the candidates to an AST that we can replace the `@apply` rule // with. - let candidateAst = compileCandidates(candidates, designSystem, { + let compiled = compileCandidates(candidates, designSystem, { onInvalidCandidate: (candidate) => { throw new Error(`Cannot apply unknown utility class: ${candidate}`) }, - }).astNodes + }) + + let candidateAst = compiled.astNodes // Collect the nodes to insert in place of the `@apply` rule. When a rule // was used, we want to insert its children instead of the rule because we diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 3db0432dd3a9..4c5cd40da1ef 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -378,10 +378,12 @@ export function optimizeAst( } } + let fallback = decl(property, initialValue ?? 'initial') + if (inherits) { - propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial')) + propertyFallbacksRoot.push(fallback) } else { - propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial')) + propertyFallbacksUniversal.push(fallback) } } @@ -632,11 +634,13 @@ export function optimizeAst( let fallbackAst = [] if (propertyFallbacksRoot.length > 0) { - fallbackAst.push(rule(':root, :host', propertyFallbacksRoot)) + let wrapper = rule(':root, :host', propertyFallbacksRoot) + fallbackAst.push(wrapper) } if (propertyFallbacksUniversal.length > 0) { - fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)) + let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal) + fallbackAst.push(wrapper) } if (fallbackAst.length > 0) { @@ -658,23 +662,25 @@ export function optimizeAst( return true }) + let layerPropertiesStatement = atRule('@layer', 'properties', []) + newAst.splice( firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex, 0, - atRule('@layer', 'properties', []), + layerPropertiesStatement, ) - newAst.push( - rule('@layer properties', [ - atRule( - '@supports', - // We can't write a supports query for `@property` directly so we have to test for - // features that are added around the same time in Mozilla and Safari. - '((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))', - fallbackAst, - ), - ]), - ) + let block = rule('@layer properties', [ + atRule( + '@supports', + // We can't write a supports query for `@property` directly so we have to test for + // features that are added around the same time in Mozilla and Safari. + '((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))', + fallbackAst, + ), + ]) + + newAst.push(block) } } @@ -710,7 +716,8 @@ export function toCss(ast: AstNode[]) { // @layer base, components, utilities; // ``` if (node.nodes.length === 0) { - return `${indent}${node.name} ${node.params};\n` + let css = `${indent}${node.name} ${node.params};\n` + return css } css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n` @@ -725,7 +732,7 @@ export function toCss(ast: AstNode[]) { css += `${indent}/*${node.value}*/\n` } - // These should've been handled already by `prepareAstForPrinting` which + // These should've been handled already by `optimizeAst` 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') { @@ -743,10 +750,7 @@ export function toCss(ast: AstNode[]) { let css = '' for (let node of ast) { - let result = stringify(node) - if (result !== '') { - css += result - } + css += stringify(node, 0) } return css diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index effd33e5b203..343e9fbaea53 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -3,7 +3,13 @@ import { atRule, context, walk, WalkAction, type AstNode } from './ast' import * as CSS from './css-parser' import * as ValueParser from './value-parser' -type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }> +type LoadStylesheet = ( + id: string, + basedir: string, +) => Promise<{ + base: string + content: string +}> export async function substituteAtImports( ast: AstNode[], @@ -148,15 +154,18 @@ function buildImportNodes( let root = importedAst if (layer !== null) { - root = [atRule('@layer', layer, root)] + let node = atRule('@layer', layer, root) + root = [node] } if (media !== null) { - root = [atRule('@media', media, root)] + let node = atRule('@media', media, root) + root = [node] } if (supports !== null) { - root = [atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)] + let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root) + root = [node] } return root diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index a781cdcb0c78..eb23cec18d07 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -30,7 +30,10 @@ export async function applyCompatibilityHooks({ path: string, base: string, resourceHint: 'plugin' | 'config', - ) => Promise<{ module: any; base: string }> + ) => Promise<{ + base: string + module: any + }> sources: { base: string; pattern: string; negated: boolean }[] }) { let features = Features.None diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index ca7d96dee648..6a8f95c58a5b 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1508,10 +1508,10 @@ describe('addBase', () => { }, async loadStylesheet() { return { + base: '', content: css` @plugin "inside"; `, - base: '', } }, }) @@ -1533,6 +1533,7 @@ describe('addBase', () => { let compiler = await compile(input, { loadModule: async () => ({ + base: '/root', module: plugin(function ({ addBase }) { addBase({ ':root': { @@ -1542,7 +1543,6 @@ describe('addBase', () => { }, }) }), - base: '/root', }), }) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index f63682f63e01..968bb7f432f4 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -412,8 +412,8 @@ describe('--theme(…)', () => { [], { loadModule: async () => ({ - module: () => {}, base: '/root', + module: () => {}, }), }, ), @@ -771,7 +771,10 @@ describe('theme(…)', () => { } `, { - loadModule: async () => ({ module: {}, base: '/root' }), + loadModule: async () => ({ + base: '/root', + module: {}, + }), }, ) @@ -1196,6 +1199,7 @@ describe('in plugins', () => { { async loadModule() { return { + base: '/root', module: plugin(({ addBase, addUtilities }) => { addBase({ '.my-base-rule': { @@ -1212,7 +1216,6 @@ describe('in plugins', () => { }, }) }), - base: '/root', } }, }, @@ -1253,6 +1256,7 @@ describe('in JS config files', () => { `, { loadModule: async () => ({ + base: '/root', module: { theme: { extend: { @@ -1279,7 +1283,6 @@ describe('in JS config files', () => { }), ], }, - base: '/root', }), }, ) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index df10fa850035..c492ed353ce4 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -31,8 +31,11 @@ const AT_SIGN = 0x40 const EXCLAMATION_MARK = 0x21 export function parse(input: string) { - if (input[0] === '\uFEFF') input = input.slice(1) - input = input.replaceAll('\r\n', '\n') + // 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. + if (input[0] === '\uFEFF') input = ' ' + input.slice(1) + input = input.replaceAll('\r\n', ' \n') let ast: AstNode[] = [] let licenseComments: Comment[] = [] @@ -104,7 +107,8 @@ export function parse(input: string) { // Collect all license comments so that we can hoist them to the top of // the AST. if (commentString.charCodeAt(2) === EXCLAMATION_MARK) { - licenseComments.push(comment(commentString.slice(2, -2))) + let node = comment(commentString.slice(2, -2)) + licenseComments.push(node) } } @@ -503,7 +507,9 @@ export function parse(input: string) { // means that we have an at-rule that is not terminated with a semicolon at // the end of the input. if (buffer.charCodeAt(0) === AT_SIGN) { - ast.push(parseAtRule(buffer)) + let node = parseAtRule(buffer) + + ast.push(node) } // When we are done parsing then everything should be balanced. If we still @@ -525,6 +531,9 @@ export function parse(input: string) { } export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule { + let name = buffer + let params = '' + // Assumption: The smallest at-rule in CSS right now is `@page`, this means // that we can always skip the first 5 characters and start at the // sixth (at index 5). @@ -545,13 +554,13 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule { for (let i = 5 /* '@page'.length */; i < buffer.length; i++) { let currentChar = buffer.charCodeAt(i) if (currentChar === SPACE || currentChar === OPEN_PAREN) { - let name = buffer.slice(0, i).trim() - let params = buffer.slice(i).trim() - return atRule(name, params, nodes) + name = buffer.slice(0, i) + params = buffer.slice(i) + break } } - return atRule(buffer.trim(), '', nodes) + return atRule(name.trim(), params.trim(), nodes) } function parseDeclaration( diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 00e73a90b405..22dd6b053a0f 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -126,11 +126,11 @@ describe('compiling CSS', () => { { async loadStylesheet(id) { return { + base: '', content: fs.readFileSync( path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id), 'utf-8', ), - base: '', } }, }, @@ -2320,6 +2320,7 @@ describe('Parsing theme values from CSS', () => { { async loadStylesheet() { return { + base: '', content: css` @theme { --color-tomato: #e10c04; @@ -2329,7 +2330,6 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, - base: '', } }, }, @@ -2407,6 +2407,7 @@ describe('Parsing theme values from CSS', () => { { async loadStylesheet() { return { + base: '', content: css` @theme { --color-tomato: #e10c04; @@ -2416,7 +2417,6 @@ describe('Parsing theme values from CSS', () => { @tailwind utilities; `, - base: '', } }, }, @@ -2704,6 +2704,7 @@ describe('Parsing theme values from CSS', () => { { loadModule: async () => { return { + base: '/root', module: plugin(({}) => {}, { theme: { extend: { @@ -2714,7 +2715,6 @@ describe('Parsing theme values from CSS', () => { }, }, }), - base: '/root', } }, }, @@ -2750,6 +2750,7 @@ describe('Parsing theme values from CSS', () => { { loadModule: async () => { return { + base: '/root', module: { theme: { extend: { @@ -2760,7 +2761,6 @@ describe('Parsing theme values from CSS', () => { }, }, }, - base: '/root', } }, }, @@ -2839,10 +2839,10 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') }, - base: '/root', }), }, ), @@ -2857,10 +2857,10 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') }, - base: '/root', }), }, ), @@ -2877,10 +2877,10 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') }, - base: '/root', }), }, ), @@ -2899,6 +2899,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: plugin.withOptions((options) => { expect(options).toEqual({ color: 'red', @@ -2912,7 +2913,6 @@ describe('plugins', () => { }) } }), - base: '/root', }), }, ) @@ -2951,6 +2951,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: plugin.withOptions((options) => { expect(options).toEqual({ 'is-null': null, @@ -2971,7 +2972,6 @@ describe('plugins', () => { return () => {} }), - base: '/root', }), }, ) @@ -2991,6 +2991,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: plugin.withOptions((options) => { return ({ addUtilities }) => { addUtilities({ @@ -3000,7 +3001,6 @@ describe('plugins', () => { }) } }), - base: '/root', }), }, ), @@ -3029,6 +3029,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: plugin(({ addUtilities }) => { addUtilities({ '.text-primary': { @@ -3036,7 +3037,6 @@ describe('plugins', () => { }, }) }), - base: '/root', }), }, ), @@ -3054,7 +3054,10 @@ describe('plugins', () => { } `, { - loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), + loadModule: async () => ({ + base: '/root', + module: plugin(() => {}), + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -3075,7 +3078,10 @@ describe('plugins', () => { } `, { - loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), + loadModule: async () => ({ + base: '/root', + module: plugin(() => {}), + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(` @@ -3099,10 +3105,10 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') }, - base: '/root', }), }, ) @@ -3131,10 +3137,10 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', ['&:hover', '&:focus']) }, - base: '/root', }), }, ) @@ -3164,13 +3170,13 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '&:hover': '@slot', '&:focus': '@slot', }) }, - base: '/root', }), }, ) @@ -3199,6 +3205,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '@media (hover: hover)': { @@ -3207,7 +3214,6 @@ describe('plugins', () => { '&:focus': '@slot', }) }, - base: '/root', }), }, ) @@ -3248,6 +3254,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '&': { @@ -3257,7 +3264,6 @@ describe('plugins', () => { }, }) }, - base: '/root', }), }, ) @@ -3286,10 +3292,10 @@ describe('plugins', () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('dark', '&:is([data-theme=dark] *)') }, - base: '/root', }), }, ) @@ -4142,6 +4148,7 @@ test('addBase', async () => { `, { loadModule: async () => ({ + base: '/root', module: ({ addBase }: PluginAPI) => { addBase({ body: { @@ -4149,7 +4156,6 @@ test('addBase', async () => { }, }) }, - base: '/root', }), }, ) @@ -4201,6 +4207,7 @@ describe('`@reference "…" imports`', () => { let loadStylesheet = async (id: string, base: string) => { if (id === './foo/baz.css') { return { + base: '/root/foo', content: css` .foo { color: red; @@ -4213,14 +4220,13 @@ describe('`@reference "…" imports`', () => { } @custom-variant hocus (&:hover, &:focus); `, - base: '/root/foo', } } return { + base: '/root/foo', content: css` @import './foo/baz.css'; `, - base: '/root/foo', } } @@ -4249,19 +4255,19 @@ describe('`@reference "…" imports`', () => { let loadStylesheet = async (id: string, base: string) => { if (id === './foo/baz.css') { return { + base: '/root/foo', content: css` @layer utilities { @tailwind utilities; } `, - base: '/root/foo', } } return { + base: '/root/foo', content: css` @import './foo/baz.css'; `, - base: '/root/foo', } } @@ -4311,6 +4317,7 @@ describe('`@reference "…" imports`', () => { ['animate-spin', 'match-utility-initial', 'match-components-initial'], { loadModule: async () => ({ + base: '/root', module: ({ addBase, addUtilities, @@ -4344,7 +4351,6 @@ describe('`@reference "…" imports`', () => { { values: { initial: 'initial' } }, ) }, - base: '/root', }), }, ), @@ -4366,22 +4372,23 @@ describe('`@reference "…" imports`', () => { switch (id) { case './one.css': { return { + base: '/root', content: css` @import './two.css' layer(two); `, - base: '/root', } } case './two.css': { return { + base: '/root', content: css` @import './three.css' layer(three); `, - base: '/root', } } case './three.css': { return { + base: '/root', content: css` .foo { color: red; @@ -4400,10 +4407,11 @@ describe('`@reference "…" imports`', () => { } } `, - base: '/root', } } } + + throw new Error('unreachable') } await expect( @@ -4438,6 +4446,7 @@ describe('`@reference "…" imports`', () => { test('supports `@import "…" reference` syntax', async () => { let loadStylesheet = async () => { return { + base: '/root/foo', content: css` .foo { color: red; @@ -4450,7 +4459,6 @@ describe('`@reference "…" imports`', () => { } @custom-variant hocus (&:hover, &:focus); `, - base: '/root/foo', } } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index e8f34e366327..792f7452fe95 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -56,8 +56,17 @@ type CompileOptions = { id: string, base: string, resourceHint: 'plugin' | 'config', - ) => Promise<{ module: Plugin | Config; base: string }> - loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> + ) => Promise<{ + base: string + module: Plugin | Config + }> + loadStylesheet?: ( + id: string, + base: string, + ) => Promise<{ + base: string + content: string + }> } function throwOnLoadModule(): never { @@ -593,8 +602,8 @@ async function parseCss( for (let [key, value] of designSystem.theme.entries()) { if (value.options & ThemeOptions.REFERENCE) continue - - nodes.push(decl(escape(key), value.value)) + let node = decl(escape(key), value.value) + nodes.push(node) } let keyframesRules = designSystem.theme.getKeyframes() diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 5da1dcac7572..e1a1388f6912 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -2526,7 +2526,7 @@ test('matchVariant sorts deterministically', async () => { for (let classList of classLists) { let output = await compileCss('@tailwind utilities; @plugin "./plugin.js";', classList, { - loadModule(id: string) { + async loadModule(id: string) { return { base: '/', module: createPlugin(({ matchVariant }) => { From 68ce361ef4eaa548d7decd137e48dbd7ab3f187d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 18 Apr 2025 10:40:54 -0400 Subject: [PATCH 02/41] Return resolved path in `loadStylesheet` and `loadModule` Source map support needs this in loadStylesheet but it makes more sense to be consistent and require it for both. --- packages/@tailwindcss-browser/src/index.ts | 4 + packages/@tailwindcss-node/src/compile.ts | 4 + packages/tailwindcss/src/at-import.ts | 1 + .../src/compat/apply-compat-hooks.ts | 1 + .../tailwindcss/src/compat/config.test.ts | 11 +++ .../tailwindcss/src/compat/plugin-api.test.ts | 83 +++++++++++++++++++ .../tailwindcss/src/css-functions.test.ts | 5 ++ packages/tailwindcss/src/index.test.ts | 32 +++++++ packages/tailwindcss/src/index.ts | 2 + packages/tailwindcss/src/intellisense.test.ts | 11 +++ packages/tailwindcss/src/prefix.test.ts | 5 ++ packages/tailwindcss/src/variants.test.ts | 1 + 12 files changed, 160 insertions(+) diff --git a/packages/@tailwindcss-browser/src/index.ts b/packages/@tailwindcss-browser/src/index.ts index e7688a1219e7..74992157f5d5 100644 --- a/packages/@tailwindcss-browser/src/index.ts +++ b/packages/@tailwindcss-browser/src/index.ts @@ -116,6 +116,7 @@ async function loadStylesheet(id: string, base: string) { function load() { if (id === 'tailwindcss') { return { + path: 'virtual:tailwindcss/index.css', base, content: assets.css.index, } @@ -125,6 +126,7 @@ async function loadStylesheet(id: string, base: string) { id === './preflight.css' ) { return { + path: 'virtual:tailwindcss/preflight.css', base, content: assets.css.preflight, } @@ -134,6 +136,7 @@ async function loadStylesheet(id: string, base: string) { id === './theme.css' ) { return { + path: 'virtual:tailwindcss/theme.css', base, content: assets.css.theme, } @@ -143,6 +146,7 @@ async function loadStylesheet(id: string, base: string) { id === './utilities.css' ) { return { + path: 'virtual:tailwindcss/utilities.css', base, content: assets.css.utilities, } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 85c4bd42f948..02c488afd0ba 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -125,6 +125,7 @@ export async function loadModule( let module = await importModule(pathToFileURL(resolvedPath).href) return { + path: resolvedPath, base: path.dirname(resolvedPath), module: module.default ?? module, } @@ -144,6 +145,7 @@ export async function loadModule( onDependency(file) } return { + path: resolvedPath, base: path.dirname(resolvedPath), module: module.default ?? module, } @@ -164,6 +166,7 @@ async function loadStylesheet( let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8') if (file) { return { + path: resolvedPath, base: path.dirname(resolvedPath), content: file, } @@ -172,6 +175,7 @@ async function loadStylesheet( let file = await fsPromises.readFile(resolvedPath, 'utf-8') return { + path: resolvedPath, base: path.dirname(resolvedPath), content: file, } diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 343e9fbaea53..2d2682ddd3b0 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -7,6 +7,7 @@ type LoadStylesheet = ( id: string, basedir: string, ) => Promise<{ + path: string base: string content: string }> diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index eb23cec18d07..fb619561bc13 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -31,6 +31,7 @@ export async function applyCompatibilityHooks({ base: string, resourceHint: 'plugin' | 'config', ) => Promise<{ + path: string base: string module: any }> diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 9e6c3b58a389..ea64fe3a1239 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1156,6 +1156,7 @@ test('utilities must be prefixed', async () => { let compiler = await compile(input, { loadModule: async (id, base) => ({ + path: '', base, module: { prefix: 'tw' }, }), @@ -1183,6 +1184,7 @@ test('utilities must be prefixed', async () => { // Non-prefixed utilities are ignored compiler = await compile(input, { loadModule: async (id, base) => ({ + path: '', base, module: { prefix: 'tw' }, }), @@ -1202,6 +1204,7 @@ test('utilities used in @apply must be prefixed', async () => { `, { loadModule: async (id, base) => ({ + path: '', base, module: { prefix: 'tw' }, }), @@ -1228,6 +1231,7 @@ test('utilities used in @apply must be prefixed', async () => { `, { loadModule: async (id, base) => ({ + path: '', base, module: { prefix: 'tw' }, }), @@ -1258,6 +1262,7 @@ test('Prefixes configured in CSS take precedence over those defined in JS config { async loadModule(id, base) { return { + path: '', base, module: { prefix: 'tw' }, } @@ -1282,6 +1287,7 @@ test('a prefix must be letters only', async () => { { async loadModule(id, base) { return { + path: '', base, module: { prefix: '__' }, } @@ -1305,6 +1311,7 @@ test('important: `#app`', async () => { let compiler = await compile(input, { loadModule: async (_, base) => ({ + path: '', base, module: { important: '#app' }, }), @@ -1342,6 +1349,7 @@ test('important: true', async () => { let compiler = await compile(input, { loadModule: async (_, base) => ({ + path: '', base, module: { important: true }, }), @@ -1378,6 +1386,7 @@ test('blocklisted candidates are not generated', async () => { { async loadModule(id, base) { return { + path: '', base, module: { blocklist: ['bg-white'], @@ -1421,6 +1430,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => { { async loadModule(id, base) { return { + path: '', base, module: { blocklist: ['bg-white'], @@ -1473,6 +1483,7 @@ test('old theme values are merged with their renamed counterparts in the CSS the { async loadModule(id, base) { return { + path: '', base, module: plugin(function ({ theme }) { didCallPluginFn() diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 6a8f95c58a5b..465974679646 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -17,6 +17,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ addBase, theme }) { @@ -80,6 +81,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(function ({ addUtilities, theme }) { addUtilities({ @@ -116,6 +118,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -164,6 +167,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -211,6 +215,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -265,6 +270,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(function ({ addUtilities, theme }) { addUtilities({ @@ -313,6 +319,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ addUtilities, theme }) { @@ -398,6 +405,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -452,6 +460,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -499,6 +508,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(function ({ matchUtilities, theme }) { matchUtilities( @@ -557,6 +567,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -611,6 +622,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -660,6 +672,7 @@ describe('theme', async () => { await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ theme }) { @@ -695,6 +708,7 @@ describe('theme', async () => { let { build } = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(function ({ matchUtilities, theme }) { function utility(name: string, themeKey: string) { @@ -942,6 +956,7 @@ describe('theme', async () => { await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( ({ theme }) => { @@ -984,6 +999,7 @@ describe('theme', async () => { await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(({ theme }) => { fn(theme('transitionTimingFunction.DEFAULT')) @@ -1015,6 +1031,7 @@ describe('theme', async () => { await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(({ theme }) => { fn(theme('color.red.100')) @@ -1043,6 +1060,7 @@ describe('theme', async () => { await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(({ theme }) => { fn(theme('i.do.not.exist')) @@ -1069,6 +1087,7 @@ describe('theme', async () => { let { build } = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(({ addUtilities, matchUtilities }) => { addUtilities({ @@ -1124,6 +1143,7 @@ describe('theme', async () => { let { build } = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(function ({ matchUtilities }) { function utility(name: string, themeKey: string) { @@ -1342,6 +1362,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -1400,6 +1421,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin(function ({ matchUtilities, theme }) { matchUtilities( @@ -1446,6 +1468,7 @@ describe('theme', async () => { let compiler = await compile(input, { loadModule: async (id, base) => { return { + path: '', base, module: plugin( function ({ matchUtilities, theme }) { @@ -1493,6 +1516,7 @@ describe('addBase', () => { loadModule: async (id, base) => { if (id === 'inside') { return { + path: '', base, module: plugin(function ({ addBase }) { addBase({ inside: { color: 'red' } }) @@ -1500,6 +1524,7 @@ describe('addBase', () => { } } return { + path: '', base, module: plugin(function ({ addBase }) { addBase({ outside: { color: 'red' } }) @@ -1508,6 +1533,7 @@ describe('addBase', () => { }, async loadStylesheet() { return { + path: '', base: '', content: css` @plugin "inside"; @@ -1533,6 +1559,7 @@ describe('addBase', () => { let compiler = await compile(input, { loadModule: async () => ({ + path: '', base: '/root', module: plugin(function ({ addBase }) { addBase({ @@ -1571,6 +1598,7 @@ describe('addVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') @@ -1605,6 +1633,7 @@ describe('addVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ addVariant }: PluginAPI) => { addVariant('hocus', ['&:hover', '&:focus']) @@ -1640,6 +1669,7 @@ describe('addVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { @@ -1677,6 +1707,7 @@ describe('addVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { @@ -1728,6 +1759,7 @@ describe('addVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ addVariant }: PluginAPI) => { addVariant( @@ -1769,6 +1801,7 @@ describe('addVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { @@ -1811,6 +1844,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('potato', (flavor) => `.potato-${flavor} &`) @@ -1845,6 +1879,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) @@ -1883,6 +1918,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant( @@ -1929,6 +1965,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('tooltip', (side) => `&${side}`, { @@ -1968,6 +2005,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('alphabet', (side) => `&${side}`, { @@ -2010,6 +2048,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('test', (selector) => @@ -2042,6 +2081,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2094,6 +2134,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2149,6 +2190,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2221,6 +2263,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2275,6 +2318,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2347,6 +2391,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2415,6 +2460,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('testmin', (value) => `@media (min-width: ${value})`, { @@ -2495,6 +2541,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('foo', (value) => `.foo${value} &`, { @@ -2529,6 +2576,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('foo', (value) => `.foo${value} &`) @@ -2553,6 +2601,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { @@ -2585,6 +2634,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { @@ -2615,6 +2665,7 @@ describe('matchVariant', () => { { loadModule: async (id, base) => { return { + path: '', base, module: ({ matchVariant }: PluginAPI) => { matchVariant('my-container', (value, { modifier }) => { @@ -2669,6 +2720,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -2710,6 +2762,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities([ @@ -2743,6 +2796,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities([ @@ -2782,6 +2836,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities([ @@ -2816,6 +2871,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -2857,6 +2913,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -2913,6 +2970,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -2942,6 +3000,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -2985,6 +3044,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -3026,6 +3086,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -3122,6 +3183,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -3175,6 +3237,7 @@ describe('addUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addUtilities }: PluginAPI) => { addUtilities({ @@ -3240,6 +3303,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3320,6 +3384,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3361,6 +3426,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3410,6 +3476,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3480,6 +3547,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3554,6 +3622,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3609,6 +3678,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3647,6 +3717,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3691,6 +3762,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3818,6 +3890,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3903,6 +3976,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -3961,6 +4035,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -4028,6 +4103,7 @@ describe('matchUtilities()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities({ @@ -4056,6 +4132,7 @@ describe('matchUtilities()', () => { { async loadModule(base) { return { + path: '', base, module: ({ matchUtilities }: PluginAPI) => { matchUtilities( @@ -4122,6 +4199,7 @@ describe('addComponents()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ addComponents }: PluginAPI) => { addComponents({ @@ -4190,6 +4268,7 @@ describe('matchComponents()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ matchComponents }: PluginAPI) => { matchComponents( @@ -4235,6 +4314,7 @@ describe('prefix()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ prefix }: PluginAPI) => { fn(prefix('btn')) @@ -4258,6 +4338,7 @@ describe('config()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ config }: PluginAPI) => { fn(config()) @@ -4285,6 +4366,7 @@ describe('config()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ config }: PluginAPI) => { fn(config('theme')) @@ -4310,6 +4392,7 @@ describe('config()', () => { { async loadModule(id, base) { return { + path: '', base, module: ({ config }: PluginAPI) => { fn(config('somekey', 'defaultvalue')) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 968bb7f432f4..9c7502b8d4e5 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -412,6 +412,7 @@ describe('--theme(…)', () => { [], { loadModule: async () => ({ + path: '', base: '/root', module: () => {}, }), @@ -772,6 +773,7 @@ describe('theme(…)', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: {}, }), @@ -1199,6 +1201,7 @@ describe('in plugins', () => { { async loadModule() { return { + path: '', base: '/root', module: plugin(({ addBase, addUtilities }) => { addBase({ @@ -1256,6 +1259,7 @@ describe('in JS config files', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: { theme: { @@ -1317,6 +1321,7 @@ test('replaces CSS theme() function with values inside imported stylesheets', as { async loadStylesheet() { return { + path: '', base: '/bar.css', content: css` .red { diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 22dd6b053a0f..cc22114d33a8 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -126,6 +126,7 @@ describe('compiling CSS', () => { { async loadStylesheet(id) { return { + path: '', base: '', content: fs.readFileSync( path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id), @@ -401,6 +402,7 @@ describe('@apply', () => { { async loadStylesheet() { return { + path: '', base: '/bar.css', content: css` .foo { @@ -2320,6 +2322,7 @@ describe('Parsing theme values from CSS', () => { { async loadStylesheet() { return { + path: '', base: '', content: css` @theme { @@ -2407,6 +2410,7 @@ describe('Parsing theme values from CSS', () => { { async loadStylesheet() { return { + path: '', base: '', content: css` @theme { @@ -2704,6 +2708,7 @@ describe('Parsing theme values from CSS', () => { { loadModule: async () => { return { + path: '', base: '/root', module: plugin(({}) => {}, { theme: { @@ -2750,6 +2755,7 @@ describe('Parsing theme values from CSS', () => { { loadModule: async () => { return { + path: '', base: '/root', module: { theme: { @@ -2839,6 +2845,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') @@ -2857,6 +2864,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') @@ -2877,6 +2885,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') @@ -2899,6 +2908,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: plugin.withOptions((options) => { expect(options).toEqual({ @@ -2951,6 +2961,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: plugin.withOptions((options) => { expect(options).toEqual({ @@ -2991,6 +3002,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: plugin.withOptions((options) => { return ({ addUtilities }) => { @@ -3029,6 +3041,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: plugin(({ addUtilities }) => { addUtilities({ @@ -3055,6 +3068,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: plugin(() => {}), }), @@ -3079,6 +3093,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: plugin(() => {}), }), @@ -3105,6 +3120,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') @@ -3137,6 +3153,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', ['&:hover', '&:focus']) @@ -3170,6 +3187,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { @@ -3205,6 +3223,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { @@ -3254,6 +3273,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { @@ -3292,6 +3312,7 @@ describe('plugins', () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addVariant }: PluginAPI) => { addVariant('dark', '&:is([data-theme=dark] *)') @@ -4148,6 +4169,7 @@ test('addBase', async () => { `, { loadModule: async () => ({ + path: '', base: '/root', module: ({ addBase }: PluginAPI) => { addBase({ @@ -4187,6 +4209,7 @@ it("should error when `layer(…)` is used, but it's not the first param", async { async loadStylesheet() { return { + path: '', base: '/bar.css', content: css` .foo { @@ -4207,6 +4230,7 @@ describe('`@reference "…" imports`', () => { let loadStylesheet = async (id: string, base: string) => { if (id === './foo/baz.css') { return { + path: '', base: '/root/foo', content: css` .foo { @@ -4223,6 +4247,7 @@ describe('`@reference "…" imports`', () => { } } return { + path: '', base: '/root/foo', content: css` @import './foo/baz.css'; @@ -4255,6 +4280,7 @@ describe('`@reference "…" imports`', () => { let loadStylesheet = async (id: string, base: string) => { if (id === './foo/baz.css') { return { + path: '', base: '/root/foo', content: css` @layer utilities { @@ -4264,6 +4290,7 @@ describe('`@reference "…" imports`', () => { } } return { + path: '', base: '/root/foo', content: css` @import './foo/baz.css'; @@ -4317,6 +4344,7 @@ describe('`@reference "…" imports`', () => { ['animate-spin', 'match-utility-initial', 'match-components-initial'], { loadModule: async () => ({ + path: '', base: '/root', module: ({ addBase, @@ -4372,6 +4400,7 @@ describe('`@reference "…" imports`', () => { switch (id) { case './one.css': { return { + path: '', base: '/root', content: css` @import './two.css' layer(two); @@ -4380,6 +4409,7 @@ describe('`@reference "…" imports`', () => { } case './two.css': { return { + path: '', base: '/root', content: css` @import './three.css' layer(three); @@ -4388,6 +4418,7 @@ describe('`@reference "…" imports`', () => { } case './three.css': { return { + path: '', base: '/root', content: css` .foo { @@ -4446,6 +4477,7 @@ describe('`@reference "…" imports`', () => { test('supports `@import "…" reference` syntax', async () => { let loadStylesheet = async () => { return { + path: '', base: '/root/foo', content: css` .foo { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 792f7452fe95..541ec11a07c0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -57,6 +57,7 @@ type CompileOptions = { base: string, resourceHint: 'plugin' | 'config', ) => Promise<{ + path: string base: string module: Plugin | Config }> @@ -64,6 +65,7 @@ type CompileOptions = { id: string, base: string, ) => Promise<{ + path: string base: string content: string }> diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 95d5d954d2ce..04a6ed7961b9 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -172,10 +172,12 @@ test('Utilities do not show wrapping selector in intellisense', async () => { let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), loadModule: async () => ({ + path: '', base: '', module: { important: '#app', @@ -208,6 +210,7 @@ test('Utilities, when marked as important, show as important in intellisense', a let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), @@ -239,10 +242,12 @@ test('Static utilities from plugins are listed in hovers and completions', async let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), loadModule: async () => ({ + path: '', base: '', module: plugin(({ addUtilities }) => { addUtilities({ @@ -274,10 +279,12 @@ test('Functional utilities from plugins are listed in hovers and completions', a let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), loadModule: async () => ({ + path: '', base: '', module: plugin(({ matchUtilities }) => { matchUtilities( @@ -420,10 +427,12 @@ test('Custom at-rule variants do not show up as a value under `group`', async () let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), loadModule: async () => ({ + path: '', base: '', module: plugin(({ addVariant }) => { addVariant('variant-3', '@media baz') @@ -510,6 +519,7 @@ test('Custom functional @utility', async () => { let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), @@ -587,6 +597,7 @@ test('Theme keys with underscores are suggested with underscores', async () => { let design = await __unstable__loadDesignSystem(input, { loadStylesheet: async (_, base) => ({ + path: '', base, content: '@tailwind utilities;', }), diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts index 9556c34e3213..de426ab459fa 100644 --- a/packages/tailwindcss/src/prefix.test.ts +++ b/packages/tailwindcss/src/prefix.test.ts @@ -169,6 +169,7 @@ test('JS theme functions do not use the prefix', async () => { { async loadModule(id, base) { return { + path: '', base, module: plugin(({ addUtilities, theme }) => { addUtilities({ @@ -206,6 +207,7 @@ test('a prefix can be configured via @import theme(…)', async () => { let compiler = await compile(input, { async loadStylesheet(id, base) { return { + path: '', base, content: css` @theme { @@ -250,6 +252,7 @@ test('a prefix can be configured via @import theme(…)', async () => { compiler = await compile(input, { async loadStylesheet(id, base) { return { + path: '', base, content: css` @theme { @@ -275,6 +278,7 @@ test('a prefix can be configured via @import prefix(…)', async () => { let compiler = await compile(input, { async loadStylesheet(id, base) { return { + path: '', base, content: css` @theme { @@ -314,6 +318,7 @@ test('a prefix can be configured via @import prefix(…)', async () => { compiler = await compile(input, { async loadStylesheet(id, base) { return { + path: '', base, content: css` @theme { diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index e1a1388f6912..016191dac0a7 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -2528,6 +2528,7 @@ test('matchVariant sorts deterministically', async () => { let output = await compileCss('@tailwind utilities; @plugin "./plugin.js";', classList, { async loadModule(id: string) { return { + path: '', base: '/', module: createPlugin(({ matchVariant }) => { matchVariant('is-data', (value) => `&:is([data-${value}])`, { From c53f34487155df956f2f6a08de233f6f5c7ee0d2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 11:18:47 -0400 Subject: [PATCH 03/41] Skip over the CR in CRLF when parsing CSS --- packages/tailwindcss/src/css-parser.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index c492ed353ce4..69eb1fca0a05 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -18,6 +18,7 @@ const SINGLE_QUOTE = 0x27 const COLON = 0x3a const SEMICOLON = 0x3b const LINE_BREAK = 0x0a +const CARRIAGE_RETURN = 0xd const SPACE = 0x20 const TAB = 0x09 const OPEN_CURLY = 0x7b @@ -53,6 +54,14 @@ export function parse(input: string) { for (let i = 0; i < input.length; i++) { let currentChar = input.charCodeAt(i) + // Skip over the CR in CRLF. This allows code below to only check for a line + // break even if we're looking at a Windows newline. Peeking the input still + // has to check for CRLF but that happens less often. + if (currentChar === CARRIAGE_RETURN) { + peekChar = input.charCodeAt(i + 1) + if (peekChar === LINE_BREAK) continue + } + // Current character is a `\` therefore the next character is escaped, // consume it together with the next character and continue. // From e7652b7a71aa67ad755aea7099dc5304ed26e769 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 11:25:26 -0400 Subject: [PATCH 04/41] Check for CRLF when scanning consecutive whitespace --- packages/tailwindcss/src/css-parser.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 69eb1fca0a05..e98f5382080d 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -191,7 +191,12 @@ export function parse(input: string) { else if ( (currentChar === SPACE || currentChar === LINE_BREAK || currentChar === TAB) && (peekChar = input.charCodeAt(i + 1)) && - (peekChar === SPACE || peekChar === LINE_BREAK || peekChar === TAB) + (peekChar === SPACE || + peekChar === LINE_BREAK || + peekChar === TAB || + (peekChar === CARRIAGE_RETURN && + (peekChar = input.charCodeAt(i + 2)) && + peekChar == LINE_BREAK)) ) { continue } From da86e231d49f681f0d2867e42b6baabe15beac27 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 11:27:11 -0400 Subject: [PATCH 05/41] Handle CRLF in strings --- packages/tailwindcss/src/css-parser.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index e98f5382080d..e458ec124de9 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -159,7 +159,11 @@ export function parse(input: string) { // ^ Missing " // } // ``` - else if (peekChar === SEMICOLON && input.charCodeAt(j + 1) === LINE_BREAK) { + else if ( + peekChar === SEMICOLON && + (input.charCodeAt(j + 1) === LINE_BREAK || + (input.charCodeAt(j + 1) === CARRIAGE_RETURN && input.charCodeAt(j + 2) === LINE_BREAK)) + ) { throw new Error( `Unterminated string: ${input.slice(start, j + 1) + String.fromCharCode(currentChar)}`, ) @@ -175,7 +179,10 @@ export function parse(input: string) { // ^ Missing " // } // ``` - else if (peekChar === LINE_BREAK) { + else if ( + peekChar === LINE_BREAK || + (peekChar === CARRIAGE_RETURN && input.charCodeAt(j + 1) === LINE_BREAK) + ) { throw new Error( `Unterminated string: ${input.slice(start, j) + String.fromCharCode(currentChar)}`, ) From 65409a98d90f6862ea9d403d5479123cc5e7accd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 11:28:41 -0400 Subject: [PATCH 06/41] =?UTF-8?q?Don=E2=80=99t=20replace=20CRLF=20with=20L?= =?UTF-8?q?F=20before=20parsing=20CSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that we can handle CRLF correctly in the parser this is no longer necessary --- packages/tailwindcss/src/css-parser.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index e458ec124de9..89b06da5c9fd 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -36,7 +36,6 @@ export function parse(input: string) { // *before* processing do NOT change the length of the string. This // would invalidate the mechanism used to track source locations. if (input[0] === '\uFEFF') input = ' ' + input.slice(1) - input = input.replaceAll('\r\n', ' \n') let ast: AstNode[] = [] let licenseComments: Comment[] = [] From 8319bd83b3d760d6e158de6af8b90d08442ab058 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 11:29:05 -0400 Subject: [PATCH 07/41] Update CSS parser tests to preserve CRLF where needed --- packages/tailwindcss/src/css-parser.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index 379b4b942793..f7ee47a61145 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -86,7 +86,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { kind: 'comment', value: `! * License #2 - `, + `.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), }, ]) }) @@ -368,7 +368,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { background-color: red; /* A comment */ content: 'Hello, world!'; - }`, + }`.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), important: false, }, ]) @@ -396,7 +396,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { background-color: red; /* A comment ; */ content: 'Hello, world!'; - }`, + }`.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), important: false, }, { @@ -406,7 +406,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { background-color: red; /* A comment } */ content: 'Hello, world!'; - }`, + }`.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), important: false, }, ]) From f08f89d7ba7cfc15bb2b3df798700dd3231da338 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:47:20 -0400 Subject: [PATCH 08/41] Track source locations in the AST These are stored as character offsets for the original input (`src`) as well as the printed output (`dst`) --- packages/tailwindcss/src/ast.ts | 19 +++++++++++++ .../tailwindcss/src/source-maps/source.ts | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 packages/tailwindcss/src/source-maps/source.ts diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 4c5cd40da1ef..5f3032b54b0b 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,6 +1,7 @@ import { Polyfills } from '.' import { parseAtRule } from './css-parser' import type { DesignSystem } from './design-system' +import type { Source, SourceLocation } from './source-maps/source' import { Theme, ThemeOptions } from './theme' import { DefaultMap } from './utils/default-map' import { extractUsedVariables } from './utils/variables' @@ -12,6 +13,9 @@ export type StyleRule = { kind: 'rule' selector: string nodes: AstNode[] + + src?: SourceLocation + dst?: SourceLocation } export type AtRule = { @@ -19,6 +23,9 @@ export type AtRule = { name: string params: string nodes: AstNode[] + + src?: SourceLocation + dst?: SourceLocation } export type Declaration = { @@ -26,22 +33,34 @@ export type Declaration = { property: string value: string | undefined important: boolean + + src?: SourceLocation + dst?: SourceLocation } export type Comment = { kind: 'comment' value: string + + src?: SourceLocation + dst?: SourceLocation } export type Context = { kind: 'context' context: Record nodes: AstNode[] + + src?: undefined + dst?: undefined } export type AtRoot = { kind: 'at-root' nodes: AstNode[] + + src?: undefined + dst?: undefined } export type Rule = StyleRule | AtRule diff --git a/packages/tailwindcss/src/source-maps/source.ts b/packages/tailwindcss/src/source-maps/source.ts new file mode 100644 index 000000000000..c3a4f8988488 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/source.ts @@ -0,0 +1,27 @@ +/** + * The source code for one or more nodes in the AST + * + * This generally corresponds to a stylesheet + */ +export interface Source { + /** + * The path to the file that contains the referenced source code + * + * If this references the *output* source code, this is `null`. + */ + file: string | null + + /** + * The referenced source code + */ + code: string +} + +/** + * The file and offsets within it that this node covers + * + * This can represent either: + * - A location in the original CSS which caused this node to be created + * - A location in the output CSS where this node resides + */ +export type SourceLocation = [source: Source, start: number, end: number] From e95e6e239f0cfa7aa69ef29592e5e4a32d1089ef Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:36:57 -0400 Subject: [PATCH 09/41] 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 02c488afd0ba..5ce6299af6a2 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 ab490e103849..d48f88ab3841 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 89b06da5c9fd..6a5ce83204bd 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 541ec11a07c0..10dba2386d61 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 From c129f48caeb949b37c3e639434e52943dff562ff Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Apr 2025 13:29:55 -0400 Subject: [PATCH 10/41] Track locations inside `@import` --- packages/tailwindcss/src/at-import.ts | 5 +++-- packages/tailwindcss/src/index.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 2d2682ddd3b0..8853a9e5fdcc 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -17,6 +17,7 @@ export async function substituteAtImports( base: string, loadStylesheet: LoadStylesheet, recurseCount = 0, + track = false, ) { let features = Features.None let promises: Promise[] = [] @@ -52,8 +53,8 @@ export async function substituteAtImports( } let loaded = await loadStylesheet(uri, base) - let ast = CSS.parse(loaded.content) - await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) + let ast = CSS.parse(loaded.content, { from: track ? loaded.path : undefined }) + await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, track) contextNode.nodes = buildImportNodes( [context({ base: loaded.base }, ast)], diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 10dba2386d61..6adb8fffc792 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -145,7 +145,7 @@ async function parseCss( let features = Features.None ast = [contextNode({ base }, ast)] as AstNode[] - features |= await substituteAtImports(ast, base, loadStylesheet) + features |= await substituteAtImports(ast, base, loadStylesheet, 0, from !== undefined) let important = null as boolean | null let theme = new Theme() From 3bc80cda67a4f3eb2eefff4e5ae38fa36ad9a166 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 18 Apr 2025 13:12:09 -0400 Subject: [PATCH 11/41] Track wrapping at-rules created by imports --- packages/tailwindcss/src/at-import.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 8853a9e5fdcc..5f13cce99c51 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -57,6 +57,7 @@ export async function substituteAtImports( await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, track) contextNode.nodes = buildImportNodes( + node, [context({ base: loaded.base }, ast)], layer, media, @@ -148,6 +149,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) { } function buildImportNodes( + importNode: AstNode, importedAst: AstNode[], layer: string | null, media: string | null, @@ -157,16 +159,19 @@ function buildImportNodes( if (layer !== null) { let node = atRule('@layer', layer, root) + node.src = importNode.src root = [node] } if (media !== null) { let node = atRule('@media', media, root) + node.src = importNode.src root = [node] } if (supports !== null) { let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root) + node.src = importNode.src root = [node] } From 3143eb6e7ad176470ebf9e1387951aaebfc8b83c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:26:37 -0400 Subject: [PATCH 12/41] Track theme variables --- packages/tailwindcss/src/index.ts | 4 +++- packages/tailwindcss/src/theme.ts | 15 +++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 6adb8fffc792..2e1968d405d2 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -541,7 +541,7 @@ async function parseCss( if (child.kind === 'comment') return if (child.kind === 'declaration' && child.property.startsWith('--')) { - theme.add(unescape(child.property), child.value ?? '', themeOptions) + theme.add(unescape(child.property), child.value ?? '', themeOptions, child.src) return } @@ -559,6 +559,7 @@ async function parseCss( // theme later, and delete any other `@theme` rules. if (!firstThemeRule) { firstThemeRule = styleRule(':root, :host', []) + firstThemeRule.src = node.src replaceWith([firstThemeRule]) } else { replaceWith([]) @@ -607,6 +608,7 @@ async function parseCss( for (let [key, value] of designSystem.theme.entries()) { if (value.options & ThemeOptions.REFERENCE) continue let node = decl(escape(key), value.value) + node.src = value.src nodes.push(node) } diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index 74375b6f723d..2928afd3bd55 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -1,4 +1,4 @@ -import { type AtRule } from './ast' +import { type AtRule, type Declaration } from './ast' import { escape, unescape } from './utils/escape' export const enum ThemeOptions { @@ -40,11 +40,18 @@ export class Theme { public prefix: string | null = null constructor( - private values = new Map(), + private values = new Map< + string, + { + value: string + options: ThemeOptions + src: Declaration['src'] + } + >(), private keyframes = new Set([]), ) {} - add(key: string, value: string, options = ThemeOptions.NONE): void { + add(key: string, value: string, options = ThemeOptions.NONE, src?: Declaration['src']): void { if (key.endsWith('-*')) { if (value !== 'initial') { throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``) @@ -68,7 +75,7 @@ export class Theme { if (value === 'initial') { this.values.delete(key) } else { - this.values.set(key, { value, options }) + this.values.set(key, { value, options, src }) } } From 386e72158ccd9d80f63547a639c15e788d19d13d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:40:49 -0400 Subject: [PATCH 13/41] Track generated utilities --- packages/tailwindcss/src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 2e1968d405d2..04a99b927d85 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -763,6 +763,16 @@ export async function compileAst( onInvalidCandidate, }).astNodes + if (opts.from) { + walk(newNodes, (node) => { + // We do this conditionally to preserve source locations from both + // `@utility` and `@custom-variant`. Even though generated nodes are + // cached this should be fine because `utilitiesNode.src` should not + // change without a full rebuild which destroys the cache. + node.src ??= utilitiesNode.src + }) + } + // 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. From fcd4e2d2bc7c8cc174bbcd8e573f894ba74df32b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:48:31 -0400 Subject: [PATCH 14/41] Track locations when optimizing --- packages/tailwindcss/src/ast.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 5f3032b54b0b..a3f1835b0aef 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -398,6 +398,7 @@ export function optimizeAst( } let fallback = decl(property, initialValue ?? 'initial') + fallback.src = node.src if (inherits) { propertyFallbacksRoot.push(fallback) @@ -644,6 +645,7 @@ export function optimizeAst( value: ValueParser.toCss(ast), } let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [declaration]) + colorMixQuery.src = declaration.src parent.splice(idx, 1, fallback, colorMixQuery) } } @@ -654,11 +656,13 @@ export function optimizeAst( if (propertyFallbacksRoot.length > 0) { let wrapper = rule(':root, :host', propertyFallbacksRoot) + wrapper.src = propertyFallbacksRoot[0].src fallbackAst.push(wrapper) } if (propertyFallbacksUniversal.length > 0) { let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal) + wrapper.src = propertyFallbacksUniversal[0].src fallbackAst.push(wrapper) } @@ -682,6 +686,7 @@ export function optimizeAst( }) let layerPropertiesStatement = atRule('@layer', 'properties', []) + layerPropertiesStatement.src = fallbackAst[0].src newAst.splice( firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex, @@ -699,6 +704,9 @@ export function optimizeAst( ), ]) + block.src = fallbackAst[0].src + block.nodes[0].src = fallbackAst[0].src + newAst.push(block) } } From 6febfd966a4c2c5f16a5301dfb86e619ab1f19cd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:48:26 -0400 Subject: [PATCH 15/41] Track locations when printing --- packages/tailwindcss/src/ast.bench.ts | 28 ++++++ packages/tailwindcss/src/ast.ts | 140 +++++++++++++++++++++++++- packages/tailwindcss/src/index.ts | 2 +- 3 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 packages/tailwindcss/src/ast.bench.ts diff --git a/packages/tailwindcss/src/ast.bench.ts b/packages/tailwindcss/src/ast.bench.ts new file mode 100644 index 000000000000..a2d8b28920f2 --- /dev/null +++ b/packages/tailwindcss/src/ast.bench.ts @@ -0,0 +1,28 @@ +import { bench } from 'vitest' +import { toCss } from './ast' +import * as CSS from './css-parser' + +const css = String.raw +const input = css` + @theme { + --color-primary: #333; + } + @tailwind utilities; + .foo { + color: red; + /* comment */ + &:hover { + color: blue; + @apply font-bold; + } + } +` +const ast = CSS.parse(input) + +bench('toCss', () => { + toCss(ast) +}) + +bench('toCss with source maps', () => { + toCss(ast, true) +}) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index a3f1835b0aef..e4b5d5a9ab52 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -714,7 +714,14 @@ export function optimizeAst( return newAst } -export function toCss(ast: AstNode[]) { +export function toCss(ast: AstNode[], track?: boolean) { + let pos = 0 + + let source: Source = { + file: null, + code: '', + } + function stringify(node: AstNode, depth = 0): string { let css = '' let indent = ' '.repeat(depth) @@ -722,15 +729,70 @@ export function toCss(ast: AstNode[]) { // Declaration if (node.kind === 'declaration') { css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n` + + if (track) { + // indent + pos += indent.length + + // node.property + let start = pos + pos += node.property.length + + // `: ` + pos += 2 + + // node.value + pos += node.value?.length ?? 0 + + // !important + if (node.important) { + pos += 11 + } + + let end = pos + + // `;\n` + pos += 2 + + node.dst = [source, start, end] + } } // Rule else if (node.kind === 'rule') { css += `${indent}${node.selector} {\n` + + if (track) { + // indent + pos += indent.length + + // node.selector + let start = pos + pos += node.selector.length + + // ` ` + pos += 1 + + let end = pos + node.dst = [source, start, end] + + // `{\n` + pos += 2 + } + for (let child of node.nodes) { css += stringify(child, depth + 1) } + css += `${indent}}\n` + + if (track) { + // indent + pos += indent.length + + // `}\n` + pos += 2 + } } // AtRule @@ -744,19 +806,93 @@ export function toCss(ast: AstNode[]) { // ``` if (node.nodes.length === 0) { let css = `${indent}${node.name} ${node.params};\n` + + if (track) { + // indent + pos += indent.length + + // node.name + let start = pos + pos += node.name.length + + // ` ` + pos += 1 + + // node.params + pos += node.params.length + let end = pos + + // `;\n` + pos += 2 + + node.dst = [source, start, end] + } + return css } css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n` + + if (track) { + // indent + pos += indent.length + + // node.name + let start = pos + pos += node.name.length + + if (node.params) { + // ` ` + pos += 1 + + // node.params + pos += node.params.length + } + + // ` ` + pos += 1 + + let end = pos + node.dst = [source, start, end] + + // `{\n` + pos += 2 + } + for (let child of node.nodes) { css += stringify(child, depth + 1) } + css += `${indent}}\n` + + if (track) { + // indent + pos += indent.length + + // `}\n` + pos += 2 + } } // Comment else if (node.kind === 'comment') { css += `${indent}/*${node.value}*/\n` + + if (track) { + // indent + pos += indent.length + + // The comment itself. We do this instead of just the inside because + // it seems more useful to have the entire comment span tracked. + let start = pos + pos += 2 + node.value.length + 2 + let end = pos + + node.dst = [source, start, end] + + // `\n` + pos += 1 + } } // These should've been handled already by `optimizeAst` which @@ -780,6 +916,8 @@ export function toCss(ast: AstNode[]) { css += stringify(node, 0) } + source.code = css + return css } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 04a99b927d85..b109713ee265 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -814,7 +814,7 @@ export async function compile( return compiledCss } - compiledCss = toCss(newAst) + compiledCss = toCss(newAst, !!opts.from) compiledAst = newAst return compiledCss From 6b9ace26799f5dbcdefb64a6175fc52cf205a4da Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 18:04:03 -0400 Subject: [PATCH 16/41] Track utilities generated by `@apply` --- packages/tailwindcss/src/apply.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 6038c4b8dfbb..0b7b3ad16f24 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -167,8 +167,13 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { }, }) + let src = node.src let candidateAst = compiled.astNodes + walk(candidateAst, (node) => { + node.src = src + }) + // Collect the nodes to insert in place of the `@apply` rule. When a rule // was used, we want to insert its children instead of the rule because we // don't want the wrapping selector. From 9a9b47a1faf2339f172dd329e35fa3e272c77201 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 18:05:04 -0400 Subject: [PATCH 17/41] Track utilities per-candidate in `@apply` --- packages/tailwindcss/src/apply.ts | 41 ++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 0b7b3ad16f24..71cfc1e039ce 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -2,6 +2,7 @@ import { Features } from '.' import { rule, toCss, walk, WalkAction, type AstNode } from './ast' import { compileCandidates } from './compile' import type { DesignSystem } from './design-system' +import type { SourceLocation } from './source-maps/source' import { DefaultMap } from './utils/default-map' export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { @@ -155,12 +156,20 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let node = parent.nodes[i] if (node.kind !== 'at-rule' || node.name !== '@apply') continue - let candidates = node.params.split(/\s+/g) + let parts = node.params.split(/(\s+)/g) + let candidateOffsets: Record = {} + + let offset = 0 + for (let [idx, part] of parts.entries()) { + if (idx % 2 === 0) candidateOffsets[part] = offset + offset += part.length + } // Replace the `@apply` rule with the actual utility classes { // Parse the candidates to an AST that we can replace the `@apply` rule // with. + let candidates = Object.keys(candidateOffsets) let compiled = compileCandidates(candidates, designSystem, { onInvalidCandidate: (candidate) => { throw new Error(`Cannot apply unknown utility class: ${candidate}`) @@ -168,12 +177,36 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { }) let src = node.src - let candidateAst = compiled.astNodes - walk(candidateAst, (node) => { - node.src = src + let details = compiled.astNodes.map((node) => { + let candidate = compiled.nodeSorting.get(node)?.candidate + let candidateOffset = candidate ? candidateOffsets[candidate] : undefined + + node = structuredClone(node) + + if (!src || !candidate || candidateOffset === undefined) { + return { node, src } + } + + let candidateSrc: SourceLocation = [src[0], src[1], src[2]] + + candidateSrc[1] += 7 + candidateOffset + candidateSrc[2] = candidateSrc[1] + candidate.length + + return { node: structuredClone(node), src: candidateSrc } }) + for (let { node, src } of details) { + // While the original nodes may have come from an `@utility` we still + // want to replace the source because the `@apply` is ultimately the + // reason the node was emitted into the AST. + walk([node], (node) => { + node.src = src + }) + } + + let candidateAst = details.map((d) => d.node) + // Collect the nodes to insert in place of the `@apply` rule. When a rule // was used, we want to insert its children instead of the rule because we // don't want the wrapping selector. From a0a9a33952cd8716d08e8561481818fb6f3366b3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 12 Jan 2025 22:41:25 -0500 Subject: [PATCH 18/41] Use offset information to generate source maps --- packages/tailwindcss/package.json | 5 +- packages/tailwindcss/src/index.ts | 10 + .../src/source-maps/line-table.test.ts | 87 ++++ .../tailwindcss/src/source-maps/line-table.ts | 100 +++++ .../src/source-maps/source-map.test.ts | 421 ++++++++++++++++++ .../tailwindcss/src/source-maps/source-map.ts | 194 ++++++++ .../src/source-maps/translation-map.test.ts | 279 ++++++++++++ 7 files changed, 1095 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss/src/source-maps/line-table.test.ts create mode 100644 packages/tailwindcss/src/source-maps/line-table.ts create mode 100644 packages/tailwindcss/src/source-maps/source-map.test.ts create mode 100644 packages/tailwindcss/src/source-maps/source-map.ts create mode 100644 packages/tailwindcss/src/source-maps/translation-map.test.ts diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 92110f1ca2c3..91c05a22b5dc 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -127,9 +127,12 @@ "utilities.css" ], "devDependencies": { + "@ampproject/remapping": "^2.3.0", "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", + "dedent": "1.5.3", "lightningcss": "catalog:", - "dedent": "1.5.3" + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1" } } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index b109713ee265..6e493c3689d0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -26,6 +26,7 @@ import { applyVariant, compileCandidates } from './compile' import { substituteFunctions } from './css-functions' import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' +import { createSourceMap, type DecodedSourceMap } from './source-maps/source-map' import { Theme, ThemeOptions } from './theme' import { createCssUtility } from './utilities' import { expand } from './utils/brace-expansion' @@ -791,6 +792,8 @@ export async function compileAst( } } +export type { DecodedSourceMap } + export async function compile( css: string, opts: CompileOptions = {}, @@ -799,6 +802,7 @@ export async function compile( root: Root features: Features build(candidates: string[]): string + buildSourceMap(): DecodedSourceMap }> { let ast = CSS.parse(css, { from: opts.from }) let api = await compileAst(ast, opts) @@ -819,6 +823,12 @@ export async function compile( return compiledCss }, + + buildSourceMap() { + return createSourceMap({ + ast: compiledAst, + }) + }, } } diff --git a/packages/tailwindcss/src/source-maps/line-table.test.ts b/packages/tailwindcss/src/source-maps/line-table.test.ts new file mode 100644 index 000000000000..23c46243b726 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/line-table.test.ts @@ -0,0 +1,87 @@ +import dedent from 'dedent' +import { expect, test } from 'vitest' +import { createLineTable } from './line-table' + +const css = dedent + +test('line tables', () => { + let text = css` + .foo { + color: red; + } + ` + + let table = createLineTable(`${text}\n`) + + // Line 1: `.foo {\n` + expect(table.find(0)).toEqual({ line: 1, column: 0 }) + expect(table.find(1)).toEqual({ line: 1, column: 1 }) + expect(table.find(2)).toEqual({ line: 1, column: 2 }) + expect(table.find(3)).toEqual({ line: 1, column: 3 }) + expect(table.find(4)).toEqual({ line: 1, column: 4 }) + expect(table.find(5)).toEqual({ line: 1, column: 5 }) + expect(table.find(6)).toEqual({ line: 1, column: 6 }) + + // Line 2: ` color: red;\n` + expect(table.find(6 + 1)).toEqual({ line: 2, column: 0 }) + expect(table.find(6 + 2)).toEqual({ line: 2, column: 1 }) + expect(table.find(6 + 3)).toEqual({ line: 2, column: 2 }) + expect(table.find(6 + 4)).toEqual({ line: 2, column: 3 }) + expect(table.find(6 + 5)).toEqual({ line: 2, column: 4 }) + expect(table.find(6 + 6)).toEqual({ line: 2, column: 5 }) + expect(table.find(6 + 7)).toEqual({ line: 2, column: 6 }) + expect(table.find(6 + 8)).toEqual({ line: 2, column: 7 }) + expect(table.find(6 + 9)).toEqual({ line: 2, column: 8 }) + expect(table.find(6 + 10)).toEqual({ line: 2, column: 9 }) + expect(table.find(6 + 11)).toEqual({ line: 2, column: 10 }) + expect(table.find(6 + 12)).toEqual({ line: 2, column: 11 }) + expect(table.find(6 + 13)).toEqual({ line: 2, column: 12 }) + + // Line 3: `}\n` + expect(table.find(20 + 1)).toEqual({ line: 3, column: 0 }) + expect(table.find(20 + 2)).toEqual({ line: 3, column: 1 }) + + // After the new line + expect(table.find(22 + 1)).toEqual({ line: 4, column: 0 }) +}) + +test('line tables findOffset', () => { + let text = css` + .foo { + color: red; + } + ` + + let table = createLineTable(`${text}\n`) + + // Line 1: `.foo {\n` + expect(table.findOffset({ line: 1, column: 0 })).toEqual(0) + expect(table.findOffset({ line: 1, column: 1 })).toEqual(1) + expect(table.findOffset({ line: 1, column: 2 })).toEqual(2) + expect(table.findOffset({ line: 1, column: 3 })).toEqual(3) + expect(table.findOffset({ line: 1, column: 4 })).toEqual(4) + expect(table.findOffset({ line: 1, column: 5 })).toEqual(5) + expect(table.findOffset({ line: 1, column: 6 })).toEqual(6) + + // Line 2: ` color: red;\n` + expect(table.findOffset({ line: 2, column: 0 })).toEqual(6 + 1) + expect(table.findOffset({ line: 2, column: 1 })).toEqual(6 + 2) + expect(table.findOffset({ line: 2, column: 2 })).toEqual(6 + 3) + expect(table.findOffset({ line: 2, column: 3 })).toEqual(6 + 4) + expect(table.findOffset({ line: 2, column: 4 })).toEqual(6 + 5) + expect(table.findOffset({ line: 2, column: 5 })).toEqual(6 + 6) + expect(table.findOffset({ line: 2, column: 6 })).toEqual(6 + 7) + expect(table.findOffset({ line: 2, column: 7 })).toEqual(6 + 8) + expect(table.findOffset({ line: 2, column: 8 })).toEqual(6 + 9) + expect(table.findOffset({ line: 2, column: 9 })).toEqual(6 + 10) + expect(table.findOffset({ line: 2, column: 10 })).toEqual(6 + 11) + expect(table.findOffset({ line: 2, column: 11 })).toEqual(6 + 12) + expect(table.findOffset({ line: 2, column: 12 })).toEqual(6 + 13) + + // Line 3: `}\n` + expect(table.findOffset({ line: 3, column: 0 })).toEqual(20 + 1) + expect(table.findOffset({ line: 3, column: 1 })).toEqual(20 + 2) + + // After the new line + expect(table.findOffset({ line: 4, column: 0 })).toEqual(22 + 1) +}) diff --git a/packages/tailwindcss/src/source-maps/line-table.ts b/packages/tailwindcss/src/source-maps/line-table.ts new file mode 100644 index 000000000000..d42be8055206 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/line-table.ts @@ -0,0 +1,100 @@ +/** + * Line offset tables are the key to generating our source maps. They allow us + * to store indexes with our AST nodes and later convert them into positions as + * when given the source that the indexes refer to. + */ + +const LINE_BREAK = 0x0a + +/** + * A position in source code + * + * https://tc39.es/ecma426/#sec-position-record-type + */ +export interface Position { + /** The line number, one-based */ + line: number + + /** The column/character number, one-based */ + column: number +} + +/** + * A table that lets you turn an offset into a line number and column + */ +export interface LineTable { + /** + * Find the line/column position in the source code for a given offset + * + * Searching for a given offset takes O(log N) time where N is the number of + * lines of code. + * + * @param offset The index for which to find the position + */ + find(offset: number): Position + + /** + * Find the most likely byte offset for given a position + * + * @param offset The position for which to find the byte offset + */ + findOffset(pos: Position): number +} + +/** + * Compute a lookup table to allow for efficient line/column lookups based on + * offsets in the source code. + * + * Creating this table is an O(N) operation where N is the length of the source + */ +export function createLineTable(source: string): LineTable { + let table: number[] = [0] + + // Compute the offsets for the start of each line + for (let i = 0; i < source.length; i++) { + if (source.charCodeAt(i) === LINE_BREAK) { + table.push(i + 1) + } + } + + function find(offset: number) { + // Based on esbuild's binary search for line numbers + let line = 0 + let count = table.length + while (count > 0) { + // `| 0` causes integer division + let mid = (count / 2) | 0 + let i = line + mid + if (table[i] <= offset) { + line = i + 1 + count = count - mid - 1 + } else { + count = mid + } + } + + line -= 1 + + let column = offset - table[line] + + return { + line: line + 1, + column: column, + } + } + + function findOffset({ line, column }: Position) { + line -= 1 + line = Math.min(Math.max(line, 0), table.length - 1) + + let offsetA = table[line] + let offsetB = table[line + 1] ?? offsetA + + return Math.min(Math.max(offsetA + column, 0), offsetB) + } + + return { + find, + findOffset, + } +} diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts new file mode 100644 index 000000000000..7a6e0471f8b5 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -0,0 +1,421 @@ +import remapping from '@ampproject/remapping' +import dedent from 'dedent' +import MagicString from 'magic-string' +import * as fs from 'node:fs/promises' +import * as path from 'node:path' +import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js' +import { test } from 'vitest' +import { compile } from '..' +import createPlugin from '../plugin' +import { DefaultMap } from '../utils/default-map' +import type { DecodedSource, DecodedSourceMap } from './source-map' +const css = dedent + +interface RunOptions { + input: string + candidates?: string[] + options?: Parameters[1] +} + +async function run({ input, candidates, options }: RunOptions) { + let source = new MagicString(input) + let root = path.resolve(__dirname, '../..') + + let compiler = await compile(source.toString(), { + from: 'input.css', + async loadStylesheet(id, base) { + let resolvedPath = path.resolve(root, id === 'tailwindcss' ? 'index.css' : id) + + return { + path: path.relative(root, resolvedPath), + base, + content: await fs.readFile(resolvedPath, 'utf-8'), + } + }, + ...options, + }) + + let css = compiler.build(candidates ?? []) + let decoded = compiler.buildSourceMap() + let rawMap = toRawSourceMap(decoded) + let combined = remapping(rawMap, () => null) + let map = JSON.parse(rawMap.toString()) as RawSourceMap + + let sources = combined.sources + let annotations = formattedMappings(map) + + return { css, map, sources, annotations } +} + +function toRawSourceMap(map: DecodedSourceMap): string { + let generator = new SourceMapGenerator() + + let id = 1 + let sourceTable = new DefaultMap< + DecodedSource | null, + { + url: string + content: string + } + >((src) => { + return { + url: src?.url ?? ``, + content: src?.content ?? '', + } + }) + + for (let mapping of map.mappings) { + let original = sourceTable.get(mapping.originalPosition?.source ?? null) + + generator.addMapping({ + generated: mapping.generatedPosition, + original: mapping.originalPosition, + source: original.url, + name: mapping.name ?? undefined, + }) + + generator.setSourceContent(original.url, original.content) + } + + return generator.toString() +} + +/** + * An string annotation that represents a source map + * + * It's not meant to be exhaustive just enough to + * verify that the source map is working and that + * lines are mapped back to the original source + * + * Including when using @apply with multiple classes + */ +function formattedMappings(map: RawSourceMap) { + const smc = new SourceMapConsumer(map) + const annotations: Record< + number, + { + original: { start: [number, number]; end: [number, number] } + generated: { start: [number, number]; end: [number, number] } + source: string + } + > = {} + + smc.eachMapping((mapping) => { + let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || { + ...mapping, + + original: { + start: [mapping.originalLine, mapping.originalColumn], + end: [mapping.originalLine, mapping.originalColumn], + }, + + generated: { + start: [mapping.generatedLine, mapping.generatedColumn], + end: [mapping.generatedLine, mapping.generatedColumn], + }, + + source: mapping.source, + }) + + annotation.generated.end[0] = mapping.generatedLine + annotation.generated.end[1] = mapping.generatedColumn + + annotation.original.end[0] = mapping.originalLine! + annotation.original.end[1] = mapping.originalColumn! + }) + + return Object.values(annotations).map((annotation) => { + return `${annotation.source}: ${formatRange(annotation.generated)} <- ${formatRange(annotation.original)}` + }) +} + +function formatRange(range: { start: [number, number]; end: [number, number] }) { + if (range.start[0] === range.end[0]) { + // This range is on the same line + // and the columns are the same + if (range.start[1] === range.end[1]) { + return `${range.start[0]}:${range.start[1]}` + } + + // This range is on the same line + // but the columns are different + return `${range.start[0]}:${range.start[1]}-${range.end[1]}` + } + + // This range spans multiple lines + return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}` +} + +test('source maps trace back to @import location', async ({ expect }) => { + let { sources, annotations } = await run({ + input: css` + @import 'tailwindcss'; + + .foo { + @apply underline; + } + `, + }) + + // All CSS should be mapped back to the original source file + expect(sources).toEqual([ + // + 'index.css', + 'theme.css', + 'preflight.css', + 'input.css', + ]) + expect(sources.length).toBe(4) + + // The output CSS should include annotations linking back to: + // 1. The class definition `.foo` + // 2. The `@apply underline` line inside of it + expect(annotations).toEqual([ + 'index.css: 1:0-41 <- 1:0-41', + 'index.css: 2:0-13 <- 3:0-34', + 'theme.css: 3:2-15 <- 1:0-15', + 'theme.css: 4:4 <- 2:2-4:0', + 'theme.css: 5:22 <- 4:22', + 'theme.css: 6:4 <- 6:2-8:0', + 'theme.css: 7:13 <- 8:13', + 'theme.css: 8:4-43 <- 446:2-54', + 'theme.css: 9:4-48 <- 449:2-59', + 'index.css: 12:0-12 <- 4:0-37', + 'preflight.css: 13:2-59 <- 7:0-11:23', + 'preflight.css: 14:4-26 <- 12:2-24', + 'preflight.css: 15:4-13 <- 13:2-11', + 'preflight.css: 16:4-14 <- 14:2-12', + 'preflight.css: 17:4-19 <- 15:2-17', + 'preflight.css: 19:2-14 <- 28:0-29:6', + 'preflight.css: 20:4-20 <- 30:2-18', + 'preflight.css: 21:4-34 <- 31:2-32', + 'preflight.css: 22:4-15 <- 32:2-13', + 'preflight.css: 23:4-159 <- 33:2-42:3', + 'preflight.css: 24:4-71 <- 43:2-73', + 'preflight.css: 25:4-75 <- 44:2-77', + 'preflight.css: 26:4-44 <- 45:2-42', + 'preflight.css: 28:2-5 <- 54:0-3', + 'preflight.css: 29:4-13 <- 55:2-11', + 'preflight.css: 30:4-18 <- 56:2-16', + 'preflight.css: 31:4-25 <- 57:2-23', + 'preflight.css: 33:2-22 <- 64:0-20', + 'preflight.css: 34:4-45 <- 65:2-43', + 'preflight.css: 35:4-37 <- 66:2-35', + 'preflight.css: 37:2-25 <- 73:0-78:3', + 'preflight.css: 38:4-22 <- 79:2-20', + 'preflight.css: 39:4-24 <- 80:2-22', + 'preflight.css: 41:2-4 <- 87:0-2', + 'preflight.css: 42:4-18 <- 88:2-16', + 'preflight.css: 43:4-36 <- 89:2-34', + 'preflight.css: 44:4-28 <- 90:2-26', + 'preflight.css: 46:2-12 <- 97:0-98:7', + 'preflight.css: 47:4-23 <- 99:2-21', + 'preflight.css: 49:2-23 <- 109:0-112:4', + 'preflight.css: 50:4-148 <- 113:2-123:3', + 'preflight.css: 51:4-76 <- 124:2-78', + 'preflight.css: 52:4-80 <- 125:2-82', + 'preflight.css: 53:4-18 <- 126:2-16', + 'preflight.css: 55:2-8 <- 133:0-6', + 'preflight.css: 56:4-18 <- 134:2-16', + 'preflight.css: 58:2-11 <- 141:0-142:4', + 'preflight.css: 59:4-18 <- 143:2-16', + 'preflight.css: 60:4-18 <- 144:2-16', + 'preflight.css: 61:4-22 <- 145:2-20', + 'preflight.css: 62:4-28 <- 146:2-26', + 'preflight.css: 64:2-6 <- 149:0-4', + 'preflight.css: 65:4-19 <- 150:2-17', + 'preflight.css: 67:2-6 <- 153:0-4', + 'preflight.css: 68:4-15 <- 154:2-13', + 'preflight.css: 70:2-8 <- 163:0-6', + 'preflight.css: 71:4-18 <- 164:2-16', + 'preflight.css: 72:4-25 <- 165:2-23', + 'preflight.css: 73:4-29 <- 166:2-27', + 'preflight.css: 75:2-18 <- 173:0-16', + 'preflight.css: 76:4-17 <- 174:2-15', + 'preflight.css: 78:2-11 <- 181:0-9', + 'preflight.css: 79:4-28 <- 182:2-26', + 'preflight.css: 81:2-10 <- 189:0-8', + 'preflight.css: 82:4-22 <- 190:2-20', + 'preflight.css: 84:2-15 <- 197:0-199:5', + 'preflight.css: 85:4-20 <- 200:2-18', + 'preflight.css: 87:2-56 <- 209:0-216:7', + 'preflight.css: 88:4-18 <- 217:2-16', + 'preflight.css: 89:4-26 <- 218:2-24', + 'preflight.css: 91:2-13 <- 225:0-226:6', + 'preflight.css: 92:4-19 <- 227:2-17', + 'preflight.css: 93:4-16 <- 228:2-14', + 'preflight.css: 95:2-68 <- 238:0-243:23', + 'preflight.css: 96:4-17 <- 244:2-15', + 'preflight.css: 97:4-34 <- 245:2-32', + 'preflight.css: 98:4-36 <- 246:2-34', + 'preflight.css: 99:4-27 <- 247:2-25', + 'preflight.css: 100:4-18 <- 248:2-16', + 'preflight.css: 101:4-20 <- 249:2-18', + 'preflight.css: 102:4-33 <- 250:2-31', + 'preflight.css: 103:4-14 <- 251:2-12', + 'preflight.css: 105:2-49 <- 258:0-47', + 'preflight.css: 106:4-23 <- 259:2-21', + 'preflight.css: 108:2-56 <- 266:0-54', + 'preflight.css: 109:4-30 <- 267:2-28', + 'preflight.css: 111:2-25 <- 274:0-23', + 'preflight.css: 112:4-26 <- 275:2-24', + 'preflight.css: 114:2-16 <- 282:0-14', + 'preflight.css: 115:4-14 <- 283:2-12', + 'preflight.css: 117:2-92 <- 291:0-292:49', + 'preflight.css: 118:4-18 <- 293:2-16', + 'preflight.css: 119:6-25 <- 294:4-61', + 'preflight.css: 120:6-53 <- 294:4-61', + 'preflight.css: 121:8-65 <- 294:4-61', + 'preflight.css: 125:2-11 <- 302:0-9', + 'preflight.css: 126:4-20 <- 303:2-18', + 'preflight.css: 128:2-30 <- 310:0-28', + 'preflight.css: 129:4-28 <- 311:2-26', + 'preflight.css: 131:2-32 <- 319:0-30', + 'preflight.css: 132:4-19 <- 320:2-17', + 'preflight.css: 133:4-23 <- 321:2-21', + 'preflight.css: 135:2-26 <- 328:0-24', + 'preflight.css: 136:4-24 <- 329:2-22', + 'preflight.css: 138:2-41 <- 336:0-39', + 'preflight.css: 139:4-14 <- 337:2-12', + 'preflight.css: 141:2-329 <- 340:0-348:39', + 'preflight.css: 142:4-20 <- 349:2-18', + 'preflight.css: 144:2-19 <- 356:0-17', + 'preflight.css: 145:4-20 <- 357:2-18', + 'preflight.css: 147:2-96 <- 364:0-366:23', + 'preflight.css: 148:4-22 <- 367:2-20', + 'preflight.css: 150:2-59 <- 374:0-375:28', + 'preflight.css: 151:4-16 <- 376:2-14', + 'preflight.css: 153:2-47 <- 383:0-45', + 'preflight.css: 154:4-28 <- 384:2-26', + 'index.css: 157:0-16 <- 5:0-42', + 'input.css: 158:0-5 <- 3:0-5', + 'input.css: 159:2-33 <- 4:9-18', + ]) +}) + +test('source maps are generated for utilities', async ({ expect }) => { + let { + sources, + css: output, + annotations, + } = await run({ + input: css` + @import './utilities.css'; + @plugin "./plugin.js"; + @utility custom { + color: orange; + } + `, + candidates: ['custom', 'custom-js', 'flex'], + options: { + loadModule: async (_, base) => ({ + path: '', + base, + module: createPlugin(({ addUtilities }) => { + addUtilities({ '.custom-js': { color: 'blue' } }) + }), + }), + }, + }) + + // All CSS should be mapped back to the original source file + expect(sources).toEqual(['utilities.css', 'input.css']) + expect(sources.length).toBe(2) + + // The output CSS should include annotations linking back to: + expect(annotations).toEqual([ + // @tailwind utilities + 'utilities.css: 1:0-6 <- 1:0-19', + 'utilities.css: 2:2-15 <- 1:0-19', + 'utilities.css: 4:0-8 <- 1:0-19', + // color: orange + 'input.css: 5:2-15 <- 4:2-15', + // @tailwind utilities + 'utilities.css: 7:0-11 <- 1:0-19', + 'utilities.css: 8:2-13 <- 1:0-19', + ]) + + expect(output).toMatchInlineSnapshot(` + ".flex { + display: flex; + } + .custom { + color: orange; + } + .custom-js { + color: blue; + } + " + `) +}) + +test('utilities have source maps pointing to the utilities node', async ({ expect }) => { + let { sources, annotations } = await run({ + input: `@tailwind utilities;`, + candidates: [ + // + 'underline', + ], + }) + + expect(sources).toEqual(['input.css']) + + expect(annotations).toEqual([ + // + 'input.css: 1:0-11 <- 1:0-19', + 'input.css: 2:2-33 <- 1:0-19', + ]) +}) + +test('@apply generates source maps', async ({ expect }) => { + let { sources, annotations } = await run({ + input: css` + .foo { + color: blue; + @apply text-[#000] hover:text-[#f00]; + @apply underline; + color: red; + } + `, + }) + + expect(sources).toEqual(['input.css']) + + expect(annotations).toEqual([ + 'input.css: 1:0-5 <- 1:0-5', + 'input.css: 2:2-13 <- 2:2-13', + 'input.css: 3:2-13 <- 3:9-20', + 'input.css: 4:2-10 <- 3:21-38', + 'input.css: 5:4-26 <- 3:21-38', + 'input.css: 6:6-17 <- 3:21-38', + 'input.css: 9:2-33 <- 4:9-18', + 'input.css: 10:2-12 <- 5:2-12', + ]) +}) + +test('license comments preserve source locations', async ({ expect }) => { + let { sources, annotations } = await run({ + input: `/*! some comment */`, + }) + + expect(sources).toEqual(['input.css']) + + expect(annotations).toEqual([ + // + 'input.css: 1:0-19 <- 1:0-19', + ]) +}) + +test('license comments with new lines preserve source locations', async ({ expect }) => { + let { sources, annotations, css } = await run({ + input: `/*! some \n comment */`, + }) + + expect(sources).toEqual(['input.css']) + + expect(annotations).toEqual([ + // + 'input.css: 1:0 <- 1:0-2:0', + 'input.css: 2:11 <- 2:11', + ]) +}) diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts new file mode 100644 index 000000000000..1e048cdd00e3 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/source-map.ts @@ -0,0 +1,194 @@ +import { walk, type AstNode } from '../ast' +import { DefaultMap } from '../utils/default-map' +import { createLineTable, type LineTable, type Position } from './line-table' +import type { Source } from './source' + +// https://tc39.es/ecma426/#sec-original-position-record-type +export interface OriginalPosition extends Position { + source: DecodedSource +} + +/** + * A "decoded" sourcemap + * + * @see https://tc39.es/ecma426/#decoded-source-map-record + */ +export interface DecodedSourceMap { + file: string | null + sources: DecodedSource[] + mappings: DecodedMapping[] +} + +/** + * A "decoded" source + * + * @see https://tc39.es/ecma426/#decoded-source-record + */ +export interface DecodedSource { + url: string | null + content: string | null + ignore: boolean +} + +/** + * A "decoded" mapping + * + * @see https://tc39.es/ecma426/#decoded-mapping-record + */ +export interface DecodedMapping { + // https://tc39.es/ecma426/#sec-original-position-record-type + originalPosition: OriginalPosition | null + + // https://tc39.es/ecma426/#sec-position-record-type + generatedPosition: Position + + name: string | null +} + +/** + * Build a source map from the given AST. + * + * Our AST is build from flat CSS strings but there are many because we handle + * `@import`. This means that different nodes can have a different source. + * + * Instead of taking an input source map, we take the input CSS string we were + * originally given, as well as the source text for any imported files, and + * use that to generate a source map. + * + * We then require the use of other tools that can translate one or more + * "input" source maps into a final output source map. For example, + * `@ampproject/remapping` can be used to handle this. + * + * This also ensures that tools that expect "local" source maps are able to + * consume the source map we generate. + * + * The source map type we generate may be a bit different from "raw" source maps + * that the `source-map-js` package uses. It's a "decoded" source map that is + * represented by an object graph. It's identical to "decoded" source map from + * the ECMA-426 spec for source maps. + * + * Note that the spec itself is still evolving which means our implementation + * may need to evolve to match it. + * + * This can easily be converted to a "raw" source map by any tool that needs to. + **/ +export function createSourceMap({ ast }: { ast: AstNode[] }) { + // Compute line tables for both the original and generated source lazily so we + // don't have to do it during parsing or printing. + let lineTables = new DefaultMap((src) => createLineTable(src.code)) + let sourceTable = new DefaultMap((src) => ({ + url: src.file, + content: src.code, + ignore: false, + })) + + // Convert each mapping to a set of positions + let map: DecodedSourceMap = { + file: null, + sources: [], + mappings: [], + } + + // Get all the indexes from the mappings + function add(node: AstNode) { + if (!node.src || !node.dst) return + + let originalSource = sourceTable.get(node.src[0]) + if (!originalSource.content) return + + let originalTable = lineTables.get(node.src[0]) + let generatedTable = lineTables.get(node.dst[0]) + + let originalSlice = originalSource.content.slice(node.src[1], node.src[2]) + + // Source maps only encode single locations — not multi-line ranges + // So to properly emulate this we'll scan the original text for multiple + // lines and create mappings for each of those lines that point to the + // destination node (whether it spans multiple lines or not) + // + // This is not 100% accurate if both the source and destination preserve + // their newlines but this only happens in the case of custom properties + // + // This is _good enough_ + let offset = 0 + for (let line of originalSlice.split('\n')) { + if (line.trim() !== '') { + let originalStart = originalTable.find(node.src[1] + offset) + let generatedStart = generatedTable.find(node.dst[1]) + + map.mappings.push({ + name: null, + originalPosition: { + source: originalSource, + ...originalStart, + }, + generatedPosition: generatedStart, + }) + } + + offset += line.length + offset += 1 + } + + let originalEnd = originalTable.find(node.src[2]) + let generatedEnd = generatedTable.find(node.dst[2]) + + map.mappings.push({ + name: null, + originalPosition: { + source: originalSource, + ...originalEnd, + }, + generatedPosition: generatedEnd, + }) + } + + walk(ast, add) + + // Populate + for (let source of lineTables.keys()) { + map.sources.push(sourceTable.get(source)) + } + + // Sort the mappings in ascending order + map.mappings.sort((a, b) => { + return ( + a.generatedPosition.line - b.generatedPosition.line || + a.generatedPosition.column - b.generatedPosition.column || + (a.originalPosition?.line ?? 0) - (b.originalPosition?.line ?? 0) || + (a.originalPosition?.column ?? 0) - (b.originalPosition?.column ?? 0) + ) + }) + + return map +} + +export function createTranslationMap({ + original, + generated, +}: { + original: string + generated: string +}) { + // Compute line tables for both the original and generated source lazily so we + // don't have to do it during parsing or printing. + let originalTable = createLineTable(original) + let generatedTable = createLineTable(generated) + + type Translation = [Position, Position, Position | null, Position | null] + + return (node: AstNode) => { + if (!node.src) return [] + + let translations: Translation[] = [] + + translations.push([ + originalTable.find(node.src[1]), + originalTable.find(node.src[2]), + node.dst ? generatedTable.find(node.dst[1]) : null, + node.dst ? generatedTable.find(node.dst[2]) : null, + ]) + + return translations + } +} diff --git a/packages/tailwindcss/src/source-maps/translation-map.test.ts b/packages/tailwindcss/src/source-maps/translation-map.test.ts new file mode 100644 index 000000000000..ad0a9f435154 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/translation-map.test.ts @@ -0,0 +1,279 @@ +import dedent from 'dedent' +import { assert, expect, test } from 'vitest' +import { toCss, type AstNode } from '../ast' +import * as CSS from '../css-parser' +import { createTranslationMap } from './source-map' + +async function analyze(input: string) { + let ast = CSS.parse(input, { from: 'input.css' }) + let css = toCss(ast, true) + let translate = createTranslationMap({ + original: input, + generated: css, + }) + + function format(node: AstNode) { + let lines: string[] = [] + + for (let [oStart, oEnd, gStart, gEnd] of translate(node)) { + let src = `${oStart.line}:${oStart.column}-${oEnd.line}:${oEnd.column}` + + let dst = '(none)' + + if (gStart && gEnd) { + dst = `${gStart.line}:${gStart.column}-${gEnd.line}:${gEnd.column}` + } + + lines.push(`${dst} <- ${src}`) + } + + return lines + } + + return { ast, css, format } +} + +test('comment, single line', async () => { + let { ast, css, format } = await analyze(`/*! foo */`) + + assert(ast[0].kind === 'comment') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:10 <- 1:0-1:10", + ] + `) + + expect(css).toMatchInlineSnapshot(` + "/*! foo */ + " + `) +}) + +test('comment, multi line', async () => { + let { ast, css, format } = await analyze(`/*! foo \n bar */`) + + assert(ast[0].kind === 'comment') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-2:7 <- 1:0-2:7", + ] + `) + + expect(css).toMatchInlineSnapshot(` + "/*! foo + bar */ + " + `) +}) + +test('declaration, normal property, single line', async () => { + let { ast, css, format } = await analyze(`.foo { color: red; }`) + + assert(ast[0].kind === 'rule') + assert(ast[0].nodes[0].kind === 'declaration') + expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` + [ + "2:2-2:12 <- 1:7-1:17", + ] + `) + + expect(css).toMatchInlineSnapshot(` + ".foo { + color: red; + } + " + `) +}) + +test('declaration, normal property, multi line', async () => { + // Works, no changes needed + let { ast, css, format } = await analyze(dedent` + .foo { + grid-template-areas: + "a b c" + "d e f" + "g h i"; + } + `) + + assert(ast[0].kind === 'rule') + assert(ast[0].nodes[0].kind === 'declaration') + expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` + [ + "2:2-2:46 <- 2:2-5:11", + ] + `) + + expect(css).toMatchInlineSnapshot(` + ".foo { + grid-template-areas: "a b c" "d e f" "g h i"; + } + " + `) +}) + +test('declaration, custom property, single line', async () => { + let { ast, css, format } = await analyze(`.foo { --foo: bar; }`) + + assert(ast[0].kind === 'rule') + assert(ast[0].nodes[0].kind === 'declaration') + expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` + [ + "2:2-2:12 <- 1:7-1:17", + ] + `) + + expect(css).toMatchInlineSnapshot(` + ".foo { + --foo: bar; + } + " + `) +}) + +test('declaration, custom property, multi line', async () => { + let { ast, css, format } = await analyze(dedent` + .foo { + --foo: bar\nbaz; + } + `) + + assert(ast[0].kind === 'rule') + assert(ast[0].nodes[0].kind === 'declaration') + expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` + [ + "2:2-3:3 <- 2:2-3:3", + ] + `) + + expect(css).toMatchInlineSnapshot(` + ".foo { + --foo: bar + baz; + } + " + `) +}) + +test('at rules, bodyless, single line', async () => { + // This intentionally has extra spaces + let { ast, css, format } = await analyze(`@layer foo, bar;`) + + assert(ast[0].kind === 'at-rule') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:15 <- 1:0-1:19", + ] + `) + + expect(css).toMatchInlineSnapshot(` + "@layer foo, bar; + " + `) +}) + +test('at rules, bodyless, multi line', async () => { + let { ast, css, format } = await analyze(dedent` + @layer + foo, + bar + ; + `) + + assert(ast[0].kind === 'at-rule') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:15 <- 1:0-4:0", + ] + `) + + expect(css).toMatchInlineSnapshot(` + "@layer foo, bar; + " + `) +}) + +test('at rules, body, single line', async () => { + let { ast, css, format } = await analyze(`@layer foo { color: red; }`) + + assert(ast[0].kind === 'at-rule') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:11 <- 1:0-1:11", + ] + `) + + expect(css).toMatchInlineSnapshot(` + "@layer foo { + color: red; + } + " + `) +}) + +test('at rules, body, multi line', async () => { + let { ast, css, format } = await analyze(dedent` + @layer + foo + { + color: baz; + } + `) + + assert(ast[0].kind === 'at-rule') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:11 <- 1:0-3:0", + ] + `) + + expect(css).toMatchInlineSnapshot(` + "@layer foo { + color: baz; + } + " + `) +}) + +test('style rules, body, single line', async () => { + let { ast, css, format } = await analyze(`.foo:is(.bar) { color: red; }`) + + assert(ast[0].kind === 'rule') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:14 <- 1:0-1:14", + ] + `) + + expect(css).toMatchInlineSnapshot(` + ".foo:is(.bar) { + color: red; + } + " + `) +}) + +test('style rules, body, multi line', async () => { + // Works, no changes needed + let { ast, css, format } = await analyze(dedent` + .foo:is( + .bar + ) { + color: red; + } + `) + + assert(ast[0].kind === 'rule') + expect(format(ast[0])).toMatchInlineSnapshot(` + [ + "1:0-1:16 <- 1:0-3:2", + ] + `) + + expect(css).toMatchInlineSnapshot(` + ".foo:is( .bar ) { + color: red; + } + " + `) +}) From b191165f44112a6df5b7c367ea9d9c4d1a4d6ebe Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 19:18:59 -0400 Subject: [PATCH 19/41] Prep for source maps in build tools --- .../src/commands/build/index.ts | 6 +- packages/@tailwindcss-node/package.json | 1 + packages/@tailwindcss-node/src/index.cts | 1 + packages/@tailwindcss-node/src/index.ts | 1 + packages/@tailwindcss-node/src/optimize.ts | 36 ++++++++--- packages/@tailwindcss-node/src/source-maps.ts | 59 +++++++++++++++++++ packages/@tailwindcss-postcss/src/index.ts | 4 +- packages/@tailwindcss-vite/src/index.ts | 4 +- packages/tailwindcss/src/test-utils/run.ts | 8 ++- 9 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 packages/@tailwindcss-node/src/source-maps.ts diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 409c398e9939..5e8ecdbae5d2 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -127,14 +127,14 @@ export async function handle(args: Result>) { if (args['--minify'] || args['--optimize']) { if (css !== previous.css) { DEBUG && I.start('Optimize CSS') - let optimizedCss = optimize(css, { + let optimized = optimize(css, { file: args['--input'] ?? 'input.css', minify: args['--minify'] ?? false, }) DEBUG && I.end('Optimize CSS') previous.css = css - previous.optimizedCss = optimizedCss - output = optimizedCss + previous.optimizedCss = optimized.code + output = optimized.code } else { output = previous.optimizedCss } diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 0844c232416f..5fc6727eea21 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -40,6 +40,7 @@ "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "catalog:", + "source-map-js": "^1.2.1", "tailwindcss": "workspace:*" } } diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts index b9284a0c8628..4bca0a5e11de 100644 --- a/packages/@tailwindcss-node/src/index.cts +++ b/packages/@tailwindcss-node/src/index.cts @@ -5,6 +5,7 @@ export * from './compile' export * from './instrumentation' export * from './normalize-path' export * from './optimize' +export * from './source-maps' export { env } // In Bun, ESM modules will also populate `require.cache`, so the module hook is diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index 83d603429388..a5e7bf5b4bc3 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -5,6 +5,7 @@ export * from './compile' export * from './instrumentation' export * from './normalize-path' export * from './optimize' +export * from './source-maps' export { env } // In Bun, ESM modules will also populate `require.cache`, so the module hook is diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts index bf7ab674a4f8..168b3f832d5d 100644 --- a/packages/@tailwindcss-node/src/optimize.ts +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -1,13 +1,29 @@ import { Features, transform } from 'lightningcss' +export interface OptimizeOptions { + /** + * The file being transformed + */ + file?: string + + /** + * Enabled minified output + */ + minify?: boolean +} + +export interface TransformResult { + code: string +} + export function optimize( input: string, - { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, -): string { + { file = 'input.css', minify = false }: OptimizeOptions = {}, +): TransformResult { function optimize(code: Buffer | Uint8Array) { return transform({ filename: file, - code: code as any, + code, minify, sourceMap: false, drafts: { @@ -25,16 +41,18 @@ export function optimize( chrome: 111 << 16, }, errorRecovery: true, - }).code + }) } // Running Lightning CSS twice to ensure that adjacent rules are merged after // nesting is applied. This creates a more optimized output. - let out = optimize(optimize(Buffer.from(input))).toString() + let result = optimize(Buffer.from(input)) + result = optimize(result.code) - // Work around an issue where the media query range syntax transpilation - // generates code that is invalid with `@media` queries level 3. - out = out.replaceAll('@media not (', '@media not all and (') + let code = result.code.toString() + code = code.replaceAll('@media not (', '@media not all and (') - return out + return { + code, + } } diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts new file mode 100644 index 000000000000..88ba54e2f258 --- /dev/null +++ b/packages/@tailwindcss-node/src/source-maps.ts @@ -0,0 +1,59 @@ +import { SourceMapGenerator } from 'source-map-js' +import type { DecodedSource, DecodedSourceMap } from '../../tailwindcss/src/source-maps/source-map' +import { DefaultMap } from '../../tailwindcss/src/utils/default-map' + +export type { DecodedSource, DecodedSourceMap } +export interface SourceMap { + readonly raw: string + readonly inline: string +} + +function serializeSourceMap(map: DecodedSourceMap): string { + let generator = new SourceMapGenerator() + + let id = 1 + let sourceTable = new DefaultMap< + DecodedSource | null, + { + url: string + content: string + } + >((src) => { + return { + url: src?.url ?? ``, + content: src?.content ?? '', + } + }) + + for (let mapping of map.mappings) { + let original = sourceTable.get(mapping.originalPosition?.source ?? null) + + generator.addMapping({ + generated: mapping.generatedPosition, + original: mapping.originalPosition, + source: original.url, + name: mapping.name, + }) + + generator.setSourceContent(original.url, original.content) + } + + return generator.toString() +} + +export function toSourceMap(map: DecodedSourceMap | string): SourceMap { + let raw = typeof map === 'string' ? map : serializeSourceMap(map) + + return { + raw, + get inline() { + let tmp = '' + + tmp += '/*# sourceMappingURL=data:application/json;base64,' + tmp += Buffer.from(raw, 'utf-8').toString('base64') + tmp += ' */\n' + + return tmp + }, + } +} diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index b06c15cb237b..79e51d2aa574 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -282,13 +282,13 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { DEBUG && I.end('AST -> CSS') DEBUG && I.start('Lightning CSS') - let ast = optimizeCss(css, { + let optimized = optimizeCss(css, { minify: typeof optimize === 'object' ? optimize.minify : true, }) DEBUG && I.end('Lightning CSS') DEBUG && I.start('CSS -> PostCSS AST') - context.optimizedPostCssAst = postcss.parse(ast, result.opts) + context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts) DEBUG && I.end('CSS -> PostCSS AST') DEBUG && I.end('Optimization') diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index a62929069b13..0a053291a162 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -106,7 +106,9 @@ export default function tailwindcss(): Plugin[] { DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') - result.code = optimize(result.code, { minify }) + result = optimize(result.code, { + minify, + }) DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') return result diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index 3ecf1a10fd36..5b4b1ca993c8 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -7,12 +7,14 @@ export async function compileCss( options: Parameters[1] = {}, ) { let { build } = await compile(css, options) - return optimize(build(candidates)).trim() + return optimize(build(candidates)).code.trim() } export async function run(candidates: string[]) { let { build } = await compile('@tailwind utilities;') - return optimize(build(candidates)).trim() + return optimize(build(candidates)).code.trim() } -export const optimizeCss = optimize +export function optimizeCss(input: string) { + return optimize(input).code +} From 2302668b48f109aa70a956b8beeb0556ec314703 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 18:54:43 -0400 Subject: [PATCH 20/41] Add integration test helpers for source maps --- integrations/package.json | 3 +- integrations/utils.ts | 87 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/integrations/package.json b/integrations/package.json index 62f458c77480..a3c1025f916e 100644 --- a/integrations/package.json +++ b/integrations/package.json @@ -4,6 +4,7 @@ "private": true, "devDependencies": { "dedent": "1.5.3", - "fast-glob": "^3.3.3" + "fast-glob": "^3.3.3", + "source-map-js": "^1.2.1" } } diff --git a/integrations/utils.ts b/integrations/utils.ts index 8affff3b4a83..f13e8581eafb 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -5,7 +5,9 @@ import fs from 'node:fs/promises' import { platform, tmpdir } from 'node:os' import path from 'node:path' import { stripVTControlCharacters } from 'node:util' +import { RawSourceMap, SourceMapConsumer } from 'source-map-js' import { test as defaultTest, type ExpectStatic } from 'vitest' +import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table' import { escape } from '../packages/tailwindcss/src/utils/escape' const REPO_ROOT = path.join(__dirname, '..') @@ -42,6 +44,7 @@ interface TestContext { expect: ExpectStatic exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise spawn(command: string, options?: ChildProcessOptions): Promise + parseSourceMap(opts: string | SourceMapOptions): SourceMap fs: { write(filePath: string, content: string, encoding?: BufferEncoding): Promise create(filePaths: string[]): Promise @@ -104,6 +107,7 @@ export function test( let context = { root, expect: options.expect, + parseSourceMap, async exec( command: string, childProcessOptions: ChildProcessOptions = {}, @@ -591,7 +595,9 @@ export async function fetchStyles(base: string, path = '/'): Promise { } return stylesheets.reduce((acc, css) => { - return acc + '\n' + css + if (acc.length > 0) acc += '\n' + acc += css + return acc }, '') } @@ -603,3 +609,82 @@ async function gracefullyRemove(dir: string) { }) } } + +const SOURCE_MAP_COMMENT = /^\/\*# sourceMappingURL=data:application\/json;base64,(.*) \*\/$/ + +export interface SourceMap { + at( + line: number, + column: number, + ): { + source: string | null + original: string + generated: string + } +} + +interface SourceMapOptions { + /** + * A raw source map + * + * This may be a string or an object. Strings will be decoded. + */ + map: string | object + + /** + * The content of the generated file the source map is for + */ + content: string + + /** + * The encoding of the source map + * + * Can be used to decode a base64 map (e.g. an inline source map URI) + */ + encoding?: BufferEncoding +} + +function parseSourceMap(opts: string | SourceMapOptions): SourceMap { + if (typeof opts === 'string') { + let lines = opts.trimEnd().split('\n') + let comment = lines.at(-1) ?? '' + let map = String(comment).match(SOURCE_MAP_COMMENT)?.[1] ?? null + if (!map) throw new Error('No source map comment found') + + return parseSourceMap({ + map, + content: lines.slice(0, -1).join('\n'), + encoding: 'base64', + }) + } + + let rawMap: RawSourceMap + let content = opts.content + + if (typeof opts.map === 'object') { + rawMap = opts.map as RawSourceMap + } else { + rawMap = JSON.parse(Buffer.from(opts.map, opts.encoding ?? 'utf-8').toString()) + } + + let map = new SourceMapConsumer(rawMap) + let generatedTable = createLineTable(content) + + return { + at(line: number, column: number) { + let pos = map.originalPositionFor({ line, column }) + let source = pos.source ? map.sourceContentFor(pos.source) : null + let originalTable = createLineTable(source ?? '') + let originalOffset = originalTable.findOffset(pos) + let generatedOffset = generatedTable.findOffset({ line, column }) + + return { + source: pos.source, + original: source + ? source.slice(originalOffset, originalOffset + 10).trim() + '...' + : '(none)', + generated: content.slice(generatedOffset, generatedOffset + 10).trim() + '...', + } + }, + } +} From acf5f3182efc0a00ae7fff097c220c301a544315 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 19:13:42 -0400 Subject: [PATCH 21/41] Update source maps when optimizing output CSS --- packages/@tailwindcss-node/package.json | 2 + packages/@tailwindcss-node/src/optimize.ts | 44 +++++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 5fc6727eea21..a7c867013202 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -37,9 +37,11 @@ } }, "dependencies": { + "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "catalog:", + "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "workspace:*" } diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts index 168b3f832d5d..9ebffae98508 100644 --- a/packages/@tailwindcss-node/src/optimize.ts +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -1,4 +1,6 @@ +import remapping from '@ampproject/remapping' import { Features, transform } from 'lightningcss' +import MagicString from 'magic-string' export interface OptimizeOptions { /** @@ -10,22 +12,31 @@ export interface OptimizeOptions { * Enabled minified output */ minify?: boolean + + /** + * The output source map before optimization + * + * If omitted an resulting source map will not be available + */ + map?: string } export interface TransformResult { code: string + map: string | undefined } export function optimize( input: string, - { file = 'input.css', minify = false }: OptimizeOptions = {}, + { file = 'input.css', minify = false, map }: OptimizeOptions = {}, ): TransformResult { - function optimize(code: Buffer | Uint8Array) { + function optimize(code: Buffer | Uint8Array, map: string | undefined) { return transform({ filename: file, code, minify, - sourceMap: false, + sourceMap: typeof map !== 'undefined', + inputSourceMap: map, drafts: { customMedia: true, }, @@ -46,13 +57,34 @@ export function optimize( // Running Lightning CSS twice to ensure that adjacent rules are merged after // nesting is applied. This creates a more optimized output. - let result = optimize(Buffer.from(input)) - result = optimize(result.code) + let result = optimize(Buffer.from(input), map) + map = result.map?.toString() + + result = optimize(result.code, map) + map = result.map?.toString() let code = result.code.toString() - code = code.replaceAll('@media not (', '@media not all and (') + + // Work around an issue where the media query range syntax transpilation + // generates code that is invalid with `@media` queries level 3. + let magic = new MagicString(code) + magic.replaceAll('@media not (', '@media not all and (') + + // We have to use a source-map-preserving method of replacing the content + // which requires the use of Magic String + remapping(…) to make sure + // the resulting map is correct + if (map !== undefined && magic.hasChanged()) { + let magicMap = magic.generateMap({ source: 'original', hires: 'boundary' }).toString() + + let remapped = remapping([magicMap, map], () => null) + + map = remapped.toString() + } + + code = magic.toString() return { code, + map, } } From 2f25e3ec3aa65a0c94cfc3062e2fd5ff0a3c7972 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 19:15:47 -0400 Subject: [PATCH 22/41] Add source map support to the CLI --- integrations/cli/index.test.ts | 194 +++++++++++++++++- .../src/commands/build/index.ts | 79 ++++++- 2 files changed, 268 insertions(+), 5 deletions(-) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index d30f6e24bb37..22d3a9c6aea0 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -23,7 +23,7 @@ describe.each([ 'Standalone CLI', path.resolve(__dirname, `../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`), ], -])('%s', (_, command) => { +])('%s', (kind, command) => { test( 'production build', { @@ -628,6 +628,198 @@ describe.each([ ]) }, ) + + test( + 'production build + inline source maps', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'ssrc/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + /* */ + `, + }, + }, + async ({ exec, expect, fs, parseSourceMap }) => { + await exec(`${command} --input src/index.css --output dist/out.css --map`) + + console.log(await fs.read('dist/out.css')) + + await fs.expectFileToContain('dist/out.css', [candidate`flex`]) + + // Make sure we can find a source map + let map = parseSourceMap(await fs.read('dist/out.css')) + + expect(map.at(1, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: + kind === 'CLI' + ? expect.stringContaining('node_modules/tailwindcss/utilities.css') + : expect.stringMatching(/\/utilities-\w+\.css$/), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: + kind === 'CLI' + ? expect.stringContaining('node_modules/tailwindcss/utilities.css') + : expect.stringMatching(/\/utilities-\w+\.css$/), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '}...', + }) + }, + ) + + test( + 'production build + separate source maps', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'ssrc/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + /* */ + `, + }, + }, + async ({ exec, expect, fs, parseSourceMap }) => { + await exec(`${command} --input src/index.css --output dist/out.css --map dist/out.css.map`) + + await fs.expectFileToContain('dist/out.css', [candidate`flex`]) + + // Make sure we can find a source map + let map = parseSourceMap({ + map: await fs.read('dist/out.css.map'), + content: await fs.read('dist/out.css'), + }) + + expect(map.at(1, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: + kind === 'CLI' + ? expect.stringContaining('node_modules/tailwindcss/utilities.css') + : expect.stringMatching(/\/utilities-\w+\.css$/), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: + kind === 'CLI' + ? expect.stringContaining('node_modules/tailwindcss/utilities.css') + : expect.stringMatching(/\/utilities-\w+\.css$/), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '}...', + }) + }, + ) + + // Skipped because Lightning CSS has a bug with source maps containing + // license comments and it breaks stuff. + test.skip( + 'production build + minify + source maps', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'ssrc/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + /* */ + `, + }, + }, + async ({ exec, expect, fs, parseSourceMap }) => { + await exec(`${command} --input src/index.css --output dist/out.css --minify --map`) + + await fs.expectFileToContain('dist/out.css', [candidate`flex`]) + + console.log(await fs.read('dist/out.css')) + + // Make sure we can find a source map + let map = parseSourceMap(await fs.read('dist/out.css')) + + expect(map.at(1, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: + kind === 'CLI' + ? expect.stringContaining('node_modules/tailwindcss/utilities.css') + : expect.stringMatching(/\/utilities-\w+\.css$/), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: + kind === 'CLI' + ? expect.stringContaining('node_modules/tailwindcss/utilities.css') + : expect.stringMatching(/\/utilities-\w+\.css$/), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '}...', + }) + }, + ) }) test( diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 5e8ecdbae5d2..936540dddca6 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -1,5 +1,12 @@ import watcher from '@parcel/watcher' -import { compile, env, Instrumentation, optimize } from '@tailwindcss/node' +import { + compile, + env, + Instrumentation, + optimize, + toSourceMap, + type SourceMap, +} from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner, type ChangedContent } from '@tailwindcss/oxide' import { existsSync, type Stats } from 'node:fs' @@ -52,6 +59,11 @@ export function options() { description: 'The current working directory', default: '.', }, + '--map': { + type: 'boolean | string', + description: 'Generate a source map', + default: false, + }, } satisfies Arg } @@ -105,6 +117,21 @@ export async function handle(args: Result>) { process.exit(1) } + // If the user passes `{bin} build --map -` then this likely means they want to output the map inline + // this is the default behavior of `{bin build} --map` to inform the user of that + if (args['--map'] === '-') { + eprintln(header()) + eprintln() + eprintln(`Use --map without a value to inline the source map`) + process.exit(1) + } + + // Resolve the map as an absolute path. If the output is true then we + // don't need to resolve it because it'll be an inline source map + if (args['--map'] && args['--map'] !== true) { + args['--map'] = path.resolve(base, args['--map']) + } + let start = process.hrtime.bigint() let input = args['--input'] @@ -120,7 +147,12 @@ export async function handle(args: Result>) { optimizedCss: '', } - async function write(css: string, args: Result>, I: Instrumentation) { + async function write( + css: string, + map: SourceMap | null, + args: Result>, + I: Instrumentation, + ) { let output = css // Optimize the output @@ -130,10 +162,14 @@ export async function handle(args: Result>) { let optimized = optimize(css, { file: args['--input'] ?? 'input.css', minify: args['--minify'] ?? false, + map: map?.raw ?? undefined, }) DEBUG && I.end('Optimize CSS') previous.css = css previous.optimizedCss = optimized.code + if (optimized.map) { + map = toSourceMap(optimized.map) + } output = optimized.code } else { output = previous.optimizedCss @@ -141,6 +177,18 @@ export async function handle(args: Result>) { } // Write the output + if (map) { + // Inline the source map + if (args['--map'] === true) { + output += `\n` + output += map.inline + } else if (typeof args['--map'] === 'string') { + DEBUG && I.start('Write source map') + await outputFile(args['--map'], map.raw) + DEBUG && I.end('Write source map') + } + } + DEBUG && I.start('Write output') if (args['--output'] && args['--output'] !== '-') { await outputFile(args['--output'], output) @@ -160,6 +208,7 @@ export async function handle(args: Result>) { async function createCompiler(css: string, I: Instrumentation) { DEBUG && I.start('Setup compiler') let compiler = await compile(css, { + from: args['--output'] ? (inputFilePath ?? 'stdin.css') : undefined, base: inputBasePath, onDependency(path) { fullRebuildPaths.push(path) @@ -231,6 +280,7 @@ export async function handle(args: Result>) { // Track the compiled CSS let compiledCss = '' + let compiledMap: SourceMap | null = null // Scan the entire `base` directory for full rebuilds. if (rebuildStrategy === 'full') { @@ -269,6 +319,12 @@ export async function handle(args: Result>) { DEBUG && I.start('Build CSS') compiledCss = compiler.build(candidates) DEBUG && I.end('Build CSS') + + if (args['--map']) { + DEBUG && I.start('Build Source Map') + compiledMap = compiler.buildSourceMap() as any + DEBUG && I.end('Build Source Map') + } } // Scan changed files only for incremental rebuilds. @@ -288,9 +344,15 @@ export async function handle(args: Result>) { DEBUG && I.start('Build CSS') compiledCss = compiler.build(newCandidates) DEBUG && I.end('Build CSS') + + if (args['--map']) { + DEBUG && I.start('Build Source Map') + compiledMap = compiler.buildSourceMap() as any + DEBUG && I.end('Build Source Map') + } } - await write(compiledCss, args, I) + await write(compiledCss, compiledMap, args, I) let end = process.hrtime.bigint() eprintln(`Done in ${formatDuration(end - start)}`) @@ -325,7 +387,16 @@ export async function handle(args: Result>) { DEBUG && I.start('Build CSS') let output = await handleError(() => compiler.build(candidates)) DEBUG && I.end('Build CSS') - await write(output, args, I) + + let map: SourceMap | null = null + + if (args['--map']) { + DEBUG && I.start('Build Source Map') + map = await handleError(() => toSourceMap(compiler.buildSourceMap())) + DEBUG && I.end('Build Source Map') + } + + await write(output, map, args, I) let end = process.hrtime.bigint() eprintln(header()) From bb78c597704935304b7d7633fdbaab6a25e9052a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 29 Apr 2025 19:16:49 -0400 Subject: [PATCH 23/41] Add source map support to Vite --- integrations/vite/source-maps.test.ts | 96 +++++++++++++++++++++++++ packages/@tailwindcss-vite/src/index.ts | 22 +++++- 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 integrations/vite/source-maps.test.ts diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts new file mode 100644 index 000000000000..299cd11676be --- /dev/null +++ b/integrations/vite/source-maps.test.ts @@ -0,0 +1,96 @@ +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' + +test( + `dev build`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "lightningcss": "^1.26.0", + "vite": "^6" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [tailwindcss()], + css: { + devSourcemap: true, + }, + build: { + sourcemap: true, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + /* */ + `, + }, + }, + async ({ fs, spawn, expect, parseSourceMap }) => { + // Source maps only work in development mode in Vite + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + let styles = await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + + // Wait until we have the right CSS + expect(styles).toContain(candidate`flex`) + + return styles + }) + + // Make sure we can find a source map + let map = parseSourceMap(styles) + + expect(map.at(1, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: expect.stringContaining('node_modules/tailwindcss/utilities.css'), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: expect.stringContaining('node_modules/tailwindcss/utilities.css'), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '}...', + }) + }, +) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 0a053291a162..00ba92db4c83 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -1,4 +1,12 @@ -import { compile, env, Features, Instrumentation, normalizePath, optimize } from '@tailwindcss/node' +import { + compile, + env, + Features, + Instrumentation, + normalizePath, + optimize, + toSourceMap, +} from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import fs from 'node:fs/promises' @@ -37,6 +45,9 @@ export default function tailwindcss(): Plugin[] { return new Root( id, config!.root, + // Currently, Vite only supports CSS source maps in development and they + // are off by default. Check to see if we need them or not. + config?.css.devSourcemap ?? false, customCssResolver, customJsResolver, ) @@ -108,6 +119,7 @@ export default function tailwindcss(): Plugin[] { DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') result = optimize(result.code, { minify, + map: result.map, }) DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') @@ -180,6 +192,7 @@ class Root { private id: string, private base: string, + private enableSourceMaps: boolean, private customCssResolver: (id: string, base: string) => Promise, private customJsResolver: (id: string, base: string) => Promise, ) {} @@ -193,6 +206,7 @@ class Root { ): Promise< | { code: string + map: string | undefined } | false > { @@ -227,6 +241,7 @@ class Root { DEBUG && I.start('Setup compiler') let addBuildDependenciesPromises: Promise[] = [] this.compiler = await compile(content, { + from: this.enableSourceMaps ? this.id : undefined, base: inputBase, shouldRewriteUrls: true, onDependency: (path) => { @@ -328,8 +343,13 @@ class Root { let code = this.compiler.build([...this.candidates]) DEBUG && I.end('Build CSS') + DEBUG && I.start('Build Source Map') + let map = this.enableSourceMaps ? toSourceMap(this.compiler.buildSourceMap()).raw : undefined + DEBUG && I.end('Build Source Map') + return { code, + map, } } From 00358d0686e7783eb2da56c5b9c2a1661dc5ed0b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Apr 2025 13:29:46 -0400 Subject: [PATCH 24/41] Add source map support to PostCSS --- integrations/postcss/index.test.ts | 65 +++++++++++++++++++ packages/@tailwindcss-postcss/src/ast.ts | 72 ++++++++++++++++++++-- packages/@tailwindcss-postcss/src/index.ts | 1 + 3 files changed, 132 insertions(+), 6 deletions(-) diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index 7a263ba92d51..3e81195eef80 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -712,3 +712,68 @@ test( await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual('')) }, ) + +test( + 'dev mode + source maps', + { + fs: { + 'package.json': json` + { + "dependencies": { + "postcss": "^8", + "postcss-cli": "^11", + "tailwindcss": "workspace:^", + "@tailwindcss/postcss": "workspace:^" + } + } + `, + 'postcss.config.js': js` + module.exports = { + map: { inline: true }, + plugins: { + '@tailwindcss/postcss': {}, + }, + } + `, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + @source not inline("inline"); + /* */ + `, + }, + }, + async ({ fs, exec, expect, parseSourceMap }) => { + await exec('pnpm postcss src/index.css --output dist/out.css') + + await fs.expectFileToContain('dist/out.css', [candidate`flex`]) + + let map = parseSourceMap(await fs.read('dist/out.css')) + + expect(map.at(1, 0)).toMatchObject({ + source: '', + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: expect.stringContaining('node_modules/tailwindcss/utilities.css'), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: expect.stringContaining('node_modules/tailwindcss/utilities.css'), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: expect.stringContaining('node_modules/tailwindcss/utilities.css'), + original: ';...', + generated: '}...', + }) + }, +) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts index 201dd4bf8ac9..a2e1535e8cec 100644 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -1,17 +1,54 @@ import postcss, { + Input, 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' +import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table' +import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source' +import { DefaultMap } from '../../tailwindcss/src/utils/default-map' const EXCLAMATION_MARK = 0x21 export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot { + let inputMap = new DefaultMap((src) => { + return new Input(src.code, { + map: source?.input.map, + from: src.file ?? undefined, + }) + }) + + let lineTables = new DefaultMap((src) => createLineTable(src.code)) + let root = postcss.root() root.source = source + function toSource(loc: SourceLocation | undefined): PostcssSource | undefined { + // Use the fallback if this node has no location info in the AST + if (!loc) return + if (!loc[0]) return + + let table = lineTables.get(loc[0]) + let start = table.find(loc[1]) + let end = table.find(loc[2]) + + return { + input: inputMap.get(loc[0]), + start: { + line: start.line, + column: start.column + 1, + offset: loc[1], + }, + end: { + line: end.line, + column: end.column + 1, + offset: loc[2], + }, + } + } + function transform(node: AstNode, parent: PostCssContainerNode) { // Declaration if (node.kind === 'declaration') { @@ -20,14 +57,14 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef value: node.value ?? '', important: node.important, }) - astNode.source = source + astNode.source = toSource(node.src) parent.append(astNode) } // Rule else if (node.kind === 'rule') { let astNode = postcss.rule({ selector: node.selector }) - astNode.source = source + astNode.source = toSource(node.src) astNode.raws.semicolon = true parent.append(astNode) for (let child of node.nodes) { @@ -38,7 +75,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef // AtRule else if (node.kind === 'at-rule') { let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params }) - astNode.source = source + astNode.source = toSource(node.src) astNode.raws.semicolon = true parent.append(astNode) for (let child of node.nodes) { @@ -53,7 +90,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef // spaces. astNode.raws.left = '' astNode.raws.right = '' - astNode.source = source + astNode.source = toSource(node.src) parent.append(astNode) } @@ -75,18 +112,38 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef } export function postCssAstToCssAst(root: PostCssRoot): AstNode[] { + let inputMap = new DefaultMap((input) => ({ + file: input.file ?? input.id ?? null, + code: input.css, + })) + + function toSource(node: PostCssChildNode): SourceLocation | undefined { + let source = node.source + if (!source) return + + let input = source.input + if (!input) return + if (source.start === undefined) return + if (source.end === undefined) return + + return [inputMap.get(input), source.start.offset, source.end.offset] + } + function transform( node: PostCssChildNode, parent: Extract['nodes'], ) { // Declaration if (node.type === 'decl') { - parent.push(decl(node.prop, node.value, node.important)) + let astNode = decl(node.prop, node.value, node.important) + astNode.src = toSource(node) + parent.push(astNode) } // Rule else if (node.type === 'rule') { let astNode = rule(node.selector) + astNode.src = toSource(node) node.each((child) => transform(child, astNode.nodes)) parent.push(astNode) } @@ -94,6 +151,7 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] { // AtRule else if (node.type === 'atrule') { let astNode = atRule(`@${node.name}`, node.params) + astNode.src = toSource(node) node.each((child) => transform(child, astNode.nodes)) parent.push(astNode) } @@ -101,7 +159,9 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] { // Comment else if (node.type === 'comment') { if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return - parent.push(comment(node.text)) + let astNode = comment(node.text) + astNode.src = toSource(node) + parent.push(astNode) } // Unknown diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 79e51d2aa574..8a37f542c94f 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -121,6 +121,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { DEBUG && I.start('Create compiler') let compiler = await compileAst(ast, { + from: result.opts.from, base: inputBasePath, shouldRewriteUrls: true, onDependency: (path) => context.fullRebuildPaths.push(path), From 502bda71b82a5e3db8e84fd93de7147e8233f455 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 16 Apr 2025 17:39:57 -0400 Subject: [PATCH 25/41] Update lockfile --- pnpm-lock.yaml | 66 +++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bd72710aa0e..e381f6f6ab5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,6 +188,9 @@ importers: fast-glob: specifier: ^3.3.3 version: 3.3.3 + source-map-js: + specifier: ^1.2.1 + version: 1.2.1 packages/@tailwindcss-browser: devDependencies: @@ -227,6 +230,9 @@ importers: packages/@tailwindcss-node: dependencies: + '@ampproject/remapping': + specifier: ^2.3.0 + version: 2.3.0 enhanced-resolve: specifier: ^5.18.1 version: 5.18.1 @@ -236,6 +242,12 @@ importers: lightningcss: specifier: 'catalog:' version: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) + magic-string: + specifier: ^0.30.17 + version: 0.30.17 + source-map-js: + specifier: ^1.2.1 + version: 1.2.1 tailwindcss: specifier: workspace:* version: link:../tailwindcss @@ -437,6 +449,9 @@ importers: packages/tailwindcss: devDependencies: + '@ampproject/remapping': + specifier: ^2.3.0 + version: 2.3.0 '@tailwindcss/oxide': specifier: workspace:^ version: link:../../crates/node @@ -449,6 +464,12 @@ importers: lightningcss: specifier: 'catalog:' version: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm) + magic-string: + specifier: ^0.30.17 + version: 0.30.17 + source-map-js: + specifier: ^1.2.1 + version: 1.2.1 playgrounds/nextjs: dependencies: @@ -2068,7 +2089,6 @@ packages: '@parcel/watcher-darwin-arm64@2.5.1': resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.5.0': @@ -2080,7 +2100,6 @@ packages: '@parcel/watcher-darwin-x64@2.5.1': resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.5.0': @@ -2128,7 +2147,6 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.5.0': @@ -2140,7 +2158,6 @@ packages: '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.5.0': @@ -2152,7 +2169,6 @@ packages: '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.5.0': @@ -2164,7 +2180,6 @@ packages: '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-wasm@2.5.0': @@ -2206,7 +2221,6 @@ packages: '@parcel/watcher-win32-x64@2.5.1': resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [win32] '@parcel/watcher@2.5.0': @@ -2621,7 +2635,6 @@ packages: bun@1.2.11: resolution: {integrity: sha512-9brVfsp6/TYVsE3lCl1MUxoyKhvljqyL1MNPErgwsOaS9g4Gzi2nY+W5WtRAXGzLrgz5jzsoGHHwyH/rTeRCIg==} - cpu: [arm64, x64, aarch64] os: [darwin, linux, win32] hasBin: true @@ -3480,13 +3493,11 @@ packages: lightningcss-darwin-arm64@1.29.2: resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.29.2: resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.29.2: @@ -3504,25 +3515,21 @@ packages: lightningcss-linux-arm64-gnu@1.29.2: resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [linux] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [linux] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [linux] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [linux] lightningcss-win32-arm64-msvc@1.29.2: @@ -3534,7 +3541,6 @@ packages: lightningcss-win32-x64-msvc@1.29.2: resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [win32] lightningcss@1.29.2: @@ -3592,8 +3598,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4113,10 +4119,6 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -6156,7 +6158,7 @@ snapshots: '@vitest/snapshot@2.0.5': dependencies: '@vitest/pretty-format': 2.0.5 - magic-string: 0.30.11 + magic-string: 0.30.17 pathe: 1.1.2 '@vitest/spy@2.0.5': @@ -6817,7 +6819,7 @@ snapshots: debug: 4.4.0 enhanced-resolve: 5.18.1 eslint: 9.25.1(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)) fast-glob: 3.3.3 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -6836,7 +6838,7 @@ snapshots: debug: 4.4.0 enhanced-resolve: 5.18.1 eslint: 9.25.1(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)) fast-glob: 3.3.3 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -6849,7 +6851,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -6860,7 +6862,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -6882,7 +6884,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.25.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -6911,7 +6913,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.25.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7560,7 +7562,7 @@ snapshots: dependencies: yallist: 3.1.1 - magic-string@0.30.11: + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -7865,7 +7867,7 @@ snapshots: dependencies: nanoid: 3.3.7 picocolors: 1.1.1 - source-map-js: 1.2.0 + source-map-js: 1.2.1 postcss@8.4.47: dependencies: @@ -8087,8 +8089,6 @@ snapshots: slash@5.1.0: {} - source-map-js@1.2.0: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -8523,7 +8523,7 @@ snapshots: chai: 5.1.1 debug: 4.3.6 execa: 8.0.1 - magic-string: 0.30.11 + magic-string: 0.30.17 pathe: 1.1.2 std-env: 3.7.0 tinybench: 2.9.0 From c9cb8a7debb0c057574946534dd0ddac3143c14c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 5 May 2025 17:56:23 -0400 Subject: [PATCH 26/41] Fix UI tests --- packages/tailwindcss/tests/ui.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index a4efc6468ac3..0c3534efd748 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -2216,7 +2216,7 @@ async function render(page: Page, content: string, extraCss: string = '') { let scanner = new Scanner({}) let candidates = scanner.scanFiles([{ content, extension: 'html' }]) - let styles = optimize(build(candidates)) + let { code: styles } = optimize(build(candidates)) content = `${content}` await page.setContent(content) From 65e2c176272591dc8aaee2953b0772db0747bfdb Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 10:36:15 -0400 Subject: [PATCH 27/41] Optimize `LineTable#find` implementation --- packages/tailwindcss/src/source-maps/line-table.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/source-maps/line-table.ts b/packages/tailwindcss/src/source-maps/line-table.ts index d42be8055206..e34ec8fd4680 100644 --- a/packages/tailwindcss/src/source-maps/line-table.ts +++ b/packages/tailwindcss/src/source-maps/line-table.ts @@ -62,8 +62,8 @@ export function createLineTable(source: string): LineTable { let line = 0 let count = table.length while (count > 0) { - // `| 0` causes integer division - let mid = (count / 2) | 0 + // `| 0` forces integer bytecode generation + let mid = (count | 0) >> 1 let i = line + mid if (table[i] <= offset) { line = i + 1 From 5e2fd98f601b24a19cae08f771d4ba1cccd1dbbb Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:14:07 -0400 Subject: [PATCH 28/41] =?UTF-8?q?Don=E2=80=99t=20double=20clone=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tailwindcss/src/apply.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 71cfc1e039ce..d0de38a3d78d 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -193,7 +193,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { candidateSrc[1] += 7 + candidateOffset candidateSrc[2] = candidateSrc[1] + candidate.length - return { node: structuredClone(node), src: candidateSrc } + return { node, src: candidateSrc } }) for (let { node, src } of details) { From 2e8d099475ea0c9e11383c09ed5c468b91dd5722 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:23:27 -0400 Subject: [PATCH 29/41] Move walk into map fn --- packages/tailwindcss/src/apply.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index d0de38a3d78d..6785b8759204 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -178,14 +178,21 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let src = node.src - let details = compiled.astNodes.map((node) => { + let candidateAst = compiled.astNodes.map((node) => { let candidate = compiled.nodeSorting.get(node)?.candidate let candidateOffset = candidate ? candidateOffsets[candidate] : undefined node = structuredClone(node) if (!src || !candidate || candidateOffset === undefined) { - return { node, src } + // While the original nodes may have come from an `@utility` we still + // want to replace the source because the `@apply` is ultimately the + // reason the node was emitted into the AST. + walk([node], (node) => { + node.src = src + }) + + return node } let candidateSrc: SourceLocation = [src[0], src[1], src[2]] @@ -193,19 +200,15 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { candidateSrc[1] += 7 + candidateOffset candidateSrc[2] = candidateSrc[1] + candidate.length - return { node, src: candidateSrc } - }) - - for (let { node, src } of details) { // While the original nodes may have come from an `@utility` we still // want to replace the source because the `@apply` is ultimately the // reason the node was emitted into the AST. walk([node], (node) => { - node.src = src + node.src = candidateSrc }) - } - let candidateAst = details.map((d) => d.node) + return node + }) // Collect the nodes to insert in place of the `@apply` rule. When a rule // was used, we want to insert its children instead of the rule because we From 78915dc156f527859cb27573bad2254d2c10f8e5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:25:29 -0400 Subject: [PATCH 30/41] Remove leftover logs --- integrations/cli/index.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 22d3a9c6aea0..5c5cd1b29036 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -653,8 +653,6 @@ describe.each([ async ({ exec, expect, fs, parseSourceMap }) => { await exec(`${command} --input src/index.css --output dist/out.css --map`) - console.log(await fs.read('dist/out.css')) - await fs.expectFileToContain('dist/out.css', [candidate`flex`]) // Make sure we can find a source map @@ -784,8 +782,6 @@ describe.each([ await fs.expectFileToContain('dist/out.css', [candidate`flex`]) - console.log(await fs.read('dist/out.css')) - // Make sure we can find a source map let map = parseSourceMap(await fs.read('dist/out.css')) From db5d97403e8ee4cf34357583cd08c777dac09a89 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:26:06 -0400 Subject: [PATCH 31/41] Fix typo --- packages/@tailwindcss-node/src/optimize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts index 9ebffae98508..4024d15b8e0a 100644 --- a/packages/@tailwindcss-node/src/optimize.ts +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -16,7 +16,7 @@ export interface OptimizeOptions { /** * The output source map before optimization * - * If omitted an resulting source map will not be available + * If omitted a resulting source map will not be available */ map?: string } From 8fdb8baaa11a9d31b15a27d236e33e44690763c4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:26:44 -0400 Subject: [PATCH 32/41] Increment unknown source ID when generating maps --- packages/@tailwindcss-node/src/source-maps.ts | 2 +- packages/tailwindcss/src/source-maps/source-map.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts index 88ba54e2f258..3f02efb24c00 100644 --- a/packages/@tailwindcss-node/src/source-maps.ts +++ b/packages/@tailwindcss-node/src/source-maps.ts @@ -20,7 +20,7 @@ function serializeSourceMap(map: DecodedSourceMap): string { } >((src) => { return { - url: src?.url ?? ``, + url: src?.url ?? ``, content: src?.content ?? '', } }) diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 7a6e0471f8b5..0f5ec5483827 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -59,7 +59,7 @@ function toRawSourceMap(map: DecodedSourceMap): string { } >((src) => { return { - url: src?.url ?? ``, + url: src?.url ?? ``, content: src?.content ?? '', } }) From 1992ba55ab9015eb8ab08248ffcb0675492e6a58 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:27:12 -0400 Subject: [PATCH 33/41] Add comment clarifying magic number --- packages/tailwindcss/src/apply.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 6785b8759204..897e569b88a1 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -197,7 +197,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let candidateSrc: SourceLocation = [src[0], src[1], src[2]] - candidateSrc[1] += 7 + candidateOffset + candidateSrc[1] += 7 /* '@apply '.length */ + candidateOffset candidateSrc[2] = candidateSrc[1] + candidate.length // While the original nodes may have come from an `@utility` we still From 58932d5a25e6c6d15085521dcc58a0cafe3c18a3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:28:17 -0400 Subject: [PATCH 34/41] Inline function to push mappings --- packages/tailwindcss/src/source-maps/source-map.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts index 1e048cdd00e3..8073b78a0792 100644 --- a/packages/tailwindcss/src/source-maps/source-map.ts +++ b/packages/tailwindcss/src/source-maps/source-map.ts @@ -90,7 +90,7 @@ export function createSourceMap({ ast }: { ast: AstNode[] }) { } // Get all the indexes from the mappings - function add(node: AstNode) { + walk(ast, (node: AstNode) => { if (!node.src || !node.dst) return let originalSource = sourceTable.get(node.src[0]) @@ -141,9 +141,7 @@ export function createSourceMap({ ast }: { ast: AstNode[] }) { }, generatedPosition: generatedEnd, }) - } - - walk(ast, add) + }) // Populate for (let source of lineTables.keys()) { From e3561fba8e578cda823b9f5f779e8c304f0c14e3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 May 2025 11:29:11 -0400 Subject: [PATCH 35/41] Update type --- packages/tailwindcss/src/source-maps/source-map.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts index 8073b78a0792..58f544568363 100644 --- a/packages/tailwindcss/src/source-maps/source-map.ts +++ b/packages/tailwindcss/src/source-maps/source-map.ts @@ -173,7 +173,12 @@ export function createTranslationMap({ let originalTable = createLineTable(original) let generatedTable = createLineTable(generated) - type Translation = [Position, Position, Position | null, Position | null] + type Translation = [ + originalStart: Position, + originalEnd: Position, + generatedStart: Position | null, + generatedEnd: Position | null, + ] return (node: AstNode) => { if (!node.src) return [] From 347d9e2971c986ea51ebf80134791c44368a2f2b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 7 May 2025 09:10:36 -0400 Subject: [PATCH 36/41] Remove unncessary settings from test --- integrations/vite/source-maps.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts index 299cd11676be..5a4b4ab79bb4 100644 --- a/integrations/vite/source-maps.test.ts +++ b/integrations/vite/source-maps.test.ts @@ -26,9 +26,6 @@ test( css: { devSourcemap: true, }, - build: { - sourcemap: true, - }, }) `, 'index.html': html` From f3fc8137f25a3e7430e4d50e7530f0bc1d4ded91 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 7 May 2025 09:10:40 -0400 Subject: [PATCH 37/41] Add line table benchmark --- .../tailwindcss/src/source-maps/line-table.bench.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/tailwindcss/src/source-maps/line-table.bench.ts diff --git a/packages/tailwindcss/src/source-maps/line-table.bench.ts b/packages/tailwindcss/src/source-maps/line-table.bench.ts new file mode 100644 index 000000000000..862e2d398297 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/line-table.bench.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +import { bench } from 'vitest' +import { createLineTable } from './line-table' + +const currentFolder = fileURLToPath(new URL('..', import.meta.url)) +const cssFile = readFileSync(currentFolder + '../preflight.css', 'utf-8') +const table = createLineTable(cssFile) + +bench('line table lookups', () => { + for (let i = 0; i < cssFile.length; ++i) table.find(i) +}) From 83e2cd296dab547fdb97f614596ea6cbaa2edbc7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 7 May 2025 16:04:45 -0400 Subject: [PATCH 38/41] Clarify perf comment The actual unoptimized bytecode contains one additional instruction but this is definitely a bit faster in benchmarks --- packages/tailwindcss/src/source-maps/line-table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/source-maps/line-table.ts b/packages/tailwindcss/src/source-maps/line-table.ts index e34ec8fd4680..e16051762786 100644 --- a/packages/tailwindcss/src/source-maps/line-table.ts +++ b/packages/tailwindcss/src/source-maps/line-table.ts @@ -62,7 +62,7 @@ export function createLineTable(source: string): LineTable { let line = 0 let count = table.length while (count > 0) { - // `| 0` forces integer bytecode generation + // `| 0` improves performance (in V8 at least) let mid = (count | 0) >> 1 let i = line + mid if (table[i] <= offset) { From ec0453dfecb4d572afc9e894211c6109931a80a4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 7 May 2025 18:40:22 -0400 Subject: [PATCH 39/41] Improve sorting of mappings --- .../tailwindcss/src/source-maps/source-map.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts index 58f544568363..a7afc54fac49 100644 --- a/packages/tailwindcss/src/source-maps/source-map.ts +++ b/packages/tailwindcss/src/source-maps/source-map.ts @@ -150,11 +150,20 @@ export function createSourceMap({ ast }: { ast: AstNode[] }) { // Sort the mappings in ascending order map.mappings.sort((a, b) => { + let aOriginal = a.originalPosition! + let aGenerated = a.generatedPosition! + let bOriginal = b.originalPosition! + let bGenerated = b.generatedPosition! + + let aSource = map.sources.indexOf(aOriginal.source) + let bSource = map.sources.indexOf(bOriginal.source) + return ( - a.generatedPosition.line - b.generatedPosition.line || - a.generatedPosition.column - b.generatedPosition.column || - (a.originalPosition?.line ?? 0) - (b.originalPosition?.line ?? 0) || - (a.originalPosition?.column ?? 0) - (b.originalPosition?.column ?? 0) + aGenerated.line - bGenerated.line || + aGenerated.column - bGenerated.column || + aSource - bSource || + aOriginal.line - bOriginal.line || + aOriginal.column - bGenerated.column ) }) From 3551daad373ac9d9123170b5d0b9bd9f32436efe Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 7 May 2025 18:52:57 -0400 Subject: [PATCH 40/41] wip visualize souce maps in tests --- .../src/source-maps/source-map.test.ts | 2217 ++++++++++++++--- .../tailwindcss/src/source-maps/visualizer.ts | 120 + 2 files changed, 2019 insertions(+), 318 deletions(-) create mode 100644 packages/tailwindcss/src/source-maps/visualizer.ts diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 0f5ec5483827..b833a84e9908 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -1,14 +1,13 @@ -import remapping from '@ampproject/remapping' import dedent from 'dedent' -import MagicString from 'magic-string' import * as fs from 'node:fs/promises' import * as path from 'node:path' -import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js' -import { test } from 'vitest' +import { expect, test } from 'vitest' import { compile } from '..' +import { toCss } from '../ast' +import * as CSS from '../css-parser' import createPlugin from '../plugin' -import { DefaultMap } from '../utils/default-map' -import type { DecodedSource, DecodedSourceMap } from './source-map' +import { createSourceMap } from './source-map' +import { visualize } from './visualizer' const css = dedent interface RunOptions { @@ -18,10 +17,9 @@ interface RunOptions { } async function run({ input, candidates, options }: RunOptions) { - let source = new MagicString(input) let root = path.resolve(__dirname, '../..') - let compiler = await compile(source.toString(), { + let compiler = await compile(input, { from: 'input.css', async loadStylesheet(id, base) { let resolvedPath = path.resolve(root, id === 'tailwindcss' ? 'index.css' : id) @@ -37,117 +35,19 @@ async function run({ input, candidates, options }: RunOptions) { let css = compiler.build(candidates ?? []) let decoded = compiler.buildSourceMap() - let rawMap = toRawSourceMap(decoded) - let combined = remapping(rawMap, () => null) - let map = JSON.parse(rawMap.toString()) as RawSourceMap - let sources = combined.sources - let annotations = formattedMappings(map) - - return { css, map, sources, annotations } + return visualize(css, decoded) } -function toRawSourceMap(map: DecodedSourceMap): string { - let generator = new SourceMapGenerator() - - let id = 1 - let sourceTable = new DefaultMap< - DecodedSource | null, - { - url: string - content: string - } - >((src) => { - return { - url: src?.url ?? ``, - content: src?.content ?? '', - } - }) - - for (let mapping of map.mappings) { - let original = sourceTable.get(mapping.originalPosition?.source ?? null) - - generator.addMapping({ - generated: mapping.generatedPosition, - original: mapping.originalPosition, - source: original.url, - name: mapping.name ?? undefined, - }) - - generator.setSourceContent(original.url, original.content) - } - - return generator.toString() +async function analyze({ input }: RunOptions) { + let ast = CSS.parse(input, { from: 'input.css' }) + let css = toCss(ast, true) + let decoded = createSourceMap({ ast }) + return visualize(css, decoded) } -/** - * An string annotation that represents a source map - * - * It's not meant to be exhaustive just enough to - * verify that the source map is working and that - * lines are mapped back to the original source - * - * Including when using @apply with multiple classes - */ -function formattedMappings(map: RawSourceMap) { - const smc = new SourceMapConsumer(map) - const annotations: Record< - number, - { - original: { start: [number, number]; end: [number, number] } - generated: { start: [number, number]; end: [number, number] } - source: string - } - > = {} - - smc.eachMapping((mapping) => { - let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || { - ...mapping, - - original: { - start: [mapping.originalLine, mapping.originalColumn], - end: [mapping.originalLine, mapping.originalColumn], - }, - - generated: { - start: [mapping.generatedLine, mapping.generatedColumn], - end: [mapping.generatedLine, mapping.generatedColumn], - }, - - source: mapping.source, - }) - - annotation.generated.end[0] = mapping.generatedLine - annotation.generated.end[1] = mapping.generatedColumn - - annotation.original.end[0] = mapping.originalLine! - annotation.original.end[1] = mapping.originalColumn! - }) - - return Object.values(annotations).map((annotation) => { - return `${annotation.source}: ${formatRange(annotation.generated)} <- ${formatRange(annotation.original)}` - }) -} - -function formatRange(range: { start: [number, number]; end: [number, number] }) { - if (range.start[0] === range.end[0]) { - // This range is on the same line - // and the columns are the same - if (range.start[1] === range.end[1]) { - return `${range.start[0]}:${range.start[1]}` - } - - // This range is on the same line - // but the columns are different - return `${range.start[0]}:${range.start[1]}-${range.end[1]}` - } - - // This range spans multiple lines - return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}` -} - -test('source maps trace back to @import location', async ({ expect }) => { - let { sources, annotations } = await run({ +test('source maps trace back to @import location', async () => { + let visualized = await run({ input: css` @import 'tailwindcss'; @@ -157,148 +57,1386 @@ test('source maps trace back to @import location', async ({ expect }) => { `, }) - // All CSS should be mapped back to the original source file - expect(sources).toEqual([ - // - 'index.css', - 'theme.css', - 'preflight.css', - 'input.css', - ]) - expect(sources.length).toBe(4) - - // The output CSS should include annotations linking back to: - // 1. The class definition `.foo` - // 2. The `@apply underline` line inside of it - expect(annotations).toEqual([ - 'index.css: 1:0-41 <- 1:0-41', - 'index.css: 2:0-13 <- 3:0-34', - 'theme.css: 3:2-15 <- 1:0-15', - 'theme.css: 4:4 <- 2:2-4:0', - 'theme.css: 5:22 <- 4:22', - 'theme.css: 6:4 <- 6:2-8:0', - 'theme.css: 7:13 <- 8:13', - 'theme.css: 8:4-43 <- 446:2-54', - 'theme.css: 9:4-48 <- 449:2-59', - 'index.css: 12:0-12 <- 4:0-37', - 'preflight.css: 13:2-59 <- 7:0-11:23', - 'preflight.css: 14:4-26 <- 12:2-24', - 'preflight.css: 15:4-13 <- 13:2-11', - 'preflight.css: 16:4-14 <- 14:2-12', - 'preflight.css: 17:4-19 <- 15:2-17', - 'preflight.css: 19:2-14 <- 28:0-29:6', - 'preflight.css: 20:4-20 <- 30:2-18', - 'preflight.css: 21:4-34 <- 31:2-32', - 'preflight.css: 22:4-15 <- 32:2-13', - 'preflight.css: 23:4-159 <- 33:2-42:3', - 'preflight.css: 24:4-71 <- 43:2-73', - 'preflight.css: 25:4-75 <- 44:2-77', - 'preflight.css: 26:4-44 <- 45:2-42', - 'preflight.css: 28:2-5 <- 54:0-3', - 'preflight.css: 29:4-13 <- 55:2-11', - 'preflight.css: 30:4-18 <- 56:2-16', - 'preflight.css: 31:4-25 <- 57:2-23', - 'preflight.css: 33:2-22 <- 64:0-20', - 'preflight.css: 34:4-45 <- 65:2-43', - 'preflight.css: 35:4-37 <- 66:2-35', - 'preflight.css: 37:2-25 <- 73:0-78:3', - 'preflight.css: 38:4-22 <- 79:2-20', - 'preflight.css: 39:4-24 <- 80:2-22', - 'preflight.css: 41:2-4 <- 87:0-2', - 'preflight.css: 42:4-18 <- 88:2-16', - 'preflight.css: 43:4-36 <- 89:2-34', - 'preflight.css: 44:4-28 <- 90:2-26', - 'preflight.css: 46:2-12 <- 97:0-98:7', - 'preflight.css: 47:4-23 <- 99:2-21', - 'preflight.css: 49:2-23 <- 109:0-112:4', - 'preflight.css: 50:4-148 <- 113:2-123:3', - 'preflight.css: 51:4-76 <- 124:2-78', - 'preflight.css: 52:4-80 <- 125:2-82', - 'preflight.css: 53:4-18 <- 126:2-16', - 'preflight.css: 55:2-8 <- 133:0-6', - 'preflight.css: 56:4-18 <- 134:2-16', - 'preflight.css: 58:2-11 <- 141:0-142:4', - 'preflight.css: 59:4-18 <- 143:2-16', - 'preflight.css: 60:4-18 <- 144:2-16', - 'preflight.css: 61:4-22 <- 145:2-20', - 'preflight.css: 62:4-28 <- 146:2-26', - 'preflight.css: 64:2-6 <- 149:0-4', - 'preflight.css: 65:4-19 <- 150:2-17', - 'preflight.css: 67:2-6 <- 153:0-4', - 'preflight.css: 68:4-15 <- 154:2-13', - 'preflight.css: 70:2-8 <- 163:0-6', - 'preflight.css: 71:4-18 <- 164:2-16', - 'preflight.css: 72:4-25 <- 165:2-23', - 'preflight.css: 73:4-29 <- 166:2-27', - 'preflight.css: 75:2-18 <- 173:0-16', - 'preflight.css: 76:4-17 <- 174:2-15', - 'preflight.css: 78:2-11 <- 181:0-9', - 'preflight.css: 79:4-28 <- 182:2-26', - 'preflight.css: 81:2-10 <- 189:0-8', - 'preflight.css: 82:4-22 <- 190:2-20', - 'preflight.css: 84:2-15 <- 197:0-199:5', - 'preflight.css: 85:4-20 <- 200:2-18', - 'preflight.css: 87:2-56 <- 209:0-216:7', - 'preflight.css: 88:4-18 <- 217:2-16', - 'preflight.css: 89:4-26 <- 218:2-24', - 'preflight.css: 91:2-13 <- 225:0-226:6', - 'preflight.css: 92:4-19 <- 227:2-17', - 'preflight.css: 93:4-16 <- 228:2-14', - 'preflight.css: 95:2-68 <- 238:0-243:23', - 'preflight.css: 96:4-17 <- 244:2-15', - 'preflight.css: 97:4-34 <- 245:2-32', - 'preflight.css: 98:4-36 <- 246:2-34', - 'preflight.css: 99:4-27 <- 247:2-25', - 'preflight.css: 100:4-18 <- 248:2-16', - 'preflight.css: 101:4-20 <- 249:2-18', - 'preflight.css: 102:4-33 <- 250:2-31', - 'preflight.css: 103:4-14 <- 251:2-12', - 'preflight.css: 105:2-49 <- 258:0-47', - 'preflight.css: 106:4-23 <- 259:2-21', - 'preflight.css: 108:2-56 <- 266:0-54', - 'preflight.css: 109:4-30 <- 267:2-28', - 'preflight.css: 111:2-25 <- 274:0-23', - 'preflight.css: 112:4-26 <- 275:2-24', - 'preflight.css: 114:2-16 <- 282:0-14', - 'preflight.css: 115:4-14 <- 283:2-12', - 'preflight.css: 117:2-92 <- 291:0-292:49', - 'preflight.css: 118:4-18 <- 293:2-16', - 'preflight.css: 119:6-25 <- 294:4-61', - 'preflight.css: 120:6-53 <- 294:4-61', - 'preflight.css: 121:8-65 <- 294:4-61', - 'preflight.css: 125:2-11 <- 302:0-9', - 'preflight.css: 126:4-20 <- 303:2-18', - 'preflight.css: 128:2-30 <- 310:0-28', - 'preflight.css: 129:4-28 <- 311:2-26', - 'preflight.css: 131:2-32 <- 319:0-30', - 'preflight.css: 132:4-19 <- 320:2-17', - 'preflight.css: 133:4-23 <- 321:2-21', - 'preflight.css: 135:2-26 <- 328:0-24', - 'preflight.css: 136:4-24 <- 329:2-22', - 'preflight.css: 138:2-41 <- 336:0-39', - 'preflight.css: 139:4-14 <- 337:2-12', - 'preflight.css: 141:2-329 <- 340:0-348:39', - 'preflight.css: 142:4-20 <- 349:2-18', - 'preflight.css: 144:2-19 <- 356:0-17', - 'preflight.css: 145:4-20 <- 357:2-18', - 'preflight.css: 147:2-96 <- 364:0-366:23', - 'preflight.css: 148:4-22 <- 367:2-20', - 'preflight.css: 150:2-59 <- 374:0-375:28', - 'preflight.css: 151:4-16 <- 376:2-14', - 'preflight.css: 153:2-47 <- 383:0-45', - 'preflight.css: 154:4-28 <- 384:2-26', - 'index.css: 157:0-16 <- 5:0-42', - 'input.css: 158:0-5 <- 3:0-5', - 'input.css: 159:2-33 <- 4:9-18', - ]) + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - index.css + - theme.css + - preflight.css + - input.css + + VISUALIZATION + /* input: index.css */ + @layer theme, base, components, utilities; + #1 ----------------------------------------- + + @import './theme.css' layer(theme); + #2 ---------------------------------- + @import './preflight.css' layer(base); + #10 ------------------------------------- + @import './utilities.css' layer(utilities); + #147 ------------------------------------------ + + /* input: theme.css */ + @theme default { + #3 --------------- + --font-sans: + #4 ^ + ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + #4 ^ + 'Noto Color Emoji'; + #5 ---------------------- + --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-mono: + #6 ^ + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + #6 ^ + monospace; + #7 ------------- + + --color-red-50: oklch(97.1% 0.013 17.38); + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); + --color-red-300: oklch(80.8% 0.114 19.571); + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-red-800: oklch(44.4% 0.177 26.899); + --color-red-900: oklch(39.6% 0.141 25.723); + --color-red-950: oklch(25.8% 0.092 26.042); + + --color-orange-50: oklch(98% 0.016 73.684); + --color-orange-100: oklch(95.4% 0.038 75.164); + --color-orange-200: oklch(90.1% 0.076 70.697); + --color-orange-300: oklch(83.7% 0.128 66.29); + --color-orange-400: oklch(75% 0.183 55.934); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-orange-600: oklch(64.6% 0.222 41.116); + --color-orange-700: oklch(55.3% 0.195 38.402); + --color-orange-800: oklch(47% 0.157 37.304); + --color-orange-900: oklch(40.8% 0.123 38.172); + --color-orange-950: oklch(26.6% 0.079 36.259); + + --color-amber-50: oklch(98.7% 0.022 95.277); + --color-amber-100: oklch(96.2% 0.059 95.617); + --color-amber-200: oklch(92.4% 0.12 95.746); + --color-amber-300: oklch(87.9% 0.169 91.605); + --color-amber-400: oklch(82.8% 0.189 84.429); + --color-amber-500: oklch(76.9% 0.188 70.08); + --color-amber-600: oklch(66.6% 0.179 58.318); + --color-amber-700: oklch(55.5% 0.163 48.998); + --color-amber-800: oklch(47.3% 0.137 46.201); + --color-amber-900: oklch(41.4% 0.112 45.904); + --color-amber-950: oklch(27.9% 0.077 45.635); + + --color-yellow-50: oklch(98.7% 0.026 102.212); + --color-yellow-100: oklch(97.3% 0.071 103.193); + --color-yellow-200: oklch(94.5% 0.129 101.54); + --color-yellow-300: oklch(90.5% 0.182 98.111); + --color-yellow-400: oklch(85.2% 0.199 91.936); + --color-yellow-500: oklch(79.5% 0.184 86.047); + --color-yellow-600: oklch(68.1% 0.162 75.834); + --color-yellow-700: oklch(55.4% 0.135 66.442); + --color-yellow-800: oklch(47.6% 0.114 61.907); + --color-yellow-900: oklch(42.1% 0.095 57.708); + --color-yellow-950: oklch(28.6% 0.066 53.813); + + --color-lime-50: oklch(98.6% 0.031 120.757); + --color-lime-100: oklch(96.7% 0.067 122.328); + --color-lime-200: oklch(93.8% 0.127 124.321); + --color-lime-300: oklch(89.7% 0.196 126.665); + --color-lime-400: oklch(84.1% 0.238 128.85); + --color-lime-500: oklch(76.8% 0.233 130.85); + --color-lime-600: oklch(64.8% 0.2 131.684); + --color-lime-700: oklch(53.2% 0.157 131.589); + --color-lime-800: oklch(45.3% 0.124 130.933); + --color-lime-900: oklch(40.5% 0.101 131.063); + --color-lime-950: oklch(27.4% 0.072 132.109); + + --color-green-50: oklch(98.2% 0.018 155.826); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-200: oklch(92.5% 0.084 155.995); + --color-green-300: oklch(87.1% 0.15 154.449); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-600: oklch(62.7% 0.194 149.214); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-green-800: oklch(44.8% 0.119 151.328); + --color-green-900: oklch(39.3% 0.095 152.535); + --color-green-950: oklch(26.6% 0.065 152.934); + + --color-emerald-50: oklch(97.9% 0.021 166.113); + --color-emerald-100: oklch(95% 0.052 163.051); + --color-emerald-200: oklch(90.5% 0.093 164.15); + --color-emerald-300: oklch(84.5% 0.143 164.978); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-500: oklch(69.6% 0.17 162.48); + --color-emerald-600: oklch(59.6% 0.145 163.225); + --color-emerald-700: oklch(50.8% 0.118 165.612); + --color-emerald-800: oklch(43.2% 0.095 166.913); + --color-emerald-900: oklch(37.8% 0.077 168.94); + --color-emerald-950: oklch(26.2% 0.051 172.552); + + --color-teal-50: oklch(98.4% 0.014 180.72); + --color-teal-100: oklch(95.3% 0.051 180.801); + --color-teal-200: oklch(91% 0.096 180.426); + --color-teal-300: oklch(85.5% 0.138 181.071); + --color-teal-400: oklch(77.7% 0.152 181.912); + --color-teal-500: oklch(70.4% 0.14 182.503); + --color-teal-600: oklch(60% 0.118 184.704); + --color-teal-700: oklch(51.1% 0.096 186.391); + --color-teal-800: oklch(43.7% 0.078 188.216); + --color-teal-900: oklch(38.6% 0.063 188.416); + --color-teal-950: oklch(27.7% 0.046 192.524); + + --color-cyan-50: oklch(98.4% 0.019 200.873); + --color-cyan-100: oklch(95.6% 0.045 203.388); + --color-cyan-200: oklch(91.7% 0.08 205.041); + --color-cyan-300: oklch(86.5% 0.127 207.078); + --color-cyan-400: oklch(78.9% 0.154 211.53); + --color-cyan-500: oklch(71.5% 0.143 215.221); + --color-cyan-600: oklch(60.9% 0.126 221.723); + --color-cyan-700: oklch(52% 0.105 223.128); + --color-cyan-800: oklch(45% 0.085 224.283); + --color-cyan-900: oklch(39.8% 0.07 227.392); + --color-cyan-950: oklch(30.2% 0.056 229.695); + + --color-sky-50: oklch(97.7% 0.013 236.62); + --color-sky-100: oklch(95.1% 0.026 236.824); + --color-sky-200: oklch(90.1% 0.058 230.902); + --color-sky-300: oklch(82.8% 0.111 230.318); + --color-sky-400: oklch(74.6% 0.16 232.661); + --color-sky-500: oklch(68.5% 0.169 237.323); + --color-sky-600: oklch(58.8% 0.158 241.966); + --color-sky-700: oklch(50% 0.134 242.749); + --color-sky-800: oklch(44.3% 0.11 240.79); + --color-sky-900: oklch(39.1% 0.09 240.876); + --color-sky-950: oklch(29.3% 0.066 243.157); + + --color-blue-50: oklch(97% 0.014 254.604); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-200: oklch(88.2% 0.059 254.128); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-blue-900: oklch(37.9% 0.146 265.522); + --color-blue-950: oklch(28.2% 0.091 267.935); + + --color-indigo-50: oklch(96.2% 0.018 272.314); + --color-indigo-100: oklch(93% 0.034 272.788); + --color-indigo-200: oklch(87% 0.065 274.039); + --color-indigo-300: oklch(78.5% 0.115 274.713); + --color-indigo-400: oklch(67.3% 0.182 276.935); + --color-indigo-500: oklch(58.5% 0.233 277.117); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-indigo-700: oklch(45.7% 0.24 277.023); + --color-indigo-800: oklch(39.8% 0.195 277.366); + --color-indigo-900: oklch(35.9% 0.144 278.697); + --color-indigo-950: oklch(25.7% 0.09 281.288); + + --color-violet-50: oklch(96.9% 0.016 293.756); + --color-violet-100: oklch(94.3% 0.029 294.588); + --color-violet-200: oklch(89.4% 0.057 293.283); + --color-violet-300: oklch(81.1% 0.111 293.571); + --color-violet-400: oklch(70.2% 0.183 293.541); + --color-violet-500: oklch(60.6% 0.25 292.717); + --color-violet-600: oklch(54.1% 0.281 293.009); + --color-violet-700: oklch(49.1% 0.27 292.581); + --color-violet-800: oklch(43.2% 0.232 292.759); + --color-violet-900: oklch(38% 0.189 293.745); + --color-violet-950: oklch(28.3% 0.141 291.089); + + --color-purple-50: oklch(97.7% 0.014 308.299); + --color-purple-100: oklch(94.6% 0.033 307.174); + --color-purple-200: oklch(90.2% 0.063 306.703); + --color-purple-300: oklch(82.7% 0.119 306.383); + --color-purple-400: oklch(71.4% 0.203 305.504); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-purple-600: oklch(55.8% 0.288 302.321); + --color-purple-700: oklch(49.6% 0.265 301.924); + --color-purple-800: oklch(43.8% 0.218 303.724); + --color-purple-900: oklch(38.1% 0.176 304.987); + --color-purple-950: oklch(29.1% 0.149 302.717); + + --color-fuchsia-50: oklch(97.7% 0.017 320.058); + --color-fuchsia-100: oklch(95.2% 0.037 318.852); + --color-fuchsia-200: oklch(90.3% 0.076 319.62); + --color-fuchsia-300: oklch(83.3% 0.145 321.434); + --color-fuchsia-400: oklch(74% 0.238 322.16); + --color-fuchsia-500: oklch(66.7% 0.295 322.15); + --color-fuchsia-600: oklch(59.1% 0.293 322.896); + --color-fuchsia-700: oklch(51.8% 0.253 323.949); + --color-fuchsia-800: oklch(45.2% 0.211 324.591); + --color-fuchsia-900: oklch(40.1% 0.17 325.612); + --color-fuchsia-950: oklch(29.3% 0.136 325.661); + + --color-pink-50: oklch(97.1% 0.014 343.198); + --color-pink-100: oklch(94.8% 0.028 342.258); + --color-pink-200: oklch(89.9% 0.061 343.231); + --color-pink-300: oklch(82.3% 0.12 346.018); + --color-pink-400: oklch(71.8% 0.202 349.761); + --color-pink-500: oklch(65.6% 0.241 354.308); + --color-pink-600: oklch(59.2% 0.249 0.584); + --color-pink-700: oklch(52.5% 0.223 3.958); + --color-pink-800: oklch(45.9% 0.187 3.815); + --color-pink-900: oklch(40.8% 0.153 2.432); + --color-pink-950: oklch(28.4% 0.109 3.907); + + --color-rose-50: oklch(96.9% 0.015 12.422); + --color-rose-100: oklch(94.1% 0.03 12.58); + --color-rose-200: oklch(89.2% 0.058 10.001); + --color-rose-300: oklch(81% 0.117 11.638); + --color-rose-400: oklch(71.2% 0.194 13.428); + --color-rose-500: oklch(64.5% 0.246 16.439); + --color-rose-600: oklch(58.6% 0.253 17.585); + --color-rose-700: oklch(51.4% 0.222 16.935); + --color-rose-800: oklch(45.5% 0.188 13.697); + --color-rose-900: oklch(41% 0.159 10.272); + --color-rose-950: oklch(27.1% 0.105 12.094); + + --color-slate-50: oklch(98.4% 0.003 247.858); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-slate-900: oklch(20.8% 0.042 265.755); + --color-slate-950: oklch(12.9% 0.042 264.695); + + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); + --color-gray-900: oklch(21% 0.034 264.665); + --color-gray-950: oklch(13% 0.028 261.692); + + --color-zinc-50: oklch(98.5% 0 0); + --color-zinc-100: oklch(96.7% 0.001 286.375); + --color-zinc-200: oklch(92% 0.004 286.32); + --color-zinc-300: oklch(87.1% 0.006 286.286); + --color-zinc-400: oklch(70.5% 0.015 286.067); + --color-zinc-500: oklch(55.2% 0.016 285.938); + --color-zinc-600: oklch(44.2% 0.017 285.786); + --color-zinc-700: oklch(37% 0.013 285.805); + --color-zinc-800: oklch(27.4% 0.006 286.033); + --color-zinc-900: oklch(21% 0.006 285.885); + --color-zinc-950: oklch(14.1% 0.005 285.823); + + --color-neutral-50: oklch(98.5% 0 0); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-600: oklch(43.9% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); + --color-neutral-950: oklch(14.5% 0 0); + + --color-stone-50: oklch(98.5% 0.001 106.423); + --color-stone-100: oklch(97% 0.001 106.424); + --color-stone-200: oklch(92.3% 0.003 48.717); + --color-stone-300: oklch(86.9% 0.005 56.366); + --color-stone-400: oklch(70.9% 0.01 56.259); + --color-stone-500: oklch(55.3% 0.013 58.071); + --color-stone-600: oklch(44.4% 0.011 73.639); + --color-stone-700: oklch(37.4% 0.01 67.558); + --color-stone-800: oklch(26.8% 0.007 34.298); + --color-stone-900: oklch(21.6% 0.006 56.043); + --color-stone-950: oklch(14.7% 0.004 49.25); + + --color-black: #000; + --color-white: #fff; + + --spacing: 0.25rem; + + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + + --container-3xs: 16rem; + --container-2xs: 18rem; + --container-xs: 20rem; + --container-sm: 24rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-xl: 36rem; + --container-2xl: 42rem; + --container-3xl: 48rem; + --container-4xl: 56rem; + --container-5xl: 64rem; + --container-6xl: 72rem; + --container-7xl: 80rem; + + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --text-5xl: 3rem; + --text-5xl--line-height: 1; + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; + --text-7xl: 4.5rem; + --text-7xl--line-height: 1; + --text-8xl: 6rem; + --text-8xl--line-height: 1; + --text-9xl: 8rem; + --text-9xl--line-height: 1; + + --font-weight-thin: 100; + --font-weight-extralight: 200; + --font-weight-light: 300; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + --font-weight-black: 900; + + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-normal: 0em; + --tracking-wide: 0.025em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; + + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; + + --radius-xs: 0.125rem; + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-4xl: 2rem; + + --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); + + --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05); + --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); + + --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05); + --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15); + --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12); + --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15); + --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); + --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); + + --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15); + --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2); + --text-shadow-sm: + 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075); + --text-shadow-md: + 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1); + --text-shadow-lg: + 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1); + + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --animate-bounce: bounce 1s infinite; + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + @keyframes ping { + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } + + @keyframes pulse { + 50% { + opacity: 0.5; + } + } + + @keyframes bounce { + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + + --blur-xs: 4px; + --blur-sm: 8px; + --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; + --blur-2xl: 40px; + --blur-3xl: 64px; + + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + + --aspect-video: 16 / 9; + + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: --theme(--font-sans, initial); + #8 ---------------------------------------------------- + --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial); + --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial); + --default-mono-font-family: --theme(--font-mono, initial); + #9 --------------------------------------------------------- + --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial); + --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial); + } + + /* Deprecated */ + @theme default inline reference { + --blur: 8px; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06); + --radius: 0.25rem; + --max-width-prose: 65ch; + } + + /* input: preflight.css */ + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. + */ + + *, + #11 ^ + ::after, + #11 ^ + ::before, + #12 ^ + ::backdrop, + #12 ^ + ::file-selector-button { + #13 ----------------------- + box-sizing: border-box; /* 1 */ + #14 ---------------------- + margin: 0; /* 2 */ + #15 --------- + padding: 0; /* 2 */ + #16 ---------- + border: 0 solid; /* 3 */ + #17 --------------- + } + + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured \`sans\` font-family by default. + 5. Use the user's configured \`sans\` font-feature-settings by default. + 6. Use the user's configured \`sans\` font-variation-settings by default. + 7. Disable tap highlights on iOS. + */ + + html, + #18 ^ + :host { + #18 ------ + line-height: 1.5; /* 1 */ + #19 ---------------- + -webkit-text-size-adjust: 100%; /* 2 */ + #20 ------------------------------ + tab-size: 4; /* 3 */ + #21 ----------- + font-family: --theme( + #22 ^ + --default-font-family, + #23 ^ + ui-sans-serif, + #23 ^ + system-ui, + #24 ^ + sans-serif, + #24 ^ + 'Apple Color Emoji', + #25 ^ + 'Segoe UI Emoji', + #25 ^ + 'Segoe UI Symbol', + #26 ^ + 'Noto Color Emoji' + #26 ^ + ); /* 4 */ + #27 --- + font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ + #28 ----------------------------------------------------------------------- + font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ + #29 --------------------------------------------------------------------------- + -webkit-tap-highlight-color: transparent; /* 7 */ + #30 ---------------------------------------- + } + + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. + */ + + hr { + #31 --- + height: 0; /* 1 */ + #32 --------- + color: inherit; /* 2 */ + #33 -------------- + border-top-width: 1px; /* 3 */ + #34 --------------------- + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. + */ + + abbr:where([title]) { + #35 -------------------- + -webkit-text-decoration: underline dotted; + #36 ----------------------------------------- + text-decoration: underline dotted; + #37 --------------------------------- + } + + /* + Remove the default font size and weight for headings. + */ + + h1, + #38 ^ + h2, + #38 ^ + h3, + #39 ^ + h4, + #39 ^ + h5, + #40 ^ + h6 { + #40 --- + font-size: inherit; + #41 ------------------ + font-weight: inherit; + #42 -------------------- + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. + */ + + a { + #43 -- + color: inherit; + #44 -------------- + -webkit-text-decoration: inherit; + #45 -------------------------------- + text-decoration: inherit; + #46 ------------------------ + } + + /* + Add the correct font weight in Edge and Safari. + */ + + b, + #47 ^ + strong { + #48 ------- + font-weight: bolder; + #49 ------------------- + } + + /* + 1. Use the user's configured \`mono\` font-family by default. + 2. Use the user's configured \`mono\` font-feature-settings by default. + 3. Use the user's configured \`mono\` font-variation-settings by default. + 4. Correct the odd \`em\` font sizing in all browsers. + */ + + code, + #50 ^ + kbd, + #50 ^ + samp, + #51 ^ + pre { + #51 ---- + font-family: --theme( + #52 ^ + --default-mono-font-family, + #53 ^ + ui-monospace, + #53 ^ + SFMono-Regular, + #54 ^ + Menlo, + #54 ^ + Monaco, + #55 ^ + Consolas, + #55 ^ + 'Liberation Mono', + #56 ^ + 'Courier New', + #56 ^ + monospace + #57 ^ + ); /* 1 */ + #57 --- + font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ + #58 ---------------------------------------------------------------------------- + font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ + #59 -------------------------------------------------------------------------------- + font-size: 1em; /* 4 */ + #60 -------------- + } + + /* + Add the correct font size in all browsers. + */ + + small { + #61 ------ + font-size: 80%; + #62 -------------- + } + + /* + Prevent \`sub\` and \`sup\` elements from affecting the line height in all browsers. + */ + + sub, + #63 ^ + sup { + #64 ---- + font-size: 75%; + #65 -------------- + line-height: 0; + #66 -------------- + position: relative; + #67 ------------------ + vertical-align: baseline; + #68 ------------------------ + } + + sub { + #69 ---- + bottom: -0.25em; + #70 --------------- + } + + sup { + #71 ---- + top: -0.5em; + #72 ----------- + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. + */ + + table { + #73 ------ + text-indent: 0; /* 1 */ + #74 -------------- + border-color: inherit; /* 2 */ + #75 --------------------- + border-collapse: collapse; /* 3 */ + #76 ------------------------- + } + + /* + Use the modern Firefox focus style for all focusable elements. + */ + + :-moz-focusring { + #77 ---------------- + outline: auto; + #78 ------------- + } + + /* + Add the correct vertical alignment in Chrome and Firefox. + */ + + progress { + #79 --------- + vertical-align: baseline; + #80 ------------------------ + } + + /* + Add the correct display in Chrome and Safari. + */ + + summary { + #81 -------- + display: list-item; + #82 ------------------ + } + + /* + Make lists unstyled by default. + */ + + ol, + #83 ^ + ul, + #83 ^ + menu { + #84 ----- + list-style: none; + #85 ---------------- + } + + /* + 1. Make replaced elements \`display: block\` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add \`vertical-align: middle\` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. + */ + + img, + #86 ^ + svg, + #86 ^ + video, + #87 ^ + canvas, + #87 ^ + audio, + #88 ^ + iframe, + #88 ^ + embed, + #89 ^ + object { + #89 ------- + display: block; /* 1 */ + #90 -------------- + vertical-align: middle; /* 2 */ + #91 ---------------------- + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) + */ + + img, + #92 ^ + video { + #93 ------ + max-width: 100%; + #94 --------------- + height: auto; + #95 ------------ + } + + /* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. + */ + + button, + #96 ^ + input, + #96 ^ + select, + #97 ^ + optgroup, + #97 ^ + textarea, + #98 ^ + ::file-selector-button { + #98 ----------------------- + font: inherit; /* 1 */ + #99 ------------- + font-feature-settings: inherit; /* 1 */ + #100 ------------------------------ + font-variation-settings: inherit; /* 1 */ + #101 -------------------------------- + letter-spacing: inherit; /* 1 */ + #102 ----------------------- + color: inherit; /* 1 */ + #103 -------------- + border-radius: 0; /* 2 */ + #104 ---------------- + background-color: transparent; /* 3 */ + #105 ----------------------------- + opacity: 1; /* 4 */ + #106 ---------- + } + + /* + Restore default font weight. + */ + + :where(select:is([multiple], [size])) optgroup { + #107 ----------------------------------------------- + font-weight: bolder; + #108 ------------------- + } + + /* + Restore indentation. + */ + + :where(select:is([multiple], [size])) optgroup option { + #109 ------------------------------------------------------ + padding-inline-start: 20px; + #110 -------------------------- + } + + /* + Restore space after button. + */ + + ::file-selector-button { + #111 ----------------------- + margin-inline-end: 4px; + #112 ---------------------- + } + + /* + Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + */ + + ::placeholder { + #113 -------------- + opacity: 1; + #114 ---------- + } + + /* + Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not + crash when using \`color-mix(…)\` with \`currentcolor\`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) + */ + + @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or + #115 ^ + (contain-intrinsic-size: 1px) /* Safari 17+ */ { + #116 ------------------------------------------------- + ::placeholder { + #117 -------------- + color: color-mix(in oklab, currentcolor 50%, transparent); + #118 --------------------------------------------------------- + #119 --------------------------------------------------------- + #120 --------------------------------------------------------- + } + } + + /* + Prevent resizing textareas horizontally by default. + */ + + textarea { + #121 --------- + resize: vertical; + #122 ---------------- + } + + /* + Remove the inner padding in Chrome and Safari on macOS. + */ + + ::-webkit-search-decoration { + #123 ---------------------------- + -webkit-appearance: none; + #124 ------------------------ + } + + /* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. + */ + + ::-webkit-date-and-time-value { + #125 ------------------------------ + min-height: 1lh; /* 1 */ + #126 --------------- + text-align: inherit; /* 2 */ + #127 ------------------- + } + + /* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to \`display: block\`. + */ + + ::-webkit-datetime-edit { + #128 ------------------------ + display: inline-flex; + #129 -------------------- + } + + /* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. + */ + + ::-webkit-datetime-edit-fields-wrapper { + #130 --------------------------------------- + padding: 0; + #131 ---------- + } + + ::-webkit-datetime-edit, + #132 ^ + ::-webkit-datetime-edit-year-field, + #132 ^ + ::-webkit-datetime-edit-month-field, + #133 ^ + ::-webkit-datetime-edit-day-field, + #133 ^ + ::-webkit-datetime-edit-hour-field, + #134 ^ + ::-webkit-datetime-edit-minute-field, + #134 ^ + ::-webkit-datetime-edit-second-field, + #135 ^ + ::-webkit-datetime-edit-millisecond-field, + #135 ^ + ::-webkit-datetime-edit-meridiem-field { + #136 --------------------------------------- + padding-block: 0; + #137 ---------------- + } + + /* + Remove the additional \`:invalid\` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) + */ + + :-moz-ui-invalid { + #138 ----------------- + box-shadow: none; + #139 ---------------- + } + + /* + Correct the inability to style the border radius in iOS Safari. + */ + + button, + #140 ^ + input:where([type='button'], [type='reset'], [type='submit']), + #140 ^ + ::file-selector-button { + #141 ----------------------- + appearance: button; + #142 ------------------ + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. + */ + + ::-webkit-inner-spin-button, + #143 ^ + ::-webkit-outer-spin-button { + #143 ---------------------------- + height: auto; + #144 ------------ + } + + /* + Make elements with the HTML hidden attribute stay hidden by default. + */ + + [hidden]:where(:not([hidden='until-found'])) { + #145 --------------------------------------------- + display: none !important; + #146 ------------------------ + } + + /* input: input.css */ + @import 'tailwindcss'; + + .foo { + #148 ----- + @apply underline; + #149 --------- + } + /* output */ + @layer theme, base, components, utilities; + #1 ----------------------------------------- + @layer theme { + #2 ------------- + :root, :host { + #3 ------------- + --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + #4 + #5 ^ + 'Noto Color Emoji'; + #5 ^ + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + #6 + #7 ^ + monospace; + #7 ^ + --default-font-family: var(--font-sans); + #8 --------------------------------------- + --default-mono-font-family: var(--font-mono); + #9 -------------------------------------------- + } + } + @layer base { + #10 ------------ + *, ::after, ::before, ::backdrop, ::file-selector-button { + #11 + #12 + #13 --------------------------------------------------------- + box-sizing: border-box; + #14 ---------------------- + margin: 0; + #15 --------- + padding: 0; + #16 ---------- + border: 0 solid; + #17 --------------- + } + html, :host { + #18 + #19 ^ + line-height: 1.5; + #19 ---------------- + -webkit-text-size-adjust: 100%; + #20 ------------------------------ + tab-size: 4; + #21 ----------- + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'); + #22 + #23 + #24 + #25 + #26 + #27 ^ + font-feature-settings: var(--default-font-feature-settings, normal); + #28 ------------------------------------------------------------------- + font-variation-settings: var(--default-font-variation-settings, normal); + #29 ----------------------------------------------------------------------- + -webkit-tap-highlight-color: transparent; + #30 ---------------------------------------- + } + hr { + #31 --- + height: 0; + #32 --------- + color: inherit; + #33 -------------- + border-top-width: 1px; + #34 --------------------- + } + abbr:where([title]) { + #35 -------------------- + -webkit-text-decoration: underline dotted; + #36 ----------------------------------------- + text-decoration: underline dotted; + #37 --------------------------------- + } + h1, h2, h3, h4, h5, h6 { + #38 + #39 + #40 + #41 ^ + font-size: inherit; + #41 ------------------ + font-weight: inherit; + #42 -------------------- + } + a { + #43 -- + color: inherit; + #44 -------------- + -webkit-text-decoration: inherit; + #45 -------------------------------- + text-decoration: inherit; + #46 ------------------------ + } + b, strong { + #47 + #48 ^ + font-weight: bolder; + #49 ------------------- + } + code, kbd, samp, pre { + #50 + #51 + #52 ^ + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); + #52 + #53 + #54 + #55 + #56 + #57 ------------------------------------------------------------------------------------------------------------------------------------------------ + font-feature-settings: var(--default-mono-font-feature-settings, normal); + #58 ------------------------------------------------------------------------ + font-variation-settings: var(--default-mono-font-variation-settings, normal); + #59 ---------------------------------------------------------------------------- + font-size: 1em; + #60 -------------- + } + small { + #61 ------ + font-size: 80%; + #62 -------------- + } + sub, sup { + #63 + #64 ^ + font-size: 75%; + #65 -------------- + line-height: 0; + #66 -------------- + position: relative; + #67 ------------------ + vertical-align: baseline; + #68 ------------------------ + } + sub { + #69 ---- + bottom: -0.25em; + #70 --------------- + } + sup { + #71 ---- + top: -0.5em; + #72 ----------- + } + table { + #73 ------ + text-indent: 0; + #74 -------------- + border-color: inherit; + #75 --------------------- + border-collapse: collapse; + #76 ------------------------- + } + :-moz-focusring { + #77 ---------------- + outline: auto; + #78 ------------- + } + progress { + #79 --------- + vertical-align: baseline; + #80 ------------------------ + } + summary { + #81 -------- + display: list-item; + #82 ------------------ + } + ol, ul, menu { + #83 + #84 ------------- + list-style: none; + #85 ---------------- + } + img, svg, video, canvas, audio, iframe, embed, object { + #86 + #87 + #88 + #89 + #90 ^ + display: block; + #90 -------------- + vertical-align: middle; + #91 ---------------------- + } + img, video { + #92 + #93 ^ + max-width: 100%; + #94 --------------- + height: auto; + #95 ------------ + } + button, input, select, optgroup, textarea, ::file-selector-button { + #96 + #97 + #98 + #99 ^ + font: inherit; + #99 ------------- + font-feature-settings: inherit; + #100 ------------------------------ + font-variation-settings: inherit; + #101 -------------------------------- + letter-spacing: inherit; + #102 ----------------------- + color: inherit; + #103 -------------- + border-radius: 0; + #104 ---------------- + background-color: transparent; + #105 ----------------------------- + opacity: 1; + #106 ---------- + } + :where(select:is([multiple], [size])) optgroup { + #107 ----------------------------------------------- + font-weight: bolder; + #108 ------------------- + } + :where(select:is([multiple], [size])) optgroup option { + #109 ------------------------------------------------------ + padding-inline-start: 20px; + #110 -------------------------- + } + ::file-selector-button { + #111 ----------------------- + margin-inline-end: 4px; + #112 ---------------------- + } + ::placeholder { + #113 -------------- + opacity: 1; + #114 ---------- + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + #115 + #116 ^ + ::placeholder { + #117 -------------- + color: currentcolor; + #118 ------------------- + @supports (color: color-mix(in lab, red, red)) { + #119 ----------------------------------------------- + color: color-mix(in oklab, currentcolor 50%, transparent); + #120 --------------------------------------------------------- + } + } + } + textarea { + #121 --------- + resize: vertical; + #122 ---------------- + } + ::-webkit-search-decoration { + #123 ---------------------------- + -webkit-appearance: none; + #124 ------------------------ + } + ::-webkit-date-and-time-value { + #125 ------------------------------ + min-height: 1lh; + #126 --------------- + text-align: inherit; + #127 ------------------- + } + ::-webkit-datetime-edit { + #128 ------------------------ + display: inline-flex; + #129 -------------------- + } + ::-webkit-datetime-edit-fields-wrapper { + #130 --------------------------------------- + padding: 0; + #131 ---------- + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + #132 + #133 + #134 + #135 + #136 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + padding-block: 0; + #137 ---------------- + } + :-moz-ui-invalid { + #138 ----------------- + box-shadow: none; + #139 ---------------- + } + button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { + #140 + #141 ---------------------------------------------------------------------------------------------- + appearance: button; + #142 ------------------ + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + #143 + #144 ^ + height: auto; + #144 ------------ + } + [hidden]:where(:not([hidden='until-found'])) { + #145 --------------------------------------------- + display: none !important; + #146 ------------------------ + } + } + @layer utilities; + #147 ---------------- + .foo { + #148 ----- + text-decoration-line: underline; + #149 ------------------------------- + } + + " + `) }) -test('source maps are generated for utilities', async ({ expect }) => { - let { - sources, - css: output, - annotations, - } = await run({ +test('source maps are generated for utilities', async () => { + let visualized = await run({ input: css` @import './utilities.css'; @plugin "./plugin.js"; @@ -318,39 +1456,51 @@ test('source maps are generated for utilities', async ({ expect }) => { }, }) - // All CSS should be mapped back to the original source file - expect(sources).toEqual(['utilities.css', 'input.css']) - expect(sources.length).toBe(2) - - // The output CSS should include annotations linking back to: - expect(annotations).toEqual([ - // @tailwind utilities - 'utilities.css: 1:0-6 <- 1:0-19', - 'utilities.css: 2:2-15 <- 1:0-19', - 'utilities.css: 4:0-8 <- 1:0-19', - // color: orange - 'input.css: 5:2-15 <- 4:2-15', - // @tailwind utilities - 'utilities.css: 7:0-11 <- 1:0-19', - 'utilities.css: 8:2-13 <- 1:0-19', - ]) - - expect(output).toMatchInlineSnapshot(` - ".flex { - display: flex; - } - .custom { - color: orange; - } - .custom-js { - color: blue; - } + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - utilities.css + - input.css + + VISUALIZATION + /* input: utilities.css */ + @tailwind utilities; + #1 ------------------- + #2 ------------------- + #3 ------------------- + #5 ------------------- + #6 ------------------- + + /* input: input.css */ + @import './utilities.css'; + @plugin "./plugin.js"; + @utility custom { + color: orange; + #4 ------------- + } + /* output */ + .flex { + #1 ------ + display: flex; + #2 ------------- + } + .custom { + #3 -------- + color: orange; + #4 ------------- + } + .custom-js { + #5 ----------- + color: blue; + #6 ----------- + } + " `) }) -test('utilities have source maps pointing to the utilities node', async ({ expect }) => { - let { sources, annotations } = await run({ +test('utilities have source maps pointing to the utilities node', async () => { + let visualized = await run({ input: `@tailwind utilities;`, candidates: [ // @@ -358,17 +1508,29 @@ test('utilities have source maps pointing to the utilities node', async ({ expec ], }) - expect(sources).toEqual(['input.css']) + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css - expect(annotations).toEqual([ - // - 'input.css: 1:0-11 <- 1:0-19', - 'input.css: 2:2-33 <- 1:0-19', - ]) + VISUALIZATION + /* input: input.css */ + @tailwind utilities; + #1 ------------------- + #2 ------------------- + /* output */ + .underline { + #1 ----------- + text-decoration-line: underline; + #2 ------------------------------- + } + + " + `) }) -test('@apply generates source maps', async ({ expect }) => { - let { sources, annotations } = await run({ +test('@apply generates source maps', async () => { + let visualized = await run({ input: css` .foo { color: blue; @@ -379,43 +1541,462 @@ test('@apply generates source maps', async ({ expect }) => { `, }) - expect(sources).toEqual(['input.css']) - - expect(annotations).toEqual([ - 'input.css: 1:0-5 <- 1:0-5', - 'input.css: 2:2-13 <- 2:2-13', - 'input.css: 3:2-13 <- 3:9-20', - 'input.css: 4:2-10 <- 3:21-38', - 'input.css: 5:4-26 <- 3:21-38', - 'input.css: 6:6-17 <- 3:21-38', - 'input.css: 9:2-33 <- 4:9-18', - 'input.css: 10:2-12 <- 5:2-12', - ]) + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo { + #1 ----- + color: blue; + #2 ----------- + @apply text-[#000] hover:text-[#f00]; + #3 ----------- + #4 ----------------- + #5 ----------------- + #6 ----------------- + @apply underline; + #7 --------- + color: red; + #8 ---------- + } + /* output */ + .foo { + #1 ----- + color: blue; + #2 ----------- + color: #000; + #3 ----------- + &:hover { + #4 -------- + @media (hover: hover) { + #5 ---------------------- + color: #f00; + #6 ----------- + } + } + text-decoration-line: underline; + #7 ------------------------------- + color: red; + #8 ---------- + } + + " + `) }) -test('license comments preserve source locations', async ({ expect }) => { - let { sources, annotations } = await run({ +test('license comments preserve source locations', async () => { + let visualized = await run({ input: `/*! some comment */`, }) - expect(sources).toEqual(['input.css']) + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css - expect(annotations).toEqual([ - // - 'input.css: 1:0-19 <- 1:0-19', - ]) + VISUALIZATION + /* input: input.css */ + /*! some comment */ + #1 ------------------- + /* output */ + /*! some comment */ + #1 ------------------- + " + `) }) -test('license comments with new lines preserve source locations', async ({ expect }) => { - let { sources, annotations, css } = await run({ +test('license comments with new lines preserve source locations', async () => { + let visualized = await run({ input: `/*! some \n comment */`, }) - expect(sources).toEqual(['input.css']) + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + /*! some + #1 ^ + comment */ + #1 ----------- + /* output */ + /*! some + #1 + comment */ + #2 ^ + " + `) +}) + +test('comment, single line', async () => { + let visualized = await analyze({ + input: `/*! foo */`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + /*! foo */ + #1 ---------- + /* output */ + /*! foo */ + #1 ---------- + + " + `) +}) + +test('comment, multi line', async () => { + let visualized = await analyze({ + input: `/*! foo \n bar */`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + /*! foo + #1 ^ + bar */ + #1 ------- + /* output */ + /*! foo + #1 + bar */ + #2 ^ + + " + `) +}) + +test('declaration, normal property, single line', async () => { + let visualized = await analyze({ + input: `.foo { color: red; }`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo { color: red; } + #1 ----- + #2 ---------- + /* output */ + .foo { + #1 ----- + color: red; + #2 ---------- + } + + " + `) +}) + +test('declaration, normal property, multi line', async () => { + // Works, no changes needed + let visualized = await analyze({ + input: dedent` + .foo { + grid-template-areas: + "a b c" + "d e f" + "g h i"; + } + `, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo { + #1 ----- + grid-template-areas: + #2 ^ + "a b c" + #2 ^ + "d e f" + #3 ^ + "g h i"; + #3 ----------- + } + /* output */ + .foo { + #1 ----- + grid-template-areas: "a b c" "d e f" "g h i"; + #2 + #3 + #4 ^ + } + + " + `) +}) + +test('declaration, custom property, single line', async () => { + let visualized = await analyze({ + input: `.foo { --foo: bar; }`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo { --foo: bar; } + #1 ----- + #2 ---------- + /* output */ + .foo { + #1 ----- + --foo: bar; + #2 ---------- + } + + " + `) +}) + +test('declaration, custom property, multi line', async () => { + let visualized = await analyze({ + input: dedent` + .foo { + --foo: bar\nbaz; + } + `, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo { + #1 ----- + --foo: bar + #2 ^ + baz; + #2 --- + } + /* output */ + .foo { + #1 ----- + --foo: bar + #2 + baz; + #3 ^ + } + + " + `) +}) + +test('at rules, bodyless, single line', async () => { + // This intentionally has extra spaces + let visualized = await analyze({ + input: `@layer foo, bar;`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + @layer foo, bar; + #1 ------------------- + /* output */ + @layer foo, bar; + #1 --------------- + + " + `) +}) + +test('at rules, bodyless, multi line', async () => { + let visualized = await analyze({ + input: dedent` + @layer + foo, + bar + ; + `, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + @layer + #1 ^ + foo, + #1 ^ + bar + #2 ^ + ; + #2 ^ + /* output */ + @layer foo, bar; + #1 + #2 --------------- + + " + `) +}) + +test('at rules, body, single line', async () => { + let visualized = await analyze({ + input: `@layer foo { color: red; }`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + @layer foo { color: red; } + #1 ----------- + #2 ---------- + /* output */ + @layer foo { + #1 ----------- + color: red; + #2 ---------- + } + + " + `) +}) + +test('at rules, body, multi line', async () => { + let visualized = await analyze({ + input: dedent` + @layer + foo + { + color: baz; + } + `, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + @layer + #1 ^ + foo + #1 ^ + { + #2 ^ + color: baz; + #2 ---------- + } + /* output */ + @layer foo { + #1 + #2 ^ + color: baz; + #2 ---------- + } + + " + `) +}) - expect(annotations).toEqual([ - // - 'input.css: 1:0 <- 1:0-2:0', - 'input.css: 2:11 <- 2:11', - ]) +test('style rules, body, single line', async () => { + let visualized = await analyze({ + input: `.foo:is(.bar) { color: red; }`, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo:is(.bar) { color: red; } + #1 -------------- + #2 ---------- + /* output */ + .foo:is(.bar) { + #1 -------------- + color: red; + #2 ---------- + } + + " + `) +}) + +test('style rules, body, multi line', async () => { + // Works, no changes needed + let visualized = await analyze({ + input: dedent` + .foo:is( + .bar + ) { + color: red; + } + `, + }) + + expect(visualized).toMatchInlineSnapshot(` + " + SOURCES + - input.css + + VISUALIZATION + /* input: input.css */ + .foo:is( + #1 ^ + .bar + #1 ^ + ) { + #2 -- + color: red; + #3 ---------- + } + /* output */ + .foo:is( .bar ) { + #1 + #2 ---------------- + color: red; + #3 ---------- + } + + " + `) }) diff --git a/packages/tailwindcss/src/source-maps/visualizer.ts b/packages/tailwindcss/src/source-maps/visualizer.ts new file mode 100644 index 000000000000..9401d3f39327 --- /dev/null +++ b/packages/tailwindcss/src/source-maps/visualizer.ts @@ -0,0 +1,120 @@ +import { DefaultMap } from '../utils/default-map' +import type { DecodedMapping, DecodedSource, DecodedSourceMap } from './source-map' + +interface RangeMapping { + id: number + source: DecodedSource + line: number + start: number + end: number | null +} + +export function visualize(generated: string, map: DecodedSourceMap) { + let outputSource: DecodedSource = { + url: 'output.css', + content: generated, + ignore: false, + } + + // Group mappings by source file + let bySource = new DefaultMap>( + () => new DefaultMap(() => []), + ) + + let mappingIds = new Map() + + let nextId = 1 + for (let mapping of map.mappings) { + let pos = mapping.originalPosition + if (!pos) continue + let source = pos.source + if (!source) continue + bySource.get(source).get(pos.line).push(mapping) + mappingIds.set(mapping, nextId++) + } + + for (let mapping of map.mappings) { + let pos = mapping.generatedPosition + if (!pos) continue + bySource.get(outputSource).get(pos.line).push(mapping) + } + + let maxIdSize = Math.ceil(Math.log10(Math.max(...mappingIds.values()))) + // `#number ` + let gutterSize = 3 + maxIdSize + + let output = '' + output += '\n' + output += 'SOURCES\n' + + for (let source of bySource.keys()) { + if (source === outputSource) continue + output += '- ' + output += source.url ?? 'unknown' + output += '\n' + } + + output += '\n' + output += 'VISUALIZATION\n' + + for (let [source, byLine] of bySource) { + if (!source.content) continue + + output += ' '.repeat(gutterSize) + output += `/* ` + if (source === outputSource) { + output += `output` + } else { + output += `input: ` + output += source.url ?? 'unknown' + } + output += ` */\n` + + let lines = source.content.split('\n').entries() + for (let [lineNum, line] of lines) { + output += ' '.repeat(gutterSize) + output += line + output += '\n' + + let pairs: DecodedMapping[][] = [] + + // Get all mappings for this line + let lineMappings = byLine.get(lineNum + 1) + + // Group consecutive mappings into pairs + for (let i = 0; i < lineMappings.length; i += 2) { + let pair = [lineMappings[i]] + if (i + 1 < lineMappings.length) { + pair.push(lineMappings[i + 1]) + } + + pairs.push(pair) + } + + for (let [start, end] of pairs) { + let id = mappingIds.get(start) + if (!id) continue + + let startPos = source === outputSource ? start.generatedPosition : start.originalPosition + if (!startPos) continue + + let endPos = source === outputSource ? end?.generatedPosition : end?.originalPosition + + output += '#' + output += `${Math.floor((id + 1) / 2)}`.padEnd(maxIdSize, ' ') + output += ' ' + output += ' '.repeat(startPos.column) + + if (endPos) { + output += '-'.repeat(Math.max(0, endPos.column - startPos.column)) + } else { + output += '^' + } + + output += '\n' + } + } + } + + return output +} From be8cc9538c347e3a4510a8b84939f6b414bc62d0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 7 May 2025 18:53:04 -0400 Subject: [PATCH 41/41] wip --- .../src/source-maps/translation-map.test.ts | 279 ------------------ 1 file changed, 279 deletions(-) delete mode 100644 packages/tailwindcss/src/source-maps/translation-map.test.ts diff --git a/packages/tailwindcss/src/source-maps/translation-map.test.ts b/packages/tailwindcss/src/source-maps/translation-map.test.ts deleted file mode 100644 index ad0a9f435154..000000000000 --- a/packages/tailwindcss/src/source-maps/translation-map.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -import dedent from 'dedent' -import { assert, expect, test } from 'vitest' -import { toCss, type AstNode } from '../ast' -import * as CSS from '../css-parser' -import { createTranslationMap } from './source-map' - -async function analyze(input: string) { - let ast = CSS.parse(input, { from: 'input.css' }) - let css = toCss(ast, true) - let translate = createTranslationMap({ - original: input, - generated: css, - }) - - function format(node: AstNode) { - let lines: string[] = [] - - for (let [oStart, oEnd, gStart, gEnd] of translate(node)) { - let src = `${oStart.line}:${oStart.column}-${oEnd.line}:${oEnd.column}` - - let dst = '(none)' - - if (gStart && gEnd) { - dst = `${gStart.line}:${gStart.column}-${gEnd.line}:${gEnd.column}` - } - - lines.push(`${dst} <- ${src}`) - } - - return lines - } - - return { ast, css, format } -} - -test('comment, single line', async () => { - let { ast, css, format } = await analyze(`/*! foo */`) - - assert(ast[0].kind === 'comment') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:10 <- 1:0-1:10", - ] - `) - - expect(css).toMatchInlineSnapshot(` - "/*! foo */ - " - `) -}) - -test('comment, multi line', async () => { - let { ast, css, format } = await analyze(`/*! foo \n bar */`) - - assert(ast[0].kind === 'comment') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-2:7 <- 1:0-2:7", - ] - `) - - expect(css).toMatchInlineSnapshot(` - "/*! foo - bar */ - " - `) -}) - -test('declaration, normal property, single line', async () => { - let { ast, css, format } = await analyze(`.foo { color: red; }`) - - assert(ast[0].kind === 'rule') - assert(ast[0].nodes[0].kind === 'declaration') - expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - [ - "2:2-2:12 <- 1:7-1:17", - ] - `) - - expect(css).toMatchInlineSnapshot(` - ".foo { - color: red; - } - " - `) -}) - -test('declaration, normal property, multi line', async () => { - // Works, no changes needed - let { ast, css, format } = await analyze(dedent` - .foo { - grid-template-areas: - "a b c" - "d e f" - "g h i"; - } - `) - - assert(ast[0].kind === 'rule') - assert(ast[0].nodes[0].kind === 'declaration') - expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - [ - "2:2-2:46 <- 2:2-5:11", - ] - `) - - expect(css).toMatchInlineSnapshot(` - ".foo { - grid-template-areas: "a b c" "d e f" "g h i"; - } - " - `) -}) - -test('declaration, custom property, single line', async () => { - let { ast, css, format } = await analyze(`.foo { --foo: bar; }`) - - assert(ast[0].kind === 'rule') - assert(ast[0].nodes[0].kind === 'declaration') - expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - [ - "2:2-2:12 <- 1:7-1:17", - ] - `) - - expect(css).toMatchInlineSnapshot(` - ".foo { - --foo: bar; - } - " - `) -}) - -test('declaration, custom property, multi line', async () => { - let { ast, css, format } = await analyze(dedent` - .foo { - --foo: bar\nbaz; - } - `) - - assert(ast[0].kind === 'rule') - assert(ast[0].nodes[0].kind === 'declaration') - expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(` - [ - "2:2-3:3 <- 2:2-3:3", - ] - `) - - expect(css).toMatchInlineSnapshot(` - ".foo { - --foo: bar - baz; - } - " - `) -}) - -test('at rules, bodyless, single line', async () => { - // This intentionally has extra spaces - let { ast, css, format } = await analyze(`@layer foo, bar;`) - - assert(ast[0].kind === 'at-rule') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:15 <- 1:0-1:19", - ] - `) - - expect(css).toMatchInlineSnapshot(` - "@layer foo, bar; - " - `) -}) - -test('at rules, bodyless, multi line', async () => { - let { ast, css, format } = await analyze(dedent` - @layer - foo, - bar - ; - `) - - assert(ast[0].kind === 'at-rule') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:15 <- 1:0-4:0", - ] - `) - - expect(css).toMatchInlineSnapshot(` - "@layer foo, bar; - " - `) -}) - -test('at rules, body, single line', async () => { - let { ast, css, format } = await analyze(`@layer foo { color: red; }`) - - assert(ast[0].kind === 'at-rule') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:11 <- 1:0-1:11", - ] - `) - - expect(css).toMatchInlineSnapshot(` - "@layer foo { - color: red; - } - " - `) -}) - -test('at rules, body, multi line', async () => { - let { ast, css, format } = await analyze(dedent` - @layer - foo - { - color: baz; - } - `) - - assert(ast[0].kind === 'at-rule') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:11 <- 1:0-3:0", - ] - `) - - expect(css).toMatchInlineSnapshot(` - "@layer foo { - color: baz; - } - " - `) -}) - -test('style rules, body, single line', async () => { - let { ast, css, format } = await analyze(`.foo:is(.bar) { color: red; }`) - - assert(ast[0].kind === 'rule') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:14 <- 1:0-1:14", - ] - `) - - expect(css).toMatchInlineSnapshot(` - ".foo:is(.bar) { - color: red; - } - " - `) -}) - -test('style rules, body, multi line', async () => { - // Works, no changes needed - let { ast, css, format } = await analyze(dedent` - .foo:is( - .bar - ) { - color: red; - } - `) - - assert(ast[0].kind === 'rule') - expect(format(ast[0])).toMatchInlineSnapshot(` - [ - "1:0-1:16 <- 1:0-3:2", - ] - `) - - expect(css).toMatchInlineSnapshot(` - ".foo:is( .bar ) { - color: red; - } - " - `) -})