`third_party/node/node_modules` is generated by `./mach vendor node`. Differential Revision: https://phabricator.services.mozilla.com/D320322
757 lines
23 KiB
JavaScript
757 lines
23 KiB
JavaScript
/*
|
|
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;
|