@depup/webpack-sources 3.3.4-depup.0 → 3.6.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/changes.json +1 -1
  3. package/lib/CachedSource.js +257 -63
  4. package/lib/CompatSource.js +42 -1
  5. package/lib/ConcatSource.js +100 -28
  6. package/lib/OriginalSource.js +111 -45
  7. package/lib/PrefixSource.js +60 -11
  8. package/lib/RawSource.js +85 -29
  9. package/lib/ReplaceSource.js +269 -73
  10. package/lib/SizeOnlySource.js +10 -1
  11. package/lib/Source.js +39 -0
  12. package/lib/SourceMapSource.js +92 -10
  13. package/lib/helpers/createMappingsSerializer.js +307 -54
  14. package/lib/helpers/getFromStreamChunks.js +165 -73
  15. package/lib/helpers/getGeneratedSourceInfo.js +7 -4
  16. package/lib/helpers/readMappings.js +6 -3
  17. package/lib/helpers/scopes.js +438 -0
  18. package/lib/helpers/splitIntoLines.js +10 -9
  19. package/lib/helpers/splitIntoPotentialTokens.js +99 -26
  20. package/lib/helpers/streamAndGetSourceAndMap.js +61 -22
  21. package/lib/helpers/streamChunks.js +3 -2
  22. package/lib/helpers/streamChunksOfCombinedSourceMap.js +7 -3
  23. package/lib/helpers/streamChunksOfRawSource.js +23 -15
  24. package/lib/helpers/streamChunksOfSourceMap.js +50 -42
  25. package/lib/index.js +3 -0
  26. package/package.json +34 -23
  27. package/types/CachedSource.d.ts +192 -0
  28. package/types/CompatSource.d.ts +87 -0
  29. package/types/ConcatSource.d.ts +75 -0
  30. package/types/OriginalSource.d.ts +87 -0
  31. package/types/PrefixSource.d.ts +59 -0
  32. package/types/RawSource.d.ts +75 -0
  33. package/types/ReplaceSource.d.ts +107 -0
  34. package/types/SizeOnlySource.d.ts +24 -0
  35. package/types/Source.d.ts +189 -0
  36. package/types/SourceMapSource.d.ts +136 -0
  37. package/types/helpers/createMappingsSerializer.d.ts +60 -0
  38. package/types/helpers/getFromStreamChunks.d.ts +16 -0
  39. package/types/helpers/getGeneratedSourceInfo.d.ts +31 -0
  40. package/types/helpers/getName.d.ts +15 -0
  41. package/types/helpers/getSource.d.ts +15 -0
  42. package/types/helpers/readMappings.d.ts +19 -0
  43. package/types/helpers/scopes.d.ts +119 -0
  44. package/types/helpers/splitIntoLines.d.ts +6 -0
  45. package/types/helpers/splitIntoPotentialTokens.d.ts +37 -0
  46. package/types/helpers/streamAndGetSourceAndMap.d.ts +39 -0
  47. package/types/helpers/streamChunks.d.ts +56 -0
  48. package/types/helpers/streamChunksOfCombinedSourceMap.d.ts +42 -0
  49. package/types/helpers/streamChunksOfRawSource.d.ts +16 -0
  50. package/types/helpers/streamChunksOfSourceMap.d.ts +19 -0
  51. package/types/helpers/stringBufferUtils.d.ts +55 -0
  52. package/types/index.d.ts +50 -0
  53. package/types.d.ts +42 -439
package/lib/RawSource.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  isDualStringBufferCachingEnabled,
13
13
  } = require("./helpers/stringBufferUtils");
14
14
 
15
+ /** @typedef {import("./Source").ClearCacheOptions} ClearCacheOptions */
15
16
  /** @typedef {import("./Source").HashLike} HashLike */
16
17
  /** @typedef {import("./Source").MapOptions} MapOptions */
17
18
  /** @typedef {import("./Source").RawSourceMap} RawSourceMap */
