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").HashLike} HashLike */
  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. }
  78. this._replacements.push(new Replacement(start, end, newValue, name));
  79. this._isSorted = false;
  80. }
  81. /**
  82. * @param {number} pos pos
  83. * @param {string} newValue new value
  84. * @param {string=} name name
  85. * @returns {void}
  86. */
  87. insert(pos, newValue, name) {
  88. if (typeof newValue !== "string") {
  89. throw new Error(
  90. `insertion must be a string, but is a ${typeof newValue}: ${newValue}`,
  91. );
  92. }
  93. this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
  94. this._isSorted = false;
  95. }
  96. /**
  97. * @returns {SourceValue} source
  98. */
  99. source() {
  100. if (this._replacements.length === 0) {
  101. return this._source.source();
  102. }
  103. let current = this._source.source();
  104. let pos = 0;
  105. const result = [];
  106. this._sortReplacements();
  107. for (const replacement of this._replacements) {
  108. const start = Math.floor(replacement.start);
  109. const end = Math.floor(replacement.end + 1);
  110. if (pos < start) {
  111. const offset = start - pos;
  112. result.push(current.slice(0, offset));
  113. current = current.slice(offset);
  114. pos = start;
  115. }
  116. result.push(replacement.content);
  117. if (pos < end) {
  118. const offset = end - pos;
  119. current = current.slice(offset);
  120. pos = end;
  121. }
  122. }
  123. result.push(current);
  124. return result.join("");
  125. }
  126. /**
  127. * @param {MapOptions=} options map options
  128. * @returns {RawSourceMap | null} map
  129. */
  130. map(options) {
  131. if (this._replacements.length === 0) {
  132. return this._source.map(options);
  133. }
  134. return getMap(this, options);
  135. }
  136. /**
  137. * @param {MapOptions=} options map options
  138. * @returns {SourceAndMap} source and map
  139. */
  140. sourceAndMap(options) {
  141. if (this._replacements.length === 0) {
  142. return this._source.sourceAndMap(options);
  143. }
  144. return getSourceAndMap(this, options);
  145. }
  146. original() {
  147. return this._source;
  148. }
  149. _sortReplacements() {
  150. if (this._isSorted) return;
  151. if (hasStableSort) {
  152. this._replacements.sort((a, b) => {
  153. const diff1 = a.start - b.start;
  154. if (diff1 !== 0) return diff1;
  155. const diff2 = a.end - b.end;
  156. if (diff2 !== 0) return diff2;
  157. return 0;
  158. });
  159. } else {
  160. for (const [i, repl] of this._replacements.entries()) repl.index = i;
  161. this._replacements.sort((a, b) => {
  162. const diff1 = a.start - b.start;
  163. if (diff1 !== 0) return diff1;
  164. const diff2 = a.end - b.end;
  165. if (diff2 !== 0) return diff2;
  166. return (
  167. /** @type {number} */ (a.index) - /** @type {number} */ (b.index)
  168. );
  169. });
  170. }
  171. this._isSorted = true;
  172. }
  173. /**
  174. * @param {Options} options options
  175. * @param {OnChunk} onChunk called for each chunk of code
  176. * @param {OnSource} onSource called for each source
  177. * @param {OnName} onName called for each name
  178. * @returns {GeneratedSourceInfo} generated source info
  179. */
  180. streamChunks(options, onChunk, onSource, onName) {
  181. this._sortReplacements();
  182. const replacements = this._replacements;
  183. let pos = 0;
  184. let i = 0;
  185. let replacementEnd = -1;
  186. let nextReplacement =
  187. i < replacements.length
  188. ? Math.floor(replacements[i].start)
  189. : MAX_SOURCE_POSITION;
  190. let generatedLineOffset = 0;
  191. let generatedColumnOffset = 0;
  192. let generatedColumnOffsetLine = 0;
  193. /** @type {(string | string[] | undefined)[]} */
  194. const sourceContents = [];
  195. /** @type {Map<string, number>} */
  196. const nameMapping = new Map();
  197. /** @type {number[]} */
  198. const nameIndexMapping = [];
  199. /**
  200. * @param {number} sourceIndex source index
  201. * @param {number} line line
  202. * @param {number} column column
  203. * @param {string} expectedChunk expected chunk
  204. * @returns {boolean} result
  205. */
  206. const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
  207. /** @type {undefined | string | string[]} */
  208. let content =
  209. sourceIndex < sourceContents.length
  210. ? sourceContents[sourceIndex]
  211. : undefined;
  212. if (content === undefined) return false;
  213. if (typeof content === "string") {
  214. content = splitIntoLines(content);
  215. sourceContents[sourceIndex] = content;
  216. }
  217. const contentLine = line <= content.length ? content[line - 1] : null;
  218. if (contentLine === null) return false;
  219. return (
  220. contentLine.slice(column, column + expectedChunk.length) ===
  221. expectedChunk
  222. );
  223. };
  224. const { generatedLine, generatedColumn } = streamChunks(
  225. this._source,
  226. { ...options, finalSource: false },
  227. (
  228. _chunk,
  229. generatedLine,
  230. generatedColumn,
  231. sourceIndex,
  232. originalLine,
  233. originalColumn,
  234. nameIndex,
  235. ) => {
  236. let chunkPos = 0;
  237. const chunk = /** @type {string} */ (_chunk);
  238. const endPos = pos + chunk.length;
  239. // Skip over when it has been replaced
  240. if (replacementEnd > pos) {
  241. // Skip over the whole chunk
  242. if (replacementEnd >= endPos) {
  243. const line = generatedLine + generatedLineOffset;
  244. if (chunk.endsWith("\n")) {
  245. generatedLineOffset--;
  246. if (generatedColumnOffsetLine === line) {
  247. // undo exiting corrections form the current line
  248. generatedColumnOffset += generatedColumn;
  249. }
  250. } else if (generatedColumnOffsetLine === line) {
  251. generatedColumnOffset -= chunk.length;
  252. } else {
  253. generatedColumnOffset = -chunk.length;
  254. generatedColumnOffsetLine = line;
  255. }
  256. pos = endPos;
  257. return;
  258. }
  259. // Partially skip over chunk
  260. chunkPos = replacementEnd - pos;
  261. if (
  262. checkOriginalContent(
  263. sourceIndex,
  264. originalLine,
  265. originalColumn,
  266. chunk.slice(0, chunkPos),
  267. )
  268. ) {
  269. originalColumn += chunkPos;
  270. }
  271. pos += chunkPos;
  272. const line = generatedLine + generatedLineOffset;
  273. if (generatedColumnOffsetLine === line) {
  274. generatedColumnOffset -= chunkPos;
  275. } else {
  276. generatedColumnOffset = -chunkPos;
  277. generatedColumnOffsetLine = line;
  278. }
  279. generatedColumn += chunkPos;
  280. }
  281. // Is a replacement in the chunk?
  282. if (nextReplacement < endPos) {
  283. do {
  284. let line = generatedLine + generatedLineOffset;
  285. if (nextReplacement > pos) {
  286. // Emit chunk until replacement
  287. const offset = nextReplacement - pos;
  288. const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
  289. onChunk(
  290. chunkSlice,
  291. line,
  292. generatedColumn +
  293. (line === generatedColumnOffsetLine
  294. ? generatedColumnOffset
  295. : 0),
  296. sourceIndex,
  297. originalLine,
  298. originalColumn,
  299. nameIndex < 0 || nameIndex >= nameIndexMapping.length
  300. ? -1
  301. : nameIndexMapping[nameIndex],
  302. );
  303. generatedColumn += offset;
  304. chunkPos += offset;
  305. pos = nextReplacement;
  306. if (
  307. checkOriginalContent(
  308. sourceIndex,
  309. originalLine,
  310. originalColumn,
  311. chunkSlice,
  312. )
  313. ) {
  314. originalColumn += chunkSlice.length;
  315. }
  316. }
  317. // Insert replacement content splitted into chunks by lines
  318. const { content, name } = replacements[i];
  319. const matches = splitIntoLines(content);
  320. let replacementNameIndex = nameIndex;
  321. if (sourceIndex >= 0 && name) {
  322. let globalIndex = nameMapping.get(name);
  323. if (globalIndex === undefined) {
  324. globalIndex = nameMapping.size;
  325. nameMapping.set(name, globalIndex);
  326. onName(globalIndex, name);
  327. }
  328. replacementNameIndex = globalIndex;
  329. }
  330. for (let m = 0; m < matches.length; m++) {
  331. const contentLine = matches[m];
  332. onChunk(
  333. contentLine,
  334. line,
  335. generatedColumn +
  336. (line === generatedColumnOffsetLine
  337. ? generatedColumnOffset
  338. : 0),
  339. sourceIndex,
  340. originalLine,
  341. originalColumn,
  342. replacementNameIndex,
  343. );
  344. // Only the first chunk has name assigned
  345. replacementNameIndex = -1;
  346. if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
  347. if (generatedColumnOffsetLine === line) {
  348. generatedColumnOffset += contentLine.length;
  349. } else {
  350. generatedColumnOffset = contentLine.length;
  351. generatedColumnOffsetLine = line;
  352. }
  353. } else {
  354. generatedLineOffset++;
  355. line++;
  356. generatedColumnOffset = -generatedColumn;
  357. generatedColumnOffsetLine = line;
  358. }
  359. }
  360. // Remove replaced content by settings this variable
  361. replacementEnd = Math.max(
  362. replacementEnd,
  363. Math.floor(replacements[i].end + 1),
  364. );
  365. // Move to next replacement
  366. i++;
  367. nextReplacement =
  368. i < replacements.length
  369. ? Math.floor(replacements[i].start)
  370. : MAX_SOURCE_POSITION;
  371. // Skip over when it has been replaced
  372. const offset = chunk.length - endPos + replacementEnd - chunkPos;
  373. if (offset > 0) {
  374. // Skip over whole chunk
  375. if (replacementEnd >= endPos) {
  376. const line = generatedLine + generatedLineOffset;
  377. if (chunk.endsWith("\n")) {
  378. generatedLineOffset--;
  379. if (generatedColumnOffsetLine === line) {
  380. // undo exiting corrections form the current line
  381. generatedColumnOffset += generatedColumn;
  382. }
  383. } else if (generatedColumnOffsetLine === line) {
  384. generatedColumnOffset -= chunk.length - chunkPos;
  385. } else {
  386. generatedColumnOffset = chunkPos - chunk.length;
  387. generatedColumnOffsetLine = line;
  388. }
  389. pos = endPos;
  390. return;
  391. }
  392. // Partially skip over chunk
  393. const line = generatedLine + generatedLineOffset;
  394. if (
  395. checkOriginalContent(
  396. sourceIndex,
  397. originalLine,
  398. originalColumn,
  399. chunk.slice(chunkPos, chunkPos + offset),
  400. )
  401. ) {
  402. originalColumn += offset;
  403. }
  404. chunkPos += offset;
  405. pos += offset;
  406. if (generatedColumnOffsetLine === line) {
  407. generatedColumnOffset -= offset;
  408. } else {
  409. generatedColumnOffset = -offset;
  410. generatedColumnOffsetLine = line;
  411. }
  412. generatedColumn += offset;
  413. }
  414. } while (nextReplacement < endPos);
  415. }
  416. // Emit remaining chunk
  417. if (chunkPos < chunk.length) {
  418. const chunkSlice = chunkPos === 0 ? chunk : chunk.slice(chunkPos);
  419. const line = generatedLine + generatedLineOffset;
  420. onChunk(
  421. chunkSlice,
  422. line,
  423. generatedColumn +
  424. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  425. sourceIndex,
  426. originalLine,
  427. originalColumn,
  428. nameIndex < 0 ? -1 : nameIndexMapping[nameIndex],
  429. );
  430. }
  431. pos = endPos;
  432. },
  433. (sourceIndex, source, sourceContent) => {
  434. while (sourceContents.length < sourceIndex) {
  435. sourceContents.push(undefined);
  436. }
  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. const 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 {HashLike} 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;