bippy 0.6.1-dev.3a24abf → 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 +7 -1
- package/dist/core.cjs +1 -1
- package/dist/core.d.cts +22 -24
- package/dist/core.d.ts +22 -24
- package/dist/core.js +1 -1
- package/dist/core2.cjs +1 -1
- package/dist/core2.d.cts +3 -3
- package/dist/core2.d.ts +3 -3
- package/dist/core2.js +1 -1
- package/dist/{types.d.cts → errors.d.cts} +174 -29
- package/dist/{types.d.ts → errors.d.ts} +174 -29
- 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.iife.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 +18 -9
- package/dist/source.d.ts +18 -9
- package/dist/source.js +13 -13
- package/package.json +9 -5
- package/src/core.ts +195 -156
- 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 +144 -96
- package/src/react-internals.ts +43 -45
- package/src/semver.ts +78 -0
- package/src/source/error-stack.ts +11 -0
- package/src/source/index.ts +9 -1
- package/src/source/inspect-hooks.ts +199 -164
- package/src/source/owner-stack.ts +21 -38
- package/src/source/parse-debug-stack.ts +4 -4
- package/src/source/symbolication.ts +358 -103
- package/src/types.ts +12 -2
- package/src/generated/react-work-tags.ts +0 -169
|
@@ -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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
return resolveUrl(sourceMapUrl, url);
|
|
221
|
+
};
|
|
222
|
+
|
|
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
|
+
};
|
|
205
254
|
|
|
206
|
-
|
|
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,45 +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 resolveSourceRoot = (
|
|
278
|
+
const resolveSourceRoot = (
|
|
279
|
+
sourceRoot: string | undefined,
|
|
280
|
+
source: string,
|
|
281
|
+
sourceMapUrl: string,
|
|
282
|
+
): string => {
|
|
215
283
|
if (!sourceRoot || SCHEME_REGEX.test(source) || source.startsWith("/")) return source;
|
|
216
284
|
const normalizedSourceRoot = sourceRoot.endsWith("/") ? sourceRoot : `${sourceRoot}/`;
|
|
217
285
|
const normalizedSource = source.replace(/^\.\//, "");
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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("/");
|
|
222
302
|
}
|
|
223
|
-
return `${normalizedSourceRoot}${normalizedSource}`;
|
|
224
303
|
};
|
|
225
304
|
|
|
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
|
-
}),
|
|
305
|
+
const resolveSourceMapSources = (rawSourceMap: StandardSourceMap, sourceMapUrl: string): string[] =>
|
|
306
|
+
rawSourceMap.sources.map((source) =>
|
|
307
|
+
resolveSourceRoot(rawSourceMap.sourceRoot, source, sourceMapUrl),
|
|
251
308
|
);
|
|
252
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
|
+
|
|
253
370
|
const allSources = new Set<string>();
|
|
254
371
|
for (const section of decodedSections) {
|
|
255
372
|
for (const source of section.map.sources) {
|
|
@@ -291,89 +408,227 @@ const isFetchableUrl = (url: string): boolean => {
|
|
|
291
408
|
return scheme === "http:" || scheme === "https:";
|
|
292
409
|
};
|
|
293
410
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 });
|
|
305
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
|
+
};
|
|
306
442
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
};
|
|
311
452
|
|
|
312
|
-
|
|
313
|
-
|
|
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 {
|
|
314
461
|
return null;
|
|
315
462
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
}
|
|
319
479
|
}
|
|
480
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
481
|
+
};
|
|
320
482
|
|
|
321
|
-
|
|
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
|
+
};
|
|
322
500
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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 {
|
|
327
517
|
return null;
|
|
328
518
|
}
|
|
519
|
+
};
|
|
329
520
|
|
|
330
|
-
|
|
331
|
-
|
|
521
|
+
export const getSourceMapUncached = async (
|
|
522
|
+
bundleUrl: string,
|
|
523
|
+
fetchFn?: SourceFetch,
|
|
524
|
+
options: SourceMapRequestOptions = {},
|
|
525
|
+
): Promise<null | SourceMap> => {
|
|
526
|
+
if (!isFetchableUrl(bundleUrl)) {
|
|
332
527
|
return null;
|
|
333
528
|
}
|
|
334
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);
|
|
335
538
|
try {
|
|
336
|
-
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
|
+
};
|
|
337
570
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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);
|
|
343
577
|
}
|
|
578
|
+
return cache;
|
|
344
579
|
};
|
|
345
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
|
+
|
|
346
600
|
export const getSourceMap = async (
|
|
347
601
|
file: string,
|
|
348
602
|
useCache = true,
|
|
349
|
-
fetchFn?:
|
|
603
|
+
fetchFn?: SourceFetch,
|
|
604
|
+
options: SourceMapRequestOptions = {},
|
|
350
605
|
): Promise<null | SourceMap> => {
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
353
612
|
}
|
|
354
613
|
|
|
355
|
-
const pendingRequest =
|
|
614
|
+
const pendingRequest = shouldUseCache ? pendingRequests.get(cacheKey) : undefined;
|
|
356
615
|
if (pendingRequest) {
|
|
357
616
|
return (await pendingRequest).sourceMap;
|
|
358
617
|
}
|
|
359
618
|
|
|
360
|
-
|
|
361
|
-
// definitive "no map" resolves to null. Only definitive results are cached:
|
|
362
|
-
// caching a transient null would pin the bundle to a degraded result for the
|
|
363
|
-
// rest of the page's lifetime, even after the network recovers.
|
|
364
|
-
const fetchPromise: Promise<SourceMapResult> = getSourceMapImpl(file, fetchFn).then(
|
|
619
|
+
const fetchPromise: Promise<SourceMapResult> = getSourceMapUncached(file, fetchFn, options).then(
|
|
365
620
|
(sourceMap) => ({ sourceMap, isTransientFailure: false }),
|
|
366
621
|
() => ({ sourceMap: null, isTransientFailure: true }),
|
|
367
622
|
);
|
|
368
|
-
if (
|
|
369
|
-
|
|
623
|
+
if (shouldUseCache) {
|
|
624
|
+
pendingRequests.set(cacheKey, fetchPromise);
|
|
370
625
|
}
|
|
371
626
|
|
|
372
627
|
const { sourceMap, isTransientFailure } = await fetchPromise;
|
|
373
|
-
if (
|
|
374
|
-
|
|
628
|
+
if (shouldUseCache) {
|
|
629
|
+
pendingRequests.delete(cacheKey);
|
|
375
630
|
if (!isTransientFailure) {
|
|
376
|
-
|
|
631
|
+
cache.set(cacheKey, sourceMap);
|
|
377
632
|
}
|
|
378
633
|
}
|
|
379
634
|
|
|
@@ -385,7 +640,7 @@ export const symbolicateStack = async (
|
|
|
385
640
|
cache = true,
|
|
386
641
|
fetchFn?: (url: string) => Promise<Response>,
|
|
387
642
|
): Promise<StackFrame[]> => {
|
|
388
|
-
return
|
|
643
|
+
return Promise.all(
|
|
389
644
|
stack.map(async (stackFrame) => {
|
|
390
645
|
if (!stackFrame.fileName) return stackFrame;
|
|
391
646
|
const sourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);
|
package/src/types.ts
CHANGED
|
@@ -89,7 +89,12 @@ export interface ContextDependency<T> extends Omit<
|
|
|
89
89
|
observedBits?: number;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export interface DebugThenableState {
|
|
93
|
+
thenables?: unknown[];
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
export interface Dependencies extends Omit<ReactReconciler.Dependencies, "firstContext"> {
|
|
97
|
+
_debugThenableState?: DebugThenableState | unknown[];
|
|
93
98
|
firstContext: ContextDependency<unknown> | null;
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -115,6 +120,11 @@ export interface ReactDebugInfo {
|
|
|
115
120
|
name?: string;
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
export interface ReactMemoCache {
|
|
124
|
+
data: unknown[][];
|
|
125
|
+
index: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
export interface FiberDebugSource extends Source {
|
|
119
129
|
columnNumber?: number;
|
|
120
130
|
}
|
|
@@ -123,7 +133,7 @@ export interface FiberUpdateQueue {
|
|
|
123
133
|
[key: string]: unknown;
|
|
124
134
|
dispatch?: unknown;
|
|
125
135
|
lastEffect: Effect | null;
|
|
126
|
-
memoCache?:
|
|
136
|
+
memoCache?: ReactMemoCache;
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
// HACK: @types/react-reconciler does not yet include React 19 debug fields or recursive server owners.
|
|
@@ -160,7 +170,7 @@ export interface Fiber<T = unknown> extends Omit<
|
|
|
160
170
|
sibling: Fiber | null;
|
|
161
171
|
stateNode: T;
|
|
162
172
|
tag: WorkTag;
|
|
163
|
-
updateQueue: FiberUpdateQueue;
|
|
173
|
+
updateQueue: FiberUpdateQueue | null;
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
export interface HostFiber<T = unknown> extends Fiber<T> {
|