@@ -30,36 +31,32 @@ class RawSource extends Source {
30
31
  constructor(value, convertToString = false) {
31
32
  super();
32
33
  const isBuffer = Buffer.isBuffer(value);
33
- if (!isBuffer && typeof value !== "string") {
34
+ if (isBuffer) {
35
+ /**
36
+ * @type {boolean}
37
+ */
38
+ this._valueIsBuffer = !convertToString;
39
+ /**
40
+ * @type {undefined | string | Buffer}
41
+ */
42
+ this._value = convertToString ? undefined : value;
43
+ /**
44
+ * @type {undefined | Buffer}
45
+ */
46
+ this._valueAsBuffer = value;
47
+ /**
48
+ * @type {undefined | string}
49
+ */
50
+ this._valueAsString = undefined;
51
+ } else if (typeof value === "string") {
52
+ const interned = internString(value);
53
+ this._valueIsBuffer = false;
54
+ this._value = interned;
55
+ this._valueAsBuffer = undefined;
56
+ this._valueAsString = interned;
57
+ } else {
34
58
  throw new TypeError("argument 'value' must be either string or Buffer");
35
59
  }
36
- /**
37
- * @private
38
- * @type {boolean}
39
- */
40
- this._valueIsBuffer = !convertToString && isBuffer;
41
- const internedString =
42
- typeof value === "string" ? internString(value) : undefined;
43
- /**
44
- * @private
45
- * @type {undefined | string | Buffer}
46
- */
47
- this._value =
48
- convertToString && isBuffer
49
- ? undefined
50
- : typeof value === "string"
51
- ? internedString
52
- : value;
53
- /**
54
- * @private
55
- * @type {undefined | Buffer}
56
- */
57
- this._valueAsBuffer = isBuffer ? value : undefined;
58
- /**
59
- * @private
60
- * @type {undefined | string}
61
- */
62
- this._valueAsString = isBuffer ? undefined : internedString;
63
60
  }
64
61
 
65
62
  isBuffer() {
@@ -82,6 +79,9 @@ class RawSource extends Source {
82
79
  return this._value;
83
80
  }
84
81
 
82
+ /**
83
+ * @returns {Buffer} buffer
84
+ */
85
85
  buffer() {
86
86
  if (this._valueAsBuffer === undefined) {
87
87
  const value = Buffer.from(/** @type {string} */ (this._value), "utf8");
@@ -93,6 +93,20 @@ class RawSource extends Source {
93
93
  return this._valueAsBuffer;
94
94
  }
95
95
 
96
+ /**
97
+ * @returns {number} size
98
+ */
99
+ size() {
100
+ if (this._cachedSize !== undefined) return this._cachedSize;
101
+ if (this._valueAsBuffer !== undefined) {
102
+ return (this._cachedSize = this._valueAsBuffer.length);
103
+ }
104
+ return (this._cachedSize = Buffer.byteLength(
105
+ /** @type {string} */ (this._valueAsString),
106
+ "utf8",
107
+ ));
108
+ }
109
+
96
110
  /**
97
111
  * @param {MapOptions=} options map options
98
112
  * @returns {RawSourceMap | null} map
@@ -127,13 +141,55 @@ class RawSource extends Source {
127
141
  );
128
142
  }
129
143
 
144
+ /**
145
+ * Release cached data held by this source. clearCache is a memory
146
+ * hint: it never affects correctness or output, only how expensive
147
+ * the next read is. Subclasses override; the base is a no-op so
148
+ * every Source supports the call. Composite sources always recurse
149
+ * into wrapped sources. When the same child is reachable via several
150
+ * parents (e.g. modules shared across webpack chunks), pass a shared
151
+ * `visited` WeakSet so each subtree is walked at most once.
152
+ * Not safe to call concurrently with source/map/sourceAndMap/
153
+ * streamChunks/updateHash on the same instance.
154
+ * @param {ClearCacheOptions=} options selectors
155
+ * @param {WeakSet<Source>=} visited de-duplication set shared across calls
156
+ * @returns {void}
157
+ */
158
+ clearCache(options, visited) {
159
+ if (visited !== undefined) {
160
+ if (visited.has(this)) return;
161
+ visited.add(this);
162
+ }
163
+ if (options !== undefined && options.source === false) return;
164
+ if (this._valueIsBuffer) {
165
+ // Buffer is the primary representation (and lives in `_value`);
166
+ // only the string form, populated lazily by streamChunks, is
167
+ // safe to drop.
168
+ this._valueAsString = undefined;
169
+ } else if (this._valueAsBuffer !== undefined && this._value !== undefined) {
170
+ // The source was constructed from a string (so `_value` and
171
+ // `_valueAsString` reference the same interned string) and the
172
+ // buffer form was materialized later by `buffer()` /
173
+ // `updateHash()`. The buffer is therefore safe to drop.
174
+ this._valueAsBuffer = undefined;
175
+ }
176
+ }
177
+
130
178
  /**
131
179
  * @param {HashLike} hash hash
132
180
  * @returns {void}
133
181
  */
134
182
  updateHash(hash) {
135
183
  hash.update("RawSource");
136
- hash.update(this.buffer());
184
+ // A string is hashed as utf8, which is what `buffer()` would have
185
+ // encoded it to, so hashing never has to materialize the buffer.
186
+ // One of the two forms is always set: the constructor sets whichever
187
+ // matches its input, and `clearCache` only drops the derived one.
188
+ if (this._valueAsBuffer === undefined) {
189
+ hash.update(/** @type {string} */ (this._valueAsString));
190
+ } else {
191
+ hash.update(this._valueAsBuffer);
192
+ }
137
193
  }
138
194
  }
139
195
 
@@ -10,6 +10,7 @@ const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
10
10
  const splitIntoLines = require("./helpers/splitIntoLines");
11
11
  const streamChunks = require("./helpers/streamChunks");
12
12
 
13
+ /** @typedef {import("./Source").ClearCacheOptions} ClearCacheOptions */
13
14
  /** @typedef {import("./Source").HashLike} HashLike */
14
15
  /** @typedef {import("./Source").MapOptions} MapOptions */
15
16
  /** @typedef {import("./Source").RawSourceMap} RawSourceMap */
@@ -31,6 +32,38 @@ const hasStableSort =
31
32
  // This is larger than max string length
32
33
  const MAX_SOURCE_POSITION = 0x20000000;
33
34
 
35
+ /**
36
+ * Stable comparator hoisted to module scope so each `_sortReplacements()`
37
+ * call doesn't allocate a fresh closure.
38
+ * @param {Replacement} a a
39
+ * @param {Replacement} b b
40
+ * @returns {number} order
41
+ */
42
+ const compareStable = (a, b) => {
43
+ const diff1 = a.start - b.start;
44
+ if (diff1 !== 0) return diff1;
45
+ const diff2 = a.end - b.end;
46
+ if (diff2 !== 0) return diff2;
47
+ return 0;
48
+ };
49
+
50
+ /**
51
+ * Index-stabilising comparator for v8 < 7.0 (pre-stable Array.prototype.sort).
52
+ * Unreachable on any supported Node — the `hasStableSort` guard always
53
+ * wins so coverage tools never see this execute.
54
+ * @param {Replacement} a a
55
+ * @param {Replacement} b b
56
+ * @returns {number} order
57
+ */
58
+ /* istanbul ignore next */
59
+ const compareUnstableFallback = (a, b) => {
60
+ const diff1 = a.start - b.start;
61
+ if (diff1 !== 0) return diff1;
62
+ const diff2 = a.end - b.end;
63
+ if (diff2 !== 0) return diff2;
64
+ return /** @type {number} */ (a.index) - /** @type {number} */ (b.index);
65
+ };
66
+
34
67
  class Replacement {
35
68
  /**
36
69
  * @param {number} start start
@@ -43,6 +76,8 @@ class Replacement {
43
76
  this.end = end;
44
77
  this.content = content;
45
78
  this.name = name;
79
+ // V8 < 7.0 only — unreachable on any supported Node.
80
+ /* istanbul ignore if */
46
81
  if (!hasStableSort) {
47
82
  this.index = -1;
48
83
  }
@@ -57,19 +92,16 @@ class ReplaceSource extends Source {
57
92
  constructor(source, name) {
58
93
  super();
59
94
  /**
60
- * @private
61
95
  * @type {Source}
62
96
  */
63
97
  this._source = source;
64
98
  /**
65
- * @private
66
99
  * @type {string | undefined}
67
100
  */
68
101
  this._name = name;
69
102
  /** @type {Replacement[]} */
70
103
  this._replacements = [];
71
104
  /**
72
- * @private
73
105
  * @type {boolean}
74
106
  */
75
107
  this._isSorted = true;
@@ -124,7 +156,7 @@ class ReplaceSource extends Source {
124
156
  if (this._replacements.length === 0) {
125
157
  return this._source.source();
126
158
  }
127
- let current = this._source.source();
159
+ const current = /** @type {string} */ (this._source.source());
128
160
  let pos = 0;
129
161
  const result = [];
130
162
 
@@ -133,22 +165,45 @@ class ReplaceSource extends Source {
133
165
  const start = Math.floor(replacement.start);
134
166
  const end = Math.floor(replacement.end + 1);
135
167
  if (pos < start) {
136
- const offset = start - pos;
137
- result.push(current.slice(0, offset));
138
- current = current.slice(offset);
168
+ // slice directly from the original string rather than repeatedly
169
+ // producing smaller intermediate strings, which avoids O(n) copies.
170
+ result.push(current.slice(pos, start));
139
171
  pos = start;
140
172
  }
141
173
  result.push(replacement.content);
142
174
  if (pos < end) {
143
- const offset = end - pos;
144
- current = current.slice(offset);
145
175
  pos = end;
146
176
  }
147
177
  }
148
- result.push(current);
178
+ if (pos < current.length) {
179
+ result.push(pos === 0 ? current : current.slice(pos));
180
+ }
149
181
  return result.join("");
150
182
  }
151
183
 
184
+ /**
185
+ * @returns {Buffer} buffer
186
+ */
187
+ buffer() {
188
+ if (this._replacements.length === 0) {
189
+ return this._source.buffer();
190
+ }
191
+ return super.buffer();
192
+ }
193
+
194
+ /**
195
+ * @returns {Buffer[]} buffers
196
+ */
197
+ buffers() {
198
+ if (this._replacements.length === 0) {
199
+ // TODO remove in the next major release
200
+ return typeof this._source.buffers === "function"
201
+ ? this._source.buffers()
202
+ : [this._source.buffer()];
203
+ }
204
+ return [this.buffer()];
205
+ }
206
+
152
207
  /**
153
208
  * @param {MapOptions=} options map options
154
209
  * @returns {RawSourceMap | null} map
@@ -177,25 +232,34 @@ class ReplaceSource extends Source {
177
232
 
178
233
  _sortReplacements() {
179
234
  if (this._isSorted) return;
235
+ const replacements = this._replacements;
236
+ // Replacements are usually appended in source order (ties keep
237
+ // insertion order, matching a stable sort), so an O(n) pre-scan
238
+ // often lets us skip the sort and its per-element comparator calls.
239
+ let isPresorted = true;
240
+ for (let i = 1; i < replacements.length; i++) {
241
+ const prev = replacements[i - 1];
242
+ const repl = replacements[i];
243
+ if (
244
+ repl.start < prev.start ||
245
+ (repl.start === prev.start && repl.end < prev.end)
246
+ ) {
247
+ isPresorted = false;
248
+ break;
249
+ }
250
+ }
251
+ if (isPresorted) {
252
+ this._isSorted = true;
253
+ return;
254
+ }
180
255
  if (hasStableSort) {
181
- this._replacements.sort((a, b) => {
182
- const diff1 = a.start - b.start;
183
- if (diff1 !== 0) return diff1;
184
- const diff2 = a.end - b.end;
185
- if (diff2 !== 0) return diff2;
186
- return 0;
187
- });
256
+ this._replacements.sort(compareStable);
188
257
  } else {
258
+ // V8 < 7.0 only — unreachable on any supported Node.
259
+ /* istanbul ignore next */
189
260
  for (const [i, repl] of this._replacements.entries()) repl.index = i;
190
- this._replacements.sort((a, b) => {
191
- const diff1 = a.start - b.start;
192
- if (diff1 !== 0) return diff1;
193
- const diff2 = a.end - b.end;
194
- if (diff2 !== 0) return diff2;
195
- return (
196
- /** @type {number} */ (a.index) - /** @type {number} */ (b.index)
197
- );
198
- });
261
+ /* istanbul ignore next */
262
+ this._replacements.sort(compareUnstableFallback);
199
263
  }
200
264
  this._isSorted = true;
201
265
  }
@@ -209,6 +273,18 @@ class ReplaceSource extends Source {
209
273
  */
210
274
  streamChunks(options, onChunk, onSource, onName) {
211
275
  this._sortReplacements();
276
+ // When the consumer only wants the final source (map() /
277
+ // sourceAndMap()), emit position-only chunks (chunk === undefined,
278
+ // like OriginalSource and RawSource do) and hand back the whole
279
+ // replaced source once at the end. This avoids allocating boundary
280
+ // slices for emission and — more importantly — the per-chunk
281
+ // `code += chunk` cons-string chain in every enclosing consumer.
282
+ // With `source: false` the caller (getMap) additionally promises not
283
+ // to read the returned source, so its assembly is skipped entirely;
284
+ // streamAndGetSourceAndMap overrides that flag because it caches the
285
+ // text.
286
+ const finalSource = Boolean(options && options.finalSource);
287
+ const needSource = !options || options.source !== false;
212
288
  const replacements = this._replacements;
213
289
  let pos = 0;
214
290
  let i = 0;
@@ -220,8 +296,15 @@ class ReplaceSource extends Source {
220
296
  let generatedLineOffset = 0;
221
297
  let generatedColumnOffset = 0;
222
298
  let generatedColumnOffsetLine = 0;
223
- /** @type {(string | string[] | undefined)[]} */
299
+ /** @type {(string | undefined)[]} */
224
300
  const sourceContents = [];
301
+ /**
302
+ * Lazily-built line-start offsets per source content. One number per
303
+ * line instead of one substring per line (`splitIntoLines`), and the
304
+ * chunk comparison below runs allocation-free via `startsWith`.
305
+ * @type {(number[] | undefined)[]}
306
+ */
307
+ const sourceContentLineStarts = [];
225
308
  /** @type {Map<string, number>} */
226
309
  const nameMapping = new Map();
227
310
  /** @type {number[]} */
@@ -234,22 +317,34 @@ class ReplaceSource extends Source {
234
317
  * @returns {boolean} result
235
318
  */
236
319
  const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
237
- /** @type {undefined | string | string[]} */
238
- let content =
320
+ const content =
239
321
  sourceIndex < sourceContents.length
240
322
  ? sourceContents[sourceIndex]
241
323
  : undefined;
242
324
  if (content === undefined) return false;
243
- if (typeof content === "string") {
244
- content = splitIntoLines(content);
245
- sourceContents[sourceIndex] = content;
325
+ let lineStarts = sourceContentLineStarts[sourceIndex];
326
+ if (lineStarts === undefined) {
327
+ // Line boundaries mirror `splitIntoLines`: every line includes
328
+ // its trailing "\n"; a final line without "\n" still counts.
329
+ lineStarts = [];
330
+ const { length } = content;
331
+ let offset = 0;
332
+ while (offset < length) {
333
+ lineStarts.push(offset);
334
+ const newline = content.indexOf("\n", offset);
335
+ if (newline === -1) break;
336
+ offset = newline + 1;
337
+ }
338
+ sourceContentLineStarts[sourceIndex] = lineStarts;
246
339
  }
247
- const contentLine = line <= content.length ? content[line - 1] : null;
248
- if (contentLine === null) return false;
249
- return (
250
- contentLine.slice(column, column + expectedChunk.length) ===
251
- expectedChunk
252
- );
340
+ if (line > lineStarts.length) return false;
341
+ const lineStart = lineStarts[line - 1];
342
+ const lineEnd =
343
+ line < lineStarts.length ? lineStarts[line] : content.length;
344
+ // The expected chunk never spans lines, so a match must fit into
345
+ // the current line (mirrors the old per-line slice comparison).
346
+ if (column + expectedChunk.length > lineEnd - lineStart) return false;
347
+ return content.startsWith(expectedChunk, lineStart + column);
253
348
  };
254
349
  const { generatedLine, generatedColumn } = streamChunks(
255
350
  this._source,
@@ -281,7 +376,9 @@ class ReplaceSource extends Source {
281
376
  } else if (generatedColumnOffsetLine === line) {
282
377
  generatedColumnOffset -= chunk.length;
283
378
  } else {
379
+ /* istanbul ignore next: pre-existing chunk-skipping cross-line case (also untested on main) */
284
380
  generatedColumnOffset = -chunk.length;
381
+ /* istanbul ignore next: pre-existing chunk-skipping cross-line case (also untested on main) */
285
382
  generatedColumnOffsetLine = line;
286
383
  }
287
384
  pos = endPos;
@@ -302,6 +399,7 @@ class ReplaceSource extends Source {
302
399
  }
303
400
  pos += chunkPos;
304
401
  const line = generatedLine + generatedLineOffset;
402
+ /* istanbul ignore else: pre-existing chunk-skipping cross-line case (also untested on main) */
305
403
  if (generatedColumnOffsetLine === line) {
306
404
  generatedColumnOffset -= chunkPos;
307
405
  } else {
@@ -320,7 +418,7 @@ class ReplaceSource extends Source {
320
418
  const offset = nextReplacement - pos;
321
419
  const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
322
420
  onChunk(
323
- chunkSlice,
421
+ finalSource ? undefined : chunkSlice,
324
422
  line,
325
423
  generatedColumn +
326
424
  (line === generatedColumnOffsetLine
@@ -350,7 +448,6 @@ class ReplaceSource extends Source {
350
448
 
351
449
  // Insert replacement content splitted into chunks by lines
352
450
  const { content, name } = replacements[i];
353
- const matches = splitIntoLines(content);
354
451
  let replacementNameIndex = nameIndex;
355
452
  if (sourceIndex >= 0 && name) {
356
453
  let globalIndex = nameMapping.get(name);
@@ -361,10 +458,16 @@ class ReplaceSource extends Source {
361
458
  }
362
459
  replacementNameIndex = globalIndex;
363
460
  }
364
- for (let m = 0; m < matches.length; m++) {
365
- const contentLine = matches[m];
461
+ // Fast path: most replacements (renamed identifiers,
462
+ // short inserts) carry single-line content. Skip
463
+ // `splitIntoLines` — and its array allocation — when
464
+ // we can tell the content has no embedded newline.
465
+ // `splitIntoLines("")` returns `[]`; emitting a zero-
466
+ // length chunk would still walk the loop, so handle
467
+ // it as a no-op explicitly.
468
+ if (content.length > 0 && !content.includes("\n")) {
366
469
  onChunk(
367
- contentLine,
470
+ finalSource ? undefined : content,
368
471
  line,
369
472
  generatedColumn +
370
473
  (line === generatedColumnOffsetLine
@@ -375,22 +478,51 @@ class ReplaceSource extends Source {
375
478
  originalColumn,
376
479
  replacementNameIndex,
377
480
  );
481
+ if (generatedColumnOffsetLine === line) {
482
+ generatedColumnOffset += content.length;
483
+ } else {
484
+ generatedColumnOffset = content.length;
485
+ generatedColumnOffsetLine = line;
486
+ }
487
+ } else if (content.length === 0) {
488
+ // Empty replacement: no chunk to emit, no column
489
+ // movement. `splitIntoLines("")` is `[]` so the
490
+ // existing loop already does nothing — explicit
491
+ // guard skips the per-call array allocation.
492
+ } else {
493
+ const matches = splitIntoLines(content);
494
+ for (let m = 0; m < matches.length; m++) {
495
+ const contentLine = matches[m];
496
+ onChunk(
497
+ finalSource ? undefined : contentLine,
498
+ line,
499
+ generatedColumn +
500
+ (line === generatedColumnOffsetLine
501
+ ? generatedColumnOffset
502
+ : 0),
503
+ sourceIndex,
504
+ originalLine,
505
+ originalColumn,
506
+ replacementNameIndex,
507
+ );
378
508
 
379
- // Only the first chunk has name assigned
380
- replacementNameIndex = -1;
509
+ // Only the first chunk has name assigned
510
+ replacementNameIndex = -1;
381
511
 
382
- if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
383
- if (generatedColumnOffsetLine === line) {
384
- generatedColumnOffset += contentLine.length;
512
+ if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
513
+ /* istanbul ignore else: pre-existing multi-line replacement cross-line case (also untested on main) */
514
+ if (generatedColumnOffsetLine === line) {
515
+ generatedColumnOffset += contentLine.length;
516
+ } else {
517
+ generatedColumnOffset = contentLine.length;
518
+ generatedColumnOffsetLine = line;
519
+ }
385
520
  } else {
386
- generatedColumnOffset = contentLine.length;
521
+ generatedLineOffset++;
522
+ line++;
523
+ generatedColumnOffset = -generatedColumn;
387
524
  generatedColumnOffsetLine = line;
388
525
  }
389
- } else {
390
- generatedLineOffset++;
391
- line++;
392
- generatedColumnOffset = -generatedColumn;
393
- generatedColumnOffsetLine = line;
394
526
  }
395
527
  }
396
528
 
@@ -456,7 +588,11 @@ class ReplaceSource extends Source {
456
588
 
457
589
  // Emit remaining chunk
458
590
  if (chunkPos < chunk.length) {
459
- const chunkSlice = chunkPos === 0 ? chunk : chunk.slice(chunkPos);
591
+ const chunkSlice = finalSource
592
+ ? undefined
593
+ : chunkPos === 0
594
+ ? chunk
595
+ : chunk.slice(chunkPos);
460
596
  const line = generatedLine + generatedLineOffset;
461
597
  onChunk(
462
598
  chunkSlice,
@@ -471,12 +607,13 @@ class ReplaceSource extends Source {
471
607
  }
472
608
  pos = endPos;
473
609
  },
474
- (sourceIndex, source, sourceContent) => {
610
+ (sourceIndex, source, sourceContent, scopeBindings) => {
611
+ /* istanbul ignore next: non-sequential sourceIndex emission is not produced by any in-tree Source */
475
612
  while (sourceContents.length < sourceIndex) {
476
613
  sourceContents.push(undefined);
477
614
  }
478
615
  sourceContents[sourceIndex] = sourceContent;
479
- onSource(sourceIndex, source, sourceContent);
616
+ onSource(sourceIndex, source, sourceContent, scopeBindings);
480
617
  },
481
618
  (nameIndex, name) => {
482
619
  let globalIndex = nameMapping.get(name);
@@ -497,11 +634,13 @@ class ReplaceSource extends Source {
497
634
 
498
635
  // Insert remaining replacements content splitted into chunks by lines
499
636
  let line = /** @type {number} */ (generatedLine) + generatedLineOffset;
500
- const matches = splitIntoLines(remainer);
501
- for (let m = 0; m < matches.length; m++) {
502
- const contentLine = matches[m];
637
+ // Fast path mirroring the in-chunk replacement loop above: skip
638
+ // splitIntoLines + per-match loop when the trailing content has no
639
+ // newlines (the common case when remaining replacements are single
640
+ // inserts).
641
+ if (remainer.length > 0 && !remainer.includes("\n")) {
503
642
  onChunk(
504
- contentLine,
643
+ finalSource ? undefined : remainer,
505
644
  line,
506
645
  /** @type {number} */
507
646
  (generatedColumn) +
@@ -511,19 +650,43 @@ class ReplaceSource extends Source {
511
650
  -1,
512
651
  -1,
513
652
  );
653
+ /* istanbul ignore else: trailing-remainer cross-line case (also untested on main) */
654
+ if (generatedColumnOffsetLine === line) {
655
+ generatedColumnOffset += remainer.length;
656
+ } else {
657
+ generatedColumnOffset = remainer.length;
658
+ generatedColumnOffsetLine = line;
659
+ }
660
+ } else {
661
+ const matches = splitIntoLines(remainer);
662
+ for (let m = 0; m < matches.length; m++) {
663
+ const contentLine = matches[m];
664
+ onChunk(
665
+ finalSource ? undefined : contentLine,
666
+ line,
667
+ /** @type {number} */
668
+ (generatedColumn) +
669
+ (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
670
+ -1,
671
+ -1,
672
+ -1,
673
+ -1,
674
+ );
514
675
 
515
- if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
516
- if (generatedColumnOffsetLine === line) {
517
- generatedColumnOffset += contentLine.length;
676
+ if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
677
+ /* istanbul ignore else: trailing-remainer multi-line cross-line case (also untested on main) */
678
+ if (generatedColumnOffsetLine === line) {
679
+ generatedColumnOffset += contentLine.length;
680
+ } else {
681
+ generatedColumnOffset = contentLine.length;
682
+ generatedColumnOffsetLine = line;
683
+ }
518
684
  } else {
519
- generatedColumnOffset = contentLine.length;
685
+ generatedLineOffset++;
686
+ line++;
687
+ generatedColumnOffset = -(/** @type {number} */ (generatedColumn));
520
688
  generatedColumnOffsetLine = line;
521
689
  }
522
- } else {
523
- generatedLineOffset++;
524
- line++;
525
- generatedColumnOffset = -(/** @type {number} */ (generatedColumn));
526
- generatedColumnOffsetLine = line;
527
690
  }
528
691
  }
529
692
 
@@ -533,9 +696,38 @@ class ReplaceSource extends Source {
533
696
  /** @type {number} */
534
697
  (generatedColumn) +
535
698
  (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
699
+ // The streamed chunks reproduce source() exactly, so the final
700
+ // source can be assembled in O(replacements) string operations
701
+ // instead of re-concatenating every emitted chunk downstream.
702
+ source:
703
+ finalSource && needSource
704
+ ? /** @type {string} */ (this.source())
705
+ : undefined,
536
706
  };
537
707
  }
538
708
 
709
+ /**
710
+ * Release cached data held by this source. clearCache is a memory
711
+ * hint: it never affects correctness or output, only how expensive
712
+ * the next read is. Subclasses override; the base is a no-op so
713
+ * every Source supports the call. Composite sources always recurse
714
+ * into wrapped sources. When the same child is reachable via several
715
+ * parents (e.g. modules shared across webpack chunks), pass a shared
716
+ * `visited` WeakSet so each subtree is walked at most once.
717
+ * Not safe to call concurrently with source/map/sourceAndMap/
718
+ * streamChunks/updateHash on the same instance.
719
+ * @param {ClearCacheOptions=} options selectors
720
+ * @param {WeakSet<Source>=} visited de-duplication set shared across calls
721
+ * @returns {void}
722
+ */
723
+ clearCache(options, visited) {
724
+ if (visited !== undefined && visited.has(this)) return;
725
+ let v = visited;
726
+ if (v === undefined) v = new WeakSet();
727
+ v.add(this);
728
+ this._source.clearCache(options, v);
729
+ }
730
+
539
731
  /**
540
732
  * @param {HashLike} hash hash
541
733
  * @returns {void}
@@ -545,10 +737,14 @@ class ReplaceSource extends Source {
545
737
  hash.update("ReplaceSource");
546
738
  this._source.updateHash(hash);
547
739
  hash.update(this._name || "");
740
+ // Feed each replacement as multiple updates instead of building one
741
+ // combined template literal per replacement. The resulting digest is
742
+ // identical (hash.update is additive over bytes), but we avoid
743
+ // allocating a new string per replacement.
548
744
  for (const repl of this._replacements) {
549
- hash.update(
550
- `${repl.start}${repl.end}${repl.content}${repl.name ? repl.name : ""}`,
551
- );
745
+ hash.update(`${repl.start}${repl.end}`);
746
+ hash.update(repl.content);
747
+ if (repl.name) hash.update(repl.name);
552
748
  }
553
749
  }
554
750
  }