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 be 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,52 @@ 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
+ // Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
87
+ function get_replacement (
88
+ filename : string ,
89
+ offset : number ,
90
+ get_location : ReturnType < typeof getLocator > ,
91
+ original : string ,
92
+ processed : Processed ,
93
+ prefix : string ,
94
+ suffix : string
95
+ ) : StringWithSourcemap {
96
+
97
+ // Convert the unchanged prefix and suffix to StringWithSourcemap
98
+ const prefix_with_map = StringWithSourcemap . from_source (
99
+ filename , prefix , get_location ( offset ) ) ;
100
+ const suffix_with_map = StringWithSourcemap . from_source (
101
+ filename , suffix , get_location ( offset + prefix . length + original . length ) ) ;
102
+
103
+ // Convert the preprocessed code and its sourcemap to a StringWithSourcemap
104
+ let decoded_map : DecodedSourceMap ;
105
+ if ( processed . map ) {
106
+ decoded_map = typeof processed . map === 'string' ? JSON . parse ( processed . map ) : processed . map ;
107
+ if ( typeof ( decoded_map . mappings ) === 'string' )
108
+ decoded_map . mappings = decode_mappings ( decoded_map . mappings ) ;
109
+ sourcemap_add_offset ( decoded_map , get_location ( offset + prefix . length ) ) ;
110
+ }
111
+ const processed_with_map = StringWithSourcemap . from_processed ( processed . code , decoded_map ) ;
112
+
113
+ // Surround the processed code with the prefix and suffix, retaining valid sourcemappings
114
+ return prefix_with_map . concat ( processed_with_map ) . concat ( suffix_with_map ) ;
68
115
}
69
116
70
117
export default async function preprocess (
@@ -76,60 +123,107 @@ export default async function preprocess(
76
123
const filename = ( options && options . filename ) || preprocessor . filename ; // legacy
77
124
const dependencies = [ ] ;
78
125
79
- const preprocessors = Array . isArray ( preprocessor ) ? preprocessor : [ preprocessor ] ;
126
+ const preprocessors = Array . isArray ( preprocessor ) ? preprocessor : [ preprocessor || { } ] ;
80
127
81
128
const markup = preprocessors . map ( p => p . markup ) . filter ( Boolean ) ;
82
129
const script = preprocessors . map ( p => p . script ) . filter ( Boolean ) ;
83
130
const style = preprocessors . map ( p => p . style ) . filter ( Boolean ) ;
84
131
132
+ // sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
133
+ // so we use sourcemap_list.unshift() to add new maps
134
+ // https://github.com/ampproject/remapping#multiple-transformations-of-a-file
135
+ const sourcemap_list : Array < DecodedSourceMap | RawSourceMap > = [ ] ;
136
+
137
+ // TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
138
+
85
139
for ( const fn of markup ) {
140
+
141
+ // run markup preprocessor
86
142
const processed = await fn ( {
87
143
content : source ,
88
144
filename
89
145
} ) ;
146
+
90
147
if ( processed && processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
91
148
source = processed ? processed . code : source ;
149
+ if ( processed && processed . map )
150
+ sourcemap_list . unshift (
151
+ typeof ( processed . map ) === 'string'
152
+ ? JSON . parse ( processed . map )
153
+ : processed . map
154
+ ) ;
92
155
}
93
156
94
157
for ( const fn of script ) {
95
- source = await replace_async (
158
+ const get_location = getLocator ( source ) ;
159
+ const res = await replace_async (
160
+ filename ,
96
161
source ,
162
+ get_location ,
97
163
/ < ! - - [ ^ ] * ?- - > | < s c r i p t ( \s [ ^ ] * ?) ? (?: > ( [ ^ ] * ?) < \/ s c r i p t > | \/ > ) / gi,
98
- async ( match , attributes = '' , content = '' ) => {
164
+ async ( match , attributes = '' , content = '' , offset ) => {
165
+ const no_change = ( ) => StringWithSourcemap . from_source (
166
+ filename , match , get_location ( offset ) ) ;
99
167
if ( ! attributes && ! content ) {
100
- return match ;
168
+ return no_change ( ) ;
101
169
}
102
170
attributes = attributes || '' ;
171
+ content = content || '' ;
172
+
173
+ // run script preprocessor
103
174
const processed = await fn ( {
104
175
content,
105
176
attributes : parse_attributes ( attributes ) ,
106
177
filename
107
178
} ) ;
108
179
if ( processed && processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
109
- return processed ? `<script${ attributes } >${ processed . code } </script>` : match ;
180
+ return processed
181
+ ? get_replacement ( filename , offset , get_location , content , processed , `<script${ attributes } >` , '</script>' )
182
+ : no_change ( ) ;
110
183
}
111
184
) ;
185
+ source = res . string ;
186
+ sourcemap_list . unshift ( res . map ) ;
112
187
}
113
188
114
189
for ( const fn of style ) {
115
- source = await replace_async (
190
+ const get_location = getLocator ( source ) ;
191
+ const res = await replace_async (
192
+ filename ,
116
193
source ,
194
+ get_location ,
117
195
/ < ! - - [ ^ ] * ?- - > | < s t y l e ( \s [ ^ ] * ?) ? (?: > ( [ ^ ] * ?) < \/ s t y l e > | \/ > ) / gi,
118
- async ( match , attributes = '' , content = '' ) => {
196
+ async ( match , attributes = '' , content = '' , offset ) => {
197
+ const no_change = ( ) => StringWithSourcemap . from_source (
198
+ filename , match , get_location ( offset ) ) ;
119
199
if ( ! attributes && ! content ) {
120
- return match ;
200
+ return no_change ( ) ;
121
201
}
202
+ attributes = attributes || '' ;
203
+ content = content || '' ;
204
+
205
+ // run style preprocessor
122
206
const processed : Processed = await fn ( {
123
207
content,
124
208
attributes : parse_attributes ( attributes ) ,
125
209
filename
126
210
} ) ;
127
211
if ( processed && processed . dependencies ) dependencies . push ( ...processed . dependencies ) ;
128
- return processed ? `<style${ attributes } >${ processed . code } </style>` : match ;
212
+ return processed
213
+ ? get_replacement ( filename , offset , get_location , content , processed , `<style${ attributes } >` , '</style>' )
214
+ : no_change ( ) ;
129
215
}
130
216
) ;
217
+ source = res . string ;
218
+ sourcemap_list . unshift ( res . map ) ;
131
219
}
132
220
221
+ // Combine all the source maps for each preprocessor function into one
222
+ const map : RawSourceMap = combine_sourcemaps (
223
+ filename ,
224
+ sourcemap_list
225
+ ) ;
226
+
133
227
return {
134
228
// TODO return separated output, in future version where svelte.compile supports it:
135
229
// style: { code: styleCode, map: styleMap },
@@ -138,7 +232,7 @@ export default async function preprocess(
138
232
139
233
code : source ,
140
234
dependencies : [ ...new Set ( dependencies ) ] ,
141
-
235
+ map : ( map as object ) ,
142
236
toString ( ) {
143
237
return source ;
144
238
}
0 commit comments