1+ import path from 'node:path'
12import type * as postcss from 'postcss'
3+ import { SourceMapConsumer , type RawSourceMap } from 'source-map-js'
24import { atRule , comment , decl , rule , type AstNode } from '../../tailwindcss/src/ast'
35import { createLineTable , type LineTable } from '../../tailwindcss/src/source-maps/line-table'
46import type { Source , SourceLocation } from '../../tailwindcss/src/source-maps/source'
57import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
68
79const EXCLAMATION_MARK = 0x21
10+ const DEBUG =
11+ process . env . DEBUG ?. includes ( '@tailwindcss/postcss' ) || process . env . DEBUG ?. includes ( 'tailwindcss' )
812
913export 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
126151export 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