UNPKG

7.49 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(mapping => {
14 return (
15 mapping &&
16 mapping.source &&
17 mapping.original &&
18 typeof mapping.original.line === 'number' &&
19 mapping.original.line > 0 &&
20 typeof mapping.original.column === 'number' &&
21 mapping.generated &&
22 typeof mapping.generated.line === 'number' &&
23 mapping.generated.line > 0 &&
24 typeof mapping.generated.column === 'number'
25 );
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 return await new SourceMapConsumer(map);
38 }
39
40 async addMap(map, lineOffset = 0, columnOffset = 0) {
41 if (!(map instanceof SourceMap) && map.version) {
42 let consumer = await this.getConsumer(map);
43
44 consumer.eachMapping(mapping => {
45 this.addConsumerMapping(mapping, lineOffset, columnOffset);
46 if (!this.sources[mapping.source]) {
47 this.sources[mapping.source] = consumer.sourceContentFor(
48 mapping.source,
49 true
50 );
51 }
52 });
53
54 if (consumer.destroy) {
55 // Only needs to happen in source-map 0.7
56 consumer.destroy();
57 }
58 } else {
59 if (!map.eachMapping) {
60 map = new SourceMap(map.mappings, map.sources);
61 }
62
63 if (lineOffset === 0 && columnOffset === 0) {
64 this.mappings = this.mappings.concat(map.mappings);
65 } else {
66 map.eachMapping(mapping => {
67 this.addMapping(mapping, lineOffset, columnOffset);
68 });
69 }
70
71 Object.keys(map.sources).forEach(sourceName => {
72 if (!this.sources[sourceName]) {
73 this.sources[sourceName] = map.sources[sourceName];
74 }
75 });
76 }
77
78 return this;
79 }
80
81 addMapping(mapping, lineOffset = 0, columnOffset = 0) {
82 mapping.generated = {
83 line: mapping.generated.line + lineOffset,
84 column: mapping.generated.column + columnOffset
85 };
86
87 this.mappings.push(mapping);
88 }
89
90 addConsumerMapping(mapping, lineOffset = 0, columnOffset = 0) {
91 if (
92 !mapping.source ||
93 !mapping.originalLine ||
94 (!mapping.originalColumn && mapping.originalColumn !== 0)
95 ) {
96 return;
97 }
98
99 this.mappings.push({
100 source: mapping.source,
101 original: {
102 line: mapping.originalLine,
103 column: mapping.originalColumn
104 },
105 generated: {
106 line: mapping.generatedLine + lineOffset,
107 column: mapping.generatedColumn + columnOffset
108 },
109 name: mapping.name
110 });
111 }
112
113 eachMapping(callback) {
114 this.mappings.forEach(callback);
115 }
116
117 generateEmptyMap(sourceName, sourceContent) {
118 this.sources[sourceName] = sourceContent;
119
120 this.lineCount = lineCounter(sourceContent);
121 for (let line = 1; line < this.lineCount + 1; line++) {
122 this.addMapping({
123 source: sourceName,
124 original: {
125 line: line,
126 column: 0
127 },
128 generated: {
129 line: line,
130 column: 0
131 }
132 });
133 }
134
135 return this;
136 }
137
138 async extendSourceMap(original, extension) {
139 if (!(extension instanceof SourceMap)) {
140 extension = await new SourceMap().addMap(extension);
141 }
142 if (!(original instanceof SourceMap)) {
143 original = await this.getConsumer(original);
144 }
145
146 extension.eachMapping(mapping => {
147 let originalMapping = original.originalPositionFor({
148 line: mapping.original.line,
149 column: mapping.original.column
150 });
151
152 if (!originalMapping.line) {
153 return false;
154 }
155
156 this.addMapping({
157 source: originalMapping.source,
158 name: originalMapping.name,
159 original: {
160 line: originalMapping.line,
161 column: originalMapping.column
162 },
163 generated: {
164 line: mapping.generated.line,
165 column: mapping.generated.column
166 }
167 });
168
169 if (!this.sources[originalMapping.source]) {
170 this.sources[originalMapping.source] = original.sourceContentFor(
171 originalMapping.source,
172 true
173 );
174 }
175 });
176
177 if (original.destroy) {
178 // Only needs to happen in source-map 0.7
179 original.destroy();
180 }
181
182 return this;
183 }
184
185 findClosest(line, column, key = 'original') {
186 if (line < 1) {
187 throw new Error('Line numbers must be >= 1');
188 }
189
190 if (column < 0) {
191 throw new Error('Column numbers must be >= 0');
192 }
193
194 if (this.mappings.length < 1) {
195 return undefined;
196 }
197
198 let startIndex = 0;
199 let stopIndex = this.mappings.length - 1;
200 let middleIndex = Math.floor((stopIndex + startIndex) / 2);
201
202 while (
203 startIndex < stopIndex &&
204 this.mappings[middleIndex][key].line !== line
205 ) {
206 if (line < this.mappings[middleIndex][key].line) {
207 stopIndex = middleIndex - 1;
208 } else if (line > this.mappings[middleIndex][key].line) {
209 startIndex = middleIndex + 1;
210 }
211 middleIndex = Math.floor((stopIndex + startIndex) / 2);
212 }
213
214 let mapping = this.mappings[middleIndex];
215 if (!mapping || mapping[key].line !== line) {
216 return this.mappings.length - 1;
217 }
218
219 while (
220 middleIndex >= 1 &&
221 this.mappings[middleIndex - 1][key].line === line
222 ) {
223 middleIndex--;
224 }
225
226 while (
227 middleIndex < this.mappings.length - 1 &&
228 this.mappings[middleIndex + 1][key].line === line &&
229 column > this.mappings[middleIndex][key].column
230 ) {
231 middleIndex++;
232 }
233
234 return middleIndex;
235 }
236
237 originalPositionFor(generatedPosition) {
238 let index = this.findClosest(
239 generatedPosition.line,
240 generatedPosition.column,
241 'generated'
242 );
243 return {
244 source: this.mappings[index].source,
245 name: this.mappings[index].name,
246 line: this.mappings[index].original.line,
247 column: this.mappings[index].original.column
248 };
249 }
250
251 generatedPositionFor(originalPosition) {
252 let index = this.findClosest(
253 originalPosition.line,
254 originalPosition.column,
255 'original'
256 );
257 return {
258 source: this.mappings[index].source,
259 name: this.mappings[index].name,
260 line: this.mappings[index].generated.line,
261 column: this.mappings[index].generated.column
262 };
263 }
264
265 sourceContentFor(fileName) {
266 return this.sources[fileName];
267 }
268
269 offset(lineOffset = 0, columnOffset = 0) {
270 this.mappings.map(mapping => {
271 mapping.generated.line = mapping.generated.line + lineOffset;
272 mapping.generated.column = mapping.generated.column + columnOffset;
273 return mapping;
274 });
275
276 if (this.lineCount != null) {
277 this.lineCount += lineOffset;
278 }
279 }
280
281 stringify(file) {
282 let generator = new SourceMapGenerator({
283 file: file
284 });
285
286 this.eachMapping(mapping => generator.addMapping(mapping));
287 Object.keys(this.sources).forEach(sourceName =>
288 generator.setSourceContent(sourceName, this.sources[sourceName])
289 );
290
291 return generator.toString();
292 }
293}
294
295module.exports = SourceMap;