diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index 7a263ba92..3e81195ee 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 201dd4bf8..a2e1535e8 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 79e51d2aa..8a37f542c 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),