Skip to content

Commit 366ee81

Browse files
committed
scss support
1 parent 7971167 commit 366ee81

File tree

5 files changed

+484
-10
lines changed

5 files changed

+484
-10
lines changed

packages/@tailwindcss-postcss/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@tailwindcss/node": "workspace:*",
3535
"@tailwindcss/oxide": "workspace:*",
3636
"postcss": "^8.4.41",
37+
"source-map-js": "^1.2.1",
3738
"tailwindcss": "workspace:*"
3839
},
3940
"devDependencies": {

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

Lines changed: 328 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
1+
import path from 'node:path'
12
import type * as postcss from 'postcss'
3+
import { SourceMapConsumer, type RawSourceMap } from 'source-map-js'
24
import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast'
35
import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table'
46
import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source'
57
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
68

79
const EXCLAMATION_MARK = 0x21
10+
const DEBUG =
11+
process.env.DEBUG?.includes('@tailwindcss/postcss') || process.env.DEBUG?.includes('tailwindcss')
812

913
export function cssAstToPostCssAst(
1014
postcss: postcss.Postcss,
1115
ast: AstNode[],
1216
source?: postcss.Source,
1317
): postcss.Root {
1418
let inputMap = new DefaultMap<Source, postcss.Input>((src) => {
19+
let map: postcss.Result['map'] | undefined
20+
21+
if (source?.input.map && typeof source.input.map.toJSON === 'function') {
22+
let sources = source.input.map.toJSON().sources ?? []
23+
let file = src.file ?? undefined
24+
25+
let shouldUseMap = false
26+
if (!file) {
27+
shouldUseMap = true
28+
} else if (sources.includes(file)) {
29+
shouldUseMap = true
30+
} else {
31+
let fileName = path.basename(file)
32+
shouldUseMap = sources.some((sourceFile) => path.basename(sourceFile) === fileName)
33+
}
34+
35+
if (shouldUseMap) {
36+
map = source.input.map
37+
}
38+
}
39+
1540
return new postcss.Input(src.code, {
16-
map: source?.input.map,
41+
map,
1742
from: src.file ?? undefined,
1843
})
1944
})
@@ -124,10 +149,211 @@ export function cssAstToPostCssAst(
124149
}
125150

126151
export function postCssAstToCssAst(root: postcss.Root): AstNode[] {
127-
let inputMap = new DefaultMap<postcss.Input, Source>((input) => ({
128-
file: input.file ?? input.id ?? null,
129-
code: input.css,
130-
}))
152+
function getRawMap(input: postcss.Input): RawSourceMap | null {
153+
let map = input.map
154+
if (!map) return null
155+
156+
if (typeof map.toJSON === 'function') {
157+
return map.toJSON() as RawSourceMap
158+
}
159+
160+
if ('sources' in map) {
161+
return map as RawSourceMap
162+
}
163+
164+
if (typeof map === 'object' && map !== null && 'text' in map) {
165+
let text = (map as { text?: unknown }).text
166+
if (typeof text === 'string') {
167+
try {
168+
return JSON.parse(text) as RawSourceMap
169+
} catch {
170+
// Ignore invalid JSON
171+
}
172+
} else if (text && typeof text === 'object' && 'sources' in (text as object)) {
173+
return text as RawSourceMap
174+
}
175+
}
176+
177+
DEBUG &&
178+
console.warn('[tw-postcss:sourcemap] unrecognized input.map', {
179+
inputFile: input.file ?? input.id ?? null,
180+
mapType: typeof map,
181+
mapKeys: typeof map === 'object' && map !== null ? Object.keys(map as object) : null,
182+
})
183+
184+
return null
185+
}
186+
187+
let rawMapCache = new DefaultMap<postcss.Input, RawSourceMap | null>((input) => getRawMap(input))
188+
189+
let consumerCache = new DefaultMap<postcss.Input, SourceMapConsumer | null>((input) => {
190+
let rawMap = rawMapCache.get(input)
191+
if (!rawMap) return null
192+
return new SourceMapConsumer(rawMap)
193+
})
194+
195+
function normalizeSourceName(source: string) {
196+
let cleaned = source
197+
let queryIndex = cleaned.indexOf('?')
198+
if (queryIndex !== -1) cleaned = cleaned.slice(0, queryIndex)
199+
let hashIndex = cleaned.indexOf('#')
200+
if (hashIndex !== -1) cleaned = cleaned.slice(0, hashIndex)
201+
202+
if (cleaned.startsWith('file://')) {
203+
try {
204+
return decodeURIComponent(new URL(cleaned).pathname)
205+
} catch {
206+
return cleaned.slice('file://'.length)
207+
}
208+
}
209+
if (/^[a-z]+:\/\//i.test(cleaned)) {
210+
cleaned = cleaned.replace(/^[a-z]+:\/\//i, '')
211+
cleaned = cleaned.replace(/^\/+/, '')
212+
}
213+
214+
cleaned = cleaned.replace(/\/\.(?=\/)/g, '')
215+
216+
if (cleaned.startsWith('./')) cleaned = cleaned.slice(2)
217+
218+
return cleaned
219+
}
220+
221+
let sourcesContentCache = new DefaultMap<postcss.Input, Map<string, string | null>>((input) => {
222+
let map = new Map<string, string | null>()
223+
224+
let consumer = consumerCache.get(input)
225+
if (consumer) {
226+
for (let source of consumer.sources) {
227+
let content: string | null
228+
try {
229+
content = consumer.sourceContentFor(source, true) ?? null
230+
} catch {
231+
content = null
232+
}
233+
map.set(source, content)
234+
}
235+
return map
236+
}
237+
238+
let rawMap = rawMapCache.get(input)
239+
let sources = rawMap?.sources ?? []
240+
let contents = rawMap?.sourcesContent ?? []
241+
242+
for (let i = 0; i < sources.length; i++) {
243+
map.set(sources[i], contents[i] ?? null)
244+
}
245+
246+
return map
247+
})
248+
249+
let normalizedSourcesCache = new DefaultMap<postcss.Input, Map<string, string>>((input) => {
250+
let map = new Map<string, string>()
251+
252+
let consumer = consumerCache.get(input)
253+
let sources = consumer?.sources ?? rawMapCache.get(input)?.sources ?? []
254+
255+
for (let source of sources) {
256+
let normalized = normalizeSourceName(source)
257+
map.set(source, source)
258+
map.set(normalized, source)
259+
map.set(path.basename(source), source)
260+
map.set(path.basename(normalized), source)
261+
}
262+
263+
return map
264+
})
265+
266+
function resolveSourceContent(input: postcss.Input, file: string) {
267+
let rawMap = rawMapCache.get(input)
268+
if (!rawMap) {
269+
DEBUG &&
270+
console.warn('[tw-postcss:sourcemap] missing raw map', {
271+
file,
272+
inputFile: input.file ?? input.id ?? null,
273+
})
274+
return {
275+
sourceName: file,
276+
content: null as string | null,
277+
}
278+
}
279+
280+
let normalizedSources = normalizedSourcesCache.get(input)
281+
let matchedSource = normalizedSources.get(file)
282+
if (!matchedSource) {
283+
let normalized = normalizeSourceName(file)
284+
matchedSource =
285+
normalizedSources.get(normalized) ??
286+
normalizedSources.get(path.basename(normalized)) ??
287+
normalizedSources.get(path.basename(file))
288+
289+
if (!matchedSource) {
290+
for (let [key, value] of normalizedSources) {
291+
if (key.endsWith(normalized) || normalized.endsWith(key)) {
292+
matchedSource = value
293+
break
294+
}
295+
}
296+
}
297+
}
298+
299+
let sourceName = matchedSource ?? file
300+
let content = sourcesContentCache.get(input).get(sourceName) ?? null
301+
302+
if (input.file) {
303+
let inputBase = path.basename(input.file)
304+
let sourceBase = path.basename(sourceName)
305+
306+
if (inputBase === sourceBase) {
307+
let normalizedSource = normalizeSourceName(sourceName)
308+
if (normalizedSource === sourceBase) {
309+
sourceName = input.file
310+
}
311+
}
312+
}
313+
314+
if (DEBUG) {
315+
let normalized = normalizeSourceName(file)
316+
let sources = consumerCache.get(input)?.sources ?? rawMap.sources
317+
console.warn('[tw-postcss:sourcemap] resolve', {
318+
file,
319+
normalized,
320+
matchedSource: matchedSource ?? null,
321+
sourceName,
322+
hasContent: content !== null,
323+
sourcesCount: sources.length,
324+
})
325+
}
326+
327+
return {
328+
sourceName,
329+
content,
330+
}
331+
}
332+
333+
let sourceObjectCache = new DefaultMap<postcss.Input, DefaultMap<string, Source>>(
334+
(input) =>
335+
new DefaultMap((file) => {
336+
let { sourceName, content } = resolveSourceContent(input, file)
337+
return {
338+
file: sourceName,
339+
code: content ?? input.css,
340+
}
341+
}),
342+
)
343+
344+
let inputMap = new DefaultMap<postcss.Input, Source>((input) => {
345+
let file = input.file ?? input.id ?? null
346+
347+
let rawMap = rawMapCache.get(input)
348+
if (rawMap && rawMap.sources.length > 0) {
349+
file = rawMap.sources[0]
350+
}
351+
352+
return {
353+
file,
354+
code: input.css,
355+
}
356+
})
131357

132358
function toSource(node: postcss.ChildNode): SourceLocation | undefined {
133359
let source = node.source
@@ -138,6 +364,103 @@ export function postCssAstToCssAst(root: postcss.Root): AstNode[] {
138364
if (source.start === undefined) return
139365
if (source.end === undefined) return
140366

367+
let consumer = consumerCache.get(input)
368+
369+
if (DEBUG && !consumer) {
370+
let rawMap = rawMapCache.get(input)
371+
console.warn('[tw-postcss:sourcemap] no consumer', {
372+
inputFile: input.file ?? input.id ?? null,
373+
hasRawMap: rawMap !== null,
374+
sourcesCount: rawMap?.sources?.length ?? 0,
375+
sourcesContentCount: rawMap?.sourcesContent?.length ?? 0,
376+
sourceRoot: rawMap?.sourceRoot ?? null,
377+
})
378+
}
379+
380+
if (consumer) {
381+
let start = consumer.originalPositionFor(
382+
{
383+
line: source.start.line,
384+
column: Math.max(source.start.column - 1, 0),
385+
},
386+
SourceMapConsumer.LEAST_UPPER_BOUND,
387+
)
388+
let end = consumer.originalPositionFor(
389+
{
390+
line: source.end.line,
391+
column: Math.max(source.end.column - 1, 0),
392+
},
393+
SourceMapConsumer.GREATEST_LOWER_BOUND,
394+
)
395+
396+
if (!end.source) {
397+
let endUpper = consumer.originalPositionFor(
398+
{
399+
line: source.end.line,
400+
column: Math.max(source.end.column - 1, 0),
401+
},
402+
SourceMapConsumer.LEAST_UPPER_BOUND,
403+
)
404+
405+
if (endUpper.source) {
406+
end = endUpper
407+
}
408+
}
409+
410+
if (!start.source) {
411+
let startLower = consumer.originalPositionFor(
412+
{
413+
line: source.start.line,
414+
column: Math.max(source.start.column - 1, 0),
415+
},
416+
SourceMapConsumer.GREATEST_LOWER_BOUND,
417+
)
418+
419+
if (startLower.source) {
420+
start = startLower
421+
}
422+
}
423+
424+
if (start.source && !end.source) {
425+
end = start
426+
} else if (!start.source && end.source) {
427+
start = end
428+
}
429+
430+
if (DEBUG && (!start.source || !end.source)) {
431+
let rawMap = rawMapCache.get(input)
432+
console.warn('[tw-postcss:sourcemap] missing original source', {
433+
inputFile: input.file ?? input.id ?? null,
434+
start,
435+
end,
436+
sourcesCount: rawMap?.sources?.length ?? 0,
437+
sourcesContentCount: rawMap?.sourcesContent?.length ?? 0,
438+
sourceRoot: rawMap?.sourceRoot ?? null,
439+
})
440+
}
441+
442+
if (start.source && end.source) {
443+
let file = start.source
444+
let sourceObject = sourceObjectCache.get(input).get(file)
445+
let table = createLineTable(sourceObject.code)
446+
447+
let startOffset = table.findOffset({
448+
line: start.line ?? 1,
449+
column: start.column ?? 0,
450+
})
451+
let endOffset = table.findOffset({
452+
line: end.line ?? 1,
453+
column: end.column ?? 0,
454+
})
455+
456+
if (endOffset < startOffset) {
457+
endOffset = startOffset
458+
}
459+
460+
return [sourceObject, startOffset, endOffset]
461+
}
462+
}
463+
141464
return [inputMap.get(input), source.start.offset, source.end.offset]
142465
}
143466

0 commit comments

Comments
 (0)