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
+
1
6
export interface Processed {
2
7
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.
4
9
dependencies ?: string [ ] ;
5
10
}
6
11
@@ -37,12 +42,18 @@ function parse_attributes(str: string) {
37
42
interface Replacement {
38
43
offset : number ;
39
44
length : number ;
40
- replacement : string ;
45
+ replacement : StringWithSourcemap ;
41
46
}
42
47
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 > {
44
55
const replacements : Array < Promise < Replacement > > = [ ] ;
45
- str . replace ( re , ( ...args ) => {
56
+ source . replace ( re , ( ...args ) => {
46
57
replacements . push (
47
58
func ( ...args ) . then (
48
59
res =>
@@ -55,16 +66,55 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
55
66
) ;
56
67
return '' ;
57
68
} ) ;
58
- let out = '' ;
69
+ const out = new StringWithSourcemap ( ) ;
59
70
let last_end = 0 ;
60
71
for ( const { offset, length, replacement } of await Promise . all (
61
72
replacements
62
73
) ) {
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 ) ;
64
78
last_end = offset + length ;
65
79
}
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 ) ;
68
118
}
69
119
70
120
export default async function preprocess (
@@ -76,60 +126,92 @@ export default async function preprocess(
76
126
const filename = ( options && options . filename ) || preprocessor . filename ; // legacy
77
127
const dependencies = [ ] ;
78
128
79
- const preprocessors = Array . isArray ( preprocessor ) ? preprocessor : [ preprocessor ] ;
129
+ const preprocessors = preprocessor
130
+ ? Array . isArray ( preprocessor ) ? preprocessor : [ preprocessor ]
131
+ : [ ] ;
80
132
81
133
const markup = preprocessors . map ( p => p . markup ) . filter ( Boolean ) ;
82
134
const script = preprocessors . map ( p => p . script ) . filter ( Boolean ) ;
83
135
const style = preprocessors . map ( p => p . style ) . filter ( Boolean ) ;
84
136
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
+
85
144
for ( const fn of markup ) {
145
+
146
+ // run markup preprocessor
86
147
const processed = await fn ( {
87
148
content : source ,
88
149
filename
89
150
} ) ;
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
+ }
92
163
}
93
164
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 ,
96
173
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 ) ) ;
99
179
if ( ! attributes && ! content ) {
100
- return match ;
180
+ return no_change ( ) ;
101
181
}
102
182
attributes = attributes || '' ;
103
- const processed = await fn ( {
183
+ content = content || '' ;
184
+
185
+ // run script preprocessor
186
+ const processed = await preprocessor ( {
104
187
content,
105
188
attributes : parse_attributes ( attributes ) ,
106
189
filename
107
190
} ) ;
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 } >` ) ;
110
195
}
111
196
) ;
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 ) ;
112
203
}
113
204
114
205
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 ) ;
131
207
}
132
208
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
+
133
215
return {
134
216
// TODO return separated output, in future version where svelte.compile supports it:
135
217
// style: { code: styleCode, map: styleMap },
@@ -138,7 +220,7 @@ export default async function preprocess(
138
220
139
221
code : source ,
140
222
dependencies : [ ...new Set ( dependencies ) ] ,
141
-
223
+ map : ( map as object ) ,
142
224
toString ( ) {
143
225
return source ;
144
226
}
0 commit comments