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