bippy 0.6.1-dev.61475ed → 0.6.1-dev.d7876ea

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 (62) hide show
  1. package/README.md +32 -48
  2. package/dist/core.cjs +1 -1
  3. package/dist/core.d.cts +42 -56
  4. package/dist/core.d.ts +42 -56
  5. package/dist/core.js +1 -1
  6. package/dist/core2.cjs +9 -0
  7. package/dist/core2.d.cts +3 -3
  8. package/dist/core2.d.ts +3 -3
  9. package/dist/core2.js +9 -0
  10. package/dist/errors.d.cts +436 -0
  11. package/dist/errors.d.ts +436 -0
  12. package/dist/index.cjs +1 -1
  13. package/dist/index.d.cts +3 -3
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.iife.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/install-hook-only.cjs +1 -1
  18. package/dist/install-hook-only.d.cts +1 -1
  19. package/dist/install-hook-only.d.ts +1 -1
  20. package/dist/install-hook-only.iife.js +1 -1
  21. package/dist/install-hook-only.js +1 -1
  22. package/dist/rdt-hook.cjs +1 -1
  23. package/dist/rdt-hook.js +1 -1
  24. package/dist/source.cjs +14 -4
  25. package/dist/source.d.cts +18 -9
  26. package/dist/source.d.ts +18 -9
  27. package/dist/source.js +14 -4
  28. package/package.json +21 -15
  29. package/src/core.ts +343 -253
  30. package/src/errors.ts +99 -0
  31. package/src/generated/react-work-tags.d.ts +261 -0
  32. package/src/generated/react-work-tags.js +240 -0
  33. package/src/rdt-hook.ts +154 -149
  34. package/src/react-internals.ts +65 -0
  35. package/src/semver.ts +78 -0
  36. package/src/source/constants.ts +1 -1
  37. package/src/source/error-stack.ts +11 -0
  38. package/src/source/get-source.ts +2 -1
  39. package/src/source/index.ts +9 -1
  40. package/src/source/inspect-hooks.ts +199 -164
  41. package/src/source/owner-stack.ts +84 -114
  42. package/src/source/parse-debug-stack.ts +4 -4
  43. package/src/source/parse-stack.ts +1 -1
  44. package/src/source/symbolication.ts +365 -89
  45. package/src/types.ts +165 -334
  46. package/src/unsubscribe.ts +2 -2
  47. package/dist/get-source.cjs +0 -19
  48. package/dist/get-source.js +0 -19
  49. package/dist/react-refresh.cjs +0 -9
  50. package/dist/react-refresh.d.cts +0 -66
  51. package/dist/react-refresh.d.ts +0 -66
  52. package/dist/react-refresh.js +0 -9
  53. package/dist/unsubscribe.d.cts +0 -298
  54. package/dist/unsubscribe.d.ts +0 -298
  55. package/src/react-refresh/constants.ts +0 -9
  56. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  57. package/src/react-refresh/index.ts +0 -173
  58. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  59. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  60. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  61. package/src/react-refresh/types.ts +0 -7
  62. package/src/react-refresh/vite-hmr-transport.ts +0 -116
@@ -1,5 +1,6 @@
1
1
  import { decode, SourceMapMappings, type SourceMapSegment } from "@jridgewell/sourcemap-codec";
2
2
 
3
+ import { BippySourceMapError } from "../errors.js";
3
4
  import { StackFrame } from "./parse-stack.js";
4
5
 
