1 | const {SourceMapConsumer, SourceMapGenerator} = require('source-map');
|
2 | const lineCounter = require('./utils/lineCounter');
|
3 |
|
4 | class 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 |
|
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 |
|
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 |
|
358 | module.exports = SourceMap;
|