| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- */
- "use strict";
- const Source = require("./Source");
- const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
- const splitIntoLines = require("./helpers/splitIntoLines");
- const streamChunks = require("./helpers/streamChunks");
- /** @typedef {import("./Source").ClearCacheOptions} ClearCacheOptions */
- /** @typedef {import("./Source").HashLike} HashLike */
- /** @typedef {import("./Source").MapOptions} MapOptions */
- /** @typedef {import("./Source").RawSourceMap} RawSourceMap */
- /** @typedef {import("./Source").SourceAndMap} SourceAndMap */
- /** @typedef {import("./Source").SourceValue} SourceValue */
- /** @typedef {import("./helpers/getGeneratedSourceInfo").GeneratedSourceInfo} GeneratedSourceInfo */
- /** @typedef {import("./helpers/streamChunks").OnChunk} OnChunk */
- /** @typedef {import("./helpers/streamChunks").OnName} OnName */
- /** @typedef {import("./helpers/streamChunks").OnSource} OnSource */
- /** @typedef {import("./helpers/streamChunks").Options} Options */
- // since v8 7.0, Array.prototype.sort is stable
- const hasStableSort =
- typeof process === "object" &&
- process.versions &&
- typeof process.versions.v8 === "string" &&
- !/^[0-6]\./.test(process.versions.v8);
- // This is larger than max string length
- const MAX_SOURCE_POSITION = 0x20000000;
- /**
- * Stable comparator hoisted to module scope so each `_sortReplacements()`
- * call doesn't allocate a fresh closure.
- * @param {Replacement} a a
- * @param {Replacement} b b
- * @returns {number} order
- */
- const compareStable = (a, b) => {
- const diff1 = a.start - b.start;
- if (diff1 !== 0) return diff1;
- const diff2 = a.end - b.end;
- if (diff2 !== 0) return diff2;
- return 0;
- };
- /**
- * Index-stabilising comparator for v8 < 7.0 (pre-stable Array.prototype.sort).
- * Unreachable on any supported Node — the `hasStableSort` guard always
- * wins so coverage tools never see this execute.
- * @param {Replacement} a a
- * @param {Replacement} b b
- * @returns {number} order
- */
- /* istanbul ignore next */
- const compareUnstableFallback = (a, b) => {
- const diff1 = a.start - b.start;
- if (diff1 !== 0) return diff1;
- const diff2 = a.end - b.end;
- if (diff2 !== 0) return diff2;
- return /** @type {number} */ (a.index) - /** @type {number} */ (b.index);
- };
- class Replacement {
- /**
- * @param {number} start start
- * @param {number} end end
- * @param {string} content content
- * @param {string=} name name
- */
- constructor(start, end, content, name) {
- this.start = start;
- this.end = end;
- this.content = content;
- this.name = name;
- // V8 < 7.0 only — unreachable on any supported Node.
- /* istanbul ignore if */
- if (!hasStableSort) {
- this.index = -1;
- }
- }
- }
- class ReplaceSource extends Source {
- /**
- * @param {Source} source source
- * @param {string=} name name
- */
- constructor(source, name) {
- super();
- /**
- * @private
- * @type {Source}
- */
- this._source = source;
- /**
- * @private
- * @type {string | undefined}
- */
- this._name = name;
- /** @type {Replacement[]} */
- this._replacements = [];
- /**
- * @private
- * @type {boolean}
- */
- this._isSorted = true;
- }
- getName() {
- return this._name;
- }
- getReplacements() {
- this._sortReplacements();
- return this._replacements;
- }
- /**
- * @param {number} start start
- * @param {number} end end
- * @param {string} newValue new value
- * @param {string=} name name
- * @returns {void}
- */
- replace(start, end, newValue, name) {
- if (typeof newValue !== "string") {
- throw new Error(
- `insertion must be a string, but is a ${typeof newValue}`,
- );
- }
- this._replacements.push(new Replacement(start, end, newValue, name));
- this._isSorted = false;
- }
- /**
- * @param {number} pos pos
- * @param {string} newValue new value
- * @param {string=} name name
- * @returns {void}
- */
- insert(pos, newValue, name) {
- if (typeof newValue !== "string") {
- throw new Error(
- `insertion must be a string, but is a ${typeof newValue}: ${newValue}`,
- );
- }
- this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
- this._isSorted = false;
- }
- /**
- * @returns {SourceValue} source
- */
- source() {
- if (this._replacements.length === 0) {
- return this._source.source();
- }
- const current = /** @type {string} */ (this._source.source());
- let pos = 0;
- const result = [];
- this._sortReplacements();
- for (const replacement of this._replacements) {
- const start = Math.floor(replacement.start);
- const end = Math.floor(replacement.end + 1);
- if (pos < start) {
- // slice directly from the original string rather than repeatedly
- // producing smaller intermediate strings, which avoids O(n) copies.
- result.push(current.slice(pos, start));
- pos = start;
- }
- result.push(replacement.content);
- if (pos < end) {
- pos = end;
- }
- }
- if (pos < current.length) {
- result.push(pos === 0 ? current : current.slice(pos));
- }
- return result.join("");
- }
- /**
- * @returns {Buffer} buffer
- */
- buffer() {
- if (this._replacements.length === 0) {
- return this._source.buffer();
- }
- return super.buffer();
- }
- /**
- * @returns {Buffer[]} buffers
- */
- buffers() {
- if (this._replacements.length === 0) {
- // TODO remove in the next major release
- return typeof this._source.buffers === "function"
- ? this._source.buffers()
- : [this._source.buffer()];
- }
- return [this.buffer()];
- }
- /**
- * @param {MapOptions=} options map options
- * @returns {RawSourceMap | null} map
- */
- map(options) {
- if (this._replacements.length === 0) {
- return this._source.map(options);
- }
- return getMap(this, options);
- }
- /**
- * @param {MapOptions=} options map options
- * @returns {SourceAndMap} source and map
- */
- sourceAndMap(options) {
- if (this._replacements.length === 0) {
- return this._source.sourceAndMap(options);
- }
- return getSourceAndMap(this, options);
- }
- original() {
- return this._source;
- }
- _sortReplacements() {
- if (this._isSorted) return;
- const replacements = this._replacements;
- // Replacements are usually appended in source order (ties keep
- // insertion order, matching a stable sort), so an O(n) pre-scan
- // often lets us skip the sort and its per-element comparator calls.
- let isPresorted = true;
- for (let i = 1; i < replacements.length; i++) {
- const prev = replacements[i - 1];
- const repl = replacements[i];
- if (
- repl.start < prev.start ||
- (repl.start === prev.start && repl.end < prev.end)
- ) {
- isPresorted = false;
- break;
- }
- }
- if (isPresorted) {
- this._isSorted = true;
- return;
- }
- if (hasStableSort) {
- this._replacements.sort(compareStable);
- } else {
- // V8 < 7.0 only — unreachable on any supported Node.
- /* istanbul ignore next */
- for (const [i, repl] of this._replacements.entries()) repl.index = i;
- /* istanbul ignore next */
- this._replacements.sort(compareUnstableFallback);
- }
- this._isSorted = true;
- }
- /**
- * @param {Options} options options
- * @param {OnChunk} onChunk called for each chunk of code
- * @param {OnSource} onSource called for each source
- * @param {OnName} onName called for each name
- * @returns {GeneratedSourceInfo} generated source info
- */
- streamChunks(options, onChunk, onSource, onName) {
- this._sortReplacements();
- // When the consumer only wants the final source (map() /
- // sourceAndMap()), emit position-only chunks (chunk === undefined,
- // like OriginalSource and RawSource do) and hand back the whole
- // replaced source once at the end. This avoids allocating boundary
- // slices for emission and — more importantly — the per-chunk
- // `code += chunk` cons-string chain in every enclosing consumer.
- // With `source: false` the caller (getMap) additionally promises not
- // to read the returned source, so its assembly is skipped entirely;
- // streamAndGetSourceAndMap overrides that flag because it caches the
- // text.
- const finalSource = Boolean(options && options.finalSource);
- const needSource = !options || options.source !== false;
- const replacements = this._replacements;
- let pos = 0;
- let i = 0;
- let replacementEnd = -1;
- let nextReplacement =
- i < replacements.length
- ? Math.floor(replacements[i].start)
- : MAX_SOURCE_POSITION;
- let generatedLineOffset = 0;
- let generatedColumnOffset = 0;
- let generatedColumnOffsetLine = 0;
- /** @type {(string | undefined)[]} */
- const sourceContents = [];
- /**
- * Lazily-built line-start offsets per source content. One number per
- * line instead of one substring per line (`splitIntoLines`), and the
- * chunk comparison below runs allocation-free via `startsWith`.
- * @type {(number[] | undefined)[]}
- */
- const sourceContentLineStarts = [];
- /** @type {Map<string, number>} */
- const nameMapping = new Map();
- /** @type {number[]} */
- const nameIndexMapping = [];
- /**
- * @param {number} sourceIndex source index
- * @param {number} line line
- * @param {number} column column
- * @param {string} expectedChunk expected chunk
- * @returns {boolean} result
- */
- const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
- const content =
- sourceIndex < sourceContents.length
- ? sourceContents[sourceIndex]
- : undefined;
- if (content === undefined) return false;
- let lineStarts = sourceContentLineStarts[sourceIndex];
- if (lineStarts === undefined) {
- // Line boundaries mirror `splitIntoLines`: every line includes
- // its trailing "\n"; a final line without "\n" still counts.
- lineStarts = [];
- const { length } = content;
- let offset = 0;
- while (offset < length) {
- lineStarts.push(offset);
- const newline = content.indexOf("\n", offset);
- if (newline === -1) break;
- offset = newline + 1;
- }
- sourceContentLineStarts[sourceIndex] = lineStarts;
- }
- if (line > lineStarts.length) return false;
- const lineStart = lineStarts[line - 1];
- const lineEnd =
- line < lineStarts.length ? lineStarts[line] : content.length;
- // The expected chunk never spans lines, so a match must fit into
- // the current line (mirrors the old per-line slice comparison).
- if (column + expectedChunk.length > lineEnd - lineStart) return false;
- return content.startsWith(expectedChunk, lineStart + column);
- };
- const { generatedLine, generatedColumn } = streamChunks(
- this._source,
- { ...options, finalSource: false },
- (
- _chunk,
- generatedLine,
- generatedColumn,
- sourceIndex,
- originalLine,
- originalColumn,
- nameIndex,
- ) => {
- let chunkPos = 0;
- const chunk = /** @type {string} */ (_chunk);
- const endPos = pos + chunk.length;
- // Skip over when it has been replaced
- if (replacementEnd > pos) {
- // Skip over the whole chunk
- if (replacementEnd >= endPos) {
- const line = generatedLine + generatedLineOffset;
- if (chunk.endsWith("\n")) {
- generatedLineOffset--;
- if (generatedColumnOffsetLine === line) {
- // undo exiting corrections form the current line
- generatedColumnOffset += generatedColumn;
- }
- } else if (generatedColumnOffsetLine === line) {
- generatedColumnOffset -= chunk.length;
- } else {
- /* istanbul ignore next: pre-existing chunk-skipping cross-line case (also untested on main) */
- generatedColumnOffset = -chunk.length;
- /* istanbul ignore next: pre-existing chunk-skipping cross-line case (also untested on main) */
- generatedColumnOffsetLine = line;
- }
- pos = endPos;
- return;
- }
- // Partially skip over chunk
- chunkPos = replacementEnd - pos;
- if (
- checkOriginalContent(
- sourceIndex,
- originalLine,
- originalColumn,
- chunk.slice(0, chunkPos),
- )
- ) {
- originalColumn += chunkPos;
- }
- pos += chunkPos;
- const line = generatedLine + generatedLineOffset;
- /* istanbul ignore else: pre-existing chunk-skipping cross-line case (also untested on main) */
- if (generatedColumnOffsetLine === line) {
- generatedColumnOffset -= chunkPos;
- } else {
- generatedColumnOffset = -chunkPos;
- generatedColumnOffsetLine = line;
- }
- generatedColumn += chunkPos;
- }
- // Is a replacement in the chunk?
- if (nextReplacement < endPos) {
- do {
- let line = generatedLine + generatedLineOffset;
- if (nextReplacement > pos) {
- // Emit chunk until replacement
- const offset = nextReplacement - pos;
- const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
- onChunk(
- finalSource ? undefined : chunkSlice,
- line,
- generatedColumn +
- (line === generatedColumnOffsetLine
- ? generatedColumnOffset
- : 0),
- sourceIndex,
- originalLine,
- originalColumn,
- nameIndex < 0 || nameIndex >= nameIndexMapping.length
- ? -1
- : nameIndexMapping[nameIndex],
- );
- generatedColumn += offset;
- chunkPos += offset;
- pos = nextReplacement;
- if (
- checkOriginalContent(
- sourceIndex,
- originalLine,
- originalColumn,
- chunkSlice,
- )
- ) {
- originalColumn += chunkSlice.length;
- }
- }
- // Insert replacement content splitted into chunks by lines
- const { content, name } = replacements[i];
- let replacementNameIndex = nameIndex;
- if (sourceIndex >= 0 && name) {
- let globalIndex = nameMapping.get(name);
- if (globalIndex === undefined) {
- globalIndex = nameMapping.size;
- nameMapping.set(name, globalIndex);
- onName(globalIndex, name);
- }
- replacementNameIndex = globalIndex;
- }
- // Fast path: most replacements (renamed identifiers,
- // short inserts) carry single-line content. Skip
- // `splitIntoLines` — and its array allocation — when
- // we can tell the content has no embedded newline.
- // `splitIntoLines("")` returns `[]`; emitting a zero-
- // length chunk would still walk the loop, so handle
- // it as a no-op explicitly.
- if (content.length > 0 && !content.includes("\n")) {
- onChunk(
- finalSource ? undefined : content,
- line,
- generatedColumn +
- (line === generatedColumnOffsetLine
- ? generatedColumnOffset
- : 0),
- sourceIndex,
- originalLine,
- originalColumn,
- replacementNameIndex,
- );
- if (generatedColumnOffsetLine === line) {
- generatedColumnOffset += content.length;
- } else {
- generatedColumnOffset = content.length;
- generatedColumnOffsetLine = line;
- }
- } else if (content.length === 0) {
- // Empty replacement: no chunk to emit, no column
- // movement. `splitIntoLines("")` is `[]` so the
- // existing loop already does nothing — explicit
- // guard skips the per-call array allocation.
- } else {
- const matches = splitIntoLines(content);
- for (let m = 0; m < matches.length; m++) {
- const contentLine = matches[m];
- onChunk(
- finalSource ? undefined : contentLine,
- line,
- generatedColumn +
- (line === generatedColumnOffsetLine
- ? generatedColumnOffset
- : 0),
- sourceIndex,
- originalLine,
- originalColumn,
- replacementNameIndex,
- );
- // Only the first chunk has name assigned
- replacementNameIndex = -1;
- if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
- /* istanbul ignore else: pre-existing multi-line replacement cross-line case (also untested on main) */
- if (generatedColumnOffsetLine === line) {
- generatedColumnOffset += contentLine.length;
- } else {
- generatedColumnOffset = contentLine.length;
- generatedColumnOffsetLine = line;
- }
- } else {
- generatedLineOffset++;
- line++;
- generatedColumnOffset = -generatedColumn;
- generatedColumnOffsetLine = line;
- }
- }
- }
- // Remove replaced content by settings this variable
- replacementEnd = Math.max(
- replacementEnd,
- Math.floor(replacements[i].end + 1),
- );
- // Move to next replacement
- i++;
- nextReplacement =
- i < replacements.length
- ? Math.floor(replacements[i].start)
- : MAX_SOURCE_POSITION;
- // Skip over when it has been replaced
- const offset = chunk.length - endPos + replacementEnd - chunkPos;
- if (offset > 0) {
- // Skip over whole chunk
- if (replacementEnd >= endPos) {
- const line = generatedLine + generatedLineOffset;
- if (chunk.endsWith("\n")) {
- generatedLineOffset--;
- if (generatedColumnOffsetLine === line) {
- // undo exiting corrections form the current line
- generatedColumnOffset += generatedColumn;
- }
- } else if (generatedColumnOffsetLine === line) {
- generatedColumnOffset -= chunk.length - chunkPos;
- } else {
- generatedColumnOffset = chunkPos - chunk.length;
- generatedColumnOffsetLine = line;
- }
- pos = endPos;
- return;
- }
- // Partially skip over chunk
- const line = generatedLine + generatedLineOffset;
- if (
- checkOriginalContent(
- sourceIndex,
- originalLine,
- originalColumn,
- chunk.slice(chunkPos, chunkPos + offset),
- )
- ) {
- originalColumn += offset;
- }
- chunkPos += offset;
- pos += offset;
- if (generatedColumnOffsetLine === line) {
- generatedColumnOffset -= offset;
- } else {
- generatedColumnOffset = -offset;
- generatedColumnOffsetLine = line;
- }
- generatedColumn += offset;
- }
- } while (nextReplacement < endPos);
- }
- // Emit remaining chunk
- if (chunkPos < chunk.length) {
- const chunkSlice = finalSource
- ? undefined
- : chunkPos === 0
- ? chunk
- : chunk.slice(chunkPos);
- const line = generatedLine + generatedLineOffset;
- onChunk(
- chunkSlice,
- line,
- generatedColumn +
- (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
- sourceIndex,
- originalLine,
- originalColumn,
- nameIndex < 0 ? -1 : nameIndexMapping[nameIndex],
- );
- }
- pos = endPos;
- },
- (sourceIndex, source, sourceContent) => {
- /* istanbul ignore next: non-sequential sourceIndex emission is not produced by any in-tree Source */
- while (sourceContents.length < sourceIndex) {
- sourceContents.push(undefined);
- }
- sourceContents[sourceIndex] = sourceContent;
- onSource(sourceIndex, source, sourceContent);
- },
- (nameIndex, name) => {
- let globalIndex = nameMapping.get(name);
- if (globalIndex === undefined) {
- globalIndex = nameMapping.size;
- nameMapping.set(name, globalIndex);
- onName(globalIndex, name);
- }
- nameIndexMapping[nameIndex] = globalIndex;
- },
- );
- // Handle remaining replacements
- let remainer = "";
- for (; i < replacements.length; i++) {
- remainer += replacements[i].content;
- }
- // Insert remaining replacements content splitted into chunks by lines
- let line = /** @type {number} */ (generatedLine) + generatedLineOffset;
- // Fast path mirroring the in-chunk replacement loop above: skip
- // splitIntoLines + per-match loop when the trailing content has no
- // newlines (the common case when remaining replacements are single
- // inserts).
- if (remainer.length > 0 && !remainer.includes("\n")) {
- onChunk(
- finalSource ? undefined : remainer,
- line,
- /** @type {number} */
- (generatedColumn) +
- (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
- -1,
- -1,
- -1,
- -1,
- );
- /* istanbul ignore else: trailing-remainer cross-line case (also untested on main) */
- if (generatedColumnOffsetLine === line) {
- generatedColumnOffset += remainer.length;
- } else {
- generatedColumnOffset = remainer.length;
- generatedColumnOffsetLine = line;
- }
- } else {
- const matches = splitIntoLines(remainer);
- for (let m = 0; m < matches.length; m++) {
- const contentLine = matches[m];
- onChunk(
- finalSource ? undefined : contentLine,
- line,
- /** @type {number} */
- (generatedColumn) +
- (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
- -1,
- -1,
- -1,
- -1,
- );
- if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
- /* istanbul ignore else: trailing-remainer multi-line cross-line case (also untested on main) */
- if (generatedColumnOffsetLine === line) {
- generatedColumnOffset += contentLine.length;
- } else {
- generatedColumnOffset = contentLine.length;
- generatedColumnOffsetLine = line;
- }
- } else {
- generatedLineOffset++;
- line++;
- generatedColumnOffset = -(/** @type {number} */ (generatedColumn));
- generatedColumnOffsetLine = line;
- }
- }
- }
- return {
- generatedLine: line,
- generatedColumn:
- /** @type {number} */
- (generatedColumn) +
- (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
- // The streamed chunks reproduce source() exactly, so the final
- // source can be assembled in O(replacements) string operations
- // instead of re-concatenating every emitted chunk downstream.
- source:
- finalSource && needSource
- ? /** @type {string} */ (this.source())
- : undefined,
- };
- }
- /**
- * Release cached data held by this source. clearCache is a memory
- * hint: it never affects correctness or output, only how expensive
- * the next read is. Subclasses override; the base is a no-op so
- * every Source supports the call. Composite sources always recurse
- * into wrapped sources. When the same child is reachable via several
- * parents (e.g. modules shared across webpack chunks), pass a shared
- * `visited` WeakSet so each subtree is walked at most once.
- * Not safe to call concurrently with source/map/sourceAndMap/
- * streamChunks/updateHash on the same instance.
- * @param {ClearCacheOptions=} options selectors
- * @param {WeakSet<Source>=} visited de-duplication set shared across calls
- * @returns {void}
- */
- clearCache(options, visited) {
- if (visited !== undefined && visited.has(this)) return;
- let v = visited;
- if (v === undefined) v = new WeakSet();
- v.add(this);
- this._source.clearCache(options, v);
- }
- /**
- * @param {HashLike} hash hash
- * @returns {void}
- */
- updateHash(hash) {
- this._sortReplacements();
- hash.update("ReplaceSource");
- this._source.updateHash(hash);
- hash.update(this._name || "");
- // Feed each replacement as multiple updates instead of building one
- // combined template literal per replacement. The resulting digest is
- // identical (hash.update is additive over bytes), but we avoid
- // allocating a new string per replacement.
- for (const repl of this._replacements) {
- hash.update(`${repl.start}${repl.end}`);
- hash.update(repl.content);
- if (repl.name) hash.update(repl.name);
- }
- }
- }
- module.exports = ReplaceSource;
- module.exports.Replacement = Replacement;
|