5
6
  export interface DecodedSourceMapSection {
@@ -10,7 +11,7 @@ export interface DecodedSourceMapSection {
10
11
  names?: string[];
11
12
  sourceRoot?: string;
12
13
  sources: string[];
13
- sourcesContent?: string[];
14
+ sourcesContent?: Array<string | null>;
14
15
  version: 3;
15
16
  };
16
17
  offset: {
@@ -19,20 +20,29 @@ export interface DecodedSourceMapSection {
19
20
  };
20
21
  }
21
22
 
22
- // https://tc39.es/ecma426/#sec-index-source-map
23
23
  export interface IndexSourceMap {
24
24
  file?: string;
25
25
  sections: Array<{
26
- map: StandardSourceMap;
26
+ map?: StandardSourceMap;
27
27
  offset: {
28
28
  column: number;
29
29
  line: number;
30
30
  };
31
+ url?: string;
31
32
  }>;
32
33
  version: 3;
33
34
  }
34
35
 
35
- export type RawSourceMap = IndexSourceMap | StandardSourceMap;
36
+ export interface SourceFetch {
37
+ (url: string, init?: RequestInit): Promise<Response>;
38
+ }
39
+
40
+ export interface SourceMapRequestOptions {
41
+ maxBundleSizeBytes?: number;
42
+ maxSourceMapSizeBytes?: number;
43
+ signal?: AbortSignal;
44
+ timeoutMs?: number;
45
+ }
36
46
 
37
47
  export interface SourceMap {
38
48
  file?: string;
@@ -42,11 +52,10 @@ export interface SourceMap {
42
52
  sections?: DecodedSourceMapSection[];
43
53
  sourceRoot?: string;
44
54
  sources: string[];
45
- sourcesContent?: string[];
55
+ sourcesContent?: Array<string | null>;
46
56
  version: 3;
47
57
  }
48
58
 
49
- // https://developer.chrome.com/blog/sourcemaps#the_anatomy_of_a_source_map
50
59
  export interface StandardSourceMap {
51
60
  file?: string;
52
61
  ignoreList?: number[];
@@ -54,27 +63,30 @@ export interface StandardSourceMap {
54
63
  names?: string[];
55
64
  sourceRoot?: string;
56
65
  sources: string[];
57
- sourcesContent?: string[];
66
+ sourcesContent?: Array<string | null>;
58
67
  version: 3;
59
68
  x_google_ignoreList?: number[];
60
69
  }
61
70
 
62
- // has a scheme, e.g. http://, https://, file://, data:, etc.
63
- // https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
64
71
  const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
65
- // inline sourcemap, e.g. data:application/json;base64,...
66
- const INLINE_SOURCEMAP_REGEX = /^data:application\/json[^,]+base64,/;
67
- // sourcemap url, e.g. //@ sourceMappingURL=... or /* @ sourceMappingURL=... */ at the end of the file
72
+ const INLINE_SOURCEMAP_REGEX = /^data:application\/json(?:;[^,]*)?,/i;
68
73
  const SOURCEMAP_REGEX =
69
74
  /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/;
70
75
 
71
76
  export const sourceMapCache = new Map<string, null | SourceMap>();
77
+ const sourceMapCachesByFetch = new WeakMap<SourceFetch, Map<string, null | SourceMap>>();
72
78
  interface SourceMapResult {
73
79
  sourceMap: null | SourceMap;
74
80
  isTransientFailure: boolean;
75
81
  }
76
82
 
77
83
  const _pendingSourceMapRequests = new Map<string, Promise<SourceMapResult>>();
84
+ const pendingSourceMapRequestsByFetch = new WeakMap<
85
+ SourceFetch,
86
+ Map<string, Promise<SourceMapResult>>
87
+ >();
88
+ const defaultMaxBundleSizeBytes = 25 * 1024 * 1024;
89
+ const defaultMaxSourceMapSizeBytes = 100 * 1024 * 1024;
78
90
 
79
91
  const getSourceFromMappings = (
80
92
  mappings: SourceMapMappings,
@@ -92,8 +104,6 @@ const getSourceFromMappings = (
92
104
  return null;
93
105
  }
94
106
 
95
- // Segments within a line are sorted by generated column, so binary search for
96
- // the last segment at or before the column.
97
107
  let closestLineSegment: null | SourceMapSegment = null;
98
108
  let lowIndex = 0;
99
109
  let highIndex = lineMapping.length - 1;
@@ -137,7 +147,6 @@ export const getSourceFromSourceMap = (
137
147
  column: number,
138
148
  ): StackFrame | null => {
139
149
  if (sourceMap.sections) {
140
- // Section offsets are 0-based while stack trace lines are 1-based.
141
150
  const lineIndex = line - 1;
142
151
  let targetSection: DecodedSourceMapSection | null = null;
143
152
 
@@ -178,9 +187,21 @@ export const getSourceFromSourceMap = (
178
187
  );
179
188
  };
180
189
 
190
+ const resolveUrl = (reference: string, baseUrl: string): string | null => {
191
+ if (INLINE_SOURCEMAP_REGEX.test(reference)) return reference;
192
+ try {
193
+ return new URL(reference, baseUrl).toString();
194
+ } catch {
195
+ try {
196
+ const resolvedUrl = new URL(reference, new URL(baseUrl, "https://bippy.invalid/"));
197
+ return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ };
203
+
181
204
  const getSourceMapUrl = (url: string, content: string): null | string => {
182
- // Walk lines backwards without content.split("\n"), which would allocate a
183
- // string per line of the entire bundle.
184
205
  let sourceMapUrl: string | undefined;
185
206
  let searchEnd = content.length;
186
207
  while (searchEnd > 0 && !sourceMapUrl) {
@@ -196,14 +217,57 @@ const getSourceMapUrl = (url: string, content: string): null | string => {
196
217
  return null;
197
218
  }
198
219
 
199
- const hasScheme = SCHEME_REGEX.test(sourceMapUrl);
200
- if (!(INLINE_SOURCEMAP_REGEX.test(sourceMapUrl) || hasScheme || sourceMapUrl.startsWith("/"))) {
201
- const urlSegments = url.split("/");
202
- urlSegments[urlSegments.length - 1] = sourceMapUrl;
203
- sourceMapUrl = urlSegments.join("/");
204
- }
220
+ return resolveUrl(sourceMapUrl, url);
221
+ };
205
222
 
206
- return sourceMapUrl;
223
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
224
+ typeof value === "object" && value !== null;
225
+
226
+ const isStringArray = (value: unknown): value is string[] =>
227
+ Array.isArray(value) && value.every((entry) => typeof entry === "string");
228
+
229
+ const isSourcesContent = (value: unknown): value is Array<string | null> =>
230
+ Array.isArray(value) && value.every((entry) => typeof entry === "string" || entry === null);
231
+
232
+ const isOptionalNumberArray = (value: unknown): value is number[] | undefined =>
233
+ value === undefined ||
234
+ (Array.isArray(value) && value.every((entry) => Number.isInteger(entry) && entry >= 0));
235
+
236
+ const isStandardSourceMap = (value: unknown): value is StandardSourceMap => {
237
+ if (!isObjectRecord(value)) return false;
238
+ const hasValidShape =
239
+ value.version === 3 &&
240
+ typeof value.mappings === "string" &&
241
+ isStringArray(value.sources) &&
242
+ (value.file === undefined || typeof value.file === "string") &&
243
+ (value.names === undefined || isStringArray(value.names)) &&
244
+ (value.sourceRoot === undefined || typeof value.sourceRoot === "string") &&
245
+ (value.sourcesContent === undefined || isSourcesContent(value.sourcesContent)) &&
246
+ isOptionalNumberArray(value.ignoreList) &&
247
+ isOptionalNumberArray(value.x_google_ignoreList);
248
+ if (!hasValidShape) return false;
249
+ if (value.sourcesContent && value.sourcesContent.length !== value.sources.length) return false;
250
+ return [...(value.ignoreList ?? []), ...(value.x_google_ignoreList ?? [])].every(
251
+ (sourceIndex) => sourceIndex < value.sources.length,
252
+ );
253
+ };
254
+
255
+ const isIndexSourceMap = (value: unknown): value is IndexSourceMap => {
256
+ if (!isObjectRecord(value) || value.version !== 3 || !Array.isArray(value.sections)) {
257
+ return false;
258
+ }
259
+ return value.sections.every((section) => {
260
+ if (!isObjectRecord(section) || !isObjectRecord(section.offset)) return false;
261
+ const hasMap = isStandardSourceMap(section.map);
262
+ const hasUrl = typeof section.url === "string" && section.url.length > 0;
263
+ return (
264
+ hasMap !== hasUrl &&
265
+ Number.isInteger(section.offset.column) &&
266
+ section.offset.column >= 0 &&
267
+ Number.isInteger(section.offset.line) &&
268
+ section.offset.line >= 0
269
+ );
270
+ });
207
271
  };
208
272
 
209
273
  const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> | undefined => {
@@ -211,29 +275,98 @@ const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> |
211
275
  return Array.isArray(ignoreList) && ignoreList.length > 0 ? new Set(ignoreList) : undefined;
212
276
  };
213
277
 
214
- const decodeStandardSourceMap = (rawSourceMap: StandardSourceMap): SourceMap => ({
215
- file: rawSourceMap.file,
216
- ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
217
- mappings: decode(rawSourceMap.mappings),
218
- names: rawSourceMap.names,
219
- sourceRoot: rawSourceMap.sourceRoot,
220
- sources: rawSourceMap.sources,
221
- sourcesContent: rawSourceMap.sourcesContent,
222
- version: 3,
223
- });
224
-
225
- const decodeIndexSourceMap = (rawSourceMap: IndexSourceMap): SourceMap => {
226
- const decodedSections: DecodedSourceMapSection[] = rawSourceMap.sections.map(
227
- ({ map, offset }) => ({
228
- map: {
229
- ...map,
230
- ignoredSourceIndices: getIgnoredSourceIndices(map),
231
- mappings: decode(map.mappings),
232
- },
233
- offset,
234
- }),
278
+ const resolveSourceRoot = (
279
+ sourceRoot: string | undefined,
280
+ source: string,
281
+ sourceMapUrl: string,
282
+ ): string => {
283
+ if (!sourceRoot || SCHEME_REGEX.test(source) || source.startsWith("/")) return source;
284
+ const normalizedSourceRoot = sourceRoot.endsWith("/") ? sourceRoot : `${sourceRoot}/`;
285
+ const normalizedSource = source.replace(/^\.\//, "");
286
+ try {
287
+ const baseUrl = SCHEME_REGEX.test(normalizedSourceRoot)
288
+ ? normalizedSourceRoot
289
+ : new URL(normalizedSourceRoot, sourceMapUrl).toString();
290
+ return new URL(normalizedSource, baseUrl).toString();
291
+ } catch {
292
+ const pathSegments = `${normalizedSourceRoot}${normalizedSource}`.split("/");
293
+ const normalizedPathSegments: string[] = [];
294
+ for (const pathSegment of pathSegments) {
295
+ if (pathSegment === "..") {
296
+ normalizedPathSegments.pop();
297
+ } else if (pathSegment !== ".") {
298
+ normalizedPathSegments.push(pathSegment);
299
+ }
300
+ }
301
+ return normalizedPathSegments.join("/");
302
+ }
303
+ };
304
+
305
+ const resolveSourceMapSources = (rawSourceMap: StandardSourceMap, sourceMapUrl: string): string[] =>
306
+ rawSourceMap.sources.map((source) =>
307
+ resolveSourceRoot(rawSourceMap.sourceRoot, source, sourceMapUrl),
235
308
  );
236
309
 
310
+ const decodeStandardSourceMap = (
311
+ rawSourceMap: StandardSourceMap,
312
+ sourceMapUrl: string,
313
+ ): SourceMap | null => {
314
+ try {
315
+ return {
316
+ file: rawSourceMap.file,
317
+ ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
318
+ mappings: decode(rawSourceMap.mappings),
319
+ names: rawSourceMap.names,
320
+ sourceRoot: rawSourceMap.sourceRoot,
321
+ sources: resolveSourceMapSources(rawSourceMap, sourceMapUrl),
322
+ sourcesContent: rawSourceMap.sourcesContent,
323
+ version: 3,
324
+ };
325
+ } catch {
326
+ return null;
327
+ }
328
+ };
329
+
330
+ interface LoadStandardSourceMap {
331
+ (url: string): Promise<StandardSourceMap | null>;
332
+ }
333
+
334
+ const decodeIndexSourceMap = async (
335
+ rawSourceMap: IndexSourceMap,
336
+ sourceMapUrl: string,
337
+ loadStandardSourceMap: LoadStandardSourceMap,
338
+ ): Promise<SourceMap | null> => {
339
+ const decodedSections: DecodedSourceMapSection[] = [];
340
+ let previousOffsetLine = -1;
341
+ let previousOffsetColumn = -1;
342
+ for (const section of rawSourceMap.sections) {
343
+ if (
344
+ section.offset.line < previousOffsetLine ||
345
+ (section.offset.line === previousOffsetLine && section.offset.column <= previousOffsetColumn)
346
+ ) {
347
+ return null;
348
+ }
349
+ previousOffsetLine = section.offset.line;
350
+ previousOffsetColumn = section.offset.column;
351
+ const sectionUrl = section.url ? resolveUrl(section.url, sourceMapUrl) : null;
352
+ const map = section.map ?? (sectionUrl ? await loadStandardSourceMap(sectionUrl) : null);
353
+ if (!map) return null;
354
+ const sectionSourceMapUrl = sectionUrl ?? sourceMapUrl;
355
+ try {
356
+ decodedSections.push({
357
+ map: {
358
+ ...map,
359
+ ignoredSourceIndices: getIgnoredSourceIndices(map),
360
+ mappings: decode(map.mappings),
361
+ sources: resolveSourceMapSources(map, sectionSourceMapUrl),
362
+ },
363
+ offset: section.offset,
364
+ });
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
369
+
237
370
  const allSources = new Set<string>();
238
371
  for (const section of decodedSections) {
239
372
  for (const source of section.map.sources) {
@@ -275,84 +408,227 @@ const isFetchableUrl = (url: string): boolean => {
275
408
  return scheme === "http:" || scheme === "https:";
276
409
  };
277
410
 
278
- // Resolves a bundle's source map, or null when the bundle definitively has
279
- // none. A thrown fetch (network error or aborted request) is left to propagate
280
- // so getSourceMap can treat it as transient and avoid caching it: a non-ok
281
- // response, a missing sourceMappingURL, or an undecodable map are definitive and
282
- // return null, but a dropped connection is not and must stay retryable.
283
- export const getSourceMapImpl = async (
284
- bundleUrl: string,
285
- fetchFn: (url: string) => Promise<Response> = fetch,
286
- ): Promise<null | SourceMap> => {
287
- if (!isFetchableUrl(bundleUrl)) {
288
- return null;
411
+ interface RequestSignal {
412
+ cleanup: () => void;
413
+ signal?: AbortSignal;
414
+ }
415
+
416
+ const createRequestSignal = (options: SourceMapRequestOptions): RequestSignal => {
417
+ if (!options.signal && options.timeoutMs === undefined) {
418
+ return { cleanup: () => {} };
419
+ }
420
+ const abortController = new AbortController();
421
+ const abortFromSource = (): void => abortController.abort(options.signal?.reason);
422
+ if (options.signal?.aborted) {
423
+ abortFromSource();
424
+ } else {
425
+ options.signal?.addEventListener("abort", abortFromSource, { once: true });
289
426
  }
427
+ const timeoutHandle =
428
+ options.timeoutMs !== undefined && options.timeoutMs >= 0
429
+ ? setTimeout(
430
+ () => abortController.abort(new BippySourceMapError("Source map request timed out")),
431
+ options.timeoutMs,
432
+ )
433
+ : undefined;
434
+ return {
435
+ cleanup: () => {
436
+ if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
437
+ options.signal?.removeEventListener("abort", abortFromSource);
438
+ },
439
+ signal: abortController.signal,
440
+ };
441
+ };
290
442
 
291
- const bundleResponse = await fetchFn(bundleUrl);
292
- if (!bundleResponse.ok) {
443
+ const readResponseText = async (
444
+ response: Response,
445
+ maxSizeBytes: number,
446
+ ): Promise<string | null> => {
447
+ const contentLength = Number(response.headers.get("content-length"));
448
+ if (Number.isFinite(contentLength) && contentLength > maxSizeBytes) return null;
449
+ const content = await response.text();
450
+ return new TextEncoder().encode(content).byteLength <= maxSizeBytes ? content : null;
451
+ };
452
+
453
+ const decodeBase64 = (encodedContent: string): string | null => {
454
+ try {
455
+ if (typeof globalThis.atob === "function") {
456
+ return new TextDecoder().decode(
457
+ Uint8Array.from(globalThis.atob(encodedContent), (character) => character.charCodeAt(0)),
458
+ );
459
+ }
460
+ } catch {
293
461
  return null;
294
462
  }
295
- const bundleContent = await bundleResponse.text();
296
- if (!bundleContent) {
297
- return null;
463
+
464
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
465
+ const normalizedContent = encodedContent.replace(/\s/g, "").replace(/=+$/, "");
466
+ const bytes: number[] = [];
467
+ let bitBuffer = 0;
468
+ let bitCount = 0;
469
+ for (const character of normalizedContent) {
470
+ const value = alphabet.indexOf(character);
471
+ if (value === -1) return null;
472
+ bitBuffer = bitBuffer * 64 + value;
473
+ bitCount += 6;
474
+ if (bitCount >= 8) {
475
+ bitCount -= 8;
476
+ bytes.push(Math.floor(bitBuffer / 2 ** bitCount) & 255);
477
+ bitBuffer %= 2 ** bitCount;
478
+ }
298
479
  }
480
+ return new TextDecoder().decode(Uint8Array.from(bytes));
481
+ };
299
482
 
300
- const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
483
+ const readInlineSourceMap = (sourceMapUrl: string, maxSizeBytes: number): unknown => {
484
+ const commaIndex = sourceMapUrl.indexOf(",");
485
+ if (commaIndex === -1) return null;
486
+ const metadata = sourceMapUrl.slice(0, commaIndex);
487
+ const encodedContent = sourceMapUrl.slice(commaIndex + 1);
488
+ if (encodedContent.length > maxSizeBytes * 2) return null;
489
+ try {
490
+ const content = metadata.toLowerCase().includes(";base64")
491
+ ? decodeBase64(encodedContent)
492
+ : decodeURIComponent(encodedContent);
493
+ if (content === null) return null;
494
+ if (new TextEncoder().encode(content).byteLength > maxSizeBytes) return null;
495
+ return JSON.parse(content);
496
+ } catch {
497
+ return null;
498
+ }
499
+ };
301
500
 
302
- if (!sourceMapUrl) return null;
303
- // inline data: maps (vite dev, babel inline sourcemaps) are decoded by
304
- // fetch itself, so they bypass the network-url check
305
- if (!isFetchableUrl(sourceMapUrl) && !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
501
+ const readSourceMapDocument = async (
502
+ sourceMapUrl: string,
503
+ sourceFetch: SourceFetch,
504
+ signal: AbortSignal | undefined,
505
+ maxSizeBytes: number,
506
+ ): Promise<unknown> => {
507
+ if (INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
508
+ return readInlineSourceMap(sourceMapUrl, maxSizeBytes);
509
+ }
510
+ const sourceMapResponse = await sourceFetch(sourceMapUrl, { signal });
511
+ if (!sourceMapResponse.ok) return null;
512
+ const sourceMapContent = await readResponseText(sourceMapResponse, maxSizeBytes);
513
+ if (sourceMapContent === null) return null;
514
+ try {
515
+ return JSON.parse(sourceMapContent);
516
+ } catch {
306
517
  return null;
307
518
  }
519
+ };
308
520
 
309
- const sourceMapResponse = await fetchFn(sourceMapUrl);
310
- if (!sourceMapResponse.ok) {
521
+ export const getSourceMapUncached = async (
522
+ bundleUrl: string,
523
+ fetchFn?: SourceFetch,
524
+ options: SourceMapRequestOptions = {},
525
+ ): Promise<null | SourceMap> => {
526
+ if (!isFetchableUrl(bundleUrl)) {
311
527
  return null;
312
528
  }
313
529
 
530
+ const sourceFetch =
531
+ fetchFn ??
532
+ (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
533
+ if (!sourceFetch) return null;
534
+ const maxBundleSizeBytes = options.maxBundleSizeBytes ?? defaultMaxBundleSizeBytes;
535
+ const maxSourceMapSizeBytes = options.maxSourceMapSizeBytes ?? defaultMaxSourceMapSizeBytes;
536
+ if (maxBundleSizeBytes < 0 || maxSourceMapSizeBytes < 0) return null;
537
+ const requestSignal = createRequestSignal(options);
314
538
  try {
315
- const rawSourceMap = (await sourceMapResponse.json()) as RawSourceMap;
539
+ const bundleResponse = await sourceFetch(bundleUrl, { signal: requestSignal.signal });
540
+ if (!bundleResponse.ok) return null;
541
+ const bundleContent = await readResponseText(bundleResponse, maxBundleSizeBytes);
542
+ if (!bundleContent) return null;
543
+ const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
544
+ if (!sourceMapUrl) return null;
545
+ if (!isFetchableUrl(sourceMapUrl) && !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) return null;
546
+
547
+ const rawSourceMap = await readSourceMapDocument(
548
+ sourceMapUrl,
549
+ sourceFetch,
550
+ requestSignal.signal,
551
+ maxSourceMapSizeBytes,
552
+ );
553
+ if (isStandardSourceMap(rawSourceMap)) {
554
+ return decodeStandardSourceMap(rawSourceMap, sourceMapUrl);
555
+ }
556
+ if (!isIndexSourceMap(rawSourceMap)) return null;
557
+ return decodeIndexSourceMap(rawSourceMap, sourceMapUrl, async (sectionUrl) => {
558
+ const sectionSourceMap = await readSourceMapDocument(
559
+ sectionUrl,
560
+ sourceFetch,
561
+ requestSignal.signal,
562
+ maxSourceMapSizeBytes,
563
+ );
564
+ return isStandardSourceMap(sectionSourceMap) ? sectionSourceMap : null;
565
+ });
566
+ } finally {
567
+ requestSignal.cleanup();
568
+ }
569
+ };
316
570
 
317
- return "sections" in rawSourceMap
318
- ? decodeIndexSourceMap(rawSourceMap)
319
- : decodeStandardSourceMap(rawSourceMap);
320
- } catch {
321
- return null;
571
+ const getSourceMapCache = (fetchFn: SourceFetch | undefined): Map<string, null | SourceMap> => {
572
+ if (!fetchFn) return sourceMapCache;
573
+ let cache = sourceMapCachesByFetch.get(fetchFn);
574
+ if (!cache) {
575
+ cache = new Map();
576
+ sourceMapCachesByFetch.set(fetchFn, cache);
322
577
  }
578
+ return cache;
323
579
  };
324
580
 
581
+ const getPendingSourceMapRequests = (
582
+ fetchFn: SourceFetch | undefined,
583
+ ): Map<string, Promise<SourceMapResult>> => {
584
+ if (!fetchFn) return _pendingSourceMapRequests;
585
+ let pendingRequests = pendingSourceMapRequestsByFetch.get(fetchFn);
586
+ if (!pendingRequests) {
587
+ pendingRequests = new Map();
588
+ pendingSourceMapRequestsByFetch.set(fetchFn, pendingRequests);
589
+ }
590
+ return pendingRequests;
591
+ };
592
+
593
+ const getSourceMapCacheKey = (file: string, options: SourceMapRequestOptions): string =>
594
+ options.maxBundleSizeBytes === undefined &&
595
+ options.maxSourceMapSizeBytes === undefined &&
596
+ options.timeoutMs === undefined
597
+ ? file
598
+ : `${file}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
599
+
325
600
  export const getSourceMap = async (
326
601
  file: string,
327
602
  useCache = true,
328
- fetchFn?: (url: string) => Promise<Response>,
603
+ fetchFn?: SourceFetch,
604
+ options: SourceMapRequestOptions = {},
329
605
  ): Promise<null | SourceMap> => {
330
- if (useCache && sourceMapCache.has(file)) {
331
- return sourceMapCache.get(file) ?? null;
606
+ const shouldUseCache = useCache && options.signal === undefined;
607
+ const cache = getSourceMapCache(fetchFn);
608
+ const pendingRequests = getPendingSourceMapRequests(fetchFn);
609
+ const cacheKey = getSourceMapCacheKey(file, options);
610
+ if (shouldUseCache && cache.has(cacheKey)) {
611
+ return cache.get(cacheKey) ?? null;
332
612
  }
333
613
 
334
- const pendingRequest = useCache ? _pendingSourceMapRequests.get(file) : undefined;
614
+ const pendingRequest = shouldUseCache ? pendingRequests.get(cacheKey) : undefined;
335
615
  if (pendingRequest) {
336
616
  return (await pendingRequest).sourceMap;
337
617
  }
338
618
 
339
- // A transient fetch failure (aborted request or network error) rejects; a
340
- // definitive "no map" resolves to null. Only definitive results are cached:
341
- // caching a transient null would pin the bundle to a degraded result for the
342
- // rest of the page's lifetime, even after the network recovers.
343
- const fetchPromise: Promise<SourceMapResult> = getSourceMapImpl(file, fetchFn).then(
619
+ const fetchPromise: Promise<SourceMapResult> = getSourceMapUncached(file, fetchFn, options).then(
344
620
  (sourceMap) => ({ sourceMap, isTransientFailure: false }),
345
621
  () => ({ sourceMap: null, isTransientFailure: true }),
346
622
  );
347
- if (useCache) {
348
- _pendingSourceMapRequests.set(file, fetchPromise);
623
+ if (shouldUseCache) {
624
+ pendingRequests.set(cacheKey, fetchPromise);
349
625
  }
350
626
 
351
627
  const { sourceMap, isTransientFailure } = await fetchPromise;
352
- if (useCache) {
353
- _pendingSourceMapRequests.delete(file);
628
+ if (shouldUseCache) {
629
+ pendingRequests.delete(cacheKey);
354
630
  if (!isTransientFailure) {
355
- sourceMapCache.set(file, sourceMap);
631
+ cache.set(cacheKey, sourceMap);
356
632
  }
357
633
  }
358
634
 
@@ -364,7 +640,7 @@ export const symbolicateStack = async (
364
640
  cache = true,
365
641
  fetchFn?: (url: string) => Promise<Response>,
366
642
  ): Promise<StackFrame[]> => {
367
- return await Promise.all(
643
+ return Promise.all(
368
644
  stack.map(async (stackFrame) => {
369
645
  if (!stackFrame.fileName) return stackFrame;
370
646
  const sourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);