bippy 0.6.1-dev.d7876ea → 0.6.1-dev.f9e6c65

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 (50) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +159 -487
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +24 -70
  5. package/dist/core.d.ts +24 -70
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +1 -1
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +1 -1
  11. package/dist/errors.d.cts +33 -59
  12. package/dist/errors.d.ts +33 -59
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +14 -3
  15. package/dist/index.d.ts +14 -3
  16. package/dist/index.js +1 -1
  17. package/dist/install-hook-only.cjs +1 -1
  18. package/dist/install-hook-only.d.cts +8 -0
  19. package/dist/install-hook-only.d.ts +8 -0
  20. package/dist/install-hook-only.js +1 -1
  21. package/dist/rdt-hook.cjs +1 -1
  22. package/dist/rdt-hook.js +1 -1
  23. package/dist/source.cjs +12 -13
  24. package/dist/source.d.cts +85 -77
  25. package/dist/source.d.ts +85 -77
  26. package/dist/source.js +12 -13
  27. package/package.json +6 -3
  28. package/src/core.ts +141 -564
  29. package/src/errors.ts +0 -65
  30. package/src/index.ts +1 -0
  31. package/src/install-hook-only.ts +2 -2
  32. package/src/rdt-hook.ts +39 -45
  33. package/src/{generated/react-work-tags.js → react-internals/generated/react-work-tags.ts} +85 -7
  34. package/src/{react-internals.ts → react-internals/index.ts} +6 -4
  35. package/src/{semver.ts → react-internals/semver.ts} +2 -0
  36. package/src/{types.ts → react-internals/types.ts} +1 -84
  37. package/src/react.ts +76 -0
  38. package/src/source/get-display-name-from-source.ts +44 -40
  39. package/src/source/get-source.ts +42 -26
  40. package/src/source/index.ts +2 -0
  41. package/src/source/inspect-hooks.ts +74 -96
  42. package/src/source/owner-stack.ts +66 -91
  43. package/src/source/parse-hook-names.ts +14 -53
  44. package/src/source/parse-stack.ts +14 -31
  45. package/src/source/renderer-dispatchers.ts +30 -0
  46. package/src/source/symbolication.ts +421 -108
  47. package/dist/index.iife.js +0 -9
  48. package/dist/install-hook-only.iife.js +0 -9
  49. package/src/generated/react-work-tags.d.ts +0 -261
  50. package/src/unsubscribe.ts +0 -17
@@ -1,7 +1,8 @@
1
- import { decode, SourceMapMappings, type SourceMapSegment } from "@jridgewell/sourcemap-codec";
1
+ import { decode, type SourceMapMappings, type SourceMapSegment } from "@jridgewell/sourcemap-codec";
2
2
 
3
3
  import { BippySourceMapError } from "../errors.js";
4
- import { StackFrame } from "./parse-stack.js";
4
+ import { SCHEME_REGEX } from "./constants.js";
5
+ import type { StackFrame } from "./parse-stack.js";
5
6
 
