bippy 0.6.1-dev.fe75557 → 0.7.0-dev.07e6032

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 (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +223 -461
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/errors.d.cts +410 -0
  6. package/dist/errors.d.ts +410 -0
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +154 -3
  9. package/dist/index.d.ts +154 -3
  10. package/dist/index.js +1 -1
  11. package/dist/install-hook-only.cjs +1 -1
  12. package/dist/install-hook-only.d.cts +9 -1
  13. package/dist/install-hook-only.d.ts +9 -1
  14. package/dist/install-hook-only.js +1 -1
  15. package/dist/rdt-hook.cjs +1 -1
  16. package/dist/rdt-hook.js +1 -1
  17. package/dist/source.cjs +14 -5
  18. package/dist/source.d.cts +86 -69
  19. package/dist/source.d.ts +86 -69
  20. package/dist/source.js +14 -5
  21. package/package.json +26 -27
  22. package/src/core.ts +367 -685
  23. package/src/errors.ts +34 -0
  24. package/src/index.ts +1 -0
  25. package/src/install-hook-only.ts +6 -2
  26. package/src/rdt-hook.ts +177 -156
  27. package/src/react-internals/generated/react-work-tags.ts +318 -0
  28. package/src/react-internals/index.ts +73 -0
  29. package/src/react-internals/semver.ts +80 -0
  30. package/src/react-internals/types.ts +186 -0
  31. package/src/react.ts +76 -0
  32. package/src/source/constants.ts +1 -1
  33. package/src/source/error-stack.ts +11 -0
  34. package/src/source/get-display-name-from-source.ts +44 -40
  35. package/src/source/get-source.ts +43 -26
  36. package/src/source/index.ts +11 -1
  37. package/src/source/inspect-hooks.ts +220 -207
  38. package/src/source/owner-stack.ts +121 -174
  39. package/src/source/parse-debug-stack.ts +4 -4
  40. package/src/source/parse-hook-names.ts +14 -53
  41. package/src/source/parse-stack.ts +14 -31
  42. package/src/source/renderer-dispatchers.ts +30 -0
  43. package/src/source/symbolication.ts +709 -120
  44. package/dist/core.d.cts +0 -214
  45. package/dist/core.d.ts +0 -214
  46. package/dist/core2.d.cts +0 -3
  47. package/dist/core2.d.ts +0 -3
  48. package/dist/get-source.cjs +0 -19
  49. package/dist/get-source.js +0 -19
  50. package/dist/index.iife.js +0 -9
  51. package/dist/install-hook-only.iife.js +0 -9
  52. package/dist/react-refresh.cjs +0 -9
  53. package/dist/react-refresh.d.cts +0 -66
  54. package/dist/react-refresh.d.ts +0 -66
  55. package/dist/react-refresh.js +0 -9
  56. package/dist/unsubscribe.d.cts +0 -298
  57. package/dist/unsubscribe.d.ts +0 -298
  58. package/src/react-refresh/constants.ts +0 -9
  59. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  60. package/src/react-refresh/index.ts +0 -173
  61. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  62. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  63. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  64. package/src/react-refresh/types.ts +0 -7
  65. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  66. package/src/types.ts +0 -438
  67. package/src/unsubscribe.ts +0 -17
@@ -1,6 +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
- import { StackFrame } from "./parse-stack.js";
3
+ import { BippySourceMapError } from "../errors.js";
4
+ import { SCHEME_REGEX } from "./constants.js";
5
+ import type { StackFrame } from "./parse-stack.js";
4
6
 
5
7
  export interface DecodedSourceMapSection {
6
8
  map: {
@@ -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,60 @@ 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;
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
+ };
78
122
 
79
123
  const getSourceFromMappings = (
80
124
  mappings: SourceMapMappings,
@@ -82,6 +126,7 @@ const getSourceFromMappings = (
82
126
  lineIndexInMappings: number,
83
127
  column: number,
84
128
  ignoredSourceIndices?: Set<number>,
129
+ names?: string[],
85
130
  ): StackFrame | null => {
86
131
  if (lineIndexInMappings < 0 || lineIndexInMappings >= mappings.length) {
87
132
  return null;
@@ -92,8 +137,6 @@ const getSourceFromMappings = (
92
137
  return null;
93
138
  }
94
139
 
95
- // Segments within a line are sorted by generated column, so binary search for
96
- // the last segment at or before the column.
97
140
  let closestLineSegment: null | SourceMapSegment = null;
98
141
  let lowIndex = 0;
99
142
  let highIndex = lineMapping.length - 1;
@@ -111,24 +154,7 @@ const getSourceFromMappings = (
111
154
  return null;
112
155
  }
113
156
 
114
- const [, sourceIndex, sourceLine, sourceColumn] = closestLineSegment;
115
-
116
- if (sourceIndex === undefined || sourceLine === undefined || sourceColumn === undefined) {
117
- return null;
118
- }
119
-
120
- const fileName = sources[sourceIndex];
121
-
122
- if (!fileName) {
123
- return null;
124
- }
125
-
126
- return {
127
- columnNumber: sourceColumn,
128
- fileName,
129
- lineNumber: sourceLine + 1,
130
- isIgnoreListed: ignoredSourceIndices?.has(sourceIndex) ?? false,
131
- };
157
+ return getSourceFromSegment(closestLineSegment, sources, ignoredSourceIndices, names);
132
158
  };
133
159
 
134
160
  export const getSourceFromSourceMap = (
@@ -137,7 +163,6 @@ export const getSourceFromSourceMap = (
137
163
  column: number,
138
164
  ): StackFrame | null => {
139
165
  if (sourceMap.sections) {
140
- // Section offsets are 0-based while stack trace lines are 1-based.
141
166
  const lineIndex = line - 1;
142
167
  let targetSection: DecodedSourceMapSection | null = null;
143
168
 
@@ -166,6 +191,7 @@ export const getSourceFromSourceMap = (
166
191
  relativeLine,
167
192
  relativeColumn,
168
193
  targetSection.map.ignoredSourceIndices,
194
+ targetSection.map.names,
169
195
  );
170
196
  }
171
197
 
@@ -175,17 +201,148 @@ export const getSourceFromSourceMap = (
175
201
  line - 1,
176
202
  column,
177
203
  sourceMap.ignoredSourceIndices,
204
+ sourceMap.names,
178
205
  );
179
206
  };
180
207
 
181
- 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.
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
+
295
+ const resolveUrl = (reference: string, baseUrl: string): string | null => {
296
+ if (INLINE_SOURCEMAP_REGEX.test(reference)) return reference;
297
+ try {
298
+ return new URL(reference, baseUrl).toString();
299
+ } catch {
300
+ try {
301
+ const resolvedUrl = new URL(reference, new URL(baseUrl, "https://bippy.invalid/"));
302
+ return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+ };
308
+
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
+
184
341
  let sourceMapUrl: string | undefined;
185
- let searchEnd = content.length;
342
+ let searchEnd = bundleContent.length;
186
343
  while (searchEnd > 0 && !sourceMapUrl) {
187
- const lineStart = content.lastIndexOf("\n", searchEnd - 1) + 1;
188
- 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);
189
346
  if (regexMatch) {
190
347
  sourceMapUrl = regexMatch[1] || regexMatch[2];
191
348
  }
@@ -193,47 +350,176 @@ const getSourceMapUrl = (url: string, content: string): null | string => {
193
350
  }
194
351
 
195
352
  if (!sourceMapUrl) {
196
- return null;
353
+ return getMetroSourceMapUrl(bundleUrl);
197
354
  }
198
355
 
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("/");
356
+ return resolveUrl(sourceMapUrl, bundleUrl);
357
+ };
358
+
359
+ const isStandardSourceMap = (value: unknown): value is StandardSourceMap => {
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;
204
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,
394
+ );
395
+ };
205
396
 
206
- return sourceMapUrl;
397
+ const isIndexSourceMap = (value: unknown): value is IndexSourceMap => {
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)) {
402
+ return false;
403
+ }
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;
414
+ return (
415
+ hasMap !== hasUrl &&
416
+ typeof offsetColumn === "number" &&
417
+ Number.isInteger(offsetColumn) &&
418
+ offsetColumn >= 0 &&
419
+ typeof offsetLine === "number" &&
420
+ Number.isInteger(offsetLine) &&
421
+ offsetLine >= 0
422
+ );
423
+ });
207
424
  };
208
425
 
209
426
  const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> | undefined => {
210
427
  const ignoreList = rawSourceMap.ignoreList ?? rawSourceMap.x_google_ignoreList;
211
- return Array.isArray(ignoreList) && ignoreList.length > 0 ? new Set(ignoreList) : undefined;
212
- };
213
-
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
- }),
428
+ return ignoreList?.length ? new Set(ignoreList) : undefined;
429
+ };
430
+
431
+ const resolveSourceRoot = (
432
+ sourceRoot: string | undefined,
433
+ source: string,
434
+ sourceMapUrl: string,
435
+ ): string => {
436
+ if (!sourceRoot || SCHEME_REGEX.test(source) || source.startsWith("/")) return source;
437
+ const normalizedSourceRoot = sourceRoot.endsWith("/") ? sourceRoot : `${sourceRoot}/`;
438
+ const normalizedSource = source.replace(/^\.\//, "");
439
+ try {
440
+ const baseUrl = SCHEME_REGEX.test(normalizedSourceRoot)
441
+ ? normalizedSourceRoot
442
+ : new URL(normalizedSourceRoot, sourceMapUrl).toString();
443
+ return new URL(normalizedSource, baseUrl).toString();
444
+ } catch {
445
+ const pathSegments = `${normalizedSourceRoot}${normalizedSource}`.split("/");
446
+ const normalizedPathSegments: string[] = [];
447
+ for (const pathSegment of pathSegments) {
448
+ if (pathSegment === "..") {
449
+ normalizedPathSegments.pop();
450
+ } else if (pathSegment !== ".") {
451
+ normalizedPathSegments.push(pathSegment);
452
+ }
453
+ }
454
+ return normalizedPathSegments.join("/");
455
+ }
456
+ };
457
+
458
+ const resolveSourceMapSources = (rawSourceMap: StandardSourceMap, sourceMapUrl: string): string[] =>
459
+ rawSourceMap.sources.map((source) =>
460
+ resolveSourceRoot(rawSourceMap.sourceRoot, source, sourceMapUrl),
235
461
  );
236
462
 
463
+ const decodeStandardSourceMap = (
464
+ rawSourceMap: StandardSourceMap,
465
+ sourceMapUrl: string,
466
+ ): SourceMap | null => {
467
+ try {
468
+ return {
469
+ file: rawSourceMap.file,
470
+ ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
471
+ mappings: decode(rawSourceMap.mappings),
472
+ names: rawSourceMap.names,
473
+ sourceRoot: rawSourceMap.sourceRoot,
474
+ sources: resolveSourceMapSources(rawSourceMap, sourceMapUrl),
475
+ sourcesContent: rawSourceMap.sourcesContent,
476
+ version: 3,
477
+ };
478
+ } catch {
479
+ return null;
480
+ }
481
+ };
482
+
483
+ interface LoadStandardSourceMap {
484
+ (url: string): Promise<StandardSourceMap | null>;
485
+ }
486
+
487
+ const decodeIndexSourceMap = async (
488
+ rawSourceMap: IndexSourceMap,
489
+ sourceMapUrl: string,
490
+ loadStandardSourceMap: LoadStandardSourceMap,
491
+ ): Promise<SourceMap | null> => {
492
+ const decodedSections: DecodedSourceMapSection[] = [];
493
+ let previousOffsetLine = -1;
494
+ let previousOffsetColumn = -1;
495
+ for (const section of rawSourceMap.sections) {
496
+ if (
497
+ section.offset.line < previousOffsetLine ||
498
+ (section.offset.line === previousOffsetLine && section.offset.column <= previousOffsetColumn)
499
+ ) {
500
+ return null;
501
+ }
502
+ previousOffsetLine = section.offset.line;
503
+ previousOffsetColumn = section.offset.column;
504
+ const sectionUrl = section.url ? resolveUrl(section.url, sourceMapUrl) : null;
505
+ const map = section.map ?? (sectionUrl ? await loadStandardSourceMap(sectionUrl) : null);
506
+ if (!map) return null;
507
+ const sectionSourceMapUrl = sectionUrl ?? sourceMapUrl;
508
+ try {
509
+ decodedSections.push({
510
+ map: {
511
+ ...map,
512
+ ignoredSourceIndices: getIgnoredSourceIndices(map),
513
+ mappings: decode(map.mappings),
514
+ sources: resolveSourceMapSources(map, sectionSourceMapUrl),
515
+ },
516
+ offset: section.offset,
517
+ });
518
+ } catch {
519
+ return null;
520
+ }
521
+ }
522
+
237
523
  const allSources = new Set<string>();
238
524
  for (const section of decodedSections) {
239
525
  for (const source of section.map.sources) {
@@ -253,7 +539,7 @@ const decodeIndexSourceMap = (rawSourceMap: IndexSourceMap): SourceMap => {
253
539
  };
254
540
  };
255
541
 
256
- const isFetchableUrl = (url: string): boolean => {
542
+ const isFetchableUrl = (url: string, shouldAllowCustomProtocol = false): boolean => {
257
543
  if (!url) {
258
544
  return false;
259
545
  }
@@ -272,87 +558,389 @@ const isFetchableUrl = (url: string): boolean => {
272
558
 
273
559
  const scheme = schemeMatch[0].toLowerCase();
274
560
 
275
- return scheme === "http:" || scheme === "https:";
561
+ return shouldAllowCustomProtocol || defaultFetchableProtocols.has(scheme);
276
562
  };
277
563
 
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;
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;
289
579
  }
580
+ };
290
581
 
291
- const bundleResponse = await fetchFn(bundleUrl);
292
- if (!bundleResponse.ok) {
582
+ const getUrlProtocol = (url: string): string | null => {
583
+ try {
584
+ return new URL(url).protocol;
585
+ } catch {
293
586
  return null;
294
587
  }
295
- const bundleContent = await bundleResponse.text();
296
- if (!bundleContent) {
297
- return null;
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;
298
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
+ };
299
628
 
300
- const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
629
+ const isTransientHttpStatus = (status: number): boolean =>
630
+ status === 408 || status === 425 || status === 429 || status >= 500;
301
631
 
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;
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;
638
+ };
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
+
647
+ interface RequestSignal {
648
+ cleanup: () => void;
649
+ signal?: AbortSignal;
650
+ }
651
+
652
+ const createRequestSignal = (options: SourceMapRequestOptions): RequestSignal => {
653
+ if (options.timeoutMs === undefined || options.timeoutMs < 0) {
654
+ return { cleanup: () => {}, signal: options.signal };
655
+ }
656
+ const abortController = new AbortController();
657
+ const abortFromSource = (): void => abortController.abort(options.signal?.reason);
658
+ if (options.signal?.aborted) {
659
+ abortFromSource();
660
+ } else {
661
+ options.signal?.addEventListener("abort", abortFromSource, { once: true });
307
662
  }
663
+ const timeoutHandle = setTimeout(
664
+ () => abortController.abort(new BippySourceMapError("Source map request timed out")),
665
+ options.timeoutMs,
666
+ );
667
+ return {
668
+ cleanup: () => {
669
+ clearTimeout(timeoutHandle);
670
+ options.signal?.removeEventListener("abort", abortFromSource);
671
+ },
672
+ signal: abortController.signal,
673
+ };
674
+ };
675
+
676
+ const readResponseText = async (
677
+ response: Response,
678
+ maxSizeBytes: number,
679
+ ): Promise<string | null> => {
680
+ const contentLength = Number(response.headers.get("content-length"));
681
+ if (Number.isFinite(contentLength) && contentLength > maxSizeBytes) return 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;
308
691
 
309
- const sourceMapResponse = await fetchFn(sourceMapUrl);
310
- if (!sourceMapResponse.ok) {
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
+ }
708
+ };
709
+
710
+ const decodeBase64 = (encodedContent: string): string | null => {
711
+ try {
712
+ if (typeof globalThis.atob === "function") {
713
+ return new TextDecoder().decode(
714
+ Uint8Array.from(globalThis.atob(encodedContent), (character) => character.charCodeAt(0)),
715
+ );
716
+ }
717
+ } catch {
311
718
  return null;
312
719
  }
313
720
 
721
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
722
+ const normalizedContent = encodedContent.replace(/\s/g, "").replace(/=+$/, "");
723
+ const bytes: number[] = [];
724
+ let bitBuffer = 0;
725
+ let bitCount = 0;
726
+ for (const character of normalizedContent) {
727
+ const value = alphabet.indexOf(character);
728
+ if (value === -1) return null;
729
+ bitBuffer = bitBuffer * 64 + value;
730
+ bitCount += 6;
731
+ if (bitCount >= 8) {
732
+ bitCount -= 8;
733
+ bytes.push(Math.floor(bitBuffer / 2 ** bitCount) & 255);
734
+ bitBuffer %= 2 ** bitCount;
735
+ }
736
+ }
737
+ return new TextDecoder().decode(Uint8Array.from(bytes));
738
+ };
739
+
740
+ const readInlineSourceMap = (sourceMapUrl: string, maxSizeBytes: number): unknown => {
741
+ const commaIndex = sourceMapUrl.indexOf(",");
742
+ if (commaIndex === -1) return null;
743
+ const metadata = sourceMapUrl.slice(0, commaIndex);
744
+ const encodedContent = sourceMapUrl.slice(commaIndex + 1);
745
+ if (encodedContent.length > maxSizeBytes * 2) return null;
314
746
  try {
315
- const rawSourceMap = (await sourceMapResponse.json()) as RawSourceMap;
747
+ const content = metadata.toLowerCase().includes(";base64")
748
+ ? decodeBase64(encodedContent)
749
+ : decodeURIComponent(encodedContent);
750
+ if (content === null) return null;
751
+ if (new TextEncoder().encode(content).byteLength > maxSizeBytes) return null;
752
+ return JSON.parse(content);
753
+ } catch {
754
+ return null;
755
+ }
756
+ };
316
757
 
317
- return "sections" in rawSourceMap
318
- ? decodeIndexSourceMap(rawSourceMap)
319
- : decodeStandardSourceMap(rawSourceMap);
758
+ const readSourceMapDocument = async (
759
+ sourceMapUrl: string,
760
+ sourceFetch: SourceFetch,
761
+ signal: AbortSignal | undefined,
762
+ maxSizeBytes: number,
763
+ shouldRejectRedirects: boolean,
764
+ ): Promise<unknown> => {
765
+ if (INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
766
+ return readInlineSourceMap(sourceMapUrl, maxSizeBytes);
767
+ }
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;
774
+ const sourceMapContent = await readResponseText(sourceMapResponse, maxSizeBytes);
775
+ if (sourceMapContent === null) return null;
776
+ try {
777
+ return JSON.parse(sourceMapContent);
320
778
  } catch {
321
779
  return null;
322
780
  }
323
781
  };
324
782
 
783
+ const getSourceMapUncachedInternal = async (
784
+ bundleUrl: string,
785
+ fetchFn?: SourceFetch,
786
+ options: SourceMapRequestOptions = {},
787
+ ): Promise<null | SourceMap> => {
788
+ const shouldAllowCustomProtocol = fetchFn !== undefined;
789
+ if (!isFetchableUrl(bundleUrl, shouldAllowCustomProtocol)) {
790
+ return null;
791
+ }
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
+
799
+ const sourceFetch =
800
+ fetchFn ??
801
+ (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
802
+ if (!sourceFetch) return null;
803
+ const maxBundleSizeBytes = options.maxBundleSizeBytes ?? defaultMaxBundleSizeBytes;
804
+ const maxSourceMapSizeBytes = options.maxSourceMapSizeBytes ?? defaultMaxSourceMapSizeBytes;
805
+ if (maxBundleSizeBytes < 0 || maxSourceMapSizeBytes < 0) return null;
806
+ const requestSignal = createRequestSignal(options);
807
+ try {
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;
814
+ const bundleContent = await readResponseText(bundleResponse, maxBundleSizeBytes);
815
+ if (bundleContent === null) return null;
816
+ const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent, bundleResponse);
817
+ if (!sourceMapUrl) return null;
818
+ if (
819
+ !isAllowedSourceMapUrl(
820
+ bundleUrl,
821
+ sourceMapUrl,
822
+ shouldRejectRedirects,
823
+ shouldAllowCustomProtocol,
824
+ )
825
+ ) {
826
+ return null;
827
+ }
828
+
829
+ const rawSourceMap = await readSourceMapDocument(
830
+ sourceMapUrl,
831
+ sourceFetch,
832
+ requestSignal.signal,
833
+ maxSourceMapSizeBytes,
834
+ shouldRejectRedirects,
835
+ );
836
+ if (isStandardSourceMap(rawSourceMap)) {
837
+ return decodeStandardSourceMap(rawSourceMap, sourceMapUrl);
838
+ }
839
+ if (!isIndexSourceMap(rawSourceMap)) return null;
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
+ }
851
+ const sectionSourceMap = await readSourceMapDocument(
852
+ sectionUrl,
853
+ sourceFetch,
854
+ requestSignal.signal,
855
+ maxSourceMapSizeBytes,
856
+ shouldRejectRedirects,
857
+ );
858
+ return isStandardSourceMap(sectionSourceMap) ? sectionSourceMap : null;
859
+ });
860
+ } finally {
861
+ requestSignal.cleanup();
862
+ }
863
+ };
864
+
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;
875
+ }
876
+ };
877
+
878
+ const getPerFetchMap = <Value>(
879
+ mapsByFetch: WeakMap<SourceFetch, Map<string, Value>>,
880
+ globalMap: Map<string, Value>,
881
+ fetchFn: SourceFetch | undefined,
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;
890
+ };
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
+
900
+ const getSourceMapCacheKey = (file: string, options: SourceMapRequestOptions): string =>
901
+ options.allowUnsafeServerFetch === undefined &&
902
+ options.maxBundleSizeBytes === undefined &&
903
+ options.maxSourceMapSizeBytes === undefined &&
904
+ options.timeoutMs === undefined
905
+ ? file
906
+ : `${file}\0${options.allowUnsafeServerFetch ?? ""}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
907
+
325
908
  export const getSourceMap = async (
326
909
  file: string,
327
910
  useCache = true,
328
- fetchFn?: (url: string) => Promise<Response>,
911
+ fetchFn?: SourceFetch,
912
+ options: SourceMapRequestOptions = {},
329
913
  ): Promise<null | SourceMap> => {
330
- if (useCache && sourceMapCache.has(file)) {
331
- return sourceMapCache.get(file) ?? null;
914
+ const shouldUseCache = useCache && options.signal === undefined;
915
+ const cache = getSourceMapCache(fetchFn);
916
+ const pendingRequests = getPendingSourceMapRequests(fetchFn);
917
+ const cacheKey = getSourceMapCacheKey(file, options);
918
+ if (shouldUseCache && cache.has(cacheKey)) {
919
+ return cache.get(cacheKey) ?? null;
332
920
  }
333
921
 
334
- const pendingRequest = useCache ? _pendingSourceMapRequests.get(file) : undefined;
922
+ const pendingRequest = shouldUseCache ? pendingRequests.get(cacheKey) : undefined;
335
923
  if (pendingRequest) {
336
924
  return (await pendingRequest).sourceMap;
337
925
  }
338
926
 
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(
927
+ const fetchPromise: Promise<SourceMapResult> = getSourceMapUncachedInternal(
928
+ file,
929
+ fetchFn,
930
+ options,
931
+ ).then(
344
932
  (sourceMap) => ({ sourceMap, isTransientFailure: false }),
345
933
  () => ({ sourceMap: null, isTransientFailure: true }),
346
934
  );
347
- if (useCache) {
348
- _pendingSourceMapRequests.set(file, fetchPromise);
935
+ if (shouldUseCache) {
936
+ pendingRequests.set(cacheKey, fetchPromise);
349
937
  }
350
938
 
351
939
  const { sourceMap, isTransientFailure } = await fetchPromise;
352
- if (useCache) {
353
- _pendingSourceMapRequests.delete(file);
940
+ if (shouldUseCache) {
941
+ pendingRequests.delete(cacheKey);
354
942
  if (!isTransientFailure) {
355
- sourceMapCache.set(file, sourceMap);
943
+ cache.set(cacheKey, sourceMap);
356
944
  }
357
945
  }
358
946
 
@@ -362,9 +950,9 @@ export const getSourceMap = async (
362
950
  export const symbolicateStack = async (
363
951
  stack: StackFrame[],
364
952
  cache = true,
365
- fetchFn?: (url: string) => Promise<Response>,
953
+ fetchFn?: SourceFetch,
366
954
  ): Promise<StackFrame[]> => {
367
- return await Promise.all(
955
+ return Promise.all(
368
956
  stack.map(async (stackFrame) => {
369
957
  if (!stackFrame.fileName) return stackFrame;
370
958
  const sourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);
@@ -388,6 +976,7 @@ export const symbolicateStack = async (
388
976
  ? stackFrame.source.replace(stackFrame.fileName, symbolicatedSource.fileName)
389
977
  : stackFrame.source,
390
978
  fileName: symbolicatedSource.fileName,
979
+ functionName: symbolicatedSource.functionName ?? stackFrame.functionName,
391
980
  lineNumber: symbolicatedSource.lineNumber,
392
981
  columnNumber: symbolicatedSource.columnNumber,
393
982
  isIgnoreListed: symbolicatedSource.isIgnoreListed,