UNPKG

webpack-sources

Version:

Source code handling classes for webpack

757 lines (716 loc) 23.3 kB
/* 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;