@specific.dev/spectest 0.81.0 → 0.81.1
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/dist/coverage.d.ts +34 -18
- package/dist/coverage.js +143 -89
- package/package.json +1 -1
- package/src/coverage.test.ts +56 -36
- package/src/coverage.ts +137 -86
package/dist/coverage.d.ts
CHANGED
|
@@ -145,7 +145,7 @@ export declare const NODE_COVERAGE_HOOK: string;
|
|
|
145
145
|
* `sourceMappingURL` (it caches maps under NODE_V8_COVERAGE with or
|
|
146
146
|
* without `--enable-source-maps`), else from `<script>.map` next to the
|
|
147
147
|
* script, read out of the container once
|
|
148
|
-
* ({@link
|
|
148
|
+
* ({@link resolveSourceMapsFromContainer}). The second way is the one to
|
|
149
149
|
* build a short-lived bundled process in: a map Node finds is serialized
|
|
150
150
|
* whole into every exit dump — 12 MB per invocation for a bundled CLI,
|
|
151
151
|
* inside the test's own time — and parsed again by the harness; a map
|
|
@@ -224,33 +224,49 @@ export interface SourceMapEntry {
|
|
|
224
224
|
data: Record<string, unknown>;
|
|
225
225
|
lineLengths: number[];
|
|
226
226
|
}
|
|
227
|
-
/** Finds the source
|
|
228
|
-
*
|
|
229
|
-
export type SourceMapResolver = (
|
|
227
|
+
/** Finds the source maps of the scripts a dump carries no map for, in
|
|
228
|
+
* one go. A script absent from the result has none. */
|
|
229
|
+
export type SourceMapResolver = (scriptUrls: string[]) => Promise<Map<string, SourceMapEntry>>;
|
|
230
|
+
/**
|
|
231
|
+
* The probe {@link resolveSourceMapsFromContainer} runs in the container:
|
|
232
|
+
* one `sh` loop over every script path, printing `<path>\t<ref>` — the
|
|
233
|
+
* last `sourceMappingURL` value in the file, else `<path>.map` when that
|
|
234
|
+
* file exists, else `-`; `!` for a script that is not there. One exec per
|
|
235
|
+
* capture per service, whatever the number of scripts: a Next.js server
|
|
236
|
+
* loads hundreds of chunk scripts with no map, and a `docker cp` per
|
|
237
|
+
* script (the first cut) took a capture past its budget on a busy host.
|
|
238
|
+
*/
|
|
239
|
+
export declare function sourceMapProbeCommand(scriptPaths: string[]): string;
|
|
230
240
|
/** The length of every line of `text`, the way Node computes it for
|
|
231
241
|
* `source-map-cache`: split on `\n` (and U+2028/2029), `\r` kept, the
|
|
232
242
|
* last line included. Lengths in UTF-16 units, which is what V8's byte
|
|
233
243
|
* offsets count in. */
|
|
234
244
|
export declare function lineLengthsOf(text: string): number[];
|
|
235
|
-
/** The last `sourceMappingURL` comment of a script, or `null`. */
|
|
236
|
-
export declare function sourceMappingUrlOf(script: string): string | null;
|
|
237
245
|
/**
|
|
238
|
-
* The source
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
246
|
+
* The source maps of `scriptUrls`, read out of the container. One probe
|
|
247
|
+
* exec ({@link sourceMapProbeCommand}) finds, per script, the map a
|
|
248
|
+
* `sourceMappingURL` comment names or `<script>.map` next to it; only a
|
|
249
|
+
* script that has one is then read (`docker cp`, the script for its line
|
|
250
|
+
* lengths and the map file, or the inline `data:` map decoded). A map's
|
|
251
|
+
* `sourcesContent` is dropped. Scripts that are not `file://` URLs, have
|
|
252
|
+
* no map, or whose map is not there or not JSON are absent from the
|
|
253
|
+
* result. The caller remembers every outcome, so this runs once per
|
|
254
|
+
* script per branch.
|
|
255
|
+
*
|
|
256
|
+
* With no comment the map next to the script is the shape to build a
|
|
257
|
+
* short-lived bundled process in: Node caches a map it finds through a
|
|
258
|
+
* comment into every dump it writes under NODE_V8_COVERAGE, flag or no
|
|
259
|
+
* flag, and a bundled CLI's map is tens of MB per exit; a map it does not
|
|
260
|
+
* find costs nothing there.
|
|
245
261
|
*/
|
|
246
|
-
export declare function
|
|
262
|
+
export declare function resolveSourceMapsFromContainer(ctx: CoverageCaptureContext, scriptUrls: string[]): Promise<Map<string, SourceMapEntry>>;
|
|
247
263
|
/**
|
|
248
264
|
* Give every kept script of a compacted document a `source-map-cache`
|
|
249
265
|
* entry it lacks: a stub when the map shipped earlier on this branch,
|
|
250
|
-
* else the map `resolve` finds (
|
|
251
|
-
* remembered), else nothing — and that outcome
|
|
252
|
-
* script with no map is looked up once per
|
|
253
|
-
* `resolve` supplied.
|
|
266
|
+
* else the map `resolve` finds (asked once for all such scripts; shipped
|
|
267
|
+
* whole, with its hash, and remembered), else nothing — and that outcome
|
|
268
|
+
* is remembered too, so a script with no map is looked up once per
|
|
269
|
+
* branch. Returns how many maps `resolve` supplied.
|
|
254
270
|
*/
|
|
255
271
|
export declare function attachSourceMaps(doc: Record<string, unknown>, resolve: SourceMapResolver, shipped?: Map<string, string>, missing?: Set<string>): Promise<number>;
|
|
256
272
|
/**
|
package/dist/coverage.js
CHANGED
|
@@ -189,7 +189,7 @@ if (process.env.NODE_V8_COVERAGE && require("node:worker_threads").isMainThread)
|
|
|
189
189
|
* `sourceMappingURL` (it caches maps under NODE_V8_COVERAGE with or
|
|
190
190
|
* without `--enable-source-maps`), else from `<script>.map` next to the
|
|
191
191
|
* script, read out of the container once
|
|
192
|
-
* ({@link
|
|
192
|
+
* ({@link resolveSourceMapsFromContainer}). The second way is the one to
|
|
193
193
|
* build a short-lived bundled process in: a map Node finds is serialized
|
|
194
194
|
* whole into every exit dump — 12 MB per invocation for a bundled CLI,
|
|
195
195
|
* inside the test's own time — and parsed again by the harness; a map
|
|
@@ -234,7 +234,7 @@ export function node() {
|
|
|
234
234
|
await removeShippedV8Reports(ctx.reportDir);
|
|
235
235
|
const live = await dumpAllNodeProcesses(ctx.reportDir, ctx.signal);
|
|
236
236
|
const t1 = performance.now();
|
|
237
|
-
const stats = await prepareV8Reports(ctx.reportDir, (
|
|
237
|
+
const stats = await prepareV8Reports(ctx.reportDir, (urls) => resolveSourceMapsFromContainer(ctx, urls));
|
|
238
238
|
if (stats.dumps === 0) {
|
|
239
239
|
await ctx.writeReport(NODE_COVERAGE_EMPTY_REPORT, '{"result":[]}\n');
|
|
240
240
|
SHIPPED_V8_REPORTS.add((await import("node:path")).join(ctx.reportDir, NODE_COVERAGE_EMPTY_REPORT));
|
|
@@ -397,6 +397,30 @@ export function compactV8Document(doc, shipped) {
|
|
|
397
397
|
}
|
|
398
398
|
return out;
|
|
399
399
|
}
|
|
400
|
+
/** Shell-quote one argument for `sh -c`. */
|
|
401
|
+
function shQuote(s) {
|
|
402
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The probe {@link resolveSourceMapsFromContainer} runs in the container:
|
|
406
|
+
* one `sh` loop over every script path, printing `<path>\t<ref>` — the
|
|
407
|
+
* last `sourceMappingURL` value in the file, else `<path>.map` when that
|
|
408
|
+
* file exists, else `-`; `!` for a script that is not there. One exec per
|
|
409
|
+
* capture per service, whatever the number of scripts: a Next.js server
|
|
410
|
+
* loads hundreds of chunk scripts with no map, and a `docker cp` per
|
|
411
|
+
* script (the first cut) took a capture past its budget on a busy host.
|
|
412
|
+
*/
|
|
413
|
+
export function sourceMapProbeCommand(scriptPaths) {
|
|
414
|
+
const script = 'for f in "$@"; do ' +
|
|
415
|
+
'if [ -f "$f" ]; then ' +
|
|
416
|
+
"ref=$(grep -o 'sourceMappingURL=[^[:space:]]*' -- \"$f\" 2>/dev/null | tail -n 1 | cut -c18-); " +
|
|
417
|
+
'if [ -n "$ref" ]; then printf \'%s\\t%s\\n\' "$f" "$ref"; ' +
|
|
418
|
+
'elif [ -f "$f.map" ]; then printf \'%s\\t%s.map\\n\' "$f" "$f"; ' +
|
|
419
|
+
'else printf \'%s\\t-\\n\' "$f"; fi; ' +
|
|
420
|
+
'else printf \'%s\\t!\\n\' "$f"; fi; ' +
|
|
421
|
+
"done";
|
|
422
|
+
return `sh -c ${shQuote(script)} sh ${scriptPaths.map(shQuote).join(" ")}`;
|
|
423
|
+
}
|
|
400
424
|
/** The length of every line of `text`, the way Node computes it for
|
|
401
425
|
* `source-map-cache`: split on `\n` (and U+2028/2029), `\r` kept, the
|
|
402
426
|
* last line included. Lengths in UTF-16 units, which is what V8's byte
|
|
@@ -414,96 +438,119 @@ export function lineLengthsOf(text) {
|
|
|
414
438
|
out.push(len);
|
|
415
439
|
return out;
|
|
416
440
|
}
|
|
417
|
-
/** The last `sourceMappingURL` comment of a script, or `null`. */
|
|
418
|
-
export function sourceMappingUrlOf(script) {
|
|
419
|
-
const re = /\/\/[#@]\s*sourceMappingURL=(\S+)/g;
|
|
420
|
-
let last = null;
|
|
421
|
-
let m;
|
|
422
|
-
while ((m = re.exec(script)) !== null)
|
|
423
|
-
last = m[1];
|
|
424
|
-
return last;
|
|
425
|
-
}
|
|
426
441
|
/**
|
|
427
|
-
* The source
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
442
|
+
* The source maps of `scriptUrls`, read out of the container. One probe
|
|
443
|
+
* exec ({@link sourceMapProbeCommand}) finds, per script, the map a
|
|
444
|
+
* `sourceMappingURL` comment names or `<script>.map` next to it; only a
|
|
445
|
+
* script that has one is then read (`docker cp`, the script for its line
|
|
446
|
+
* lengths and the map file, or the inline `data:` map decoded). A map's
|
|
447
|
+
* `sourcesContent` is dropped. Scripts that are not `file://` URLs, have
|
|
448
|
+
* no map, or whose map is not there or not JSON are absent from the
|
|
449
|
+
* result. The caller remembers every outcome, so this runs once per
|
|
450
|
+
* script per branch.
|
|
451
|
+
*
|
|
452
|
+
* With no comment the map next to the script is the shape to build a
|
|
453
|
+
* short-lived bundled process in: Node caches a map it finds through a
|
|
454
|
+
* comment into every dump it writes under NODE_V8_COVERAGE, flag or no
|
|
455
|
+
* flag, and a bundled CLI's map is tens of MB per exit; a map it does not
|
|
456
|
+
* find costs nothing there.
|
|
434
457
|
*/
|
|
435
|
-
export async function
|
|
436
|
-
|
|
437
|
-
|
|
458
|
+
export async function resolveSourceMapsFromContainer(ctx, scriptUrls) {
|
|
459
|
+
const out = new Map();
|
|
460
|
+
if (!ctx.readContainerFile)
|
|
461
|
+
return out;
|
|
438
462
|
const { fileURLToPath, pathToFileURL } = await import("node:url");
|
|
439
463
|
const path = await import("node:path");
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
catch {
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
const script = await ctx.readContainerFile(scriptPath);
|
|
448
|
-
if (!script)
|
|
449
|
-
return null;
|
|
450
|
-
const text = script.toString("utf8");
|
|
451
|
-
// No comment: the map next to the script, by convention (`x.js.map`).
|
|
452
|
-
// That is the shape to build a short-lived bundled process in: Node
|
|
453
|
-
// caches a map it finds through a comment into every dump it writes
|
|
454
|
-
// under NODE_V8_COVERAGE, flag or no flag, and a bundled CLI's map is
|
|
455
|
-
// tens of MB per exit; a map it does not find costs nothing there and
|
|
456
|
-
// is read here once per branch.
|
|
457
|
-
const ref = sourceMappingUrlOf(text) ?? `${path.basename(scriptPath)}.map`;
|
|
458
|
-
let mapText;
|
|
459
|
-
let mapUrl;
|
|
460
|
-
if (ref.startsWith("data:")) {
|
|
461
|
-
const comma = ref.indexOf(",");
|
|
462
|
-
if (comma < 0)
|
|
463
|
-
return null;
|
|
464
|
-
const head = ref.slice(5, comma);
|
|
465
|
-
const body = ref.slice(comma + 1);
|
|
464
|
+
const byPath = new Map();
|
|
465
|
+
for (const url of scriptUrls) {
|
|
466
|
+
if (!url.startsWith("file://"))
|
|
467
|
+
continue;
|
|
466
468
|
try {
|
|
467
|
-
|
|
469
|
+
byPath.set(fileURLToPath(url), url);
|
|
468
470
|
}
|
|
469
471
|
catch {
|
|
470
|
-
|
|
472
|
+
// not a path
|
|
471
473
|
}
|
|
472
|
-
mapUrl = null;
|
|
473
474
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
if (byPath.size === 0)
|
|
476
|
+
return out;
|
|
477
|
+
let probe;
|
|
478
|
+
try {
|
|
479
|
+
probe = (await ctx.exec(sourceMapProbeCommand([...byPath.keys()]))).stdout;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return out; // no shell, no grep: no maps from this container
|
|
483
|
+
}
|
|
484
|
+
for (const line of probe.split("\n")) {
|
|
485
|
+
const tab = line.indexOf("\t");
|
|
486
|
+
if (tab < 0)
|
|
487
|
+
continue;
|
|
488
|
+
const scriptPath = line.slice(0, tab);
|
|
489
|
+
const ref = line.slice(tab + 1).trim();
|
|
490
|
+
const url = byPath.get(scriptPath);
|
|
491
|
+
if (!url || ref === "-" || ref === "!" || ref === "")
|
|
492
|
+
continue;
|
|
493
|
+
let mapText;
|
|
494
|
+
let mapUrl;
|
|
495
|
+
if (ref.startsWith("data:")) {
|
|
496
|
+
const comma = ref.indexOf(",");
|
|
497
|
+
if (comma < 0)
|
|
498
|
+
continue;
|
|
499
|
+
const head = ref.slice(5, comma);
|
|
500
|
+
const body = ref.slice(comma + 1);
|
|
477
501
|
try {
|
|
478
|
-
|
|
502
|
+
mapText = /;base64$/i.test(head) ? Buffer.from(body, "base64").toString("utf8") : decodeURIComponent(body);
|
|
479
503
|
}
|
|
480
504
|
catch {
|
|
481
|
-
|
|
505
|
+
continue;
|
|
482
506
|
}
|
|
483
|
-
|
|
484
|
-
else if (/^[a-z]+:/i.test(ref)) {
|
|
485
|
-
return null; // an http(s) map is not in the container
|
|
507
|
+
mapUrl = null;
|
|
486
508
|
}
|
|
487
509
|
else {
|
|
488
|
-
mapPath
|
|
510
|
+
let mapPath;
|
|
511
|
+
if (ref.startsWith("file://")) {
|
|
512
|
+
try {
|
|
513
|
+
mapPath = fileURLToPath(ref);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else if (/^[a-z]+:/i.test(ref)) {
|
|
520
|
+
continue; // an http(s) map is not in the container
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
let rel = ref;
|
|
524
|
+
try {
|
|
525
|
+
rel = decodeURIComponent(ref);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// keep as written
|
|
529
|
+
}
|
|
530
|
+
mapPath = path.resolve(path.dirname(scriptPath), rel);
|
|
531
|
+
}
|
|
532
|
+
const map = await ctx.readContainerFile(mapPath);
|
|
533
|
+
if (!map)
|
|
534
|
+
continue;
|
|
535
|
+
mapText = map.toString("utf8");
|
|
536
|
+
mapUrl = pathToFileURL(mapPath).href;
|
|
489
537
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
538
|
+
let data;
|
|
539
|
+
try {
|
|
540
|
+
data = JSON.parse(mapText);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!data || typeof data !== "object" || !("mappings" in data))
|
|
546
|
+
continue;
|
|
547
|
+
const script = await ctx.readContainerFile(scriptPath);
|
|
548
|
+
if (!script)
|
|
549
|
+
continue;
|
|
550
|
+
const { sourcesContent: _dropped, ...slim } = data;
|
|
551
|
+
out.set(url, { url: mapUrl, data: slim, lineLengths: lineLengthsOf(script.toString("utf8")) });
|
|
502
552
|
}
|
|
503
|
-
|
|
504
|
-
return null;
|
|
505
|
-
const { sourcesContent: _dropped, ...slim } = data;
|
|
506
|
-
return { url: mapUrl, data: slim, lineLengths: lineLengthsOf(text) };
|
|
553
|
+
return out;
|
|
507
554
|
}
|
|
508
555
|
/** The content hash of a map entry, over its JSON with the spectest
|
|
509
556
|
* keys removed. */
|
|
@@ -516,15 +563,15 @@ function sourceMapEntryHash(entry) {
|
|
|
516
563
|
/**
|
|
517
564
|
* Give every kept script of a compacted document a `source-map-cache`
|
|
518
565
|
* entry it lacks: a stub when the map shipped earlier on this branch,
|
|
519
|
-
* else the map `resolve` finds (
|
|
520
|
-
* remembered), else nothing — and that outcome
|
|
521
|
-
* script with no map is looked up once per
|
|
522
|
-
* `resolve` supplied.
|
|
566
|
+
* else the map `resolve` finds (asked once for all such scripts; shipped
|
|
567
|
+
* whole, with its hash, and remembered), else nothing — and that outcome
|
|
568
|
+
* is remembered too, so a script with no map is looked up once per
|
|
569
|
+
* branch. Returns how many maps `resolve` supplied.
|
|
523
570
|
*/
|
|
524
571
|
export async function attachSourceMaps(doc, resolve, shipped = SHIPPED_SOURCE_MAPS, missing = NO_SOURCE_MAP) {
|
|
525
572
|
const result = Array.isArray(doc.result) ? doc.result : [];
|
|
526
573
|
const cache = (doc["source-map-cache"] ?? {});
|
|
527
|
-
|
|
574
|
+
const unknown = [];
|
|
528
575
|
for (const s of result) {
|
|
529
576
|
const url = s.url;
|
|
530
577
|
if (typeof url !== "string" || url in cache)
|
|
@@ -534,17 +581,24 @@ export async function attachSourceMaps(doc, resolve, shipped = SHIPPED_SOURCE_MA
|
|
|
534
581
|
cache[url] = { [SOURCE_MAP_REF_KEY]: prior };
|
|
535
582
|
continue;
|
|
536
583
|
}
|
|
537
|
-
if (missing.has(url))
|
|
538
|
-
continue;
|
|
539
|
-
const entry = await resolve(url);
|
|
540
|
-
if (!entry) {
|
|
541
|
-
missing.add(url);
|
|
584
|
+
if (missing.has(url) || unknown.includes(url))
|
|
542
585
|
continue;
|
|
586
|
+
unknown.push(url);
|
|
587
|
+
}
|
|
588
|
+
let found = 0;
|
|
589
|
+
if (unknown.length > 0) {
|
|
590
|
+
const entries = await resolve(unknown);
|
|
591
|
+
for (const url of unknown) {
|
|
592
|
+
const entry = entries.get(url);
|
|
593
|
+
if (!entry) {
|
|
594
|
+
missing.add(url);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const hash = sourceMapEntryHash(entry);
|
|
598
|
+
cache[url] = { ...entry, [SOURCE_MAP_HASH_KEY]: hash };
|
|
599
|
+
shipped.set(url, hash);
|
|
600
|
+
found++;
|
|
543
601
|
}
|
|
544
|
-
const hash = sourceMapEntryHash(entry);
|
|
545
|
-
cache[url] = { ...entry, [SOURCE_MAP_HASH_KEY]: hash };
|
|
546
|
-
shipped.set(url, hash);
|
|
547
|
-
found++;
|
|
548
602
|
}
|
|
549
603
|
if (Object.keys(cache).length > 0)
|
|
550
604
|
doc["source-map-cache"] = cache;
|
package/package.json
CHANGED
package/src/coverage.test.ts
CHANGED
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
appendEnvFlag,
|
|
15
15
|
attachSourceMaps,
|
|
16
16
|
lineLengthsOf,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
resolveSourceMapsFromContainer,
|
|
18
|
+
sourceMapProbeCommand,
|
|
19
19
|
browser,
|
|
20
20
|
command,
|
|
21
21
|
coverageReportsMode,
|
|
@@ -191,8 +191,17 @@ function fakeCtx(dir: string, signal = new AbortController().signal): CoverageCa
|
|
|
191
191
|
service: "api",
|
|
192
192
|
reportDir: dir,
|
|
193
193
|
signal,
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// The probe runs through a real shell; the "container" paths are
|
|
195
|
+
// this directory's own.
|
|
196
|
+
async exec(command: string) {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const p = spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"] });
|
|
199
|
+
let stdout = "";
|
|
200
|
+
let stderr = "";
|
|
201
|
+
p.stdout!.on("data", (b: Buffer) => (stdout += b));
|
|
202
|
+
p.stderr!.on("data", (b: Buffer) => (stderr += b));
|
|
203
|
+
p.on("exit", (code) => (code === 0 ? resolve({ stdout, stderr }) : reject(new Error(stderr || `exit ${code}`))));
|
|
204
|
+
});
|
|
196
205
|
},
|
|
197
206
|
async writeReport(name, content) {
|
|
198
207
|
await fs.writeFile(path.join(dir, name), content);
|
|
@@ -209,10 +218,21 @@ describe("source maps by reference", () => {
|
|
|
209
218
|
expect(lineLengthsOf("")).toEqual([0]);
|
|
210
219
|
expect(lineLengthsOf("x\n")).toEqual([1, 0]);
|
|
211
220
|
});
|
|
212
|
-
test("
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
test("the probe: one exec, one line per script — the last comment's ref, the .map next to it, or none", async () => {
|
|
222
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covprobe-"));
|
|
223
|
+
await fs.writeFile(path.join(dir, "a.js"), "x\n//# sourceMappingURL=old.map\ny\n//# sourceMappingURL=maps/a.map\n");
|
|
224
|
+
await fs.writeFile(path.join(dir, "b.js"), "x();\n");
|
|
225
|
+
await fs.writeFile(path.join(dir, "b.js.map"), "{}");
|
|
226
|
+
await fs.writeFile(path.join(dir, "it's.js"), "x();\n");
|
|
227
|
+
const r = await fakeCtx(dir).exec(
|
|
228
|
+
sourceMapProbeCommand([path.join(dir, "a.js"), path.join(dir, "b.js"), path.join(dir, "it's.js"), path.join(dir, "gone.js")]),
|
|
229
|
+
);
|
|
230
|
+
expect(r.stdout).toBe(
|
|
231
|
+
`${path.join(dir, "a.js")}\tmaps/a.map\n` +
|
|
232
|
+
`${path.join(dir, "b.js")}\t${path.join(dir, "b.js.map")}\n` +
|
|
233
|
+
`${path.join(dir, "it's.js")}\t-\n` +
|
|
234
|
+
`${path.join(dir, "gone.js")}\t!\n`,
|
|
235
|
+
);
|
|
216
236
|
});
|
|
217
237
|
test("resolves the map a comment names, drops sourcesContent, computes line lengths", async () => {
|
|
218
238
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
@@ -222,46 +242,46 @@ describe("source maps by reference", () => {
|
|
|
222
242
|
path.join(dir, "maps", "cli.map"),
|
|
223
243
|
JSON.stringify({ version: 3, sources: ["../src/cli.ts"], mappings: "AAAA;AACA", names: [], sourcesContent: ["x"] }),
|
|
224
244
|
);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
expect(
|
|
228
|
-
|
|
229
|
-
expect(entry
|
|
245
|
+
const url = `file://${path.join(dir, "cli.js")}`;
|
|
246
|
+
const out = await resolveSourceMapsFromContainer(fakeCtx(dir), [url, "node:fs"]);
|
|
247
|
+
expect([...out.keys()]).toEqual([url]);
|
|
248
|
+
const entry = out.get(url)!;
|
|
249
|
+
expect(entry.url).toBe(`file://${path.join(dir, "maps", "cli.map")}`);
|
|
250
|
+
expect(entry.data).toEqual({ version: 3, sources: ["../src/cli.ts"], mappings: "AAAA;AACA", names: [] });
|
|
251
|
+
expect(entry.lineLengths).toEqual([12, 15, 33, 0]);
|
|
230
252
|
});
|
|
231
|
-
test("with no comment, <script>.map next to the script is the map", async () => {
|
|
253
|
+
test("with no comment, <script>.map next to the script is the map; inline data: maps decode; the rest is absent", async () => {
|
|
232
254
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
233
255
|
await fs.writeFile(path.join(dir, "cli.js"), "x();\n");
|
|
234
256
|
await fs.writeFile(path.join(dir, "cli.js.map"), JSON.stringify({ version: 3, sources: ["cli.ts"], mappings: "AAAA" }));
|
|
235
|
-
const entry = await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "cli.js")}`);
|
|
236
|
-
expect(entry!.url).toBe(`file://${path.join(dir, "cli.js.map")}`);
|
|
237
|
-
expect(entry!.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA" });
|
|
238
|
-
});
|
|
239
|
-
test("resolves an inline data: URL map; no map, no file, or a foreign URL is null", async () => {
|
|
240
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
241
257
|
const map = JSON.stringify({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
242
258
|
await fs.writeFile(
|
|
243
259
|
path.join(dir, "inline.js"),
|
|
244
260
|
`x();\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(map).toString("base64")}\n`,
|
|
245
261
|
);
|
|
246
|
-
const inline = await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "inline.js")}`);
|
|
247
|
-
expect(inline!.url).toBeNull();
|
|
248
|
-
expect(inline!.data).toEqual({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
249
262
|
await fs.writeFile(path.join(dir, "bare.js"), "x();\n");
|
|
250
|
-
expect(await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "bare.js")}`)).toBeNull();
|
|
251
263
|
await fs.writeFile(path.join(dir, "gone.js"), "x();\n//# sourceMappingURL=gone.js.map\n");
|
|
252
|
-
expect(await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "gone.js")}`)).toBeNull();
|
|
253
264
|
await fs.writeFile(path.join(dir, "http.js"), "x();\n//# sourceMappingURL=https://cdn.test/x.map\n");
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
await fs.writeFile(path.join(dir, "notjson.js"), "x();\n");
|
|
266
|
+
await fs.writeFile(path.join(dir, "notjson.js.map"), "nope");
|
|
267
|
+
const u = (n: string) => `file://${path.join(dir, n)}`;
|
|
268
|
+
const out = await resolveSourceMapsFromContainer(fakeCtx(dir), [
|
|
269
|
+
u("cli.js"), u("inline.js"), u("bare.js"), u("gone.js"), u("http.js"), u("notjson.js"), u("missing.js"),
|
|
270
|
+
]);
|
|
271
|
+
expect([...out.keys()].sort()).toEqual([u("cli.js"), u("inline.js")].sort());
|
|
272
|
+
expect(out.get(u("cli.js"))!.url).toBe(`file://${path.join(dir, "cli.js.map")}`);
|
|
273
|
+
expect(out.get(u("cli.js"))!.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA" });
|
|
274
|
+
expect(out.get(u("inline.js"))!.url).toBeNull();
|
|
275
|
+
expect(out.get(u("inline.js"))!.data).toEqual({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
256
276
|
const noRead = { ...fakeCtx(dir), readContainerFile: undefined };
|
|
257
|
-
expect(await
|
|
277
|
+
expect((await resolveSourceMapsFromContainer(noRead, [u("cli.js")])).size).toBe(0);
|
|
258
278
|
});
|
|
259
|
-
test("attachSourceMaps: a map once, a stub after, a missing map looked up once", async () => {
|
|
260
|
-
const calls: string[] = [];
|
|
279
|
+
test("attachSourceMaps: one batched lookup, a map once, a stub after, a missing map looked up once", async () => {
|
|
280
|
+
const calls: string[][] = [];
|
|
261
281
|
const entry = { url: null, data: { version: 3, sources: ["a.ts"], mappings: "AAAA" }, lineLengths: [4] };
|
|
262
|
-
const resolve = async (
|
|
263
|
-
calls.push(
|
|
264
|
-
return
|
|
282
|
+
const resolve = async (urls: string[]) => {
|
|
283
|
+
calls.push(urls);
|
|
284
|
+
return new Map(urls.filter((u) => u.endsWith("/a.js")).map((u) => [u, entry]));
|
|
265
285
|
};
|
|
266
286
|
const shipped = new Map<string, string>();
|
|
267
287
|
const missing = new Set<string>();
|
|
@@ -277,16 +297,16 @@ describe("source maps by reference", () => {
|
|
|
277
297
|
expect(c1["file:///app/a.js"][SOURCE_MAP_HASH_KEY]).toMatch(/^sha256:/);
|
|
278
298
|
expect(c1["file:///app/a.js"].lineLengths).toEqual([4]);
|
|
279
299
|
expect("file:///app/b.js" in c1).toBe(false);
|
|
280
|
-
expect(calls).toEqual(["file:///app/a.js", "file:///app/b.js"]);
|
|
300
|
+
expect(calls).toEqual([["file:///app/a.js", "file:///app/b.js"]]);
|
|
281
301
|
const second = dump() as Record<string, unknown>;
|
|
282
302
|
expect(await attachSourceMaps(second, resolve, shipped, missing)).toBe(0);
|
|
283
303
|
const c2 = second["source-map-cache"] as Record<string, Record<string, unknown>>;
|
|
284
304
|
expect(c2["file:///app/a.js"]).toEqual({ [SOURCE_MAP_REF_KEY]: c1["file:///app/a.js"][SOURCE_MAP_HASH_KEY] });
|
|
285
|
-
expect(calls.length).toBe(
|
|
305
|
+
expect(calls.length).toBe(1); // b.js is not asked again
|
|
286
306
|
// A dump that carries its own entry is left alone.
|
|
287
307
|
const own = { ...dump(), "source-map-cache": { "file:///app/b.js": { data: { mappings: "" }, lineLengths: [1] } } } as Record<string, unknown>;
|
|
288
308
|
expect(await attachSourceMaps(own, resolve, shipped, missing)).toBe(0);
|
|
289
|
-
expect(calls.length).toBe(
|
|
309
|
+
expect(calls.length).toBe(1);
|
|
290
310
|
});
|
|
291
311
|
});
|
|
292
312
|
|
package/src/coverage.ts
CHANGED
|
@@ -292,7 +292,7 @@ if (process.env.NODE_V8_COVERAGE && require("node:worker_threads").isMainThread)
|
|
|
292
292
|
* `sourceMappingURL` (it caches maps under NODE_V8_COVERAGE with or
|
|
293
293
|
* without `--enable-source-maps`), else from `<script>.map` next to the
|
|
294
294
|
* script, read out of the container once
|
|
295
|
-
* ({@link
|
|
295
|
+
* ({@link resolveSourceMapsFromContainer}). The second way is the one to
|
|
296
296
|
* build a short-lived bundled process in: a map Node finds is serialized
|
|
297
297
|
* whole into every exit dump — 12 MB per invocation for a bundled CLI,
|
|
298
298
|
* inside the test's own time — and parsed again by the harness; a map
|
|
@@ -337,7 +337,7 @@ export function node(): CoverageAdapter {
|
|
|
337
337
|
await removeShippedV8Reports(ctx.reportDir);
|
|
338
338
|
const live = await dumpAllNodeProcesses(ctx.reportDir, ctx.signal);
|
|
339
339
|
const t1 = performance.now();
|
|
340
|
-
const stats = await prepareV8Reports(ctx.reportDir, (
|
|
340
|
+
const stats = await prepareV8Reports(ctx.reportDir, (urls) => resolveSourceMapsFromContainer(ctx, urls));
|
|
341
341
|
if (stats.dumps === 0) {
|
|
342
342
|
await ctx.writeReport(NODE_COVERAGE_EMPTY_REPORT, '{"result":[]}\n');
|
|
343
343
|
SHIPPED_V8_REPORTS.add((await import("node:path")).join(ctx.reportDir, NODE_COVERAGE_EMPTY_REPORT));
|
|
@@ -537,9 +537,36 @@ export interface SourceMapEntry {
|
|
|
537
537
|
lineLengths: number[];
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
-
/** Finds the source
|
|
541
|
-
*
|
|
542
|
-
export type SourceMapResolver = (
|
|
540
|
+
/** Finds the source maps of the scripts a dump carries no map for, in
|
|
541
|
+
* one go. A script absent from the result has none. */
|
|
542
|
+
export type SourceMapResolver = (scriptUrls: string[]) => Promise<Map<string, SourceMapEntry>>;
|
|
543
|
+
|
|
544
|
+
/** Shell-quote one argument for `sh -c`. */
|
|
545
|
+
function shQuote(s: string): string {
|
|
546
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The probe {@link resolveSourceMapsFromContainer} runs in the container:
|
|
551
|
+
* one `sh` loop over every script path, printing `<path>\t<ref>` — the
|
|
552
|
+
* last `sourceMappingURL` value in the file, else `<path>.map` when that
|
|
553
|
+
* file exists, else `-`; `!` for a script that is not there. One exec per
|
|
554
|
+
* capture per service, whatever the number of scripts: a Next.js server
|
|
555
|
+
* loads hundreds of chunk scripts with no map, and a `docker cp` per
|
|
556
|
+
* script (the first cut) took a capture past its budget on a busy host.
|
|
557
|
+
*/
|
|
558
|
+
export function sourceMapProbeCommand(scriptPaths: string[]): string {
|
|
559
|
+
const script =
|
|
560
|
+
'for f in "$@"; do ' +
|
|
561
|
+
'if [ -f "$f" ]; then ' +
|
|
562
|
+
"ref=$(grep -o 'sourceMappingURL=[^[:space:]]*' -- \"$f\" 2>/dev/null | tail -n 1 | cut -c18-); " +
|
|
563
|
+
'if [ -n "$ref" ]; then printf \'%s\\t%s\\n\' "$f" "$ref"; ' +
|
|
564
|
+
'elif [ -f "$f.map" ]; then printf \'%s\\t%s.map\\n\' "$f" "$f"; ' +
|
|
565
|
+
'else printf \'%s\\t-\\n\' "$f"; fi; ' +
|
|
566
|
+
'else printf \'%s\\t!\\n\' "$f"; fi; ' +
|
|
567
|
+
"done";
|
|
568
|
+
return `sh -c ${shQuote(script)} sh ${scriptPaths.map(shQuote).join(" ")}`;
|
|
569
|
+
}
|
|
543
570
|
|
|
544
571
|
/** The length of every line of `text`, the way Node computes it for
|
|
545
572
|
* `source-map-cache`: split on `\n` (and U+2028/2029), `\r` kept, the
|
|
@@ -559,87 +586,104 @@ export function lineLengthsOf(text: string): number[] {
|
|
|
559
586
|
return out;
|
|
560
587
|
}
|
|
561
588
|
|
|
562
|
-
/** The last `sourceMappingURL` comment of a script, or `null`. */
|
|
563
|
-
export function sourceMappingUrlOf(script: string): string | null {
|
|
564
|
-
const re = /\/\/[#@]\s*sourceMappingURL=(\S+)/g;
|
|
565
|
-
let last: string | null = null;
|
|
566
|
-
let m: RegExpExecArray | null;
|
|
567
|
-
while ((m = re.exec(script)) !== null) last = m[1]!;
|
|
568
|
-
return last;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
589
|
/**
|
|
572
|
-
* The source
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
590
|
+
* The source maps of `scriptUrls`, read out of the container. One probe
|
|
591
|
+
* exec ({@link sourceMapProbeCommand}) finds, per script, the map a
|
|
592
|
+
* `sourceMappingURL` comment names or `<script>.map` next to it; only a
|
|
593
|
+
* script that has one is then read (`docker cp`, the script for its line
|
|
594
|
+
* lengths and the map file, or the inline `data:` map decoded). A map's
|
|
595
|
+
* `sourcesContent` is dropped. Scripts that are not `file://` URLs, have
|
|
596
|
+
* no map, or whose map is not there or not JSON are absent from the
|
|
597
|
+
* result. The caller remembers every outcome, so this runs once per
|
|
598
|
+
* script per branch.
|
|
599
|
+
*
|
|
600
|
+
* With no comment the map next to the script is the shape to build a
|
|
601
|
+
* short-lived bundled process in: Node caches a map it finds through a
|
|
602
|
+
* comment into every dump it writes under NODE_V8_COVERAGE, flag or no
|
|
603
|
+
* flag, and a bundled CLI's map is tens of MB per exit; a map it does not
|
|
604
|
+
* find costs nothing there.
|
|
579
605
|
*/
|
|
580
|
-
export async function
|
|
606
|
+
export async function resolveSourceMapsFromContainer(
|
|
581
607
|
ctx: CoverageCaptureContext,
|
|
582
|
-
|
|
583
|
-
): Promise<SourceMapEntry
|
|
584
|
-
|
|
608
|
+
scriptUrls: string[],
|
|
609
|
+
): Promise<Map<string, SourceMapEntry>> {
|
|
610
|
+
const out = new Map<string, SourceMapEntry>();
|
|
611
|
+
if (!ctx.readContainerFile) return out;
|
|
585
612
|
const { fileURLToPath, pathToFileURL } = await import("node:url");
|
|
586
613
|
const path = await import("node:path");
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
} catch {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
const script = await ctx.readContainerFile(scriptPath);
|
|
594
|
-
if (!script) return null;
|
|
595
|
-
const text = script.toString("utf8");
|
|
596
|
-
// No comment: the map next to the script, by convention (`x.js.map`).
|
|
597
|
-
// That is the shape to build a short-lived bundled process in: Node
|
|
598
|
-
// caches a map it finds through a comment into every dump it writes
|
|
599
|
-
// under NODE_V8_COVERAGE, flag or no flag, and a bundled CLI's map is
|
|
600
|
-
// tens of MB per exit; a map it does not find costs nothing there and
|
|
601
|
-
// is read here once per branch.
|
|
602
|
-
const ref = sourceMappingUrlOf(text) ?? `${path.basename(scriptPath)}.map`;
|
|
603
|
-
let mapText: string;
|
|
604
|
-
let mapUrl: string | null;
|
|
605
|
-
if (ref.startsWith("data:")) {
|
|
606
|
-
const comma = ref.indexOf(",");
|
|
607
|
-
if (comma < 0) return null;
|
|
608
|
-
const head = ref.slice(5, comma);
|
|
609
|
-
const body = ref.slice(comma + 1);
|
|
614
|
+
const byPath = new Map<string, string>();
|
|
615
|
+
for (const url of scriptUrls) {
|
|
616
|
+
if (!url.startsWith("file://")) continue;
|
|
610
617
|
try {
|
|
611
|
-
|
|
618
|
+
byPath.set(fileURLToPath(url), url);
|
|
612
619
|
} catch {
|
|
613
|
-
|
|
620
|
+
// not a path
|
|
614
621
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
622
|
+
}
|
|
623
|
+
if (byPath.size === 0) return out;
|
|
624
|
+
let probe: string;
|
|
625
|
+
try {
|
|
626
|
+
probe = (await ctx.exec(sourceMapProbeCommand([...byPath.keys()]))).stdout;
|
|
627
|
+
} catch {
|
|
628
|
+
return out; // no shell, no grep: no maps from this container
|
|
629
|
+
}
|
|
630
|
+
for (const line of probe.split("\n")) {
|
|
631
|
+
const tab = line.indexOf("\t");
|
|
632
|
+
if (tab < 0) continue;
|
|
633
|
+
const scriptPath = line.slice(0, tab);
|
|
634
|
+
const ref = line.slice(tab + 1).trim();
|
|
635
|
+
const url = byPath.get(scriptPath);
|
|
636
|
+
if (!url || ref === "-" || ref === "!" || ref === "") continue;
|
|
637
|
+
let mapText: string;
|
|
638
|
+
let mapUrl: string | null;
|
|
639
|
+
if (ref.startsWith("data:")) {
|
|
640
|
+
const comma = ref.indexOf(",");
|
|
641
|
+
if (comma < 0) continue;
|
|
642
|
+
const head = ref.slice(5, comma);
|
|
643
|
+
const body = ref.slice(comma + 1);
|
|
619
644
|
try {
|
|
620
|
-
|
|
645
|
+
mapText = /;base64$/i.test(head) ? Buffer.from(body, "base64").toString("utf8") : decodeURIComponent(body);
|
|
621
646
|
} catch {
|
|
622
|
-
|
|
647
|
+
continue;
|
|
623
648
|
}
|
|
624
|
-
|
|
625
|
-
return null; // an http(s) map is not in the container
|
|
649
|
+
mapUrl = null;
|
|
626
650
|
} else {
|
|
627
|
-
mapPath
|
|
651
|
+
let mapPath: string;
|
|
652
|
+
if (ref.startsWith("file://")) {
|
|
653
|
+
try {
|
|
654
|
+
mapPath = fileURLToPath(ref);
|
|
655
|
+
} catch {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
} else if (/^[a-z]+:/i.test(ref)) {
|
|
659
|
+
continue; // an http(s) map is not in the container
|
|
660
|
+
} else {
|
|
661
|
+
let rel = ref;
|
|
662
|
+
try {
|
|
663
|
+
rel = decodeURIComponent(ref);
|
|
664
|
+
} catch {
|
|
665
|
+
// keep as written
|
|
666
|
+
}
|
|
667
|
+
mapPath = path.resolve(path.dirname(scriptPath), rel);
|
|
668
|
+
}
|
|
669
|
+
const map = await ctx.readContainerFile(mapPath);
|
|
670
|
+
if (!map) continue;
|
|
671
|
+
mapText = map.toString("utf8");
|
|
672
|
+
mapUrl = pathToFileURL(mapPath).href;
|
|
628
673
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
674
|
+
let data: Record<string, unknown>;
|
|
675
|
+
try {
|
|
676
|
+
data = JSON.parse(mapText) as Record<string, unknown>;
|
|
677
|
+
} catch {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (!data || typeof data !== "object" || !("mappings" in data)) continue;
|
|
681
|
+
const script = await ctx.readContainerFile(scriptPath);
|
|
682
|
+
if (!script) continue;
|
|
683
|
+
const { sourcesContent: _dropped, ...slim } = data;
|
|
684
|
+
out.set(url, { url: mapUrl, data: slim, lineLengths: lineLengthsOf(script.toString("utf8")) });
|
|
639
685
|
}
|
|
640
|
-
|
|
641
|
-
const { sourcesContent: _dropped, ...slim } = data;
|
|
642
|
-
return { url: mapUrl, data: slim, lineLengths: lineLengthsOf(text) };
|
|
686
|
+
return out;
|
|
643
687
|
}
|
|
644
688
|
|
|
645
689
|
/** The content hash of a map entry, over its JSON with the spectest
|
|
@@ -654,10 +698,10 @@ function sourceMapEntryHash(entry: Record<string, unknown>): string {
|
|
|
654
698
|
/**
|
|
655
699
|
* Give every kept script of a compacted document a `source-map-cache`
|
|
656
700
|
* entry it lacks: a stub when the map shipped earlier on this branch,
|
|
657
|
-
* else the map `resolve` finds (
|
|
658
|
-
* remembered), else nothing — and that outcome
|
|
659
|
-
* script with no map is looked up once per
|
|
660
|
-
* `resolve` supplied.
|
|
701
|
+
* else the map `resolve` finds (asked once for all such scripts; shipped
|
|
702
|
+
* whole, with its hash, and remembered), else nothing — and that outcome
|
|
703
|
+
* is remembered too, so a script with no map is looked up once per
|
|
704
|
+
* branch. Returns how many maps `resolve` supplied.
|
|
661
705
|
*/
|
|
662
706
|
export async function attachSourceMaps(
|
|
663
707
|
doc: Record<string, unknown>,
|
|
@@ -667,7 +711,7 @@ export async function attachSourceMaps(
|
|
|
667
711
|
): Promise<number> {
|
|
668
712
|
const result = Array.isArray(doc.result) ? (doc.result as V8Script[]) : [];
|
|
669
713
|
const cache = ((doc["source-map-cache"] as Record<string, unknown> | undefined) ?? {}) as Record<string, unknown>;
|
|
670
|
-
|
|
714
|
+
const unknown: string[] = [];
|
|
671
715
|
for (const s of result) {
|
|
672
716
|
const url = s.url;
|
|
673
717
|
if (typeof url !== "string" || url in cache) continue;
|
|
@@ -676,16 +720,23 @@ export async function attachSourceMaps(
|
|
|
676
720
|
cache[url] = { [SOURCE_MAP_REF_KEY]: prior };
|
|
677
721
|
continue;
|
|
678
722
|
}
|
|
679
|
-
if (missing.has(url)) continue;
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
723
|
+
if (missing.has(url) || unknown.includes(url)) continue;
|
|
724
|
+
unknown.push(url);
|
|
725
|
+
}
|
|
726
|
+
let found = 0;
|
|
727
|
+
if (unknown.length > 0) {
|
|
728
|
+
const entries = await resolve(unknown);
|
|
729
|
+
for (const url of unknown) {
|
|
730
|
+
const entry = entries.get(url);
|
|
731
|
+
if (!entry) {
|
|
732
|
+
missing.add(url);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const hash = sourceMapEntryHash(entry as unknown as Record<string, unknown>);
|
|
736
|
+
cache[url] = { ...entry, [SOURCE_MAP_HASH_KEY]: hash };
|
|
737
|
+
shipped.set(url, hash);
|
|
738
|
+
found++;
|
|
684
739
|
}
|
|
685
|
-
const hash = sourceMapEntryHash(entry as unknown as Record<string, unknown>);
|
|
686
|
-
cache[url] = { ...entry, [SOURCE_MAP_HASH_KEY]: hash };
|
|
687
|
-
shipped.set(url, hash);
|
|
688
|
-
found++;
|
|
689
740
|
}
|
|
690
741
|
if (Object.keys(cache).length > 0) doc["source-map-cache"] = cache;
|
|
691
742
|
return found;
|