|
1 | 1 | type SourceMap = any; |
2 | | -import sourceMap from "source-map"; |
| 2 | +import remapping from "@ampproject/remapping"; |
3 | 3 |
|
4 | 4 | export default function mergeSourceMap( |
5 | 5 | inputMap: SourceMap, |
6 | 6 | map: SourceMap, |
7 | 7 | ): SourceMap { |
8 | | - const input = buildMappingData(inputMap); |
9 | | - const output = buildMappingData(map); |
| 8 | + const result = remapping([rootless(map), rootless(inputMap)], () => null); |
10 | 9 |
|
11 | | - const mergedGenerator = new sourceMap.SourceMapGenerator(); |
12 | | - for (const { source } of input.sources) { |
13 | | - if (typeof source.content === "string") { |
14 | | - mergedGenerator.setSourceContent(source.path, source.content); |
15 | | - } |
16 | | - } |
17 | | - |
18 | | - if (output.sources.length === 1) { |
19 | | - const defaultSource = output.sources[0]; |
20 | | - |
21 | | - const insertedMappings = new Map(); |
22 | | - |
23 | | - // Process each generated range in the input map, e.g. each range over the |
24 | | - // code that Babel was originally given. |
25 | | - eachInputGeneratedRange(input, (generated, original, source) => { |
26 | | - // Then pick out each range over Babel's _output_ that corresponds with |
27 | | - // the given range on the code given to Babel. |
28 | | - eachOverlappingGeneratedOutputRange(defaultSource, generated, item => { |
29 | | - // It's possible that multiple input ranges will overlap the same |
30 | | - // generated range. Since sourcemap don't traditionally represent |
31 | | - // generated locations with multiple original locations, we explicitly |
32 | | - // skip generated locations once we've seen them the first time. |
33 | | - const key = makeMappingKey(item); |
34 | | - if (insertedMappings.has(key)) return; |
35 | | - insertedMappings.set(key, item); |
36 | | - |
37 | | - mergedGenerator.addMapping({ |
38 | | - source: source.path, |
39 | | - original: { |
40 | | - line: original.line, |
41 | | - column: original.columnStart, |
42 | | - }, |
43 | | - generated: { |
44 | | - line: item.line, |
45 | | - column: item.columnStart, |
46 | | - }, |
47 | | - name: original.name, |
48 | | - }); |
49 | | - }); |
50 | | - }); |
51 | | - |
52 | | - // Since mappings are manipulated using single locations, but are interpreted |
53 | | - // as ranges, the insertions above may not actually have their ending |
54 | | - // locations mapped yet. Here be go through each one and ensure that it has |
55 | | - // a well-defined ending location, if one wasn't already created by the start |
56 | | - // of a different range. |
57 | | - for (const item of insertedMappings.values()) { |
58 | | - if (item.columnEnd === Infinity) { |
59 | | - continue; |
60 | | - } |
61 | | - |
62 | | - const clearItem = { |
63 | | - line: item.line, |
64 | | - columnStart: item.columnEnd, |
65 | | - }; |
66 | | - |
67 | | - const key = makeMappingKey(clearItem); |
68 | | - if (insertedMappings.has(key)) { |
69 | | - continue; |
70 | | - } |
71 | | - |
72 | | - // Insert mappings with no original position to terminate any mappings |
73 | | - // that were found above, so that they don't expand beyond their correct |
74 | | - // range. |
75 | | - // @ts-expect-error todo(flow->ts) original and source field are missing |
76 | | - mergedGenerator.addMapping({ |
77 | | - generated: { |
78 | | - line: clearItem.line, |
79 | | - column: clearItem.columnStart, |
80 | | - }, |
81 | | - }); |
82 | | - } |
83 | | - } |
84 | | - |
85 | | - const result = mergedGenerator.toJSON(); |
86 | | - // addMapping expects a relative path, and setSourceContent expects an |
87 | | - // absolute path. To avoid this whole confusion, we leave the root out |
88 | | - // entirely, and add it at the end here. |
89 | | - if (typeof input.sourceRoot === "string") { |
90 | | - result.sourceRoot = input.sourceRoot; |
| 10 | + if (typeof inputMap.sourceRoot === "string") { |
| 11 | + result.sourceRoot = inputMap.sourceRoot; |
91 | 12 | } |
92 | 13 | return result; |
93 | 14 | } |
94 | 15 |
|
95 | | -function makeMappingKey(item: { line: number; columnStart: number }) { |
96 | | - return `${item.line}/${item.columnStart}`; |
97 | | -} |
98 | | - |
99 | | -function eachOverlappingGeneratedOutputRange( |
100 | | - outputFile: ResolvedFileMappings, |
101 | | - inputGeneratedRange: ResolvedGeneratedRange, |
102 | | - callback: (range: ResolvedGeneratedRange) => unknown, |
103 | | -) { |
104 | | - // Find the Babel-generated mappings that overlap with this range in the |
105 | | - // input sourcemap. Generated locations within the input sourcemap |
106 | | - // correspond with the original locations in the map Babel generates. |
107 | | - const overlappingOriginal = filterApplicableOriginalRanges( |
108 | | - outputFile, |
109 | | - inputGeneratedRange, |
110 | | - ); |
111 | | - |
112 | | - for (const { generated } of overlappingOriginal) { |
113 | | - for (const item of generated) { |
114 | | - callback(item); |
115 | | - } |
116 | | - } |
117 | | -} |
118 | | - |
119 | | -function filterApplicableOriginalRanges( |
120 | | - { mappings }: ResolvedFileMappings, |
121 | | - { line, columnStart, columnEnd }: ResolvedGeneratedRange, |
122 | | -): OriginalMappings { |
123 | | - // The mapping array is sorted by original location, so we can |
124 | | - // binary-search it for the overlapping ranges. |
125 | | - return filterSortedArray(mappings, ({ original: outOriginal }) => { |
126 | | - if (line > outOriginal.line) return -1; |
127 | | - if (line < outOriginal.line) return 1; |
128 | | - |
129 | | - if (columnStart >= outOriginal.columnEnd) return -1; |
130 | | - if (columnEnd <= outOriginal.columnStart) return 1; |
131 | | - |
132 | | - return 0; |
133 | | - }); |
134 | | -} |
135 | | - |
136 | | -function eachInputGeneratedRange( |
137 | | - map: ResolvedMappings, |
138 | | - callback: ( |
139 | | - c: ResolvedGeneratedRange, |
140 | | - b: ResolvedOriginalRange, |
141 | | - a: ResolvedSource, |
142 | | - ) => unknown, |
143 | | -) { |
144 | | - for (const { source, mappings } of map.sources) { |
145 | | - for (const { original, generated } of mappings) { |
146 | | - for (const item of generated) { |
147 | | - callback(item, original, source); |
148 | | - } |
149 | | - } |
150 | | - } |
151 | | -} |
152 | | - |
153 | | -type ResolvedMappings = { |
154 | | - file: string | undefined | null; |
155 | | - sourceRoot: string | undefined | null; |
156 | | - sources: Array<ResolvedFileMappings>; |
157 | | -}; |
158 | | - |
159 | | -type ResolvedFileMappings = { |
160 | | - source: ResolvedSource; |
161 | | - mappings: OriginalMappings; |
162 | | -}; |
163 | | - |
164 | | -type OriginalMappings = Array<{ |
165 | | - original: ResolvedOriginalRange; |
166 | | - generated: Array<ResolvedGeneratedRange>; |
167 | | -}>; |
168 | | - |
169 | | -type ResolvedSource = { |
170 | | - path: string; |
171 | | - content: string | null; |
172 | | -}; |
173 | | - |
174 | | -type ResolvedOriginalRange = { |
175 | | - line: number; |
176 | | - columnStart: number; |
177 | | - columnEnd: number; |
178 | | - name: string | null; |
179 | | -}; |
180 | | - |
181 | | -type ResolvedGeneratedRange = { |
182 | | - line: number; |
183 | | - columnStart: number; |
184 | | - columnEnd: number; |
185 | | -}; |
186 | | - |
187 | | -function buildMappingData(map: SourceMap): ResolvedMappings { |
188 | | - const consumer = new sourceMap.SourceMapConsumer({ |
| 16 | +function rootless(map: SourceMap): SourceMap { |
| 17 | + return { |
189 | 18 | ...map, |
190 | 19 |
|
191 | | - // This is a bit hack. .addMapping expects source values to be relative, |
192 | | - // but eachMapping returns mappings with absolute paths. To avoid that |
193 | | - // incompatibility, we leave the sourceRoot out here and add it to the |
194 | | - // final map at the end instead. |
| 20 | + // This is a bit hack. Remapping will create absolute sources in our |
| 21 | + // sourcemap, but we want to maintain sources relative to the sourceRoot. |
| 22 | + // We'll re-add the sourceRoot after remapping. |
195 | 23 | sourceRoot: null, |
196 | | - }); |
197 | | - |
198 | | - const sources = new Map(); |
199 | | - const mappings = new Map(); |
200 | | - |
201 | | - let last = null; |
202 | | - |
203 | | - consumer.computeColumnSpans(); |
204 | | - |
205 | | - consumer.eachMapping( |
206 | | - m => { |
207 | | - if (m.originalLine === null) return; |
208 | | - |
209 | | - let source = sources.get(m.source); |
210 | | - if (!source) { |
211 | | - source = { |
212 | | - path: m.source, |
213 | | - content: consumer.sourceContentFor(m.source, true), |
214 | | - }; |
215 | | - sources.set(m.source, source); |
216 | | - } |
217 | | - |
218 | | - let sourceData = mappings.get(source); |
219 | | - if (!sourceData) { |
220 | | - sourceData = { |
221 | | - source, |
222 | | - mappings: [], |
223 | | - }; |
224 | | - mappings.set(source, sourceData); |
225 | | - } |
226 | | - |
227 | | - const obj = { |
228 | | - line: m.originalLine, |
229 | | - columnStart: m.originalColumn, |
230 | | - columnEnd: Infinity, |
231 | | - name: m.name, |
232 | | - }; |
233 | | - |
234 | | - if ( |
235 | | - last && |
236 | | - last.source === source && |
237 | | - last.mapping.line === m.originalLine |
238 | | - ) { |
239 | | - last.mapping.columnEnd = m.originalColumn; |
240 | | - } |
241 | | - |
242 | | - last = { |
243 | | - source, |
244 | | - mapping: obj, |
245 | | - }; |
246 | | - |
247 | | - sourceData.mappings.push({ |
248 | | - original: obj, |
249 | | - generated: consumer |
250 | | - .allGeneratedPositionsFor({ |
251 | | - source: m.source, |
252 | | - line: m.originalLine, |
253 | | - column: m.originalColumn, |
254 | | - }) |
255 | | - .map(item => ({ |
256 | | - line: item.line, |
257 | | - columnStart: item.column, |
258 | | - // source-map's lastColumn is inclusive, not exclusive, so we need |
259 | | - // to add 1 to it. |
260 | | - columnEnd: item.lastColumn + 1, |
261 | | - })), |
262 | | - }); |
263 | | - }, |
264 | | - null, |
265 | | - sourceMap.SourceMapConsumer.ORIGINAL_ORDER, |
266 | | - ); |
267 | | - |
268 | | - return { |
269 | | - file: map.file, |
270 | | - sourceRoot: map.sourceRoot, |
271 | | - sources: Array.from(mappings.values()), |
272 | 24 | }; |
273 | 25 | } |
274 | | - |
275 | | -function findInsertionLocation<T>( |
276 | | - array: Array<T>, |
277 | | - callback: (item: T) => number, |
278 | | -): number { |
279 | | - let left = 0; |
280 | | - let right = array.length; |
281 | | - while (left < right) { |
282 | | - const mid = Math.floor((left + right) / 2); |
283 | | - const item = array[mid]; |
284 | | - |
285 | | - const result = callback(item); |
286 | | - if (result === 0) { |
287 | | - left = mid; |
288 | | - break; |
289 | | - } |
290 | | - if (result >= 0) { |
291 | | - right = mid; |
292 | | - } else { |
293 | | - left = mid + 1; |
294 | | - } |
295 | | - } |
296 | | - |
297 | | - // Ensure the value is the start of any set of matches. |
298 | | - let i = left; |
299 | | - if (i < array.length) { |
300 | | - while (i >= 0 && callback(array[i]) >= 0) { |
301 | | - i--; |
302 | | - } |
303 | | - return i + 1; |
304 | | - } |
305 | | - |
306 | | - return i; |
307 | | -} |
308 | | - |
309 | | -function filterSortedArray<T>( |
310 | | - array: Array<T>, |
311 | | - callback: (item: T) => number, |
312 | | -): Array<T> { |
313 | | - const start = findInsertionLocation(array, callback); |
314 | | - |
315 | | - const results = []; |
316 | | - for (let i = start; i < array.length && callback(array[i]) === 0; i++) { |
317 | | - results.push(array[i]); |
318 | | - } |
319 | | - |
320 | | - return results; |
321 | | -} |
0 commit comments