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

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 (66) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -48
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +45 -60
  5. package/dist/core.d.ts +45 -60
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +9 -0
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +9 -0
  11. package/dist/errors.d.cts +420 -0
  12. package/dist/errors.d.ts +420 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +11 -3
  15. package/dist/index.d.ts +11 -3
  16. package/dist/index.iife.js +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/install-hook-only.cjs +1 -1
  19. package/dist/install-hook-only.d.cts +9 -1
  20. package/dist/install-hook-only.d.ts +9 -1
  21. package/dist/install-hook-only.iife.js +1 -1
  22. package/dist/install-hook-only.js +1 -1
  23. package/dist/rdt-hook.cjs +1 -1
  24. package/dist/rdt-hook.js +1 -1
  25. package/dist/source.cjs +14 -5
  26. package/dist/source.d.cts +27 -11
  27. package/dist/source.d.ts +27 -11
  28. package/dist/source.js +14 -5
  29. package/package.json +22 -16
  30. package/src/core.ts +349 -328
  31. package/src/errors.ts +34 -0
  32. package/src/install-hook-only.ts +2 -2
  33. package/src/rdt-hook.ts +160 -161
  34. package/src/react-internals/generated/react-work-tags.ts +318 -0
  35. package/src/react-internals/index.ts +67 -0
  36. package/src/react-internals/semver.ts +80 -0
  37. package/src/react-internals/types.ts +197 -0
  38. package/src/source/constants.ts +1 -1
  39. package/src/source/error-stack.ts +11 -0
  40. package/src/source/get-display-name-from-source.ts +32 -25
  41. package/src/source/get-source.ts +5 -5
  42. package/src/source/index.ts +9 -1
  43. package/src/source/inspect-hooks.ts +215 -182
  44. package/src/source/owner-stack.ts +127 -166
  45. package/src/source/parse-debug-stack.ts +4 -4
  46. package/src/source/parse-hook-names.ts +1 -1
  47. package/src/source/parse-stack.ts +1 -3
  48. package/src/source/symbolication.ts +490 -90
  49. package/dist/get-source.cjs +0 -19
  50. package/dist/get-source.js +0 -19
  51. package/dist/react-refresh.cjs +0 -9
  52. package/dist/react-refresh.d.cts +0 -66
  53. package/dist/react-refresh.d.ts +0 -66
  54. package/dist/react-refresh.js +0 -9
  55. package/dist/unsubscribe.d.cts +0 -298
  56. package/dist/unsubscribe.d.ts +0 -298
  57. package/src/react-refresh/constants.ts +0 -9
  58. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  59. package/src/react-refresh/index.ts +0 -173
  60. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  61. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  62. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  63. package/src/react-refresh/types.ts +0 -7
  64. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  65. package/src/types.ts +0 -438
  66. package/src/unsubscribe.ts +0 -17
@@ -1,5 +1,7 @@
1
1
  import { decode, SourceMapMappings, type SourceMapSegment } from "@jridgewell/sourcemap-codec";
2
2
 
3
+ import { BippySourceMapError } from "../errors.js";
4
+ import { SCHEME_REGEX } from "./constants.js";
3
5
  import { StackFrame } from "./parse-stack.js";
4
6
 
