UNPKG

10.4 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6
7const Source = require("./Source");
8const { SourceNode } = require("source-map");
9const { getSourceAndMap, getMap, getNode, getListMap } = require("./helpers");
10
11class Replacement {
12 constructor(start, end, content, insertIndex, name) {
13 this.start = start;
14 this.end = end;
15 this.content = content;
16 this.insertIndex = insertIndex;
17 this.name = name;
18 }
19}
20
21class ReplaceSource extends Source {
22 constructor(source, name) {
23 super();
24 this._source = source;
25 this._name = name;
26 /** @type {Replacement[]} */
27 this._replacements = [];
28 this._isSorted = true;
29 }
30
31 getName() {
32 return this._name;
33 }
34
35 getReplacements() {
36 const replacements = Array.from(this._replacements);
37 replacements.sort((a, b) => {
38 return a.insertIndex - b.insertIndex;
39 });
40 return replacements;
41 }
42
43 replace(start, end, newValue, name) {
44 if (typeof newValue !== "string")
45 throw new Error(
46 "insertion must be a string, but is a " + typeof newValue
47 );
48 this._replacements.push(
49 new Replacement(start, end, newValue, this._replacements.length, name)
50 );
51 this._isSorted = false;
52 }
53
54 insert(pos, newValue, name) {
55 if (typeof newValue !== "string")
56 throw new Error(
57 "insertion must be a string, but is a " +
58 typeof newValue +
59 ": " +
60 newValue
61 );
62 this._replacements.push(
63 new Replacement(pos, pos - 1, newValue, this._replacements.length, name)
64 );
65 this._isSorted = false;
66 }
67
68 source() {
69 return this._replaceString(this._source.source());
70 }
71
72 map(options) {
73 if (this._replacements.length === 0) {
74 return this._source.map(options);
75 }
76 return getMap(this, options);
77 }
78
79 sourceAndMap(options) {
80 if (this._replacements.length === 0) {
81 return this._source.sourceAndMap(options);
82 }
83 return getSourceAndMap(this, options);
84 }
85
86 original() {
87 return this._source;
88 }
89
90 _sortReplacements() {
91 if (this._isSorted) return;
92 this._replacements.sort(function (a, b) {
93 const diff1 = b.end - a.end;
94 if (diff1 !== 0) return diff1;
95 const diff2 = b.start - a.start;
96 if (diff2 !== 0) return diff2;
97 return b.insertIndex - a.insertIndex;
98 });
99 this._isSorted = true;
100 }
101
102 _replaceString(str) {
103 if (typeof str !== "string")
104 throw new Error(
105 "str must be a string, but is a " + typeof str + ": " + str
106 );
107 this._sortReplacements();
108 const result = [str];
109 this._replacements.forEach(function (repl) {
110 const remSource = result.pop();
111 const splitted1 = this._splitString(remSource, Math.floor(repl.end + 1));
112 const splitted2 = this._splitString(splitted1[0], Math.floor(repl.start));
113 result.push(splitted1[1], repl.content, splitted2[0]);
114 }, this);
115
116 // write out result array in reverse order
117 let resultStr = "";
118 for (let i = result.length - 1; i >= 0; --i) {
119 resultStr += result[i];
120 }
121 return resultStr;
122 }
123
124 node(options) {
125 const node = getNode(this._source, options);
126 if (this._replacements.length === 0) {
127 return node;
128 }
129 this._sortReplacements();
130 const replace = new ReplacementEnumerator(this._replacements);
131 const output = [];
132 let position = 0;
133 const sources = Object.create(null);
134 const sourcesInLines = Object.create(null);
135
136 // We build a new list of SourceNodes in "output"
137 // from the original mapping data
138
139 const result = new SourceNode();
140
141 // We need to add source contents manually
142 // because "walk" will not handle it
143 node.walkSourceContents(function (sourceFile, sourceContent) {
144 result.setSourceContent(sourceFile, sourceContent);
145 sources["$" + sourceFile] = sourceContent;
146 });
147
148 const replaceInStringNode = this._replaceInStringNode.bind(
149 this,
150 output,
151 replace,
152 function getOriginalSource(mapping) {
153 const key = "$" + mapping.source;
154 let lines = sourcesInLines[key];
155 if (!lines) {
156 const source = sources[key];
157 if (!source) return null;
158 lines = source.split("\n").map(function (line) {
159 return line + "\n";
160 });
161 sourcesInLines[key] = lines;
162 }
163 // line is 1-based
164 if (mapping.line > lines.length) return null;
165 const line = lines[mapping.line - 1];
166 return line.substr(mapping.column);
167 }
168 );
169
170 node.walk(function (chunk, mapping) {
171 position = replaceInStringNode(chunk, position, mapping);
172 });
173
174 // If any replacements occur after the end of the original file, then we append them
175 // directly to the end of the output
176 const remaining = replace.footer();
177 if (remaining) {
178 output.push(remaining);
179 }
180
181 result.add(output);
182
183 return result;
184 }
185
186 listMap(options) {
187 let map = getListMap(this._source, options);
188 this._sortReplacements();
189 let currentIndex = 0;
190 const replacements = this._replacements;
191 let idxReplacement = replacements.length - 1;
192 let removeChars = 0;
193 map = map.mapGeneratedCode(function (str) {
194 const newCurrentIndex = currentIndex + str.length;
195 if (removeChars > str.length) {
196 removeChars -= str.length;
197 str = "";
198 } else {
199 if (removeChars > 0) {
200 str = str.substr(removeChars);
201 currentIndex += removeChars;
202 removeChars = 0;
203 }
204 let finalStr = "";
205 while (
206 idxReplacement >= 0 &&
207 replacements[idxReplacement].start < newCurrentIndex
208 ) {
209 const repl = replacements[idxReplacement];
210 const start = Math.floor(repl.start);
211 const end = Math.floor(repl.end + 1);
212 const before = str.substr(0, Math.max(0, start - currentIndex));
213 if (end <= newCurrentIndex) {
214 const after = str.substr(Math.max(0, end - currentIndex));
215 finalStr += before + repl.content;
216 str = after;
217 currentIndex = Math.max(currentIndex, end);
218 } else {
219 finalStr += before + repl.content;
220 str = "";
221 removeChars = end - newCurrentIndex;
222 }
223 idxReplacement--;
224 }
225 str = finalStr + str;
226 }
227 currentIndex = newCurrentIndex;
228 return str;
229 });
230 let extraCode = "";
231 while (idxReplacement >= 0) {
232 extraCode += replacements[idxReplacement].content;
233 idxReplacement--;
234 }
235 if (extraCode) {
236 map.add(extraCode);
237 }
238 return map;
239 }
240
241 _splitString(str, position) {
242 return position <= 0
243 ? ["", str]
244 : [str.substr(0, position), str.substr(position)];
245 }
246
247 _replaceInStringNode(
248 output,
249 replace,
250 getOriginalSource,
251 node,
252 position,
253 mapping
254 ) {
255 let original = undefined;
256
257 do {
258 let splitPosition = replace.position - position;
259 // If multiple replaces occur in the same location then the splitPosition may be
260 // before the current position for the subsequent splits. Ensure it is >= 0
261 if (splitPosition < 0) {
262 splitPosition = 0;
263 }
264 if (splitPosition >= node.length || replace.done) {
265 if (replace.emit) {
266 const nodeEnd = new SourceNode(
267 mapping.line,
268 mapping.column,
269 mapping.source,
270 node,
271 mapping.name
272 );
273 output.push(nodeEnd);
274 }
275 return position + node.length;
276 }
277
278 const originalColumn = mapping.column;
279
280 // Try to figure out if generated code matches original code of this segement
281 // If this is the case we assume that it's allowed to move mapping.column
282 // Because getOriginalSource can be expensive we only do it when neccessary
283
284 let nodePart;
285 if (splitPosition > 0) {
286 nodePart = node.slice(0, splitPosition);
287 if (original === undefined) {
288 original = getOriginalSource(mapping);
289 }
290 if (
291 original &&
292 original.length >= splitPosition &&
293 original.startsWith(nodePart)
294 ) {
295 mapping.column += splitPosition;
296 original = original.substr(splitPosition);
297 }
298 }
299
300 const emit = replace.next();
301 if (!emit) {
302 // Stop emitting when we have found the beginning of the string to replace.
303 // Emit the part of the string before splitPosition
304 if (splitPosition > 0) {
305 const nodeStart = new SourceNode(
306 mapping.line,
307 originalColumn,
308 mapping.source,
309 nodePart,
310 mapping.name
311 );
312 output.push(nodeStart);
313 }
314
315 // Emit the replacement value
316 if (replace.value) {
317 output.push(
318 new SourceNode(
319 mapping.line,
320 mapping.column,
321 mapping.source,
322 replace.value,
323 mapping.name || replace.name
324 )
325 );
326 }
327 }
328
329 // Recurse with remainder of the string as there may be multiple replaces within a single node
330 node = node.substr(splitPosition);
331 position += splitPosition;
332 // eslint-disable-next-line no-constant-condition
333 } while (true);
334 }
335
336 updateHash(hash) {
337 this._sortReplacements();
338 hash.update("ReplaceSource");
339 this._source.updateHash(hash);
340 hash.update(this._name || "");
341 for (const repl of this._replacements) {
342 hash.update(`${repl.start}`);
343 hash.update(`${repl.end}`);
344 hash.update(`${repl.content}`);
345 hash.update(`${repl.insertIndex}`);
346 hash.update(`${repl.name}`);
347 }
348 }
349}
350
351class ReplacementEnumerator {
352 /**
353 * @param {Replacement[]} replacements list of replacements
354 */
355 constructor(replacements) {
356 this.replacements = replacements || [];
357 this.index = this.replacements.length;
358 this.done = false;
359 this.emit = false;
360 // Set initial start position
361 this.next();
362 }
363
364 next() {
365 if (this.done) return true;
366 if (this.emit) {
367 // Start point found. stop emitting. set position to find end
368 const repl = this.replacements[this.index];
369 const end = Math.floor(repl.end + 1);
370 this.position = end;
371 this.value = repl.content;
372 this.name = repl.name;
373 } else {
374 // End point found. start emitting. set position to find next start
375 this.index--;
376 if (this.index < 0) {
377 this.done = true;
378 } else {
379 const nextRepl = this.replacements[this.index];
380 const start = Math.floor(nextRepl.start);
381 this.position = start;
382 }
383 }
384 if (this.position < 0) this.position = 0;
385 this.emit = !this.emit;
386 return this.emit;
387 }
388
389 footer() {
390 if (!this.done && !this.emit) this.next(); // If we finished _replaceInNode mid emit we advance to next entry
391 if (this.done) {
392 return [];
393 } else {
394 let resultStr = "";
395 for (let i = this.index; i >= 0; i--) {
396 const repl = this.replacements[i];
397 // this doesn't need to handle repl.name, because in SourceMaps generated code
398 // without pointer to original source can't have a name
399 resultStr += repl.content;
400 }
401 return resultStr;
402 }
403 }
404}
405
406module.exports = ReplaceSource;