ReplaceSource.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  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").ClearCacheOptions} ClearCacheOptions */
  11. /** @typedef {import("./Source").HashLike} HashLike */
  12. /** @typedef {import("./Source").MapOptions} MapOptions */
  13. /** @typedef {import("./Source").RawSourceMap} RawSourceMap */
  14. /** @typedef {import("./Source").SourceAndMap} SourceAndMap */
  15. /** @typedef {import("./Source").SourceValue} SourceValue */
  16. /** @typedef {import("./helpers/getGeneratedSourceInfo").GeneratedSourceInfo} GeneratedSourceInfo */
  17. /** @typedef {import("./helpers/streamChunks").OnChunk} OnChunk */
  18. /** @typedef {import("./helpers/streamChunks").OnName} OnName */
  19. /** @typedef {import("./helpers/streamChunks").OnSource} OnSource */
  20. /** @typedef {import("./helpers/streamChunks").Options} Options */
  21. // since v8 7.0, Array.prototype.sort is stable
  22. const hasStableSort =
  23. typeof process === "object" &&
  24. process.versions &&
  25. typeof process.versions.v8 === "string" &&
  26. !/^[0-6]\./.test(process.versions.v8);
  27. // This is larger than max string length
  28. const MAX_SOURCE_POSITION = 0x20000000;
  29. /**
  30. * Stable comparator hoisted to module scope so each `_sortReplacements()`
  31. * call doesn't allocate a fresh closure.
  32. * @param {Replacement} a a
  33. * @param {Replacement} b b
  34. * @returns {number} order
  35. */
  36. const compareStable = (a, b) => {
  37. const diff1 = a.start - b.start;
  38. if (diff1 !== 0) return diff1;
  39. const diff2 = a.end - b.end;
  40. if (diff2 !== 0) return diff2;
  41. return 0;
  42. };
  43. /**
  44. * Index-stabilising comparator for v8 < 7.0 (pre-stable Array.prototype.sort).
  45. * Unreachable on any supported Node — the `hasStableSort` guard always
  46. * wins so coverage tools never see this execute.
  47. * @param {Replacement} a a
  48. * @param {Replacement} b b
  49. * @returns {number} order
  50. */
  51. /* istanbul ignore next */
  52. const compareUnstableFallback = (a, b) => {
  53. const diff1 = a.start - b.start;
  54. if (diff1 !== 0) return diff1;
  55. const diff2 = a.end - b.end;
  56. if (diff2 !== 0) return diff2;
  57. return /** @type {number} */ (a.index) - /** @type {number} */ (b.index);
  58. };
  59. class Replacement {
  60. /**
  61. * @param {number} start start
  62. * @param {number} end end
  63. * @param {string} content content
  64. * @param {string=} name name
  65. */
  66. constructor(start, end, content, name) {
  67. this.start = start;
  68. this.end = end;
  69. this.content = content;
  70. this.name = name;
  71. // V8 < 7.0 only — unreachable on any supported Node.
  72. /* istanbul ignore if */
  73. if (!hasStableSort) {
  74. this.index = -1;
  75. }
  76. }
  77. }
  78. class ReplaceSource extends Source {
  79. /**
  80. * @param {Source} source source
  81. * @param {string=} name name
  82. */
  83. constructor(source, name) {
  84. super();
  85. /**
  86. * @private
  87. * @type {Source}
  88. */
  89. this._source = source;
  90. /**
  91. * @private
  92. * @type {string | undefined}
  93. */
  94. this._name = name;
  95. /** @type {Replacement[]} */
  96. this._replacements = [];
  97. /**
  98. * @private
  99. * @type {boolean}
  100. */
  101. this._isSorted = true;
  102. }
  103. getName() {
  104. return this._name;
  105. }
  106. getReplacements() {
  107. this._sortReplacements();
  108. return this._replacements;
  109. }
  110. /**
  111. * @param {number} start start
  112. * @param {number} end end
  113. * @param {string} newValue new value
  114. * @param {string=} name name
  115. * @returns {void}
  116. */
  117. replace(start, end, newValue, name) {
  118. if (typeof newValue !== "string") {
  119. throw new Error(
  120. `insertion must be a string, but is a ${typeof newValue}`,
  121. );
  122. }
  123. this._replacements.push(new Replacement(start, end, newValue, name));
  124. this._isSorted = false;
  125. }
  126. /**
  127. * @param {number} pos pos
  128. * @param {string} newValue new value
  129. * @param {string=} name name
  130. * @returns {void}
  131. */
  132. insert(pos, newValue, name) {
  133. if (typeof newValue !== "string") {
  134. throw new Error(
  135. `insertion must be a string, but is a ${typeof newValue}: ${newValue}`,
  136. );
  137. }
  138. this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
  139. this._isSorted = false;
  140. }
  141. /**
  142. * @returns {SourceValue} source
  143. */
  144. source() {
  145. if (this._replacements.length === 0) {
  146. return this._source.source();
  147. }
  148. const current = /** @type {string} */ (this._source.source());
  149. let pos = 0;
  150. const result = [];
  151. this._sortReplacements();
  152. for (const replacement of this._replacements) {
  153. const start = Math.floor(replacement.start);
  154. const end = Math.floor(replacement.end + 1);
  155. if (pos < start) {
  156. // slice directly from the original string rather than repeatedly
  157. // producing smaller intermediate strings, which avoids O(n) copies.
  158. result.push(current.slice(pos, start));
  159. pos = start;
  160. }
  161. result.push(replacement.content);
  162. if (pos < end) {
  163. pos = end;
  164. }
  165. }
  166. if (pos < current.length) {
  167. result.push(pos === 0 ? current : current.slice(pos));
  168. }
  169. return result.join("");
  170. }
  171. /**
  172. * @returns {Buffer} buffer
  173. */
  174. buffer() {
  175. if (this._replacements.length === 0) {
  176. return this._source.buffer();
  177. }
  178. return super.buffer();
  179. }
  180. /**
  181. * @returns {Buffer[]} buffers
  182. */
  183. buffers() {
  184. if (this._replacements.length === 0) {
  185. // TODO remove in the next major release
  186. return typeof this._source.buffers === "function"
  187. ? this._source.buffers()
  188. : [this._source.buffer()];
  189. }
  190. return [this.buffer()];
  191. }
  192. /**
  193. * @param {MapOptions=} options map options
  194. * @returns {RawSourceMap | null} map
  195. */
  196. map(options) {
  197. if (this._replacements.length === 0) {
  198. return this._source.map(options);
  199. }
  200. return getMap(this, options);
  201. }
  202. /**
  203. * @param {MapOptions=} options map options
  204. * @returns {SourceAndMap} source and map
  205. */
  206. sourceAndMap(options) {
  207. if (this._replacements.length === 0) {
  208. return this._source.sourceAndMap(options);
  209. }
  210. return getSourceAndMap(this, options);
  211. }
  212. original() {
  213. return this._source;
  214. }
  215. _sortReplacements() {
  216. if (this._isSorted) return;
  217. const replacements = this._replacements;
  218. // Replacements are usually appended in source order (ties keep
  219. // insertion order, matching a stable sort), so an O(n) pre-scan
  220. // often lets us skip the sort and its per-element comparator calls.
  221. let isPresorted = true;
  222. for (let i = 1; i < replacements.length; i++) {
  223. const prev = replacements[i - 1];
  224. const repl = replacements[i];
  225. if (
  226. repl.start < prev.start ||
  227. (repl.start === prev.start && repl.end < prev.end)
  228. ) {
  229. isPresorted = false;
  230. break;
  231. }
  232. }
  233. if (isPresorted) {
  234. this._isSorted = true;
  235. return;
  236. }
  237. if (hasStableSort) {
  238. this._replacements.sort(compareStable);
  239. } else {
  240. // V8 < 7.0 only — unreachable on any supported Node.
  241. /* istanbul ignore next */
  242. for (const [i, repl] of this._replacements.entries()) repl.index = i;
  243. /* istanbul ignore next */
  244. this._replacements.sort(compareUnstableFallback);
  245. }
  246. this._isSorted = true;
  247. }
  248. /**
  249. * @param {Options} options options
  250. * @param {OnChunk} onChunk called for each chunk of code
  251. * @param {OnSource} onSource called for each source
  252. * @param {OnName} onName called for each name
  253. * @returns {GeneratedSourceInfo} generated source info
  254. */
  255. streamChunks(options, onChunk, onSource, onName) {
  256. this._sortReplacements();
  257. // When the consumer only wants the final source (map() /
  258. // sourceAndMap()), emit position-only chunks (chunk === undefined,
  259. // like OriginalSource and RawSource do) and hand back the whole
  260. // replaced source once at the end. This avoids allocating boundary
  261. // slices for emission and — more importantly — the per-chunk
  262. // `code += chunk` cons-string chain in every enclosing consumer.
  263. // With `source: false` the caller (getMap) additionally promises not
  264. // to read the returned source, so its assembly is skipped entirely;
  265. // streamAndGetSourceAndMap overrides that flag because it caches the
  266. // text.
  267. const finalSource = Boolean(options && options.finalSource);
  268. const needSource = !options || options.source !== false;
  269. const replacements = this._replacements;
  270. let pos = 0;
  271. let i = 0;
  272. let replacementEnd = -1;
  273. let nextReplacement =
  274. i < replacements.length
  275. ? Math.floor(replacements[i].start)
  276. : MAX_SOURCE_POSITION;
  277. let generatedLineOffset = 0;
  278. let generatedColumnOffset = 0;
  279. let generatedColumnOffsetLine = 0;
  280. /** @type {(string | undefined)[]} */
  281. const sourceContents = [];
  282. /**
  283. * Lazily-built line-start offsets per source content. One number per
  284. * line instead of one substring per line (`splitIntoLines`), and the
  285. * chunk comparison below runs allocation-free via `startsWith`.
  286. * @type {(number[] | undefined)[]}
  287. */
  288. const sourceContentLineStarts = [];
  289. /** @type {Map<string, number>} */
  290. const nameMapping = new Map();
  291. /** @type {number[]} */
  292. const nameIndexMapping = [];
  293. /**
  294. * @param {number} sourceIndex source index
  295. * @param {number} line line
  296. * @param {number} column column
  297. * @param {string} expectedChunk expected chunk
  298. * @returns {boolean} result
  299. */
  300. const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
  301. const content =
  302. sourceIndex < sourceContents.length
  303. ? sourceContents[sourceIndex]
  304. : undefined;
  305. if (content === undefined) return false;
  306. let lineStarts = sourceContentLineStarts[sourceIndex];
  307. if (lineStarts === undefined) {
  308. // Line boundaries mirror `splitIntoLines`: every line includes
  309. // its trailing "\n"; a final line without "\n" still counts.
  310. lineStarts = [];
  311. const { length } = content;
  312. let offset = 0;
  313. while (offset < length) {
  314. lineStarts.push(offset);
  315. const newline = content.indexOf("\n", offset);
  316. if (newline === -1) break;
  317. offset = newline + 1;
  318. }
  319. sourceContentLineStarts[sourceIndex] = lineStarts;
  320. }
  321. if (line > lineStarts.length) return false;
  322. const lineStart = lineStarts[line - 1];
  323. const lineEnd =
  324. line < lineStarts.length ? lineStarts[line] : content.length;
  325. // The expected chunk never spans lines, so a match must fit into
  326. // the current line (mirrors the old per-line slice comparison).
  327. if (column + expectedChunk.length > lineEnd - lineStart) return false;
  328. return content.startsWith(expectedChunk, lineStart + column);
  329. };
  330. const { generatedLine, generatedColumn } = streamChunks(
  331. this._source,
  332. { ...options, finalSource: false },
  333. (
  334. _chunk,
  335. generatedLine,
  336. generatedColumn,
  337. sourceIndex,
  338. originalLine,
  339. originalColumn,
  340. nameIndex,
  341. ) => {
  342. let chunkPos = 0;
  343. const chunk = /** @type {string} */ (_chunk);
  344. const endPos = pos + chunk.length;
  345. // Skip over when it has been replaced
  346. if (replacementEnd > pos) {
  347. // Skip over the whole chunk
  348. if (replacementEnd >= endPos) {
  349. const line = generatedLine + generatedLineOffset;
  350. if (chunk.endsWith("\n")) {
  351. generatedLineOffset--;
  352. if (generatedColumnOffsetLine === line) {
  353. // undo exiting corrections form the current line
  354. generatedColumnOffset += generatedColumn;
  355. }
  356. } else if (generatedColumnOffsetLine === line) {
  357. generatedColumnOffset -= chunk.length;
  358. } else {
  359. /* istanbul ignore next: pre-existing chunk-skipping cross-line case (also untested on main) */
  360. generatedColumnOffset = -chunk.length;
  361. /* istanbul ignore next: pre-existing chunk-skipping cross-line case (also untested on main) */
  362. generatedColumnOffsetLine = line;
  363. }
  364. pos = endPos;
  365. return;
  366. }
  367. // Partially skip over chunk
  368. chunkPos = replacementEnd - pos;
  369. if (
  370. checkOriginalContent(
  371. sourceIndex,
  372. originalLine,
  373. originalColumn,
  374. chunk.slice(0, chunkPos),
  375. )
  376. ) {
  377. originalColumn += chunkPos;
  378. }
  379. pos += chunkPos;
  380. const line = generatedLine + generatedLineOffset;
  381. /* istanbul ignore else: pre-existing chunk-skipping cross-line case (also untested on main) */
  382. if (generatedColumnOffsetLine === line) {
  383. generatedColumnOffset -= chunkPos;
  384. } else {
  385. generatedColumnOffset = -chunkPos;
  386. generatedColumnOffsetLine = line;
  387. }
  388. generatedColumn += chunkPos;
  389. }
  390. // Is a replacement in the chunk?
  391. if (nextReplacement < endPos) {
  392. do {
  393. let line = generatedLine + generatedLineOffset;
  394. if (nextReplacement > pos) {
  395. // Emit chunk until replacement
  396. const offset = nextReplacement - pos;
  397. const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
  398. onChunk(
  399. finalSource ? undefined : chunkSlice,
  400. line,
  401. generatedColumn +
  402. (line === generatedColumnOffsetLine
  403. ? generatedColumnOffset
  404. : 0),
  405. sourceIndex,
  406. originalLine,
  407. originalColumn,
  408. nameIndex < 0 || nameIndex >= nameIndexMapping.length
  409. ? -1
  410. : nameIndexMapping[nameIndex],
  411. );
  412. generatedColumn += offset;
  413. chunkPos += offset;
  414. pos = nextReplacement;
  415. if (
  416. checkOriginalContent(
  417. sourceIndex,
  418. originalLine,
  419. originalColumn,
  420. chunkSlice,
  421. )
  422. ) {
  423. originalColumn += chunkSlice.length;
  424. }
  425. }
  426. // Insert replacement content splitted into chunks by lines
  427. const { content, name } = replacements[i];
  428. let replacementNameIndex = nameIndex;
  429. if (sourceIndex >= 0 && name) {
  430. let globalIndex = nameMapping.get(name);
  431. if (globalIndex === undefined) {
  432. globalIndex = nameMapping.size;
  433. nameMapping.set(name, globalIndex);
  434. onName(globalIndex, name);
  435. }
  436. replacementNameIndex = globalIndex;
  437. }
  438. // Fast path: most replacements (renamed identifiers,
  439. // short inserts) carry single-line content. Skip
  440. // `splitIntoLines` — and its array allocation — when
  441. // we can tell the content has no embedded newline.
  442. // `splitIntoLines("")` returns `[]`; emitting a zero-
  443. // length chunk would still walk the loop, so handle
  444. // it as a no-op explicitly.
  445. if (content.length > 0 && !content.includes("\n")) {
  446. onChunk(
  447. finalSource ? undefined : content,
  448. line,
  449. generatedColumn +
  450. (line === generatedColumnOffsetLine
  451. ? generatedColumnOffset
  452. : 0),
  453. sourceIndex,
  454. originalLine,
  455. originalColumn,
  456. replacementNameIndex,
  457. );
  458. if (generatedColumnOffsetLine === line) {
  459. generatedColumnOffset += content.length;
  460. } else {
  461. generatedColumnOffset = content.length;
  462. generatedColumnOffsetLine = line;
  463. }
  464. } else if (content.length === 0) {
  465. // Empty replacement: no chunk to emit, no column
  466. // movement. `splitIntoLines("")` is `[]` so the
  467. // existing loop already does nothing — explicit
  468. // guard skips the per-call array allocation.
  469. } else {
  470. const matches = splitIntoLines(content);
  471. for (let m = 0; m < matches.length; m++) {
  472. const contentLine = matches[m];
  473. onChunk(
  474. finalSource ? undefined : contentLine,
  475. line,
  476. generatedColumn +
  477. (line === generatedColumnOffsetLine
  478. ? generatedColumnOffset
  479. : 0),
  480. sourceIndex,
  481. originalLine,
  482. originalColumn,
  483. replacementNameIndex,
  484. );
  485. // Only the first chunk has name assigned
  486. replacementNameIndex = -1;
  487. if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
  488. /* istanbul ignore else: pre-existing multi-line replacement cross-line case (also untested on main) */
  489. if (generatedColumnOffsetLine === line) {
  490. generatedColumnOffset += contentLine.length;
  491. } else {
  492. generatedColumnOffset = contentLine.length;
  493. generatedColumnOffsetLine = line;
  494. }
  495. } else {
  496. generatedLineOffset++;
  497. line++;
  498. generatedColumnOffset = -generatedColumn;
  499. generatedColumnOffsetLine = line;
  500. }
  501. }
  502. }
  503. // Remove replaced content by settings this variable
  504. replacementEnd = Math.max(
  505. replacementEnd,
  506. Math.floor(replacements[i].end + 1),
  507. );
  508. // Move to next replacement
  509. i++;
  510. nextReplacement =
  511. i < replacements.length
  512. ? Math.floor(replacements[i].start)
  513. : MAX_SOURCE_POSITION;
  514. // Skip over when it has been replaced
  515. const offset = chunk.length - endPos + replacementEnd - chunkPos;
  516. if (offset > 0) {
  517. // Skip over whole chunk
  518. if (replacementEnd >= endPos) {
  519. const line = generatedLine + generatedLineOffset;
  520. if (chunk.endsWith("\n")) {
  521. generatedLineOffset--;
  522. if (generatedColumnOffsetLine === line) {
  523. // undo exiting corrections form the current line
  524. generatedColumnOffset += generatedColumn;
  525. }
  526. } else if (generatedColumnOffsetLine === line) {
  527. generatedColumnOffset -= chunk.length - chunkPos;
  528. } else {
  529. generatedColumnOffset = chunkPos - chunk.length;
  530. generatedColumnOffsetLine = line;
  531. }
  532. pos = endPos;
  533. return;
  534. }
  535. // Partially skip over chunk
  536. const line = generatedLine + generatedLineOffset;
  537. if (
  538. checkOriginalContent(
  539. sourceIndex,
  540. originalLine,
  541. originalColumn,
  542. chunk.slice(chunkPos, chunkPos + offset),
  543. )
  544. ) {
  545. originalColumn += offset;
  546. }
  547. chunkPos += offset;
  548. pos += offset;
  549. if (generatedColumnOffsetLine === line) {
  550. generatedColumnOffset -= offset;
  551. } else {
  552. generatedColumnOffset = -offset;
  553. generatedColumnOffsetLine = line;
  554. }
  555. generatedColumn += offset;
  556. }
  557. } while (nextReplacement < endPos);
  558. }
  559. // Emit remaining chunk
  560. if (chunkPos < chunk.length) {
  561. const chunkSlice = finalSource
  562. ? undefined
  563. : chunkPos === 0
  564. ? chunk
  565. : chunk.slice(chunkPos);
  566. const line = generatedLine + generatedLineOffset;
  567. onChunk(
  568. chunkSlice,
  569. line,
  570. generatedColumn +
  571. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  572. sourceIndex,
  573. originalLine,
  574. originalColumn,
  575. nameIndex < 0 ? -1 : nameIndexMapping[nameIndex],
  576. );
  577. }
  578. pos = endPos;
  579. },
  580. (sourceIndex, source, sourceContent) => {
  581. /* istanbul ignore next: non-sequential sourceIndex emission is not produced by any in-tree Source */
  582. while (sourceContents.length < sourceIndex) {
  583. sourceContents.push(undefined);
  584. }
  585. sourceContents[sourceIndex] = sourceContent;
  586. onSource(sourceIndex, source, sourceContent);
  587. },
  588. (nameIndex, name) => {
  589. let globalIndex = nameMapping.get(name);
  590. if (globalIndex === undefined) {
  591. globalIndex = nameMapping.size;
  592. nameMapping.set(name, globalIndex);
  593. onName(globalIndex, name);
  594. }
  595. nameIndexMapping[nameIndex] = globalIndex;
  596. },
  597. );
  598. // Handle remaining replacements
  599. let remainer = "";
  600. for (; i < replacements.length; i++) {
  601. remainer += replacements[i].content;
  602. }
  603. // Insert remaining replacements content splitted into chunks by lines
  604. let line = /** @type {number} */ (generatedLine) + generatedLineOffset;
  605. // Fast path mirroring the in-chunk replacement loop above: skip
  606. // splitIntoLines + per-match loop when the trailing content has no
  607. // newlines (the common case when remaining replacements are single
  608. // inserts).
  609. if (remainer.length > 0 && !remainer.includes("\n")) {
  610. onChunk(
  611. finalSource ? undefined : remainer,
  612. line,
  613. /** @type {number} */
  614. (generatedColumn) +
  615. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  616. -1,
  617. -1,
  618. -1,
  619. -1,
  620. );
  621. /* istanbul ignore else: trailing-remainer cross-line case (also untested on main) */
  622. if (generatedColumnOffsetLine === line) {
  623. generatedColumnOffset += remainer.length;
  624. } else {
  625. generatedColumnOffset = remainer.length;
  626. generatedColumnOffsetLine = line;
  627. }
  628. } else {
  629. const matches = splitIntoLines(remainer);
  630. for (let m = 0; m < matches.length; m++) {
  631. const contentLine = matches[m];
  632. onChunk(
  633. finalSource ? undefined : contentLine,
  634. line,
  635. /** @type {number} */
  636. (generatedColumn) +
  637. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  638. -1,
  639. -1,
  640. -1,
  641. -1,
  642. );
  643. if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
  644. /* istanbul ignore else: trailing-remainer multi-line cross-line case (also untested on main) */
  645. if (generatedColumnOffsetLine === line) {
  646. generatedColumnOffset += contentLine.length;
  647. } else {
  648. generatedColumnOffset = contentLine.length;
  649. generatedColumnOffsetLine = line;
  650. }
  651. } else {
  652. generatedLineOffset++;
  653. line++;
  654. generatedColumnOffset = -(/** @type {number} */ (generatedColumn));
  655. generatedColumnOffsetLine = line;
  656. }
  657. }
  658. }
  659. return {
  660. generatedLine: line,
  661. generatedColumn:
  662. /** @type {number} */
  663. (generatedColumn) +
  664. (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
  665. // The streamed chunks reproduce source() exactly, so the final
  666. // source can be assembled in O(replacements) string operations
  667. // instead of re-concatenating every emitted chunk downstream.
  668. source:
  669. finalSource && needSource
  670. ? /** @type {string} */ (this.source())
  671. : undefined,
  672. };
  673. }
  674. /**
  675. * Release cached data held by this source. clearCache is a memory
  676. * hint: it never affects correctness or output, only how expensive
  677. * the next read is. Subclasses override; the base is a no-op so
  678. * every Source supports the call. Composite sources always recurse
  679. * into wrapped sources. When the same child is reachable via several
  680. * parents (e.g. modules shared across webpack chunks), pass a shared
  681. * `visited` WeakSet so each subtree is walked at most once.
  682. * Not safe to call concurrently with source/map/sourceAndMap/
  683. * streamChunks/updateHash on the same instance.
  684. * @param {ClearCacheOptions=} options selectors
  685. * @param {WeakSet<Source>=} visited de-duplication set shared across calls
  686. * @returns {void}
  687. */
  688. clearCache(options, visited) {
  689. if (visited !== undefined && visited.has(this)) return;
  690. let v = visited;
  691. if (v === undefined) v = new WeakSet();
  692. v.add(this);
  693. this._source.clearCache(options, v);
  694. }
  695. /**
  696. * @param {HashLike} hash hash
  697. * @returns {void}
  698. */
  699. updateHash(hash) {
  700. this._sortReplacements();
  701. hash.update("ReplaceSource");
  702. this._source.updateHash(hash);
  703. hash.update(this._name || "");
  704. // Feed each replacement as multiple updates instead of building one
  705. // combined template literal per replacement. The resulting digest is
  706. // identical (hash.update is additive over bytes), but we avoid
  707. // allocating a new string per replacement.
  708. for (const repl of this._replacements) {
  709. hash.update(`${repl.start}${repl.end}`);
  710. hash.update(repl.content);
  711. if (repl.name) hash.update(repl.name);
  712. }
  713. }
  714. }
  715. module.exports = ReplaceSource;
  716. module.exports.Replacement = Replacement;