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