6
7
  export interface DecodedSourceMapSection {
7
8
  map: {
@@ -38,6 +39,7 @@ export interface SourceFetch {
38
39
  }
39
40
 
40
41
  export interface SourceMapRequestOptions {
42
+ allowUnsafeServerFetch?: boolean;
41
43
  maxBundleSizeBytes?: number;
42
44
  maxSourceMapSizeBytes?: number;
43
45
  signal?: AbortSignal;
@@ -68,7 +70,6 @@ export interface StandardSourceMap {
68
70
  x_google_ignoreList?: number[];
69
71
  }
70
72
 
71
- const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
72
73
  const INLINE_SOURCEMAP_REGEX = /^data:application\/json(?:;[^,]*)?,/i;
73
74
  const SOURCEMAP_REGEX =
74
75
  /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/;
@@ -80,13 +81,44 @@ interface SourceMapResult {
80
81
  isTransientFailure: boolean;
81
82
  }
82
83
 
83
- const _pendingSourceMapRequests = new Map<string, Promise<SourceMapResult>>();
84
+ class TransientSourceMapError extends Error {}
85
+
86
+ const pendingSourceMapRequests = new Map<string, Promise<SourceMapResult>>();
84
87
  const pendingSourceMapRequestsByFetch = new WeakMap<
85
88
  SourceFetch,
86
89
  Map<string, Promise<SourceMapResult>>
87
90
  >();
88
91
  const defaultMaxBundleSizeBytes = 25 * 1024 * 1024;
89
92
  const defaultMaxSourceMapSizeBytes = 100 * 1024 * 1024;
93
+ const extensionProtocols = new Set([
94
+ "chrome-extension:",
95
+ "moz-extension:",
96
+ "safari-web-extension:",
97
+ ]);
98
+ const defaultFetchableProtocols = new Set(["http:", "https:", ...extensionProtocols]);
99
+
100
+ const getSourceFromSegment = (
101
+ segment: SourceMapSegment,
102
+ sources: string[],
103
+ ignoredSourceIndices?: Set<number>,
104
+ names?: string[],
105
+ ): StackFrame | null => {
106
+ const [, sourceIndex, sourceLine, sourceColumn, nameIndex] = segment;
107
+ if (sourceIndex === undefined || sourceLine === undefined || sourceColumn === undefined) {
108
+ return null;
109
+ }
110
+
111
+ const fileName = sources[sourceIndex];
112
+ if (!fileName) return null;
113
+
114
+ return {
115
+ columnNumber: sourceColumn,
116
+ fileName,
117
+ functionName: nameIndex === undefined ? undefined : names?.[nameIndex] || undefined,
118
+ isIgnoreListed: ignoredSourceIndices?.has(sourceIndex) ?? false,
119
+ lineNumber: sourceLine + 1,
120
+ };
121
+ };
90
122
 
91
123
  const getSourceFromMappings = (
92
124
  mappings: SourceMapMappings,
@@ -94,6 +126,7 @@ const getSourceFromMappings = (
94
126
  lineIndexInMappings: number,
95
127
  column: number,
96
128
  ignoredSourceIndices?: Set<number>,
129
+ names?: string[],
97
130
  ): StackFrame | null => {
98
131
  if (lineIndexInMappings < 0 || lineIndexInMappings >= mappings.length) {
99
132
  return null;
@@ -121,24 +154,7 @@ const getSourceFromMappings = (
121
154
  return null;
122
155
  }
123
156
 
124
- const [, sourceIndex, sourceLine, sourceColumn] = closestLineSegment;
125
-
126
- if (sourceIndex === undefined || sourceLine === undefined || sourceColumn === undefined) {
127
- return null;
128
- }
129
-
130
- const fileName = sources[sourceIndex];
131
-
132
- if (!fileName) {
133
- return null;
134
- }
135
-
136
- return {
137
- columnNumber: sourceColumn,
138
- fileName,
139
- lineNumber: sourceLine + 1,
140
- isIgnoreListed: ignoredSourceIndices?.has(sourceIndex) ?? false,
141
- };
157
+ return getSourceFromSegment(closestLineSegment, sources, ignoredSourceIndices, names);
142
158
  };
143
159
 
144
160
  export const getSourceFromSourceMap = (
@@ -175,6 +191,7 @@ export const getSourceFromSourceMap = (
175
191
  relativeLine,
176
192
  relativeColumn,
177
193
  targetSection.map.ignoredSourceIndices,
194
+ targetSection.map.names,
178
195
  );
179
196
  }
180
197
 
@@ -184,9 +201,97 @@ export const getSourceFromSourceMap = (
184
201
  line - 1,
185
202
  column,
186
203
  sourceMap.ignoredSourceIndices,
204
+ sourceMap.names,
187
205
  );
188
206
  };
189
207
 
208
+ const getSourceFromMappingsByFunctionName = (
209
+ mappings: SourceMapMappings,
210
+ sources: string[],
211
+ names: string[] | undefined,
212
+ functionName: string,
213
+ ignoredSourceIndices?: Set<number>,
214
+ ): StackFrame | null => {
215
+ if (!names) return null;
216
+ const functionNameIndex = names.indexOf(functionName);
217
+ if (functionNameIndex === -1) return null;
218
+
219
+ let ignoredSource: StackFrame | null = null;
220
+ for (const lineMapping of mappings) {
221
+ for (const segment of lineMapping) {
222
+ if (segment[4] !== functionNameIndex) continue;
223
+ const source = getSourceFromSegment(segment, sources, ignoredSourceIndices, names);
224
+ if (!source) continue;
225
+ if (!source.isIgnoreListed) return source;
226
+ ignoredSource ??= source;
227
+ }
228
+ }
229
+ return ignoredSource;
230
+ };
231
+
232
+ export const getSourceFromSourceMapByFunctionName = (
233
+ sourceMap: SourceMap,
234
+ functionName: string,
235
+ ): StackFrame | null => {
236
+ if (sourceMap.sections) {
237
+ let ignoredSource: StackFrame | null = null;
238
+ for (const section of sourceMap.sections) {
239
+ const source = getSourceFromMappingsByFunctionName(
240
+ section.map.mappings,
241
+ section.map.sources,
242
+ section.map.names,
243
+ functionName,
244
+ section.map.ignoredSourceIndices,
245
+ );
246
+ if (!source) continue;
247
+ if (!source.isIgnoreListed) return source;
248
+ ignoredSource ??= source;
249
+ }
250
+ return ignoredSource;
251
+ }
252
+
253
+ return getSourceFromMappingsByFunctionName(
254
+ sourceMap.mappings,
255
+ sourceMap.sources,
256
+ sourceMap.names,
257
+ functionName,
258
+ sourceMap.ignoredSourceIndices,
259
+ );
260
+ };
261
+
262
+ const findSourceContentByFileName = (
263
+ sources: string[],
264
+ sourcesContent: Array<string | null> | undefined,
265
+ fileName: string,
266
+ ): string | null => {
267
+ if (!sourcesContent) return null;
268
+ const sourceIndex = sources.indexOf(fileName);
269
+ return sourceIndex === -1 ? null : (sourcesContent[sourceIndex] ?? null);
270
+ };
271
+
272
+ export const getSourceContentFromSourceMap = (
273
+ sourceMap: SourceMap,
274
+ originalFileName: string,
275
+ ): string | null => {
276
+ const sourceContent = findSourceContentByFileName(
277
+ sourceMap.sources,
278
+ sourceMap.sourcesContent,
279
+ originalFileName,
280
+ );
281
+ if (sourceContent !== null) return sourceContent;
282
+
283
+ if (!sourceMap.sections) return null;
284
+ for (const section of sourceMap.sections) {
285
+ const sectionSourceContent = findSourceContentByFileName(
286
+ section.map.sources,
287
+ section.map.sourcesContent,
288
+ originalFileName,
289
+ );
290
+ if (sectionSourceContent !== null) return sectionSourceContent;
291
+ }
292
+ return null;
293
+ };
294
+
190
295
  const resolveUrl = (reference: string, baseUrl: string): string | null => {
191
296
  if (INLINE_SOURCEMAP_REGEX.test(reference)) return reference;
192
297
  try {
@@ -201,12 +306,43 @@ const resolveUrl = (reference: string, baseUrl: string): string | null => {
201
306
  }
202
307
  };
203
308
 
204
- const getSourceMapUrl = (url: string, content: string): null | string => {
309
+ const getMetroSourceMapUrl = (bundleUrl: string): string | null => {
310
+ try {
311
+ const parsedBundleUrl = new URL(bundleUrl);
312
+ const bundleExtensionIndex = parsedBundleUrl.pathname.lastIndexOf(".bundle");
313
+ if (bundleExtensionIndex === -1) return null;
314
+ const embeddedSearch = parsedBundleUrl.pathname.slice(bundleExtensionIndex + ".bundle".length);
315
+ if (embeddedSearch && !embeddedSearch.startsWith("//&")) return null;
316
+ parsedBundleUrl.pathname = `${parsedBundleUrl.pathname.slice(0, bundleExtensionIndex)}.map`;
317
+ if (embeddedSearch) {
318
+ const embeddedSearchParameters = new URLSearchParams(embeddedSearch.slice("//&".length));
319
+ for (const [parameterName, parameterValue] of embeddedSearchParameters) {
320
+ parsedBundleUrl.searchParams.set(parameterName, parameterValue);
321
+ }
322
+ }
323
+ parsedBundleUrl.hash = "";
324
+ return parsedBundleUrl.toString();
325
+ } catch {
326
+ return null;
327
+ }
328
+ };
329
+
330
+ const getSourceMapUrl = (
331
+ bundleUrl: string,
332
+ bundleContent: string,
333
+ bundleResponse: Response,
334
+ ): null | string => {
335
+ const sourceMapHeader =
336
+ bundleResponse.headers.get("sourcemap") ?? bundleResponse.headers.get("x-sourcemap");
337
+ if (sourceMapHeader) {
338
+ return resolveUrl(sourceMapHeader.trim(), bundleUrl);
339
+ }
340
+
205
341
  let sourceMapUrl: string | undefined;
206
- let searchEnd = content.length;
342
+ let searchEnd = bundleContent.length;
207
343
  while (searchEnd > 0 && !sourceMapUrl) {
208
- const lineStart = content.lastIndexOf("\n", searchEnd - 1) + 1;
209
- const regexMatch = content.slice(lineStart, searchEnd).match(SOURCEMAP_REGEX);
344
+ const lineStart = bundleContent.lastIndexOf("\n", searchEnd - 1) + 1;
345
+ const regexMatch = bundleContent.slice(lineStart, searchEnd).match(SOURCEMAP_REGEX);
210
346
  if (regexMatch) {
211
347
  sourceMapUrl = regexMatch[1] || regexMatch[2];
212
348
  }
@@ -214,65 +350,82 @@ const getSourceMapUrl = (url: string, content: string): null | string => {
214
350
  }
215
351
 
216
352
  if (!sourceMapUrl) {
217
- return null;
353
+ return getMetroSourceMapUrl(bundleUrl);
218
354
  }
219
355
 
220
- return resolveUrl(sourceMapUrl, url);
356
+ return resolveUrl(sourceMapUrl, bundleUrl);
221
357
  };
222
358
 
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
359
  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,
360
+ if (typeof value !== "object" || value === null) return false;
361
+ const version = Reflect.get(value, "version");
362
+ const mappings = Reflect.get(value, "mappings");
363
+ const file = Reflect.get(value, "file");
364
+ const names = Reflect.get(value, "names");
365
+ const sourceRoot = Reflect.get(value, "sourceRoot");
366
+ const sources = Reflect.get(value, "sources");
367
+ const sourcesContent = Reflect.get(value, "sourcesContent");
368
+ const ignoreList = Reflect.get(value, "ignoreList");
369
+ const googleIgnoreList = Reflect.get(value, "x_google_ignoreList");
370
+ if (
371
+ version !== 3 ||
372
+ typeof mappings !== "string" ||
373
+ (file !== undefined && typeof file !== "string") ||
374
+ (names !== undefined &&
375
+ (!Array.isArray(names) || names.some((entry) => typeof entry !== "string"))) ||
376
+ (sourceRoot !== undefined && typeof sourceRoot !== "string") ||
377
+ (sourcesContent !== undefined &&
378
+ (!Array.isArray(sourcesContent) ||
379
+ sourcesContent.some((entry) => typeof entry !== "string" && entry !== null))) ||
380
+ (ignoreList !== undefined &&
381
+ (!Array.isArray(ignoreList) ||
382
+ ignoreList.some((entry) => !Number.isInteger(entry) || entry < 0))) ||
383
+ (googleIgnoreList !== undefined &&
384
+ (!Array.isArray(googleIgnoreList) ||
385
+ googleIgnoreList.some((entry) => !Number.isInteger(entry) || entry < 0)))
386
+ ) {
387
+ return false;
388
+ }
389
+ if (!Array.isArray(sources) || sources.some((entry) => typeof entry !== "string")) return false;
390
+ if (sourcesContent && sourcesContent.length !== sources.length) return false;
391
+ const sourceCount = sources.length;
392
+ return [...(ignoreList ?? []), ...(googleIgnoreList ?? [])].every(
393
+ (sourceIndex) => sourceIndex < sourceCount,
252
394
  );
253
395
  };
254
396
 
255
397
  const isIndexSourceMap = (value: unknown): value is IndexSourceMap => {
256
- if (!isObjectRecord(value) || value.version !== 3 || !Array.isArray(value.sections)) {
398
+ if (typeof value !== "object" || value === null) return false;
399
+ const version = Reflect.get(value, "version");
400
+ const sections = Reflect.get(value, "sections");
401
+ if (version !== 3 || !Array.isArray(sections)) {
257
402
  return false;
258
403
  }
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;
404
+ return sections.every((section: unknown) => {
405
+ if (typeof section !== "object" || section === null) return false;
406
+ const map = Reflect.get(section, "map");
407
+ const url = Reflect.get(section, "url");
408
+ const offset = Reflect.get(section, "offset");
409
+ if (typeof offset !== "object" || offset === null) return false;
410
+ const offsetColumn = Reflect.get(offset, "column");
411
+ const offsetLine = Reflect.get(offset, "line");
412
+ const hasMap = isStandardSourceMap(map);
413
+ const hasUrl = typeof url === "string" && url.length > 0;
263
414
  return (
264
415
  hasMap !== hasUrl &&
265
- Number.isInteger(section.offset.column) &&
266
- section.offset.column >= 0 &&
267
- Number.isInteger(section.offset.line) &&
268
- section.offset.line >= 0
416
+ typeof offsetColumn === "number" &&
417
+ Number.isInteger(offsetColumn) &&
418
+ offsetColumn >= 0 &&
419
+ typeof offsetLine === "number" &&
420
+ Number.isInteger(offsetLine) &&
421
+ offsetLine >= 0
269
422
  );
270
423
  });
271
424
  };
272
425
 
273
426
  const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> | undefined => {
274
427
  const ignoreList = rawSourceMap.ignoreList ?? rawSourceMap.x_google_ignoreList;
275
- return Array.isArray(ignoreList) && ignoreList.length > 0 ? new Set(ignoreList) : undefined;
428
+ return ignoreList?.length ? new Set(ignoreList) : undefined;
276
429
  };
277
430
 
278
431
  const resolveSourceRoot = (
@@ -386,7 +539,7 @@ const decodeIndexSourceMap = async (
386
539
  };
387
540
  };
388
541
 
389
- const isFetchableUrl = (url: string): boolean => {
542
+ const isFetchableUrl = (url: string, shouldAllowCustomProtocol = false): boolean => {
390
543
  if (!url) {
391
544
  return false;
392
545
  }
@@ -405,17 +558,100 @@ const isFetchableUrl = (url: string): boolean => {
405
558
 
406
559
  const scheme = schemeMatch[0].toLowerCase();
407
560
 
408
- return scheme === "http:" || scheme === "https:";
561
+ return shouldAllowCustomProtocol || defaultFetchableProtocols.has(scheme);
562
+ };
563
+
564
+ const isServerRuntime = (): boolean =>
565
+ typeof window === "undefined" &&
566
+ typeof process !== "undefined" &&
567
+ typeof process.versions?.node === "string";
568
+
569
+ const isAbsoluteHttpUrl = (url: string): boolean => {
570
+ try {
571
+ const parsedUrl = new URL(url);
572
+ return (
573
+ (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") &&
574
+ !parsedUrl.username &&
575
+ !parsedUrl.password
576
+ );
577
+ } catch {
578
+ return false;
579
+ }
580
+ };
581
+
582
+ const getUrlProtocol = (url: string): string | null => {
583
+ try {
584
+ return new URL(url).protocol;
585
+ } catch {
586
+ return null;
587
+ }
588
+ };
589
+
590
+ const isExtensionUrl = (url: string): boolean => {
591
+ const protocol = getUrlProtocol(url);
592
+ return protocol !== null && extensionProtocols.has(protocol);
593
+ };
594
+
595
+ const isSameOrigin = (firstUrl: string, secondUrl: string): boolean => {
596
+ try {
597
+ const firstParsedUrl = new URL(firstUrl);
598
+ const secondParsedUrl = new URL(secondUrl);
599
+ return (
600
+ firstParsedUrl.protocol === secondParsedUrl.protocol &&
601
+ firstParsedUrl.host === secondParsedUrl.host
602
+ );
603
+ } catch {
604
+ return false;
605
+ }
606
+ };
607
+
608
+ const isAllowedSourceMapUrl = (
609
+ bundleUrl: string,
610
+ sourceMapUrl: string,
611
+ shouldRejectCrossOrigin: boolean,
612
+ shouldAllowCustomProtocol: boolean,
613
+ ): boolean => {
614
+ if (INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) return true;
615
+ if (!isFetchableUrl(sourceMapUrl, shouldAllowCustomProtocol)) return false;
616
+ const bundleProtocol = getUrlProtocol(bundleUrl);
617
+ const sourceMapProtocol = getUrlProtocol(sourceMapUrl);
618
+ const hasCustomProtocol =
619
+ (bundleProtocol !== null && !defaultFetchableProtocols.has(bundleProtocol)) ||
620
+ (sourceMapProtocol !== null && !defaultFetchableProtocols.has(sourceMapProtocol));
621
+ const shouldRequireSameOrigin =
622
+ shouldRejectCrossOrigin ||
623
+ hasCustomProtocol ||
624
+ isExtensionUrl(bundleUrl) ||
625
+ isExtensionUrl(sourceMapUrl);
626
+ return !shouldRequireSameOrigin || isSameOrigin(bundleUrl, sourceMapUrl);
627
+ };
628
+
629
+ const isTransientHttpStatus = (status: number): boolean =>
630
+ status === 408 || status === 425 || status === 429 || status >= 500;
631
+
632
+ const assertResponseIsCacheable = (response: Response): boolean => {
633
+ if (response.ok) return true;
634
+ if (isTransientHttpStatus(response.status)) {
635
+ throw new TransientSourceMapError();
636
+ }
637
+ return false;
409
638
  };
410
639
 
640
+ const isResponseUrlAllowed = (
641
+ requestedUrl: string,
642
+ response: Response,
643
+ shouldRejectCrossOrigin: boolean,
644
+ ): boolean =>
645
+ !shouldRejectCrossOrigin || response.url.length === 0 || isSameOrigin(requestedUrl, response.url);
646
+
411
647
  interface RequestSignal {
412
648
  cleanup: () => void;
413
649
  signal?: AbortSignal;
414
650
  }
415
651
 
416
652
  const createRequestSignal = (options: SourceMapRequestOptions): RequestSignal => {
417
- if (!options.signal && options.timeoutMs === undefined) {
418
- return { cleanup: () => {} };
653
+ if (options.timeoutMs === undefined || options.timeoutMs < 0) {
654
+ return { cleanup: () => {}, signal: options.signal };
419
655
  }
420
656
  const abortController = new AbortController();
421
657
  const abortFromSource = (): void => abortController.abort(options.signal?.reason);
@@ -424,16 +660,13 @@ const createRequestSignal = (options: SourceMapRequestOptions): RequestSignal =>
424
660
  } else {
425
661
  options.signal?.addEventListener("abort", abortFromSource, { once: true });
426
662
  }
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;
663
+ const timeoutHandle = setTimeout(
664
+ () => abortController.abort(new BippySourceMapError("Source map request timed out")),
665
+ options.timeoutMs,
666
+ );
434
667
  return {
435
668
  cleanup: () => {
436
- if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
669
+ clearTimeout(timeoutHandle);
437
670
  options.signal?.removeEventListener("abort", abortFromSource);
438
671
  },
439
672
  signal: abortController.signal,
@@ -446,8 +679,32 @@ const readResponseText = async (
446
679
  ): Promise<string | null> => {
447
680
  const contentLength = Number(response.headers.get("content-length"));
448
681
  if (Number.isFinite(contentLength) && contentLength > maxSizeBytes) return null;
449
- const content = await response.text();
450
- return new TextEncoder().encode(content).byteLength <= maxSizeBytes ? content : null;
682
+ if (!response.body) {
683
+ const responseText = await response.text();
684
+ return new TextEncoder().encode(responseText).byteLength > maxSizeBytes ? null : responseText;
685
+ }
686
+
687
+ const reader = response.body.getReader();
688
+ const decoder = new TextDecoder();
689
+ const decodedChunks: string[] = [];
690
+ let totalSizeBytes = 0;
691
+
692
+ try {
693
+ while (true) {
694
+ const { done, value } = await reader.read();
695
+ if (done) break;
696
+ totalSizeBytes += value.byteLength;
697
+ if (totalSizeBytes > maxSizeBytes) {
698
+ await reader.cancel();
699
+ return null;
700
+ }
701
+ decodedChunks.push(decoder.decode(value, { stream: true }));
702
+ }
703
+ decodedChunks.push(decoder.decode());
704
+ return decodedChunks.join("");
705
+ } finally {
706
+ reader.releaseLock();
707
+ }
451
708
  };
452
709
 
453
710
  const decodeBase64 = (encodedContent: string): string | null => {
@@ -503,12 +760,17 @@ const readSourceMapDocument = async (
503
760
  sourceFetch: SourceFetch,
504
761
  signal: AbortSignal | undefined,
505
762
  maxSizeBytes: number,
763
+ shouldRejectRedirects: boolean,
506
764
  ): Promise<unknown> => {
507
765
  if (INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
508
766
  return readInlineSourceMap(sourceMapUrl, maxSizeBytes);
509
767
  }
510
- const sourceMapResponse = await sourceFetch(sourceMapUrl, { signal });
511
- if (!sourceMapResponse.ok) return null;
768
+ const sourceMapResponse = await sourceFetch(sourceMapUrl, {
769
+ redirect: shouldRejectRedirects ? "error" : "follow",
770
+ signal,
771
+ });
772
+ if (!assertResponseIsCacheable(sourceMapResponse)) return null;
773
+ if (!isResponseUrlAllowed(sourceMapUrl, sourceMapResponse, shouldRejectRedirects)) return null;
512
774
  const sourceMapContent = await readResponseText(sourceMapResponse, maxSizeBytes);
513
775
  if (sourceMapContent === null) return null;
514
776
  try {
@@ -518,15 +780,22 @@ const readSourceMapDocument = async (
518
780
  }
519
781
  };
520
782
 
521
- export const getSourceMapUncached = async (
783
+ const getSourceMapUncachedInternal = async (
522
784
  bundleUrl: string,
523
785
  fetchFn?: SourceFetch,
524
786
  options: SourceMapRequestOptions = {},
525
787
  ): Promise<null | SourceMap> => {
526
- if (!isFetchableUrl(bundleUrl)) {
788
+ const shouldAllowCustomProtocol = fetchFn !== undefined;
789
+ if (!isFetchableUrl(bundleUrl, shouldAllowCustomProtocol)) {
527
790
  return null;
528
791
  }
529
792
 
793
+ const isServer = isServerRuntime();
794
+ const shouldRejectRedirects = isServer || isExtensionUrl(bundleUrl);
795
+ if (isServer && !fetchFn) {
796
+ if (!options.allowUnsafeServerFetch || !isAbsoluteHttpUrl(bundleUrl)) return null;
797
+ }
798
+
530
799
  const sourceFetch =
531
800
  fetchFn ??
532
801
  (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
@@ -536,30 +805,55 @@ export const getSourceMapUncached = async (
536
805
  if (maxBundleSizeBytes < 0 || maxSourceMapSizeBytes < 0) return null;
537
806
  const requestSignal = createRequestSignal(options);
538
807
  try {
539
- const bundleResponse = await sourceFetch(bundleUrl, { signal: requestSignal.signal });
540
- if (!bundleResponse.ok) return null;
808
+ const bundleResponse = await sourceFetch(bundleUrl, {
809
+ redirect: shouldRejectRedirects ? "error" : "follow",
810
+ signal: requestSignal.signal,
811
+ });
812
+ if (!assertResponseIsCacheable(bundleResponse)) return null;
813
+ if (!isResponseUrlAllowed(bundleUrl, bundleResponse, shouldRejectRedirects)) return null;
541
814
  const bundleContent = await readResponseText(bundleResponse, maxBundleSizeBytes);
542
- if (!bundleContent) return null;
543
- const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
815
+ if (bundleContent === null) return null;
816
+ const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent, bundleResponse);
544
817
  if (!sourceMapUrl) return null;
545
- if (!isFetchableUrl(sourceMapUrl) && !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) return null;
818
+ if (
819
+ !isAllowedSourceMapUrl(
820
+ bundleUrl,
821
+ sourceMapUrl,
822
+ shouldRejectRedirects,
823
+ shouldAllowCustomProtocol,
824
+ )
825
+ ) {
826
+ return null;
827
+ }
546
828
 
547
829
  const rawSourceMap = await readSourceMapDocument(
548
830
  sourceMapUrl,
549
831
  sourceFetch,
550
832
  requestSignal.signal,
551
833
  maxSourceMapSizeBytes,
834
+ shouldRejectRedirects,
552
835
  );
553
836
  if (isStandardSourceMap(rawSourceMap)) {
554
837
  return decodeStandardSourceMap(rawSourceMap, sourceMapUrl);
555
838
  }
556
839
  if (!isIndexSourceMap(rawSourceMap)) return null;
557
840
  return decodeIndexSourceMap(rawSourceMap, sourceMapUrl, async (sectionUrl) => {
841
+ if (
842
+ !isAllowedSourceMapUrl(
843
+ bundleUrl,
844
+ sectionUrl,
845
+ shouldRejectRedirects,
846
+ shouldAllowCustomProtocol,
847
+ )
848
+ ) {
849
+ return null;
850
+ }
558
851
  const sectionSourceMap = await readSourceMapDocument(
559
852
  sectionUrl,
560
853
  sourceFetch,
561
854
  requestSignal.signal,
562
855
  maxSourceMapSizeBytes,
856
+ shouldRejectRedirects,
563
857
  );
564
858
  return isStandardSourceMap(sectionSourceMap) ? sectionSourceMap : null;
565
859
  });
@@ -568,34 +862,48 @@ export const getSourceMapUncached = async (
568
862
  }
569
863
  };
570
864
 
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);
865
+ export const getSourceMapUncached = async (
866
+ bundleUrl: string,
867
+ fetchFn?: SourceFetch,
868
+ options: SourceMapRequestOptions = {},
869
+ ): Promise<null | SourceMap> => {
870
+ try {
871
+ return await getSourceMapUncachedInternal(bundleUrl, fetchFn, options);
872
+ } catch (error) {
873
+ if (error instanceof TransientSourceMapError) return null;
874
+ throw error;
577
875
  }
578
- return cache;
579
876
  };
580
877
 
581
- const getPendingSourceMapRequests = (
878
+ const getPerFetchMap = <Value>(
879
+ mapsByFetch: WeakMap<SourceFetch, Map<string, Value>>,
880
+ globalMap: Map<string, Value>,
582
881
  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;
882
+ ): Map<string, Value> => {
883
+ if (!fetchFn) return globalMap;
884
+ let map = mapsByFetch.get(fetchFn);
885
+ if (!map) {
886
+ map = new Map();
887
+ mapsByFetch.set(fetchFn, map);
888
+ }
889
+ return map;
591
890
  };
592
891
 
892
+ const getSourceMapCache = (fetchFn: SourceFetch | undefined): Map<string, null | SourceMap> =>
893
+ getPerFetchMap(sourceMapCachesByFetch, sourceMapCache, fetchFn);
894
+
895
+ const getPendingSourceMapRequests = (
896
+ fetchFn: SourceFetch | undefined,
897
+ ): Map<string, Promise<SourceMapResult>> =>
898
+ getPerFetchMap(pendingSourceMapRequestsByFetch, pendingSourceMapRequests, fetchFn);
899
+
593
900
  const getSourceMapCacheKey = (file: string, options: SourceMapRequestOptions): string =>
901
+ options.allowUnsafeServerFetch === undefined &&
594
902
  options.maxBundleSizeBytes === undefined &&
595
903
  options.maxSourceMapSizeBytes === undefined &&
596
904
  options.timeoutMs === undefined
597
905
  ? file
598
- : `${file}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
906
+ : `${file}\0${options.allowUnsafeServerFetch ?? ""}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
599
907
 
600
908
  export const getSourceMap = async (
601
909
  file: string,
@@ -616,7 +924,11 @@ export const getSourceMap = async (
616
924
  return (await pendingRequest).sourceMap;
617
925
  }
618
926
 
619
- const fetchPromise: Promise<SourceMapResult> = getSourceMapUncached(file, fetchFn, options).then(
927
+ const fetchPromise: Promise<SourceMapResult> = getSourceMapUncachedInternal(
928
+ file,
929
+ fetchFn,
930
+ options,
931
+ ).then(
620
932
  (sourceMap) => ({ sourceMap, isTransientFailure: false }),
621
933
  () => ({ sourceMap: null, isTransientFailure: true }),
622
934
  );
@@ -638,7 +950,7 @@ export const getSourceMap = async (
638
950
  export const symbolicateStack = async (
639
951
  stack: StackFrame[],
640
952
  cache = true,
641
- fetchFn?: (url: string) => Promise<Response>,
953
+ fetchFn?: SourceFetch,
642
954
  ): Promise<StackFrame[]> => {
643
955
  return Promise.all(
644
956
  stack.map(async (stackFrame) => {
@@ -664,6 +976,7 @@ export const symbolicateStack = async (
664
976
  ? stackFrame.source.replace(stackFrame.fileName, symbolicatedSource.fileName)
665
977
  : stackFrame.source,
666
978
  fileName: symbolicatedSource.fileName,
979
+ functionName: symbolicatedSource.functionName ?? stackFrame.functionName,
667
980
  lineNumber: symbolicatedSource.lineNumber,
668
981
  columnNumber: symbolicatedSource.columnNumber,
669
982
  isIgnoreListed: symbolicatedSource.isIgnoreListed,