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