Skip to content

Commit 00358d0

Browse files
committed
Add source map support to PostCSS
1 parent bb78c59 commit 00358d0

3 files changed

Lines changed: 132 additions & 6 deletions

File tree

integrations/postcss/index.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,3 +712,68 @@ test(
712712
await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual(''))
713713
},
714714
)
715+
716+
test(
717+
'dev mode + source maps',
718+
{
719+
fs: {
720+
'package.json': json`
721+
{
722+
"dependencies": {
723+
"postcss": "^8",
724+
"postcss-cli": "^11",
725+
"tailwindcss": "workspace:^",
726+
"@tailwindcss/postcss": "workspace:^"
727+
}
728+
}
729+
`,
730+
'postcss.config.js': js`
731+
module.exports = {
732+
map: { inline: true },
733+
plugins: {
734+
'@tailwindcss/postcss': {},
735+
},
736+
}
737+
`,
738+
'src/index.html': html`
739+
<div class="flex"></div>
740+
`,
741+
'src/index.css': css`
742+
@import 'tailwindcss/utilities';
743+
@source not inline("inline");
744+
/* */
745+
`,
746+
},
747+
},
748+
async ({ fs, exec, expect, parseSourceMap }) => {
749+
await exec('pnpm postcss src/index.css --output dist/out.css')
750+
751+
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
752+
753+
let map = parseSourceMap(await fs.read('dist/out.css'))
754+
755+
expect(map.at(1, 0)).toMatchObject({
756+
source: '<no source>',
757+
original: '(none)',
758+
generated: '/*! tailwi...',
759+
})
760+
761+
expect(map.at(2, 0)).toMatchObject({
762+
source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
763+
original: '@tailwind...',
764+
generated: '.flex {...',
765+
})
766+
767+
expect(map.at(3, 2)).toMatchObject({
768+
source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
769+
original: '@tailwind...',
770+
generated: 'display: f...',
771+
})
772+
773+
expect(map.at(4, 0)).toMatchObject({
774+
source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
775+
original: ';...',
776+
generated: '}...',
777+
})
778+
},
779+
)

packages/@tailwindcss-postcss/src/ast.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,54 @@
11
import postcss, {
2+
Input,
23
type ChildNode as PostCssChildNode,
34
type Container as PostCssContainerNode,
45
type Root as PostCssRoot,
56
type Source as PostcssSource,
67
} from 'postcss'
78
import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast'
9+
import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table'
10+
import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source'
11+
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
812

913
const EXCLAMATION_MARK = 0x21
1014

1115
export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot {
16+
let inputMap = new DefaultMap<Source, Input>((src) => {
17+
return new Input(src.code, {
18+
map: source?.input.map,
19+
from: src.file ?? undefined,
20+
})
21+
})
22+
23+
let lineTables = new DefaultMap<Source, LineTable>((src) => createLineTable(src.code))
24+
1225
let root = postcss.root()
1326
root.source = source
1427

28+
function toSource(loc: SourceLocation | undefined): PostcssSource | undefined {
29+
// Use the fallback if this node has no location info in the AST
30+
if (!loc) return
31+
if (!loc[0]) return
32+
33+
let table = lineTables.get(loc[0])
34+
let start = table.find(loc[1])
35+
let end = table.find(loc[2])
36+
37+
return {
38+
input: inputMap.get(loc[0]),
39+
start: {
40+
line: start.line,
41+
column: start.column + 1,
42+
offset: loc[1],
43+
},
44+
end: {
45+
line: end.line,
46+
column: end.column + 1,
47+
offset: loc[2],
48+
},
49+
}
50+
}
51+
1552
function transform(node: AstNode, parent: PostCssContainerNode) {
1653
// Declaration
1754
if (node.kind === 'declaration') {
@@ -20,14 +57,14 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
2057
value: node.value ?? '',
2158
important: node.important,
2259
})
23-
astNode.source = source
60+
astNode.source = toSource(node.src)
2461
parent.append(astNode)
2562
}
2663

2764
// Rule
2865
else if (node.kind === 'rule') {
2966
let astNode = postcss.rule({ selector: node.selector })
30-
astNode.source = source
67+
astNode.source = toSource(node.src)
3168
astNode.raws.semicolon = true
3269
parent.append(astNode)
3370
for (let child of node.nodes) {
@@ -38,7 +75,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
3875
// AtRule
3976
else if (node.kind === 'at-rule') {
4077
let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params })
41-
astNode.source = source
78+
astNode.source = toSource(node.src)
4279
astNode.raws.semicolon = true
4380
parent.append(astNode)
4481
for (let child of node.nodes) {
@@ -53,7 +90,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
5390
// spaces.
5491
astNode.raws.left = ''
5592
astNode.raws.right = ''
56-
astNode.source = source
93+
astNode.source = toSource(node.src)
5794
parent.append(astNode)
5895
}
5996

@@ -75,33 +112,56 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
75112
}
76113

77114
export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
115+
let inputMap = new DefaultMap<Input, Source>((input) => ({
116+
file: input.file ?? input.id ?? null,
117+
code: input.css,
118+
}))
119+
120+
function toSource(node: PostCssChildNode): SourceLocation | undefined {
121+
let source = node.source
122+
if (!source) return
123+
124+
let input = source.input
125+
if (!input) return
126+
if (source.start === undefined) return
127+
if (source.end === undefined) return
128+
129+
return [inputMap.get(input), source.start.offset, source.end.offset]
130+
}
131+
78132
function transform(
79133
node: PostCssChildNode,
80134
parent: Extract<AstNode, { nodes: AstNode[] }>['nodes'],
81135
) {
82136
// Declaration
83137
if (node.type === 'decl') {
84-
parent.push(decl(node.prop, node.value, node.important))
138+
let astNode = decl(node.prop, node.value, node.important)
139+
astNode.src = toSource(node)
140+
parent.push(astNode)
85141
}
86142

87143
// Rule
88144
else if (node.type === 'rule') {
89145
let astNode = rule(node.selector)
146+
astNode.src = toSource(node)
90147
node.each((child) => transform(child, astNode.nodes))
91148
parent.push(astNode)
92149
}
93150

94151
// AtRule
95152
else if (node.type === 'atrule') {
96153
let astNode = atRule(`@${node.name}`, node.params)
154+
astNode.src = toSource(node)
97155
node.each((child) => transform(child, astNode.nodes))
98156
parent.push(astNode)
99157
}
100158

101159
// Comment
102160
else if (node.type === 'comment') {
103161
if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return
104-
parent.push(comment(node.text))
162+
let astNode = comment(node.text)
163+
astNode.src = toSource(node)
164+
parent.push(astNode)
105165
}
106166

107167
// Unknown

packages/@tailwindcss-postcss/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
121121

122122
DEBUG && I.start('Create compiler')
123123
let compiler = await compileAst(ast, {
124+
from: result.opts.from,
124125
base: inputBasePath,
125126
shouldRewriteUrls: true,
126127
onDependency: (path) => context.fullRebuildPaths.push(path),

0 commit comments

Comments
 (0)