UNPKG

9.16 kBJavaScriptView Raw
1const {SourceMapConsumer, SourceMapGenerator} = require('source-map');
2const lineCounter = require('./utils/lineCounter');
3
4class SourceMap {
5 constructor(mappings, sources) {
6 this.mappings = this.purifyMappings(mappings);
7 this.sources = sources || {};
8 this.lineCount = null;
9 }
10
11 purifyMappings(mappings) {
12 if (Array.isArray(mappings)) {
13 return mappings.filter(
14 mapping =>
15 mapping &&
16 (typeof mapping.original === 'object' &&
17 (mapping.original === null ||
18 (typeof mapping.original.line === 'number' &&
19 mapping.original.line > 0 &&
20 typeof mapping.original.column === 'number' &&
21 mapping.source))) &&
22 mapping.generated &&
23 typeof mapping.generated.line === 'number' &&
24 mapping.generated.line > 0 &&
25 typeof mapping.generated.column === 'number'
26 );
27 }
28
29 return [];
30 }
31
32 async getConsumer(map) {
33 if (map instanceof SourceMapConsumer) {
34 return map;
35 }
36 map = typeof map === 'string' ? JSON.parse(map) : map;
37 if (map.sourceRoot) delete map.sourceRoot;
38 return new SourceMapConsumer(map);
39 }
40
41 async addMap(map, lineOffset = 0, columnOffset = 0) {
42 if (typeof map === 'string' || (typeof map === 'object' && map.version)) {
43 let consumer = await this.getConsumer(map);
44 if (!consumer) return this;
45
46 consumer.eachMapping(mapping => {
47 this.addConsumerMapping(mapping, lineOffset, columnOffset);
48 if (!this.sources[mapping.source]) {
49 this.sources[mapping.source] = consumer.sourceContentFor(
50 mapping.source,
51 true
52 );
53 }
54 });
55
56 if (consumer.destroy) {
57 // Only needs to happen in source-map 0.7
58 consumer.destroy();
59 }
60 } else if (map.mappings && map.sources) {
61 if (!map.eachMapping) {
62 map = new SourceMap(map.mappings, map.sources);
63 }
64
65 if (lineOffset === 0 && columnOffset === 0) {
66 this.mappings = this.mappings.concat(map.mappings);
67 } else {
68 map.eachMapping(mapping => {
69 this.addMapping(mapping, lineOffset, columnOffset);
70 });
71 }
72
73 Object.keys(map.sources).forEach(sourceName => {
74 if (!this.sources[sourceName]) {
75 this.sources[sourceName] = map.sources[sourceName];
76 }
77 });
78 }
79
80 return this;
81 }
82
83 addMapping(mapping, lineOffset = 0, columnOffset = 0) {
84 this.mappings.push({
85 source: mapping.source,
86 name: mapping.name,
87 original: mapping.original,
88 generated: {
89 line: mapping.generated.line + lineOffset,
90 column: mapping.generated.column + columnOffset
91 }
92 });
93 }
94
95 addConsumerMapping(mapping, lineOffset = 0, columnOffset = 0) {
96 let original = null;
97 if (
98 typeof mapping.originalLine === 'number' &&
99 mapping.originalLine > 0 &&
100 typeof mapping.originalColumn === 'number'
101 ) {
102 original = {
103 line: mapping.originalLine,
104 column: mapping.originalColumn
105 };
106 }
107
108 this.mappings.push({
109 source: original ? mapping.source : null,
110 name: mapping.name,
111 original,
112 generated: {
113 line: mapping.generatedLine + lineOffset,
114 column: mapping.generatedColumn + columnOffset
115 }
116 });
117 }
118
119 eachMapping(callback) {
120 this.mappings.forEach(callback);
121 }
122
123 generateEmptyMap(sourceName, sourceContent) {
124 this.sources[sourceName] = sourceContent;
125
126 this.lineCount = lineCounter(sourceContent);
127 for (let line = 1; line < this.lineCount + 1; line++) {
128 this.addMapping({
129 source: sourceName,
130 original: {
131 line: line,
132 column: 0
133 },
134 generated: {
135 line: line,
136 column: 0
137 }
138 });
139 }
140
141 return this;
142 }
143
144 async extendSourceMap(original, extension) {
145 if (!(extension instanceof SourceMap)) {
146 extension = await new SourceMap().addMap(extension);
147 }
148 if (!(original instanceof SourceMap)) {
149 original = await this.getConsumer(original);
150 }
151
152 extension.eachMapping(mapping => {
153 let originalMapping = original.originalPositionFor({
154 line: mapping.original.line,
155 column: mapping.original.column
156 });
157
158 if (!originalMapping || !originalMapping.line) {
159 return;
160 }
161
162 this.addMapping({
163 source: originalMapping.source,
164 name: originalMapping.name,
165 original: {
166 line: originalMapping.line,
167 column: originalMapping.column
168 },
169 generated: {
170 line: mapping.generated.line,
171 column: mapping.generated.column
172 }
173 });
174
175 if (!this.sources[originalMapping.source]) {
176 this.sources[originalMapping.source] = original.sourceContentFor(
177 originalMapping.source,
178 true
179 );
180 }
181 });
182
183 if (original.destroy) {
184 // Only needs to happen in source-map 0.7
185 original.destroy();
186 }
187
188 return this;
189 }
190
191 findClosestGenerated(line, column) {
192 if (line < 1) {
193 throw new Error('Line numbers must be >= 1');
194 }
195
196 if (column < 0) {
197 throw new Error('Column numbers must be >= 0');
198 }
199
200 if (this.mappings.length < 1) {
201 return undefined;
202 }
203
204 let startIndex = 0;
205 let stopIndex = this.mappings.length - 1;
206 let middleIndex = (stopIndex + startIndex) >>> 1;
207
208 while (
209 startIndex < stopIndex &&
210 this.mappings[middleIndex].generated.line !== line
211 ) {
212 let mid = this.mappings[middleIndex].generated.line;
213 if (line < mid) {
214 stopIndex = middleIndex - 1;
215 } else if (line > mid) {
216 startIndex = middleIndex + 1;
217 }
218 middleIndex = (stopIndex + startIndex) >>> 1;
219 }
220
221 let mapping = this.mappings[middleIndex];
222 if (!mapping || mapping.generated.line !== line) {
223 return this.mappings.length - 1;
224 }
225
226 while (
227 middleIndex >= 1 &&
228 this.mappings[middleIndex - 1].generated.line === line
229 ) {
230 middleIndex--;
231 }
232
233 while (
234 middleIndex < this.mappings.length - 1 &&
235 this.mappings[middleIndex + 1].generated.line === line &&
236 column > this.mappings[middleIndex].generated.column
237 ) {
238 middleIndex++;
239 }
240
241 return middleIndex;
242 }
243
244 findClosest(line, column, key) {
245 if (line < 1) {
246 throw new Error('Line numbers must be >= 1');
247 }
248
249 if (column < 0) {
250 throw new Error('Column numbers must be >= 0');
251 }
252
253 if (this.mappings.length < 1) {
254 return undefined;
255 }
256
257 let startIndex = 0;
258 let stopIndex = this.mappings.length - 1;
259 let middleIndex = Math.floor((stopIndex + startIndex) / 2);
260
261 while (
262 startIndex < stopIndex &&
263 this.mappings[middleIndex][key].line !== line
264 ) {
265 if (line < this.mappings[middleIndex][key].line) {
266 stopIndex = middleIndex - 1;
267 } else if (line > this.mappings[middleIndex][key].line) {
268 startIndex = middleIndex + 1;
269 }
270 middleIndex = Math.floor((stopIndex + startIndex) / 2);
271 }
272
273 var mapping = this.mappings[middleIndex];
274 if (!mapping || mapping[key].line !== line) {
275 return this.mappings.length - 1;
276 }
277
278 while (
279 middleIndex >= 1 &&
280 this.mappings[middleIndex - 1][key].line === line
281 ) {
282 middleIndex--;
283 }
284
285 while (
286 middleIndex < this.mappings.length - 1 &&
287 this.mappings[middleIndex + 1][key].line === line &&
288 column > this.mappings[middleIndex][key].column
289 ) {
290 middleIndex++;
291 }
292
293 return middleIndex;
294 }
295
296 originalPositionFor(generatedPosition) {
297 let index = this.findClosestGenerated(
298 generatedPosition.line,
299 generatedPosition.column
300 );
301
302 let mapping = this.mappings[index];
303 if (!mapping || !mapping.original) {
304 return null;
305 }
306
307 return {
308 source: mapping.source,
309 name: mapping.name,
310 line: mapping.original.line,
311 column: mapping.original.column
312 };
313 }
314
315 generatedPositionFor(originalPosition) {
316 let index = this.findClosest(
317 originalPosition.line,
318 originalPosition.column,
319 'original'
320 );
321
322 let mapping = this.mappings[index];
323 return {
324 source: mapping.source,
325 name: mapping.name,
326 line: mapping.generated.line,
327 column: mapping.generated.column
328 };
329 }
330
331 sourceContentFor(fileName) {
332 return this.sources[fileName];
333 }
334
335 offset(lineOffset = 0, columnOffset = 0) {
336 this.mappings.map(mapping => {
337 mapping.generated.line = mapping.generated.line + lineOffset;
338 mapping.generated.column = mapping.generated.column + columnOffset;
339 return mapping;
340 });
341
342 if (this.lineCount != null) {
343 this.lineCount += lineOffset;
344 }
345 }
346
347 stringify(file, sourceRoot) {
348 let generator = new SourceMapGenerator({file, sourceRoot});
349 this.eachMapping(mapping => generator.addMapping(mapping));
350 Object.keys(this.sources).forEach(sourceName =>
351 generator.setSourceContent(sourceName, this.sources[sourceName])
352 );
353
354 return generator.toString();
355 }
356}
357
358module.exports = SourceMap;