ReplaceSource.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Source = require("./Source");
  7. const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
  8. const splitIntoLines = require("./helpers/splitIntoLines");
  9. const streamChunks = require("./helpers/streamChunks");
  10. /** @typedef {import("./Source").Hash} Hash */
  11. /** @typedef {import("./Source").MapOptions} MapOptions */
  12. /** @typedef {import("./Source").RawSourceMap} RawSourceMap */
  13. /** @typedef {import("./Source").SourceAndMap} SourceAndMap */
  14. /** @typedef {import("./Source").SourceValue} SourceValue */
  15. /** @typedef {import("./helpers/getGeneratedSourceInfo").GeneratedSourceInfo} GeneratedSourceInfo */
  16. /** @typedef {import("./helpers/streamChunks").OnChunk} OnChunk */
  17. /** @typedef {import("./helpers/streamChunks").OnName} OnName */
  18. /** @typedef {import("./helpers/streamChunks").OnSource} OnSource */
  19. /** @typedef {import("./helpers/streamChunks").Options} Options */
  20. // since v8 7.0, Array.prototype.sort is stable
  21. const hasStableSort =
  22. typeof process === "object" &&
  23. process.versions &&
  24. typeof process.versions.v8 === "string" &&
  25. !/^[0-6]\./.test(process.versions.v8);
  26. // This is larger than max string length
  27. const MAX_SOURCE_POSITION = 0x20000000;
  28. class Replacement {
  29. /**
  30. * @param {number} start start
  31. * @param {number} end end
  32. * @param {string} content content
  33. * @param {string=} name name
  34. */
  35. constructor(start, end, content, name) {
  36. this.start = start;
  37. this.end = end;
  38. this.content = content;
  39. this.name = name;
  40. if (!hasStableSort) {
  41. this.index = -1;
  42. }
  43. }
  44. }
  45. class ReplaceSource extends Source {
  46. /**
  47. * @param {Source} source source
  48. * @param {string=} name name
  49. */
  50. constructor(source, name) {
  51. super();
  52. this._source = source;
  53. this._name = name;
  54. /** @type {Replacement[]} */
  55. this._replacements = [];
  56. this._isSorted = true;
  57. }
  58. getName() {
  59. return this._name;
  60. }
  61. getReplacements() {
  62. this._sortReplacements();
  63. return this._replacements;
  64. }
  65. /**
  66. * @param {number} start start
  67. * @param {number} end end
  68. * @param {string} newValue new value
  69. * @param {string=} name name
  70. * @returns {void}
  71. */
  72. replace(start, end, newValue, name) {
  73. if (typeof newValue !== "string")
  74. throw new Error(
  75. "insertion must be a string, but is a " + typeof newValue
  76. );
  77. this._replacements.push(new Replacement(start, end, newValue, name));
  78. this._isSorted = false;
  79. }
  80. /**
  81. * @param {number} pos pos
  82. * @param {string} newValue new value
  83. * @param {string=} name name
  84. * @returns {void}
  85. */
  86. insert(pos, newValue, name) {
  87. if (typeof newValue !== "string")
  88. throw new Error(
  89. "insertion must be a string, but is a " +
  90. typeof newValue +
  91. ": " +
  92. newValue
  93. );
  94. this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
  95. this._isSorted = false;
  96. }
  97. /**
  98. * @returns {SourceValue} source
  99. */
  100. source() {
  101. if (this._replacements.length === 0) {
  102. return this._source.source();
  103. }
  104. let current = this._source.source();
  105. let pos = 0;
  106. const result = [];
  107. this._sortReplacements();
  108. for (const replacement of this._replacements) {
  109. const start = Math.floor(replacement.start);
  110. const end = Math.floor(replacement.end + 1);
  111. if (pos < start) {
  112. const offset = start - pos;
  113. result.push(current.slice(0, offset));
  114. current = current.slice(offset);
  115. pos = start;
  116. }
  117. result.push(replacement.content);
  118. if (pos < end) {
  119. const offset = end - pos;
  120. current = current.slice(offset);
  121. pos = end;
  122. }
  123. }
  124. result.push(current);
  125. return result.join("");
  126. }
  127. /**
  128. * @param {MapOptions=} options map options
  129. * @returns {RawSourceMap | null} map
  130. */
  131. map(options) {
  132. if (this._replacements.length === 0) {
  133. return this._source.map(options);
  134. }
  135. return getMap(this, options);
  136. }
  137. /**
  138. * @param {MapOptions=} options map options
  139. * @returns {SourceAndMap} source and map
  140. */
  141. sourceAndMap(options) {
  142. if (this._replacements.length === 0) {
  143. return this._source.sourceAndMap(options);
  144. }
  145. return getSourceAndMap(this, options);
  146. }
  147. original() {
  148. return this._source;
  149. }
  150. _sortReplacements() {
  151. if (this._isSorted) return;
  152. if (hasStableSort) {
  153. this._replacements.sort(function (a, b) {
  154. const diff1 = a.start - b.start;
  155. if (diff1 !== 0) return diff1;
  156. const diff2 = a.end - b.end;
  157. if (diff2 !== 0) return diff2;
  158. return 0;
  159. });
  160. } else {
  161. this._replacements.forEach((repl, i) => (repl.index = i));
  162. this._replacements.sort(function (a, b) {
  163. const diff1 = a.start - b.start;
  164. if (diff1 !== 0) return diff1;
  165. const diff2 = a.end - b.end;
  166. if (diff2 !== 0) return diff2;
  167. return (
  168. /** @type {number} */ (a.index) - /** @type {number} */ (b.index)
  169. );
  170. });
  171. }
  172. this._isSorted = true;
  173. }
  174. /**
  175. * @param {Options} options options
  176. * @param {OnChunk} onChunk called for each chunk of code
  177. * @param {OnSource} onSource called for each source
  178. * @param {OnName} onName called for each name
  179. * @returns {GeneratedSourceInfo} generated source info
  180. */
  181. streamChunks(options, onChunk, onSource, onName) {
  182. this._sortReplacements();
  183. const replacements = this._replacements;
  184. let pos = 0;
  185. let i = 0;
  186. let replacmentEnd = -1;
  187. let nextReplacement =
  188. i < replacements.length
  189. ? Math.floor(replacements[i].start)
  190. : MAX_SOURCE_POSITION;
  191. let generatedLineOffset = 0;
  192. let generatedColumnOffset = 0;
  193. let generatedColumnOffsetLine = 0;
  194. /** @type {(string | string[] | undefined)[]} */
  195. const sourceContents = [];
  196. /** @type {Map<string, number>} */
  197. const nameMapping = new Map();
  198. /** @type {number[]} */
  199. const nameIndexMapping = [];
  200. /**
  201. * @param {number} sourceIndex source index
  202. * @param {number} line line
  203. * @param {number} column column
  204. * @param {string} expectedChunk expected chunk
  205. * @returns {boolean} result
  206. */
  207. const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
  208. /** @type {undefined | string | string[]} */
  209. let content =
  210. sourceIndex < sourceContents.length
  211. ? sourceContents[sourceIndex]
  212. : undefined;
  213. if (content === undefined) return false;
  214. if (typeof content === "string") {
  215. content = splitIntoLines(content);
  216. sourceContents[sourceIndex] = content;
  217. }
  218. const contentLine = line <= content.length ? content[line - 1] : null;
  219. if (contentLine === null) return false;
  220. return (
  221. contentLine.slice(column, column + expectedChunk.length) ===
  222. expectedChunk
  223. );
  224. };
  225. let { generatedLine, generatedColumn } = streamChunks(
  226. this._source,
  227. Object.assign({}, options, { finalSource: false }),
  228. (
  229. _chunk,
  230. generatedLine,
  231. generatedColumn,
  232. sourceIndex,
  233. originalLine,
  234. originalColumn,
  235. nameIndex
  236. ) => {
  237. let chunkPos = 0;
  238. const chunk = /** @type {string} */ (_chunk);
  239. let endPos = pos + chunk.length;
  240. // Skip over when it has been replaced
  241. if (replacmentEnd > pos) {
  242. // Skip over the whole chunk
  243. if (replacmentEnd >= endPos) {
  244. const line = generatedLine + generatedLineOffset;
  245. if (chunk.endsWith("\n")) {
  246. generatedLineOffset--;
  247. if (generatedColumnOffsetLine === line) {
  248. // undo exiting corrections form the current line
  249. generatedColumnOffset += generatedColumn;
  250. }
  251. } else if (generatedColumnOffsetLine === line) {
  252. generatedColumnOffset -= chunk.length;
  253. } else {
  254. generatedColumnOffset = -chunk.length;
  255. generatedColumnOffsetLine = line;
  256. }
  257. pos = endPos;
  258. return;
  259. }
  260. // Partially skip over chunk
  261. chunkPos = replacmentEnd - pos;
  262. if (
  263. checkOriginalContent(
  264. sourceIndex,
  265. originalLine,
  266. originalColumn,
  267. chunk.slice(0, chunkPos)
  268. )
  269. ) {
  270. originalColumn += chunkPos;
  271. }
  272. pos += chunkPos;
  273. const line = generatedLine + generatedLineOffset;
  274. if (generatedColumnOffsetLine === line) {
  275. generatedColumnOffset -= chunkPos;
  276. } else {
  277. generatedColumnOffset = -chunkPos;
  278. generatedColumnOffsetLine = line;
  279. }
  280. generatedColumn += chunkPos;
  281. }
  282. // Is a replacement in the chunk?
  283. if (nextReplacement < endPos) {
  284. do {
  285. let line = generatedLine + generatedLineOffset;
  286. if (nextReplacement > pos) {
  287. // Emit chunk until replacement
  288. const offset = nextReplacement - pos;
  289. const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
  290. onChunk(
  291. chunkSlice,
  292. line,
  293. generatedColumn +
  294. (line === generatedColumnOffsetLine
  295. ? generatedColumnOffset
  296. : 0),
  297. sourceIndex,
  298. originalLine,
  299. originalColumn,
  300. nameIndex < 0 || nameIndex >= nameIndexMapping.length
  301. ? -1
  302. : nameIndexMapping[nameIndex]
  303. );
  304. generatedColumn += offset;
  305. chunkPos += offset;
  306. pos = nextReplacement;
  307. if (
  308. checkOriginalContent(
  309. sourceIndex,
  310. originalLine,
  311. originalColumn,
  312. chunkSlice
  313. )
  314. ) {
  315. originalColumn += chunkSlice.length;
  316. }
  317. }
  318. // Insert replacement content splitted into chunks by lines
  319. const { content, name } = replacements[i];
  320. let matches = splitIntoLines(content);
  321. let replacementNameIndex = nameIndex;
  322. if (sourceIndex >= 0 && name) {
  323. let globalIndex = nameMapping.get(name);
  324. if (globalIndex === undefined) {
  325. globalIndex = nameMapping.size;
  326. nameMapping.set(name, globalIndex);
  327. onName(globalIndex, name);
  328. }
  329. replacementNameIndex = globalIndex;
  330. }
  331. for (let m = 0; m < matches.length; m++) {
  332. const contentLine = matches[m];
  333. onChunk(
  334. contentLine,
  335. line,
  336. generatedColumn +
  337. (line === generatedColumnOffsetLine
  338. ? generatedColumnOffset
  339. : 0),
  340. sourceIndex,
  341. originalLine,
  342. originalColumn,
  343. replacementNameIndex
  344. );
  345. // Only the first chunk has name assigned
  346. replacementNameIndex = -1;
  347. if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
  348. if (generatedColumnOffsetLine === line) {
  349. generatedColumnOffset += contentLine.length;
  350. } else {
  351. generatedColumnOffset = contentLine.length;
  352. generatedColumnOffsetLine = line;
  353. }
  354. } else {
  355. generatedLineOffset++;
  356. line++;
  357. generatedColumnOffset = -generatedColumn;
  358. generatedColumnOffsetLine = line;
  359. }
  360. }
  361. // Remove replaced content by settings this variable
  362. replacmentEnd = Math.max(
  363. replacmentEnd,
  364. Math.floor(replacements[i].end + 1)
  365. );
  366. // Move to next replacment
  367. i++;
  368. nextReplacement =
  369. i < replacements.length
  370. ? Math.floor(replacements[i].start)
  371. : MAX_SOURCE_POSITION;
  372. // Skip over when it has been replaced
  373. const offset = chunk.length - endPos + replacmentEnd - chunkPos;
  374. if (offset > 0) {
  375. // Skip over whole chunk
  376. if (replacmentEnd >= endPos) {
  377. let line = generatedLine + generatedLineOffset;
  378. if (chunk.endsWith("\n")) {
  379. generatedLineOffset--;
  380. if (generatedColumnOffsetLine === line) {
  381. // undo exiting corrections form the current line
  382. generatedColumnOffset += generatedColumn;
  383. }
  384. } else if (generatedColumnOffsetLine === line) {
  385. generatedColumnOffset -= chunk.length - chunkPos;
  386. } else {
  387. generatedColumnOffset = chunkPos - chunk.length;
  388. generatedColumnOffsetLine = line;
  389. }
  390. pos = endPos;
  391. return;
  392. }
  393. // Partially skip over chunk
  394. const line = generatedLine + generatedLineOffset;
  395. if (
  396. checkOriginalContent(
  397. sourceIndex,
  398. originalLine,
  399. originalColumn,
  400. chunk.slice(chunkPos, chunkPos + offset)
  401. )
  402. ) {
  403. originalColumn += offset;
  404. }
  405. chunkPos += offset;
  406. pos += offset;
  407. if (generatedColumnOffsetLine === line) {
  408. generatedColumnOffset -= offset;
  409. } else {
  410. generatedColumnOffset = -offset;
  411. generatedColumnOffsetLine = line;
  412. }
  413. generatedColumn += offset;
  414. }
  415. } while (nextReplacement < endPos);
  416. }
  417. // Emit remaining chunk
  418. if (chunkPos < chunk.length) {
  419. const chunkSlice = chunkPos === 0 ? chunk : chunk.slice(chunkPos);
  420. const line = generatedLine + generatedLineOffset;
  421. onChunk(
  422. chunkSlice,
  423. line,
  424. generatedColumn +
  425. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  426. sourceIndex,
  427. originalLine,
  428. originalColumn,
  429. nameIndex < 0 ? -1 : nameIndexMapping[nameIndex]
  430. );
  431. }
  432. pos = endPos;
  433. },
  434. (sourceIndex, source, sourceContent) => {
  435. while (sourceContents.length < sourceIndex)
  436. sourceContents.push(undefined);
  437. sourceContents[sourceIndex] = sourceContent;
  438. onSource(sourceIndex, source, sourceContent);
  439. },
  440. (nameIndex, name) => {
  441. let globalIndex = nameMapping.get(name);
  442. if (globalIndex === undefined) {
  443. globalIndex = nameMapping.size;
  444. nameMapping.set(name, globalIndex);
  445. onName(globalIndex, name);
  446. }
  447. nameIndexMapping[nameIndex] = globalIndex;
  448. }
  449. );
  450. // Handle remaining replacements
  451. let remainer = "";
  452. for (; i < replacements.length; i++) {
  453. remainer += replacements[i].content;
  454. }
  455. // Insert remaining replacements content splitted into chunks by lines
  456. let line = /** @type {number} */ (generatedLine) + generatedLineOffset;
  457. let matches = splitIntoLines(remainer);
  458. for (let m = 0; m < matches.length; m++) {
  459. const contentLine = matches[m];
  460. onChunk(
  461. contentLine,
  462. line,
  463. /** @type {number} */
  464. (generatedColumn) +
  465. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  466. -1,
  467. -1,
  468. -1,
  469. -1
  470. );
  471. if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
  472. if (generatedColumnOffsetLine === line) {
  473. generatedColumnOffset += contentLine.length;
  474. } else {
  475. generatedColumnOffset = contentLine.length;
  476. generatedColumnOffsetLine = line;
  477. }
  478. } else {
  479. generatedLineOffset++;
  480. line++;
  481. generatedColumnOffset = -(/** @type {number} */ (generatedColumn));
  482. generatedColumnOffsetLine = line;
  483. }
  484. }
  485. return {
  486. generatedLine: line,
  487. generatedColumn:
  488. /** @type {number} */
  489. (generatedColumn) +
  490. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0)
  491. };
  492. }
  493. /**
  494. * @param {Hash} hash hash
  495. * @returns {void}
  496. */
  497. updateHash(hash) {
  498. this._sortReplacements();
  499. hash.update("ReplaceSource");
  500. this._source.updateHash(hash);
  501. hash.update(this._name || "");
  502. for (const repl of this._replacements) {
  503. hash.update(
  504. `${repl.start}${repl.end}${repl.content}${repl.name ? repl.name : ""}`
  505. );
  506. }
  507. }
  508. }
  509. module.exports = ReplaceSource;
  510. module.exports.Replacement = Replacement;