1+ import { RawSourceMap , DecodedSourceMap } from '@ampproject/remapping/dist/types/types' ;
2+ import { decode as decode_mappings } from 'sourcemap-codec' ;
3+ import { getLocator } from 'locate-character' ;
4+ import { StringWithSourcemap , sourcemap_add_offset , combine_sourcemaps } from '../utils/string_with_sourcemap' ;
5+
16export interface Processed {
27 code : string ;
3- map ?: object | string ;
8+ map ?: string | object ; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
49 dependencies ?: string [ ] ;
510}
611
@@ -37,12 +42,18 @@ function parse_attributes(str: string) {
3742interface Replacement {
3843 offset : number ;
3944 length : number ;
40- replacement : string ;
45+ replacement : StringWithSourcemap ;
4146}
4247
43- async function replace_async ( str : string , re : RegExp , func : ( ...any ) => Promise < string > ) {
48+ async function replace_async (
49+ filename : string ,
50+ source : string ,
51+ get_location : ReturnType < typeof getLocator > ,
52+ re : RegExp ,
53+ func : ( ...any ) => Promise < StringWithSourcemap >
54+ ) : Promise < StringWithSourcemap > {
4455 const replacements : Array < Promise < Replacement > > = [ ] ;
45- str . replace ( re , ( ...args ) => {
56+ source . replace ( re , ( ...args ) => {
4657 replacements . push (
4758 func ( ...args ) . then (
4859 res =>
@@ -55,16 +66,55 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
5566 ) ;
5667 return '' ;
5768 } ) ;
58- let out = '' ;
69+ const out = new StringWithSourcemap ( ) ;
5970 let last_end = 0 ;
6071 for ( const { offset, length, replacement } of await Promise . all (
6172 replacements
6273 ) ) {
63- out += str . slice ( last_end , offset ) + replacement ;
74+ // content = unchanged source characters before the replaced segment
75+ const content = StringWithSourcemap . from_source (
76+ filename , source . slice ( last_end , offset ) , get_location ( last_end ) ) ;
77+ out . concat ( content ) . concat ( replacement ) ;
6478 last_end = offset + length ;
6579 }
66- out += str . slice ( last_end ) ;
67- return out ;
80+ // final_content = unchanged source characters after last replaced segment
81+ const final_content = StringWithSourcemap . from_source (
82+ filename , source . slice ( last_end ) , get_location ( last_end ) ) ;
83+ return out . concat ( final_content ) ;
84+ }
85+
86+ /**
87+ * Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
88+ */
89+ function get_replacement (
90+ filename : string ,
91+ offset : number ,
92+ get_location : ReturnType < typeof getLocator > ,
93+ original : string ,
94+ processed : Processed ,
95+ prefix : string ,
96+ suffix : string
97+ ) : StringWithSourcemap {
98+
99+ // Convert the unchanged prefix and suffix to StringWithSourcemap
100+ const prefix_with_map = StringWithSourcemap . from_source (
101+ filename , prefix , get_location ( offset ) ) ;
102+ const suffix_with_map = StringWithSourcemap . from_source (
103+ filename , suffix , get_location ( offset + prefix . length + original . length ) ) ;
104+
105+ // Convert the preprocessed code and its sourcemap to a StringWithSourcemap
106+ let decoded_map : DecodedSourceMap ;
107+ if ( processed . map ) {
108+ decoded_map = typeof processed . map === 'string' ? JSON . parse ( processed . map ) : processed . map ;
109+ if ( typeof ( decoded_map . mappings ) === 'string' ) {
110+ decoded_map . mappings = decode_mappings ( decoded_map . mappings ) ;
111+ }
112+ sourcemap_add_offset ( decoded_map , get_location ( offset + prefix . length ) ) ;
113+ }
114+ const processed_with_map = StringWithSourcemap . from_processed ( processed . code , decoded_map ) ;
115+
116+ // Surround the processed code with the prefix and suffix, retaining valid sourcemappings
117+ return prefix_with_map . concat ( processed_with_map ) . concat ( suffix_with_map ) ;
68118}
69119
70120export default async function preprocess (
@@ -76,60 +126,92 @@ export default async function preprocess(
76126 const filename = ( options && options . filename ) || preprocessor . filename ; // legacy
77127 const dependencies = [ ] ;
78128
79- const preprocessors = Array . isArray ( preprocessor ) ? preprocessor : [ preprocessor ] ;
129+ const preprocessors = preprocessor
130+ ? Array . isArray ( preprocessor ) ? preprocessor : [ preprocessor ]
131+ : [ ] ;
80132
81133 const markup = preprocessors . map ( p => p . markup ) . filter ( Boolean ) ;
82134 const script = preprocessors . map ( p => p . script ) . filter ( Boolean ) ;
83135 const style = preprocessors . map ( p => p . style ) . filter ( Boolean ) ;
84136
137+ // sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
138+ // so we use sourcemap_list.unshift() to add new maps
139+ // https://github.com/ampproject/remapping#multiple-transformations-of-a-file
140+ const sourcemap_list : Array < DecodedSourceMap | RawSourceMap > = [ ] ;
141+
142+ // TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
143+
85144 for ( const fn of markup ) {
145+
146+ // run markup preprocessor
86147 const processed = await fn ( {
87148 content : source ,
88149 filename
89150 } ) ;
90- if ( processed && processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
91- source = processed ? processed . code : source ;
151+
152+ if ( ! processed ) continue ;
153+
154+ if ( processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
155+ source = processed . code ;
156+ if ( processed . map ) {
157+ sourcemap_list . unshift (
158+ typeof ( processed . map ) === 'string'
159+ ? JSON . parse ( processed . map )
160+ : processed . map
161+ ) ;
162+ }
92163 }
93164
94- for ( const fn of script ) {
95- source = await replace_async (
165+ async function preprocess_tag_content ( tag_name : 'style' | 'script' , preprocessor : Preprocessor ) {
166+ const get_location = getLocator ( source ) ;
167+ const tag_regex = tag_name == 'style'
168+ ? / < ! - - [ ^ ] * ?- - > | < s t y l e ( \s [ ^ ] * ?) ? (?: > ( [ ^ ] * ?) < \/ s t y l e > | \/ > ) / gi
169+ : / < ! - - [ ^ ] * ?- - > | < s c r i p t ( \s [ ^ ] * ?) ? (?: > ( [ ^ ] * ?) < \/ s c r i p t > | \/ > ) / gi;
170+
171+ const res = await replace_async (
172+ filename ,
96173 source ,
97- / < ! - - [ ^ ] * ?- - > | < s c r i p t ( \s [ ^ ] * ?) ? (?: > ( [ ^ ] * ?) < \/ s c r i p t > | \/ > ) / gi,
98- async ( match , attributes = '' , content = '' ) => {
174+ get_location ,
175+ tag_regex ,
176+ async ( match , attributes = '' , content = '' , offset ) => {
177+ const no_change = ( ) => StringWithSourcemap . from_source (
178+ filename , match , get_location ( offset ) ) ;
99179 if ( ! attributes && ! content ) {
100- return match ;
180+ return no_change ( ) ;
101181 }
102182 attributes = attributes || '' ;
103- const processed = await fn ( {
183+ content = content || '' ;
184+
185+ // run script preprocessor
186+ const processed = await preprocessor ( {
104187 content,
105188 attributes : parse_attributes ( attributes ) ,
106189 filename
107190 } ) ;
108- if ( processed && processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
109- return processed ? `<script${ attributes } >${ processed . code } </script>` : match ;
191+
192+ if ( ! processed ) return no_change ( ) ;
193+ if ( processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
194+ return get_replacement ( filename , offset , get_location , content , processed , `<${ tag_name } ${ attributes } >` , `</${ tag_name } >` ) ;
110195 }
111196 ) ;
197+ source = res . string ;
198+ sourcemap_list . unshift ( res . map ) ;
199+ }
200+
201+ for ( const fn of script ) {
202+ await preprocess_tag_content ( 'script' , fn ) ;
112203 }
113204
114205 for ( const fn of style ) {
115- source = await replace_async (
116- source ,
117- / < ! - - [ ^ ] * ?- - > | < s t y l e ( \s [ ^ ] * ?) ? (?: > ( [ ^ ] * ?) < \/ s t y l e > | \/ > ) / gi,
118- async ( match , attributes = '' , content = '' ) => {
119- if ( ! attributes && ! content ) {
120- return match ;
121- }
122- const processed : Processed = await fn ( {
123- content,
124- attributes : parse_attributes ( attributes ) ,
125- filename
126- } ) ;
127- if ( processed && processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
128- return processed ? `<style${ attributes } >${ processed . code } </style>` : match ;
129- }
130- ) ;
206+ await preprocess_tag_content ( 'style' , fn ) ;
131207 }
132208
209+ // Combine all the source maps for each preprocessor function into one
210+ const map : RawSourceMap = combine_sourcemaps (
211+ filename ,
212+ sourcemap_list
213+ ) ;
214+
133215 return {
134216 // TODO return separated output, in future version where svelte.compile supports it:
135217 // style: { code: styleCode, map: styleMap },
@@ -138,7 +220,7 @@ export default async function preprocess(
138220
139221 code : source ,
140222 dependencies : [ ...new Set ( dependencies ) ] ,
141-
223+ map : ( map as object ) ,
142224 toString ( ) {
143225 return source ;
144226 }
0 commit comments