5
7
  export interface DecodedSourceMapSection {
@@ -10,7 +12,7 @@ export interface DecodedSourceMapSection {
10
12
  names?: string[];
11
13
  sourceRoot?: string;
12
14
  sources: string[];
13
- sourcesContent?: string[];
15
+ sourcesContent?: Array<string | null>;
14
16
  version: 3;
15
17
  };
16
18
  offset: {
@@ -19,20 +21,30 @@ export interface DecodedSourceMapSection {
19
21
  };
20
22
  }
21
23
 
22
- // https://tc39.es/ecma426/#sec-index-source-map
23
24
  export interface IndexSourceMap {
24
25
  file?: string;
25
26
  sections: Array<{
26
- map: StandardSourceMap;
27
+ map?: StandardSourceMap;
27
28
  offset: {
28
29
  column: number;
29
30
  line: number;
30
31
  };
32
+ url?: string;
31
33
  }>;
32
34
  version: 3;
33
35
  }
34
36
 
35
- export type RawSourceMap = IndexSourceMap | StandardSourceMap;
37
+ export interface SourceFetch {
38
+ (url: string, init?: RequestInit): Promise<Response>;
39
+ }
40
+
41
+ export interface SourceMapRequestOptions {
42
+ allowUnsafeServerFetch?: boolean;
43
+ maxBundleSizeBytes?: number;
44
+ maxSourceMapSizeBytes?: number;
45
+ signal?: AbortSignal;
46
+ timeoutMs?: number;
47
+ }
36
48
 
37
49
  export interface SourceMap {
38
50
  file?: string;
@@ -42,11 +54,10 @@ export interface SourceMap {
42
54
  sections?: DecodedSourceMapSection[];
43
55
  sourceRoot?: string;
44
56
  sources: string[];
45
- sourcesContent?: string[];
57
+ sourcesContent?: Array<string | null>;
46
58
  version: 3;
47
59
  }
48
60
 
49
- // https://developer.chrome.com/blog/sourcemaps#the_anatomy_of_a_source_map
50
61
  export interface StandardSourceMap {
51
62
  file?: string;
52
63
  ignoreList?: number[];
@@ -54,27 +65,31 @@ export interface StandardSourceMap {
54
65
  names?: string[];
55
66
  sourceRoot?: string;
56
67
  sources: string[];
57
- sourcesContent?: string[];
68
+ sourcesContent?: Array<string | null>;
58
69
  version: 3;
59
70
  x_google_ignoreList?: number[];
60
71
  }
61
72
 
62
- // has a scheme, e.g. http://, https://, file://, data:, etc.
63
- // https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
64
- 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
73
+ const INLINE_SOURCEMAP_REGEX = /^data:application\/json(?:;[^,]*)?,/i;
68
74
  const SOURCEMAP_REGEX =
69
75
  /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/;
70
76
 
71
77
  export const sourceMapCache = new Map<string, null | SourceMap>();
78
+ const sourceMapCachesByFetch = new WeakMap<SourceFetch, Map<string, null | SourceMap>>();
72
79
  interface SourceMapResult {
73
80
  sourceMap: null | SourceMap;
74
81
  isTransientFailure: boolean;
75
82
  }
76
83
 
77
- const _pendingSourceMapRequests = new Map<string, Promise<SourceMapResult>>();
84
+ class TransientSourceMapError extends Error {}
85
+
86
+ const pendingSourceMapRequests = new Map<string, Promise<SourceMapResult>>();
87
+ const pendingSourceMapRequestsByFetch = new WeakMap<
88
+ SourceFetch,
89
+ Map<string, Promise<SourceMapResult>>
90
+ >();
91
+ const defaultMaxBundleSizeBytes = 25 * 1024 * 1024;
92
+ const defaultMaxSourceMapSizeBytes = 100 * 1024 * 1024;
78
93
 
79
94
  const getSourceFromMappings = (
80
95
  mappings: SourceMapMappings,
@@ -92,8 +107,6 @@ const getSourceFromMappings = (
92
107
  return null;
93
108
  }
94
109
 
95
- // Segments within a line are sorted by generated column, so binary search for
96
- // the last segment at or before the column.
97
110
  let closestLineSegment: null | SourceMapSegment = null;
98
111
  let lowIndex = 0;
99
112
  let highIndex = lineMapping.length - 1;
@@ -137,7 +150,6 @@ export const getSourceFromSourceMap = (
137
150
  column: number,
138
151
  ): StackFrame | null => {
139
152
  if (sourceMap.sections) {
140
- // Section offsets are 0-based while stack trace lines are 1-based.
141
153
  const lineIndex = line - 1;
142
154
  let targetSection: DecodedSourceMapSection | null = null;
143
155
 
@@ -178,9 +190,21 @@ export const getSourceFromSourceMap = (
178
190
  );
179
191
  };
180
192
 
193
+ const resolveUrl = (reference: string, baseUrl: string): string | null => {
194
+ if (INLINE_SOURCEMAP_REGEX.test(reference)) return reference;
195
+ try {
196
+ return new URL(reference, baseUrl).toString();
197
+ } catch {
198
+ try {
199
+ const resolvedUrl = new URL(reference, new URL(baseUrl, "https://bippy.invalid/"));
200
+ return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ };
206
+
181
207
  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
208
  let sourceMapUrl: string | undefined;
185
209
  let searchEnd = content.length;
186
210
  while (searchEnd > 0 && !sourceMapUrl) {
@@ -196,14 +220,74 @@ const getSourceMapUrl = (url: string, content: string): null | string => {
196
220
  return null;
197
221
  }
198
222
 
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("/");
223
+ return resolveUrl(sourceMapUrl, url);
224
+ };
225
+
226
+ const isStandardSourceMap = (value: unknown): value is StandardSourceMap => {
227
+ if (typeof value !== "object" || value === null) return false;
228
+ const version = Reflect.get(value, "version");
229
+ const mappings = Reflect.get(value, "mappings");
230
+ const file = Reflect.get(value, "file");
231
+ const names = Reflect.get(value, "names");
232
+ const sourceRoot = Reflect.get(value, "sourceRoot");
233
+ const sources = Reflect.get(value, "sources");
234
+ const sourcesContent = Reflect.get(value, "sourcesContent");
235
+ const ignoreList = Reflect.get(value, "ignoreList");
236
+ const googleIgnoreList = Reflect.get(value, "x_google_ignoreList");
237
+ if (
238
+ version !== 3 ||
239
+ typeof mappings !== "string" ||
240
+ (file !== undefined && typeof file !== "string") ||
241
+ (names !== undefined &&
242
+ (!Array.isArray(names) || names.some((entry) => typeof entry !== "string"))) ||
243
+ (sourceRoot !== undefined && typeof sourceRoot !== "string") ||
244
+ (sourcesContent !== undefined &&
245
+ (!Array.isArray(sourcesContent) ||
246
+ sourcesContent.some((entry) => typeof entry !== "string" && entry !== null))) ||
247
+ (ignoreList !== undefined &&
248
+ (!Array.isArray(ignoreList) ||
249
+ ignoreList.some((entry) => !Number.isInteger(entry) || entry < 0))) ||
250
+ (googleIgnoreList !== undefined &&
251
+ (!Array.isArray(googleIgnoreList) ||
252
+ googleIgnoreList.some((entry) => !Number.isInteger(entry) || entry < 0)))
253
+ ) {
254
+ return false;
204
255
  }
256
+ if (!Array.isArray(sources) || sources.some((entry) => typeof entry !== "string")) return false;
257
+ if (sourcesContent && sourcesContent.length !== sources.length) return false;
258
+ const sourceCount = sources.length;
259
+ return [...(ignoreList ?? []), ...(googleIgnoreList ?? [])].every(
260
+ (sourceIndex) => sourceIndex < sourceCount,
261
+ );
262
+ };
205
263
 
206
- return sourceMapUrl;
264
+ const isIndexSourceMap = (value: unknown): value is IndexSourceMap => {
265
+ if (typeof value !== "object" || value === null) return false;
266
+ const version = Reflect.get(value, "version");
267
+ const sections = Reflect.get(value, "sections");
268
+ if (version !== 3 || !Array.isArray(sections)) {
269
+ return false;
270
+ }
271
+ return sections.every((section: unknown) => {
272
+ if (typeof section !== "object" || section === null) return false;
273
+ const map = Reflect.get(section, "map");
274
+ const url = Reflect.get(section, "url");
275
+ const offset = Reflect.get(section, "offset");
276
+ if (typeof offset !== "object" || offset === null) return false;
277
+ const offsetColumn = Reflect.get(offset, "column");
278
+ const offsetLine = Reflect.get(offset, "line");
279
+ const hasMap = isStandardSourceMap(map);
280
+ const hasUrl = typeof url === "string" && url.length > 0;
281
+ return (
282
+ hasMap !== hasUrl &&
283
+ typeof offsetColumn === "number" &&
284
+ Number.isInteger(offsetColumn) &&
285
+ offsetColumn >= 0 &&
286
+ typeof offsetLine === "number" &&
287
+ Number.isInteger(offsetLine) &&
288
+ offsetLine >= 0
289
+ );
290
+ });
207
291
  };
208
292
 
209
293
  const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> | undefined => {
@@ -211,29 +295,98 @@ const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> |
211
295
  return Array.isArray(ignoreList) && ignoreList.length > 0 ? new Set(ignoreList) : undefined;
212
296
  };
213
297
 
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
- }),
298
+ const resolveSourceRoot = (
299
+ sourceRoot: string | undefined,
300
+ source: string,
301
+ sourceMapUrl: string,
302
+ ): string => {
303
+ if (!sourceRoot || SCHEME_REGEX.test(source) || source.startsWith("/")) return source;
304
+ const normalizedSourceRoot = sourceRoot.endsWith("/") ? sourceRoot : `${sourceRoot}/`;
305
+ const normalizedSource = source.replace(/^\.\//, "");
306
+ try {
307
+ const baseUrl = SCHEME_REGEX.test(normalizedSourceRoot)
308
+ ? normalizedSourceRoot
309
+ : new URL(normalizedSourceRoot, sourceMapUrl).toString();
310
+ return new URL(normalizedSource, baseUrl).toString();
311
+ } catch {
312
+ const pathSegments = `${normalizedSourceRoot}${normalizedSource}`.split("/");
313
+ const normalizedPathSegments: string[] = [];
314
+ for (const pathSegment of pathSegments) {
315
+ if (pathSegment === "..") {
316
+ normalizedPathSegments.pop();
317
+ } else if (pathSegment !== ".") {
318
+ normalizedPathSegments.push(pathSegment);
319
+ }
320
+ }
321
+ return normalizedPathSegments.join("/");
322
+ }
323
+ };
324
+
325
+ const resolveSourceMapSources = (rawSourceMap: StandardSourceMap, sourceMapUrl: string): string[] =>
326
+ rawSourceMap.sources.map((source) =>
327
+ resolveSourceRoot(rawSourceMap.sourceRoot, source, sourceMapUrl),
235
328
  );
236
329
 
330
+ const decodeStandardSourceMap = (
331
+ rawSourceMap: StandardSourceMap,
332
+ sourceMapUrl: string,
333
+ ): SourceMap | null => {
334
+ try {
335
+ return {
336
+ file: rawSourceMap.file,
337
+ ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
338
+ mappings: decode(rawSourceMap.mappings),
339
+ names: rawSourceMap.names,
340
+ sourceRoot: rawSourceMap.sourceRoot,
341
+ sources: resolveSourceMapSources(rawSourceMap, sourceMapUrl),
342
+ sourcesContent: rawSourceMap.sourcesContent,
343
+ version: 3,
344
+ };
345
+ } catch {
346
+ return null;
347
+ }
348
+ };
349
+
350
+ interface LoadStandardSourceMap {
351
+ (url: string): Promise<StandardSourceMap | null>;
352
+ }
353
+
354
+ const decodeIndexSourceMap = async (
355
+ rawSourceMap: IndexSourceMap,
356
+ sourceMapUrl: string,
357
+ loadStandardSourceMap: LoadStandardSourceMap,
358
+ ): Promise<SourceMap | null> => {
359
+ const decodedSections: DecodedSourceMapSection[] = [];
360
+ let previousOffsetLine = -1;
361
+ let previousOffsetColumn = -1;
362
+ for (const section of rawSourceMap.sections) {
363
+ if (
364
+ section.offset.line < previousOffsetLine ||
365
+ (section.offset.line === previousOffsetLine && section.offset.column <= previousOffsetColumn)
366
+ ) {
367
+ return null;
368
+ }
369
+ previousOffsetLine = section.offset.line;
370
+ previousOffsetColumn = section.offset.column;
371
+ const sectionUrl = section.url ? resolveUrl(section.url, sourceMapUrl) : null;
372
+ const map = section.map ?? (sectionUrl ? await loadStandardSourceMap(sectionUrl) : null);
373
+ if (!map) return null;
374
+ const sectionSourceMapUrl = sectionUrl ?? sourceMapUrl;
375
+ try {
376
+ decodedSections.push({
377
+ map: {
378
+ ...map,
379
+ ignoredSourceIndices: getIgnoredSourceIndices(map),
380
+ mappings: decode(map.mappings),
381
+ sources: resolveSourceMapSources(map, sectionSourceMapUrl),
382
+ },
383
+ offset: section.offset,
384
+ });
385
+ } catch {
386
+ return null;
387
+ }
388
+ }
389
+
237
390
  const allSources = new Set<string>();
238
391
  for (const section of decodedSections) {
239
392
  for (const source of section.map.sources) {
@@ -275,84 +428,331 @@ const isFetchableUrl = (url: string): boolean => {
275
428
  return scheme === "http:" || scheme === "https:";
276
429
  };
277
430
 
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;
431
+ const isServerRuntime = (): boolean =>
432
+ typeof window === "undefined" &&
433
+ typeof process !== "undefined" &&
434
+ typeof process.versions?.node === "string";
435
+
436
+ const isAbsoluteHttpUrl = (url: string): boolean => {
437
+ try {
438
+ const parsedUrl = new URL(url);
439
+ return (
440
+ (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") &&
441
+ !parsedUrl.username &&
442
+ !parsedUrl.password
443
+ );
444
+ } catch {
445
+ return false;
289
446
  }
447
+ };
290
448
 
291
- const bundleResponse = await fetchFn(bundleUrl);
292
- if (!bundleResponse.ok) {
293
- return null;
449
+ const isSameOrigin = (firstUrl: string, secondUrl: string): boolean => {
450
+ try {
451
+ return new URL(firstUrl).origin === new URL(secondUrl).origin;
452
+ } catch {
453
+ return false;
294
454
  }
295
- const bundleContent = await bundleResponse.text();
296
- if (!bundleContent) {
297
- return null;
455
+ };
456
+
457
+ const isTransientHttpStatus = (status: number): boolean =>
458
+ status === 408 || status === 425 || status === 429 || status >= 500;
459
+
460
+ const assertResponseIsCacheable = (response: Response): boolean => {
461
+ if (response.ok) return true;
462
+ if (isTransientHttpStatus(response.status)) {
463
+ throw new TransientSourceMapError();
298
464
  }
465
+ return false;
466
+ };
299
467
 
300
- const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
468
+ interface RequestSignal {
469
+ cleanup: () => void;
470
+ signal?: AbortSignal;
471
+ }
301
472
 
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)) {
306
- return null;
473
+ const createRequestSignal = (options: SourceMapRequestOptions): RequestSignal => {
474
+ if (!options.signal && options.timeoutMs === undefined) {
475
+ return { cleanup: () => {} };
476
+ }
477
+ const abortController = new AbortController();
478
+ const abortFromSource = (): void => abortController.abort(options.signal?.reason);
479
+ if (options.signal?.aborted) {
480
+ abortFromSource();
481
+ } else {
482
+ options.signal?.addEventListener("abort", abortFromSource, { once: true });
483
+ }
484
+ const timeoutHandle =
485
+ options.timeoutMs !== undefined && options.timeoutMs >= 0
486
+ ? setTimeout(
487
+ () => abortController.abort(new BippySourceMapError("Source map request timed out")),
488
+ options.timeoutMs,
489
+ )
490
+ : undefined;
491
+ return {
492
+ cleanup: () => {
493
+ if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
494
+ options.signal?.removeEventListener("abort", abortFromSource);
495
+ },
496
+ signal: abortController.signal,
497
+ };
498
+ };
499
+
500
+ const readResponseText = async (
501
+ response: Response,
502
+ maxSizeBytes: number,
503
+ ): Promise<string | null> => {
504
+ const contentLength = Number(response.headers.get("content-length"));
505
+ if (Number.isFinite(contentLength) && contentLength > maxSizeBytes) return null;
506
+ if (!response.body) return "";
507
+
508
+ const reader = response.body.getReader();
509
+ const decoder = new TextDecoder();
510
+ const decodedChunks: string[] = [];
511
+ let totalSizeBytes = 0;
512
+
513
+ try {
514
+ while (true) {
515
+ const { done, value } = await reader.read();
516
+ if (done) break;
517
+ totalSizeBytes += value.byteLength;
518
+ if (totalSizeBytes > maxSizeBytes) {
519
+ await reader.cancel();
520
+ return null;
521
+ }
522
+ decodedChunks.push(decoder.decode(value, { stream: true }));
523
+ }
524
+ decodedChunks.push(decoder.decode());
525
+ return decodedChunks.join("");
526
+ } finally {
527
+ reader.releaseLock();
307
528
  }
529
+ };
308
530
 
309
- const sourceMapResponse = await fetchFn(sourceMapUrl);
310
- if (!sourceMapResponse.ok) {
531
+ const decodeBase64 = (encodedContent: string): string | null => {
532
+ try {
533
+ if (typeof globalThis.atob === "function") {
534
+ return new TextDecoder().decode(
535
+ Uint8Array.from(globalThis.atob(encodedContent), (character) => character.charCodeAt(0)),
536
+ );
537
+ }
538
+ } catch {
311
539
  return null;
312
540
  }
313
541
 
542
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
543
+ const normalizedContent = encodedContent.replace(/\s/g, "").replace(/=+$/, "");
544
+ const bytes: number[] = [];
545
+ let bitBuffer = 0;
546
+ let bitCount = 0;
547
+ for (const character of normalizedContent) {
548
+ const value = alphabet.indexOf(character);
549
+ if (value === -1) return null;
550
+ bitBuffer = bitBuffer * 64 + value;
551
+ bitCount += 6;
552
+ if (bitCount >= 8) {
553
+ bitCount -= 8;
554
+ bytes.push(Math.floor(bitBuffer / 2 ** bitCount) & 255);
555
+ bitBuffer %= 2 ** bitCount;
556
+ }
557
+ }
558
+ return new TextDecoder().decode(Uint8Array.from(bytes));
559
+ };
560
+
561
+ const readInlineSourceMap = (sourceMapUrl: string, maxSizeBytes: number): unknown => {
562
+ const commaIndex = sourceMapUrl.indexOf(",");
563
+ if (commaIndex === -1) return null;
564
+ const metadata = sourceMapUrl.slice(0, commaIndex);
565
+ const encodedContent = sourceMapUrl.slice(commaIndex + 1);
566
+ if (encodedContent.length > maxSizeBytes * 2) return null;
314
567
  try {
315
- const rawSourceMap = (await sourceMapResponse.json()) as RawSourceMap;
568
+ const content = metadata.toLowerCase().includes(";base64")
569
+ ? decodeBase64(encodedContent)
570
+ : decodeURIComponent(encodedContent);
571
+ if (content === null) return null;
572
+ if (new TextEncoder().encode(content).byteLength > maxSizeBytes) return null;
573
+ return JSON.parse(content);
574
+ } catch {
575
+ return null;
576
+ }
577
+ };
316
578
 
317
- return "sections" in rawSourceMap
318
- ? decodeIndexSourceMap(rawSourceMap)
319
- : decodeStandardSourceMap(rawSourceMap);
579
+ const readSourceMapDocument = async (
580
+ sourceMapUrl: string,
581
+ sourceFetch: SourceFetch,
582
+ signal: AbortSignal | undefined,
583
+ maxSizeBytes: number,
584
+ shouldRejectRedirects: boolean,
585
+ ): Promise<unknown> => {
586
+ if (INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
587
+ return readInlineSourceMap(sourceMapUrl, maxSizeBytes);
588
+ }
589
+ const sourceMapResponse = await sourceFetch(sourceMapUrl, {
590
+ redirect: shouldRejectRedirects ? "error" : "follow",
591
+ signal,
592
+ });
593
+ if (!assertResponseIsCacheable(sourceMapResponse)) return null;
594
+ const sourceMapContent = await readResponseText(sourceMapResponse, maxSizeBytes);
595
+ if (sourceMapContent === null) return null;
596
+ try {
597
+ return JSON.parse(sourceMapContent);
320
598
  } catch {
321
599
  return null;
322
600
  }
323
601
  };
324
602
 
603
+ const getSourceMapUncachedInternal = async (
604
+ bundleUrl: string,
605
+ fetchFn?: SourceFetch,
606
+ options: SourceMapRequestOptions = {},
607
+ ): Promise<null | SourceMap> => {
608
+ if (!isFetchableUrl(bundleUrl)) {
609
+ return null;
610
+ }
611
+
612
+ const shouldRejectRedirects = isServerRuntime();
613
+ if (shouldRejectRedirects) {
614
+ if ((!fetchFn && !options.allowUnsafeServerFetch) || !isAbsoluteHttpUrl(bundleUrl)) return null;
615
+ }
616
+
617
+ const sourceFetch =
618
+ fetchFn ??
619
+ (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
620
+ if (!sourceFetch) return null;
621
+ const maxBundleSizeBytes = options.maxBundleSizeBytes ?? defaultMaxBundleSizeBytes;
622
+ const maxSourceMapSizeBytes = options.maxSourceMapSizeBytes ?? defaultMaxSourceMapSizeBytes;
623
+ if (maxBundleSizeBytes < 0 || maxSourceMapSizeBytes < 0) return null;
624
+ const requestSignal = createRequestSignal(options);
625
+ try {
626
+ const bundleResponse = await sourceFetch(bundleUrl, {
627
+ redirect: shouldRejectRedirects ? "error" : "follow",
628
+ signal: requestSignal.signal,
629
+ });
630
+ if (!assertResponseIsCacheable(bundleResponse)) return null;
631
+ const bundleContent = await readResponseText(bundleResponse, maxBundleSizeBytes);
632
+ if (!bundleContent) return null;
633
+ const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
634
+ if (!sourceMapUrl) return null;
635
+ if (!isFetchableUrl(sourceMapUrl) && !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) return null;
636
+ if (
637
+ shouldRejectRedirects &&
638
+ !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl) &&
639
+ !isSameOrigin(bundleUrl, sourceMapUrl)
640
+ ) {
641
+ return null;
642
+ }
643
+
644
+ const rawSourceMap = await readSourceMapDocument(
645
+ sourceMapUrl,
646
+ sourceFetch,
647
+ requestSignal.signal,
648
+ maxSourceMapSizeBytes,
649
+ shouldRejectRedirects,
650
+ );
651
+ if (isStandardSourceMap(rawSourceMap)) {
652
+ return decodeStandardSourceMap(rawSourceMap, sourceMapUrl);
653
+ }
654
+ if (!isIndexSourceMap(rawSourceMap)) return null;
655
+ return decodeIndexSourceMap(rawSourceMap, sourceMapUrl, async (sectionUrl) => {
656
+ if (
657
+ shouldRejectRedirects &&
658
+ !INLINE_SOURCEMAP_REGEX.test(sectionUrl) &&
659
+ !isSameOrigin(bundleUrl, sectionUrl)
660
+ ) {
661
+ return null;
662
+ }
663
+ const sectionSourceMap = await readSourceMapDocument(
664
+ sectionUrl,
665
+ sourceFetch,
666
+ requestSignal.signal,
667
+ maxSourceMapSizeBytes,
668
+ shouldRejectRedirects,
669
+ );
670
+ return isStandardSourceMap(sectionSourceMap) ? sectionSourceMap : null;
671
+ });
672
+ } finally {
673
+ requestSignal.cleanup();
674
+ }
675
+ };
676
+
677
+ export const getSourceMapUncached = async (
678
+ bundleUrl: string,
679
+ fetchFn?: SourceFetch,
680
+ options: SourceMapRequestOptions = {},
681
+ ): Promise<null | SourceMap> => {
682
+ try {
683
+ return await getSourceMapUncachedInternal(bundleUrl, fetchFn, options);
684
+ } catch (error) {
685
+ if (error instanceof TransientSourceMapError) return null;
686
+ throw error;
687
+ }
688
+ };
689
+
690
+ const getPerFetchMap = <Value>(
691
+ mapsByFetch: WeakMap<SourceFetch, Map<string, Value>>,
692
+ globalMap: Map<string, Value>,
693
+ fetchFn: SourceFetch | undefined,
694
+ ): Map<string, Value> => {
695
+ if (!fetchFn) return globalMap;
696
+ let map = mapsByFetch.get(fetchFn);
697
+ if (!map) {
698
+ map = new Map();
699
+ mapsByFetch.set(fetchFn, map);
700
+ }
701
+ return map;
702
+ };
703
+
704
+ const getSourceMapCache = (fetchFn: SourceFetch | undefined): Map<string, null | SourceMap> =>
705
+ getPerFetchMap(sourceMapCachesByFetch, sourceMapCache, fetchFn);
706
+
707
+ const getPendingSourceMapRequests = (
708
+ fetchFn: SourceFetch | undefined,
709
+ ): Map<string, Promise<SourceMapResult>> =>
710
+ getPerFetchMap(pendingSourceMapRequestsByFetch, pendingSourceMapRequests, fetchFn);
711
+
712
+ const getSourceMapCacheKey = (file: string, options: SourceMapRequestOptions): string =>
713
+ options.allowUnsafeServerFetch === undefined &&
714
+ options.maxBundleSizeBytes === undefined &&
715
+ options.maxSourceMapSizeBytes === undefined &&
716
+ options.timeoutMs === undefined
717
+ ? file
718
+ : `${file}\0${options.allowUnsafeServerFetch ?? ""}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
719
+
325
720
  export const getSourceMap = async (
326
721
  file: string,
327
722
  useCache = true,
328
- fetchFn?: (url: string) => Promise<Response>,
723
+ fetchFn?: SourceFetch,
724
+ options: SourceMapRequestOptions = {},
329
725
  ): Promise<null | SourceMap> => {
330
- if (useCache && sourceMapCache.has(file)) {
331
- return sourceMapCache.get(file) ?? null;
726
+ const shouldUseCache = useCache && options.signal === undefined;
727
+ const cache = getSourceMapCache(fetchFn);
728
+ const pendingRequests = getPendingSourceMapRequests(fetchFn);
729
+ const cacheKey = getSourceMapCacheKey(file, options);
730
+ if (shouldUseCache && cache.has(cacheKey)) {
731
+ return cache.get(cacheKey) ?? null;
332
732
  }
333
733
 
334
- const pendingRequest = useCache ? _pendingSourceMapRequests.get(file) : undefined;
734
+ const pendingRequest = shouldUseCache ? pendingRequests.get(cacheKey) : undefined;
335
735
  if (pendingRequest) {
336
736
  return (await pendingRequest).sourceMap;
337
737
  }
338
738
 
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(
739
+ const fetchPromise: Promise<SourceMapResult> = getSourceMapUncachedInternal(
740
+ file,
741
+ fetchFn,
742
+ options,
743
+ ).then(
344
744
  (sourceMap) => ({ sourceMap, isTransientFailure: false }),
345
745
  () => ({ sourceMap: null, isTransientFailure: true }),
346
746
  );
347
- if (useCache) {
348
- _pendingSourceMapRequests.set(file, fetchPromise);
747
+ if (shouldUseCache) {
748
+ pendingRequests.set(cacheKey, fetchPromise);
349
749
  }
350
750
 
351
751
  const { sourceMap, isTransientFailure } = await fetchPromise;
352
- if (useCache) {
353
- _pendingSourceMapRequests.delete(file);
752
+ if (shouldUseCache) {
753
+ pendingRequests.delete(cacheKey);
354
754
  if (!isTransientFailure) {
355
- sourceMapCache.set(file, sourceMap);
755
+ cache.set(cacheKey, sourceMap);
356
756
  }
357
757
  }
358
758
 
@@ -364,7 +764,7 @@ export const symbolicateStack = async (
364
764
  cache = true,
365
765
  fetchFn?: (url: string) => Promise<Response>,
366
766
  ): Promise<StackFrame[]> => {
367
- return await Promise.all(
767
+ return Promise.all(
368
768
  stack.map(async (stackFrame) => {
369
769
  if (!stackFrame.fileName) return stackFrame;
370
770
  const sourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);