ReplaceSource.js 15 KB

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