@specific.dev/spectest 0.131.14 → 0.131.16
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 +22 -0
- package/dist/coverage-next.js +62 -5
- package/dist/coverage.js +12 -2
- package/package.json +1 -1
- package/src/coverage-next.test.ts +221 -1
- package/src/coverage-next.ts +61 -4
- package/src/coverage.test.ts +4 -2
- package/src/coverage.ts +12 -2
package/README.md
CHANGED
|
@@ -83,6 +83,28 @@ Pub/Sub integrations are project-owned as of SDK 0.98.0. The former
|
|
|
83
83
|
Use a local service definition or `serviceGroup()` wrapper; the repository’s
|
|
84
84
|
`examples/google-pubsub` demonstrates this using public SDK APIs.
|
|
85
85
|
|
|
86
|
+
## Next.js coverage (0.131.16)
|
|
87
|
+
|
|
88
|
+
Use `coverage.next({ appDir: "ops", sourceRoot: "." })` in your Spectest
|
|
89
|
+
service configuration to collect server and browser coverage. Keep Next's
|
|
90
|
+
server and browser source maps in the runtime image. `appDir` is relative to
|
|
91
|
+
the container working directory; `sourceRoot` identifies the repository
|
|
92
|
+
directory represented by the build's source-map root.
|
|
93
|
+
|
|
94
|
+
Standalone servers launched through Node wrappers can change working directory:
|
|
95
|
+
the adapter preserves the launcher's generated dependency identity in children.
|
|
96
|
+
Turbopack's generated image modules, including SVG data-URL wrappers, are
|
|
97
|
+
excluded automatically wherever the asset lives. Authored asset and application
|
|
98
|
+
sources are not excluded.
|
|
99
|
+
|
|
100
|
+
For Next 16.4.3, the adapter also corrects a reproduced Turbopack Edge source-map
|
|
101
|
+
path defect using Next's resolved build manifest (`appDir`, `distDir`, and
|
|
102
|
+
`turbopack.root`). This happens while the collector reads a map, before it creates
|
|
103
|
+
the coverage record. No application build-script workaround is required. The
|
|
104
|
+
correction is scoped to that verified version and layout; correctly emitted maps
|
|
105
|
+
and other versions retain normal source-map interpretation. Unknown source paths
|
|
106
|
+
remain unresolved and cannot authorize subset selection.
|
|
107
|
+
|
|
86
108
|
## Supabase function coverage (0.122.0)
|
|
87
109
|
|
|
88
110
|
`supabase({ dir: "supabase" })` automatically collects coverage for its edge
|
package/dist/coverage-next.js
CHANGED
|
@@ -9,15 +9,20 @@ export const NEXT_COVERAGE_HELPER_PATH = "/spectest/next-coverage.cjs";
|
|
|
9
9
|
export const NEXT_COVERAGE_HELPER = String.raw `"use strict";
|
|
10
10
|
const fs = require("node:fs"), path = require("node:path");
|
|
11
11
|
const options = JSON.parse(process.env.SPECTEST_NEXT_COVERAGE_OPTIONS || "{}");
|
|
12
|
-
|
|
12
|
+
// Preloads run again in spawned Node processes. Carry the original runtime
|
|
13
|
+
// root into children instead of interpreting appDir against each child's cwd.
|
|
14
|
+
const root = process.env.SPECTEST_COVERAGE_ROOT || process.cwd();
|
|
15
|
+
process.env.SPECTEST_COVERAGE_ROOT = root;
|
|
16
|
+
const app = path.resolve(root, options.appDir || ".");
|
|
13
17
|
const dist = path.resolve(app, options.distDir || ".next");
|
|
14
|
-
|
|
18
|
+
let build;
|
|
19
|
+
function manifest() { return build ||= JSON.parse(fs.readFileSync(path.join(dist, "required-server-files.json"), "utf8")); }
|
|
15
20
|
function launcher(m) {
|
|
16
21
|
const file = path.join(app, "server.js");
|
|
17
22
|
if (m.config.output !== "standalone" || !fs.existsSync(file)) return null;
|
|
18
23
|
const text = fs.readFileSync(file, "utf8");
|
|
19
24
|
if (!text.includes("__NEXT_PRIVATE_STANDALONE_CONFIG") || !text.includes("next/dist/server/lib/start-server")) return null;
|
|
20
|
-
return path.relative(
|
|
25
|
+
return path.relative(root, file);
|
|
21
26
|
}
|
|
22
27
|
try {
|
|
23
28
|
const file = launcher(manifest());
|
|
@@ -25,6 +30,55 @@ try {
|
|
|
25
30
|
...new Set([...JSON.parse(process.env.SPECTEST_COVERAGE_GENERATED_FILES || "[]"), file])
|
|
26
31
|
]);
|
|
27
32
|
} catch {} // Capture reports a useful error; do not change application startup.
|
|
33
|
+
|
|
34
|
+
// Next 16.4.3 gives its Edge chunker the offset from distDir to turbopack.root,
|
|
35
|
+
// although the chunker's output root is distDir/server/edge. Recover the exact
|
|
36
|
+
// serialized prefix from the build configuration, not from repository suffixes.
|
|
37
|
+
// See docs/validation/next-coverage-20261102.md for the unmodified-build repro
|
|
38
|
+
// and upstream implementation. Other versions/layouts keep normal map semantics.
|
|
39
|
+
let edgePolicy;
|
|
40
|
+
function edgeSourcePolicy() {
|
|
41
|
+
if (edgePolicy !== undefined) return edgePolicy;
|
|
42
|
+
edgePolicy = null;
|
|
43
|
+
try {
|
|
44
|
+
const version = JSON.parse(fs.readFileSync(require.resolve("next/package.json", {paths:[app]}), "utf8")).version;
|
|
45
|
+
if (version !== "16.4.3") return null;
|
|
46
|
+
const m = manifest(), config = m.config;
|
|
47
|
+
const buildRoot = config.turbopack && config.turbopack.root;
|
|
48
|
+
if (typeof m.appDir !== "string" || !path.isAbsolute(m.appDir) ||
|
|
49
|
+
typeof buildRoot !== "string" || !path.isAbsolute(buildRoot) ||
|
|
50
|
+
typeof config.distDir !== "string") return null;
|
|
51
|
+
const offset = path.relative(path.resolve(m.appDir, config.distDir), buildRoot);
|
|
52
|
+
if (!offset || !offset.split(path.sep).every(p => p === "..")) return null;
|
|
53
|
+
edgePolicy = {root:path.join(dist, "server/edge"), offset};
|
|
54
|
+
} catch {} // Unknown build metadata cannot authorize a source correction.
|
|
55
|
+
return edgePolicy;
|
|
56
|
+
}
|
|
57
|
+
exports.normalizeSourceMap = function(data, mapFile) {
|
|
58
|
+
const policy = edgeSourcePolicy();
|
|
59
|
+
if (!policy) return;
|
|
60
|
+
const relative = path.relative(policy.root, mapFile);
|
|
61
|
+
if (!relative.startsWith("chunks" + path.sep) || path.isAbsolute(relative) || relative.split(path.sep).includes("..")) return;
|
|
62
|
+
const prefix = path.relative(path.dirname(mapFile), policy.root).split(path.sep).join("/") + "/" + policy.offset.split(path.sep).join("/") + "/";
|
|
63
|
+
function visit(map, depth) {
|
|
64
|
+
if (!map || typeof map !== "object" || depth > 32 || map.sourceRoot) return;
|
|
65
|
+
if (Array.isArray(map.sections)) {
|
|
66
|
+
for (const section of map.sections) if (section && !section.url) visit(section.map, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(map.sources)) return;
|
|
69
|
+
map.sources = map.sources.map(source => {
|
|
70
|
+
if (typeof source !== "string" || !source.startsWith(prefix)) return source;
|
|
71
|
+
const suffix = source.slice(prefix.length);
|
|
72
|
+
let decoded;
|
|
73
|
+
try { decoded = decodeURIComponent(suffix); } catch { return source; }
|
|
74
|
+
// Correctly emitted paths have two extra '..' components. Never rewrite
|
|
75
|
+
// those, absolute/virtual sources, escapes or another root's sources.
|
|
76
|
+
if (/[\\:\0?#]/.test(decoded) || decoded.split("/").some(p => !p || p === "." || p === "..")) return source;
|
|
77
|
+
return "turbopack:///[project]/" + suffix;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
visit(data, 0);
|
|
81
|
+
};
|
|
28
82
|
if (require.main === module) {
|
|
29
83
|
try {
|
|
30
84
|
const m = manifest();
|
|
@@ -50,7 +104,9 @@ const GENERATED_SOURCES = [
|
|
|
50
104
|
"*/data:* <text/javascript>", // Turbopack virtual modules, including action proxies.
|
|
51
105
|
"*/.next-internal/server/app/*/actions.js (server actions loader)",
|
|
52
106
|
"*/app/*--route-entry.js",
|
|
53
|
-
"
|
|
107
|
+
"turbopack:///[project]/*.mjs (structured image object)",
|
|
108
|
+
"turbopack:///[project]/*.mjs (structured image object with data url)",
|
|
109
|
+
"*/app/*.mjs (structured image object)", // Older Next map identities.
|
|
54
110
|
];
|
|
55
111
|
/** Server + browser coverage for a built Next app. Build-time maps are required;
|
|
56
112
|
* enable native Next source maps and retain them in the runtime image.
|
|
@@ -85,7 +141,8 @@ export function next(options = {}) {
|
|
|
85
141
|
env: { ...configured.env,
|
|
86
142
|
NODE_OPTIONS: `--require ${NEXT_COVERAGE_HELPER_PATH} ${configured.env.NODE_OPTIONS}`,
|
|
87
143
|
SPECTEST_NEXT_COVERAGE_OPTIONS: JSON.stringify(options),
|
|
88
|
-
SPECTEST_COVERAGE_SERVICE: info.key,
|
|
144
|
+
SPECTEST_COVERAGE_SERVICE: info.key,
|
|
145
|
+
SPECTEST_COVERAGE_SOURCE_MAP_ADAPTER: NEXT_COVERAGE_HELPER_PATH, },
|
|
89
146
|
files: [...configured.files, { path: NEXT_COVERAGE_HELPER_PATH, content: NEXT_COVERAGE_HELPER }],
|
|
90
147
|
};
|
|
91
148
|
},
|
package/dist/coverage.js
CHANGED
|
@@ -339,9 +339,13 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
339
339
|
if (!bunRuntime) post("Debugger.enable", { maxScriptsCacheSize: 16 * 1024 * 1024 });
|
|
340
340
|
|
|
341
341
|
const GENERATED = new Map();
|
|
342
|
+
// Generated names are service-root-relative unit identities, not paths
|
|
343
|
+
// relative to a wrapper's child process. Preserve that root through execs.
|
|
344
|
+
const generatedRoot = process.env.SPECTEST_COVERAGE_ROOT || process.cwd();
|
|
345
|
+
process.env.SPECTEST_COVERAGE_ROOT = generatedRoot;
|
|
342
346
|
try {
|
|
343
347
|
for (const file of JSON.parse(process.env.SPECTEST_COVERAGE_GENERATED_FILES || "[]")) {
|
|
344
|
-
GENERATED.set(url.pathToFileURL(path.resolve(file)).href, file);
|
|
348
|
+
GENERATED.set(url.pathToFileURL(path.resolve(generatedRoot, file)).href, file);
|
|
345
349
|
}
|
|
346
350
|
} catch (e) {}
|
|
347
351
|
const generatedUnits = new Map();
|
|
@@ -349,6 +353,8 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
349
353
|
return "generated:" + process.env.SPECTEST_COVERAGE_SERVICE + ":" + file + "#" + crypto.createHash("sha256").update(text).digest("hex");
|
|
350
354
|
}
|
|
351
355
|
const SELF = url.pathToFileURL(__filename).href;
|
|
356
|
+
const SOURCE_MAP_ADAPTER = process.env.SPECTEST_COVERAGE_SOURCE_MAP_ADAPTER;
|
|
357
|
+
const SOURCE_MAP_ADAPTER_URL = SOURCE_MAP_ADAPTER ? url.pathToFileURL(path.resolve(SOURCE_MAP_ADAPTER)).href : null;
|
|
352
358
|
const SCRIPTS = path.join(dir, ${JSON.stringify(NODE_COVERAGE_SCRIPTS_SUBDIR)});
|
|
353
359
|
const MARKERS = path.join(dir, ${JSON.stringify(NODE_COVERAGE_MARKERS_SUBDIR)});
|
|
354
360
|
// V8 script ids identify a compiled script within this process. A URL
|
|
@@ -361,7 +367,7 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
361
367
|
fs.writeFileSync(path.join(dir, ".attached-" + (bunRuntime ? "bun" : "node")), "");
|
|
362
368
|
|
|
363
369
|
function isApp(u) {
|
|
364
|
-
return typeof u === "string" && u.startsWith("file://") && u.indexOf("/node_modules/") < 0 && u.indexOf("/.cache/") < 0 && u !== SELF;
|
|
370
|
+
return typeof u === "string" && u.startsWith("file://") && u.indexOf("/node_modules/") < 0 && u.indexOf("/.cache/") < 0 && u !== SELF && u !== SOURCE_MAP_ADAPTER_URL;
|
|
365
371
|
}
|
|
366
372
|
function isDep(u) {
|
|
367
373
|
return typeof u === "string" && u.startsWith("file://") && u.indexOf("/node_modules/") >= 0 && u !== SELF;
|
|
@@ -458,6 +464,10 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
458
464
|
} catch (e) {
|
|
459
465
|
return null;
|
|
460
466
|
}
|
|
467
|
+
// Framework policy runs before dependency attribution and before the final
|
|
468
|
+
// descriptor is hashed. The collector remains the sole owner of record IDs,
|
|
469
|
+
// deduplication, dump references and cleanup (including exit-time captures).
|
|
470
|
+
if (SOURCE_MAP_ADAPTER) require(SOURCE_MAP_ADAPTER).normalizeSourceMap(data, mapUrl ? url.fileURLToPath(mapUrl) : scriptPath);
|
|
461
471
|
// A source inside node_modules is a dependency: its unit id rides
|
|
462
472
|
// beside the map (\`sourceUnits[i]\`, null for the project's own), so
|
|
463
473
|
// the control plane never has to know what node_modules is.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { next, NEXT_COVERAGE_HELPER, NEXT_COVERAGE_HELPER_PATH, type NextBuildMetadata } from "./coverage-next.js";
|
|
3
|
-
import { applyCoverageAdapters, resolveCoverageSourceOptions, attachCoverageSourceOptions, node, browser, type CoverageSourceContext } from "./coverage.js";
|
|
3
|
+
import { applyCoverageAdapters, resolveCoverageSourceOptions, attachCoverageSourceOptions, node, browser, NODE_COVERAGE_HOOK, NODE_COVERAGE_SCRIPTS_SUBDIR, NODE_COVERAGE_MARKERS_SUBDIR, type CoverageSourceContext } from "./coverage.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { applyCoverageDelta, encodeCoverageBundle } from "./harness/coverage.js";
|
|
5
6
|
import { gunzipSync } from "node:zlib";
|
|
6
7
|
import { promises as fs } from "node:fs";
|
|
@@ -18,6 +19,65 @@ function context(): CoverageSourceContext {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
describe("Next adapter", () => {
|
|
22
|
+
test.each(["next", "node"])("%s preserves generated unit identity through children with different working directories", async (kind) => {
|
|
23
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "next-child-cov-"));
|
|
24
|
+
try {
|
|
25
|
+
const app = path.join(dir, "ops"), reports = path.join(dir, "reports");
|
|
26
|
+
const helper = path.join(dir, "next-coverage.cjs"), hook = path.join(dir, "coverage-hook.cjs");
|
|
27
|
+
for (const folder of ["ops/.next/server", "ops/.next/static", "elsewhere", `reports/${NODE_COVERAGE_SCRIPTS_SUBDIR}`, `reports/${NODE_COVERAGE_MARKERS_SUBDIR}`]) {
|
|
28
|
+
await fs.mkdir(path.join(dir, folder), {recursive:true});
|
|
29
|
+
}
|
|
30
|
+
await fs.writeFile(helper, NEXT_COVERAGE_HELPER);
|
|
31
|
+
await fs.writeFile(hook, NODE_COVERAGE_HOOK);
|
|
32
|
+
await fs.writeFile(path.join(app, ".next/required-server-files.json"), JSON.stringify({config:{output:"standalone"}}));
|
|
33
|
+
for (const folder of ["server", "static"]) await fs.writeFile(path.join(app, `.next/${folder}/a.js.map`), "{}");
|
|
34
|
+
const launcher = '// __NEXT_PRIVATE_STANDALONE_CONFIG next/dist/server/lib/start-server\nrequire("./authored.cjs");\n';
|
|
35
|
+
await fs.writeFile(path.join(app, "server.js"), launcher);
|
|
36
|
+
await fs.writeFile(path.join(app, "authored.cjs"), "globalThis.applicationRan = true;\n");
|
|
37
|
+
await fs.writeFile(path.join(app, "bridge.cjs"), `
|
|
38
|
+
const {execFileSync} = require("node:child_process");
|
|
39
|
+
execFileSync(process.execPath, [${JSON.stringify(path.join(app,"server.js"))}], {cwd:${JSON.stringify(path.join(dir,"elsewhere"))}, stdio:"inherit"});
|
|
40
|
+
`);
|
|
41
|
+
await fs.writeFile(path.join(dir, "wrapper.cjs"), `
|
|
42
|
+
const {execFileSync} = require("node:child_process");
|
|
43
|
+
execFileSync(process.execPath, [${JSON.stringify(path.join(app,"bridge.cjs"))}], {cwd:${JSON.stringify(app)}, stdio:"inherit"});
|
|
44
|
+
`);
|
|
45
|
+
const adapter = kind === "next" ? next({appDir:"ops"}) : node({generated:["ops/server.js"]});
|
|
46
|
+
const configured = applyCoverageAdapters("web", {image, coverage:{adapters:[adapter]}});
|
|
47
|
+
const env = {...process.env, ...configured.env,
|
|
48
|
+
SPECTEST_COVERAGE_DIR:reports,
|
|
49
|
+
...(kind === "next" ? { SPECTEST_COVERAGE_SOURCE_MAP_ADAPTER: helper } : {}),
|
|
50
|
+
NODE_OPTIONS: `${kind === "next" ? `--require ${helper} ` : ""}--require ${hook}`,
|
|
51
|
+
} as NodeJS.ProcessEnv;
|
|
52
|
+
const ctx: CoverageSourceContext = {
|
|
53
|
+
service:"web", signal:new AbortController().signal,
|
|
54
|
+
projectFiles:async()=>[], readProjectFile:async()=>null,
|
|
55
|
+
exec:async command=>({stdout:execFileSync("sh", ["-c", command.replaceAll(NEXT_COVERAGE_HELPER_PATH, helper)], {cwd:dir,env,stdio:["ignore","pipe","pipe"]}).toString(),stderr:""}),
|
|
56
|
+
};
|
|
57
|
+
const inventory = (await adapter.inventory!(ctx)).filter(unit=>unit.startsWith("generated:"));
|
|
58
|
+
expect(inventory).toHaveLength(1);
|
|
59
|
+
expect(inventory[0]).toMatch(/^generated:web:ops\/server.js#[a-f0-9]{64}$/);
|
|
60
|
+
execFileSync("node", [path.join(dir,"wrapper.cjs")], {cwd:dir,env,stdio:["ignore","pipe","pipe"]});
|
|
61
|
+
const dumps = await Promise.all((await fs.readdir(reports)).filter(n=>/^node-.*\.json$/.test(n)).map(async n=>JSON.parse(await fs.readFile(path.join(reports,n),"utf8"))));
|
|
62
|
+
const urls = dumps.flatMap(d=>d.result.map((s:{url:string})=>s.url));
|
|
63
|
+
const units = new Set(dumps.flatMap(d=>d.units ?? []).filter((unit:string)=>unit.startsWith("generated:")));
|
|
64
|
+
expect([...units]).toEqual(inventory);
|
|
65
|
+
expect(urls.some(url=>url.endsWith("/ops/server.js"))).toBe(false);
|
|
66
|
+
expect(urls.some(url=>url.endsWith("/ops/authored.cjs"))).toBe(true);
|
|
67
|
+
expect(urls.some(url=>url.endsWith("/wrapper.cjs"))).toBe(true);
|
|
68
|
+
} finally { await fs.rm(dir, {recursive:true,force:true}); }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("image wrapper exclusions cover monorepo public assets and are archived on both report types", async () => {
|
|
72
|
+
const policies = await resolveCoverageSourceOptions([next({appDir:"ops"})], context());
|
|
73
|
+
const capture = attachCoverageSourceOptions({reports:[{name:"node-1.json"},{name:"browser.lcov"}]}, policies);
|
|
74
|
+
for (const report of capture.reports) {
|
|
75
|
+
expect(report.sourceOptions!.excludeSources).toContain("turbopack:///[project]/*.mjs (structured image object)");
|
|
76
|
+
expect(report.sourceOptions!.excludeSources).toContain("turbopack:///[project]/*.mjs (structured image object with data url)");
|
|
77
|
+
expect(report.sourceOptions!.excludeSources).not.toContain("*/public/*");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
21
81
|
test("one adapter configures both collectors, uses its declared source root, and owns only its reports", async () => {
|
|
22
82
|
const adapter = next({sourceRoot:"packages"});
|
|
23
83
|
const configured = applyCoverageAdapters("web", {image,coverage:{adapters:[adapter]}});
|
|
@@ -88,4 +148,164 @@ describe("Next adapter", () => {
|
|
|
88
148
|
expect(probe).toThrow();
|
|
89
149
|
} finally {await fs.rm(dir,{recursive:true,force:true});}
|
|
90
150
|
});
|
|
151
|
+
|
|
152
|
+
test("generated image wrappers are excluded across asset locations without excluding authored modules", async () => {
|
|
153
|
+
const policies = await resolveCoverageSourceOptions([next()], context());
|
|
154
|
+
const patterns = policies[0]!.options!.excludeSources!;
|
|
155
|
+
// The wire contract treats only '*' as special (including '/').
|
|
156
|
+
const excluded = (source: string) => patterns.some(pattern => new RegExp("^" +
|
|
157
|
+
pattern.split("*").map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$").test(source));
|
|
158
|
+
for (const folder of ["app", "ops/public", "packages/ui/assets", "src/images"]) {
|
|
159
|
+
for (const suffix of ["structured image object", "structured image object with data url"]) {
|
|
160
|
+
expect(excluded(`turbopack:///[project]/${folder}/invoice.svg.mjs (${suffix})`)).toBe(true);
|
|
161
|
+
}
|
|
162
|
+
expect(excluded(`turbopack:///[project]/${folder}/invoice.svg`)).toBe(false);
|
|
163
|
+
expect(excluded(`turbopack:///[project]/${folder}/image.mjs`)).toBe(false);
|
|
164
|
+
}
|
|
165
|
+
expect(excluded("turbopack:///[project]/ops/src/middleware.ts")).toBe(false);
|
|
166
|
+
expect(excluded("https://example.com/image.mjs (structured image object with data url)")).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a cwd-changing wrapper keeps launcher units consistent and Edge source records valid across captures", async () => {
|
|
170
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "next-child-cov-"));
|
|
171
|
+
const reports = path.join(dir, "reports");
|
|
172
|
+
const helper = path.join(dir, "next-helper.cjs");
|
|
173
|
+
const hook = path.join(dir, "hook.cjs");
|
|
174
|
+
const adapter = next({ appDir: "ops" });
|
|
175
|
+
const config = adapter.configure!({ image }, { key: "web" });
|
|
176
|
+
const sourceText = "export function middleware() { return 42; }\n";
|
|
177
|
+
const source = "ops/src/middleware.ts";
|
|
178
|
+
const digest = (text: string) => createHash("sha256").update(text).digest("hex");
|
|
179
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ...config.env, SPECTEST_COVERAGE_DIR: reports,
|
|
180
|
+
NODE_OPTIONS: `--require ${helper} --require ${hook}`,
|
|
181
|
+
SPECTEST_COVERAGE_SOURCE_MAP_ADAPTER: helper };
|
|
182
|
+
delete env.SPECTEST_COVERAGE_ROOT;
|
|
183
|
+
const ctx = {
|
|
184
|
+
service: "web", reportDir: reports, signal: new AbortController().signal,
|
|
185
|
+
projectFiles: async () => [source],
|
|
186
|
+
readProjectFile: async (file: string) => file === source ? sourceText : null,
|
|
187
|
+
writeReport: (name: string, content: string) => fs.writeFile(path.join(reports, name), content),
|
|
188
|
+
async exec(command: string) {
|
|
189
|
+
const proc = Bun.spawn(["sh", "-c", command.replace(NEXT_COVERAGE_HELPER_PATH, helper)],
|
|
190
|
+
{ cwd: dir, env: { ...env, NODE_OPTIONS: "" }, stdout: "pipe", stderr: "pipe" });
|
|
191
|
+
const stdout = await new Response(proc.stdout).text();
|
|
192
|
+
const stderr = await new Response(proc.stderr).text();
|
|
193
|
+
if (await proc.exited !== 0) throw new Error(stderr);
|
|
194
|
+
return { stdout, stderr };
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
await fs.mkdir(path.join(dir, "ops/.next/server/edge/chunks"), { recursive: true });
|
|
199
|
+
await fs.mkdir(path.join(dir, "ops/.next/static"), { recursive: true });
|
|
200
|
+
await fs.mkdir(reports);
|
|
201
|
+
await fs.mkdir(path.join(dir, "node_modules/next"), { recursive: true });
|
|
202
|
+
await fs.writeFile(path.join(dir, "node_modules/next/package.json"), JSON.stringify({ version: "16.4.3" }));
|
|
203
|
+
await fs.writeFile(helper, NEXT_COVERAGE_HELPER);
|
|
204
|
+
await fs.writeFile(hook, NODE_COVERAGE_HOOK);
|
|
205
|
+
await fs.writeFile(path.join(dir, "ops/.next/required-server-files.json"), JSON.stringify({ appDir: "/build/ops", config: { output: "standalone", distDir: ".next", turbopack: { root: "/build" } } }));
|
|
206
|
+
await fs.writeFile(path.join(dir, "ops/.next/static/browser.js.map"), "{}");
|
|
207
|
+
const edge = path.join(dir, "ops/.next/server/edge/chunks/middleware.js");
|
|
208
|
+
await fs.writeFile(edge, "globalThis.edgeValue = 42;\n");
|
|
209
|
+
await fs.writeFile(edge + ".map", JSON.stringify({ version: 3, sections: [{ offset: { line: 0, column: 0 }, map: {
|
|
210
|
+
version: 3, sources: ["../../../ops/src/middleware.ts"], sourcesContent: [sourceText], mappings: "AAAA", names: [],
|
|
211
|
+
} }] }));
|
|
212
|
+
const launcher = '// __NEXT_PRIVATE_STANDALONE_CONFIG next/dist/server/lib/start-server\nrequire("./.next/server/edge/chunks/middleware.js");\n';
|
|
213
|
+
await fs.writeFile(path.join(dir, "ops/server.js"), launcher);
|
|
214
|
+
// Also exercise process.chdir before spawn, not just the spawn cwd option.
|
|
215
|
+
await fs.writeFile(path.join(dir, "wrapper.cjs"), `
|
|
216
|
+
process.chdir("ops");
|
|
217
|
+
require("node:child_process").execFileSync(process.execPath, ["server.js"], { cwd: process.cwd(), stdio: "inherit" });
|
|
218
|
+
`);
|
|
219
|
+
const run = () => execFileSync("node", ["wrapper.cjs"], { cwd: dir, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
220
|
+
const unit = `generated:web:ops/server.js#${digest(launcher)}`;
|
|
221
|
+
let correctedId: string | undefined;
|
|
222
|
+
for (let capture = 0; capture < 2; capture++) {
|
|
223
|
+
run();
|
|
224
|
+
// The exit hook must already have written the final descriptor and ID,
|
|
225
|
+
// before harness metadata/capture executes or reads project files.
|
|
226
|
+
const rawRecords = await Promise.all((await fs.readdir(path.join(reports, "scripts")))
|
|
227
|
+
.map(async name => [name, await fs.readFile(path.join(reports, "scripts", name), "utf8")] as const));
|
|
228
|
+
await resolveCoverageSourceOptions([adapter], ctx);
|
|
229
|
+
await adapter.capture!(ctx);
|
|
230
|
+
const dumps = await Promise.all((await fs.readdir(reports)).filter(n => /^node-.*\.json$/.test(n))
|
|
231
|
+
.map(async n => JSON.parse(await fs.readFile(path.join(reports, n), "utf8"))));
|
|
232
|
+
expect(dumps.flatMap(d => d.units ?? [])).toContain(unit);
|
|
233
|
+
expect(dumps.flatMap(d => d.result).some(s => s.url.endsWith("/ops/server.js"))).toBe(false);
|
|
234
|
+
const executed = dumps.flatMap(d => d.result).find(s => s.url.endsWith("/middleware.js"));
|
|
235
|
+
expect(executed).toBeDefined();
|
|
236
|
+
if (capture === 0) {
|
|
237
|
+
correctedId = executed.spectestScript;
|
|
238
|
+
const text = await fs.readFile(path.join(reports, "scripts", `${correctedId}.json`), "utf8");
|
|
239
|
+
expect(digest(text).slice(0, 32)).toBe(correctedId);
|
|
240
|
+
const map = JSON.parse(text).spectestScript.sourceMap.data.sections[0].map;
|
|
241
|
+
expect(map.sources).toEqual(["turbopack:///[project]/ops/src/middleware.ts"]);
|
|
242
|
+
expect(map.sourcesContent).toBeUndefined();
|
|
243
|
+
expect(map.x_spectestSourceHashes).toBeUndefined();
|
|
244
|
+
expect(rawRecords.find(([name]) => name === `${correctedId}.json`)?.[1]).toBe(text);
|
|
245
|
+
} else {
|
|
246
|
+
// Node deduplicates the original record; the second capture must
|
|
247
|
+
// reference the corrected record archived with the first capture.
|
|
248
|
+
expect(executed.spectestScript).toBe(correctedId);
|
|
249
|
+
expect(await fs.readdir(path.join(reports, "scripts"))).toEqual([]);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
expect(await adapter.inventory!(ctx)).toContain(unit);
|
|
253
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("Next Edge build configuration", () => {
|
|
258
|
+
async function normalize({ appDir = "ops", distDir = ".next", buildApp = "/build/ops", buildRoot = "/build",
|
|
259
|
+
version = "16.4.3", source, sourceRoot, outside = false, indexed = false,
|
|
260
|
+
}: { appDir?: string; distDir?: string; buildApp?: string; buildRoot?: string; version?: string;
|
|
261
|
+
source: string; sourceRoot?: string; outside?: boolean; indexed?: boolean }) {
|
|
262
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "next-map-config-"));
|
|
263
|
+
try {
|
|
264
|
+
const app = path.join(dir, appDir);
|
|
265
|
+
const dist = path.join(app, distDir);
|
|
266
|
+
const helper = path.join(dir, "helper.cjs");
|
|
267
|
+
await fs.mkdir(dist, { recursive: true });
|
|
268
|
+
await fs.mkdir(path.join(app, "node_modules/next"), { recursive: true });
|
|
269
|
+
await fs.writeFile(path.join(app, "node_modules/next/package.json"), JSON.stringify({ version }));
|
|
270
|
+
await fs.writeFile(path.join(dist, "required-server-files.json"), JSON.stringify({ appDir: buildApp,
|
|
271
|
+
config: { output: "standalone", distDir, turbopack: { root: buildRoot } } }));
|
|
272
|
+
await fs.writeFile(helper, NEXT_COVERAGE_HELPER);
|
|
273
|
+
const leaf = { version: 3, sources: [source], sourceRoot, names: [], mappings: "AAAA", ignoreList: [0] };
|
|
274
|
+
const map = indexed ? { version: 3, sections: [{ offset: { line: 2, column: 4 }, map: leaf }] } : leaf;
|
|
275
|
+
const script = `const map = ${JSON.stringify(map)};
|
|
276
|
+
require(${JSON.stringify(helper)}).normalizeSourceMap(map, ${JSON.stringify(path.join(dist, outside ? "server/chunks/a.js.map" : "server/edge/chunks/a.js.map"))});
|
|
277
|
+
process.stdout.write(JSON.stringify(map));`;
|
|
278
|
+
const result = JSON.parse(execFileSync("node", ["-e", script], { cwd: dir,
|
|
279
|
+
env: { ...process.env, NODE_OPTIONS: "", SPECTEST_COVERAGE_ROOT: dir,
|
|
280
|
+
SPECTEST_NEXT_COVERAGE_OPTIONS: JSON.stringify({ appDir, distDir }) } }).toString());
|
|
281
|
+
const resultLeaf = indexed ? result.sections[0].map : result;
|
|
282
|
+
expect(resultLeaf.ignoreList).toEqual([0]);
|
|
283
|
+
expect(resultLeaf.mappings).toBe("AAAA");
|
|
284
|
+
if (indexed) expect(result.sections[0].offset).toEqual({ line: 2, column: 4 });
|
|
285
|
+
return resultLeaf.sources[0];
|
|
286
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
test("uses the recorded build root and distDir independently of runtime relocation", async () => {
|
|
290
|
+
expect(await normalize({ source: "../../../ops/src/middleware.ts" }))
|
|
291
|
+
.toBe("turbopack:///[project]/ops/src/middleware.ts");
|
|
292
|
+
expect(await normalize({ appDir: ".", buildApp: "/build", source: "../../src/middleware.ts" }))
|
|
293
|
+
.toBe("turbopack:///[project]/src/middleware.ts");
|
|
294
|
+
expect(await normalize({ appDir: "deployed/web", buildApp: "/builder/packages/apps/web", buildRoot: "/builder/packages",
|
|
295
|
+
distDir: ".output", source: "../../../../apps/web/src/my%20middleware.ts", indexed: true }))
|
|
296
|
+
.toBe("turbopack:///[project]/apps/web/src/my%20middleware.ts");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("preserves fixed maps, external sources, explicit roots and unknown producers", async () => {
|
|
300
|
+
for (const source of ["../../../../../ops/src/middleware.ts", "turbopack:///[project]/ops/src/middleware.ts",
|
|
301
|
+
"file:///build/ops/src/middleware.ts", "https://example.com/a.ts", "../../../%2e%2e/secret.ts",
|
|
302
|
+
"../../../ops/src/%ZZ.ts", "../../../C%3A/secret.ts", "../../../ops//middleware.ts"]) {
|
|
303
|
+
expect(await normalize({ source })).toBe(source);
|
|
304
|
+
}
|
|
305
|
+
const source = "../../../ops/src/middleware.ts";
|
|
306
|
+
for (const extra of [{ sourceRoot: "/build" }, { outside: true }, { version: "16.4.4" },
|
|
307
|
+
{ buildRoot: "/another-root" }, { buildApp: "relative/app" }]) {
|
|
308
|
+
expect(await normalize({ source, ...extra })).toBe(source);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
91
311
|
});
|
package/src/coverage-next.ts
CHANGED
|
@@ -21,15 +21,20 @@ export const NEXT_COVERAGE_HELPER_PATH = "/spectest/next-coverage.cjs";
|
|
|
21
21
|
export const NEXT_COVERAGE_HELPER = String.raw`"use strict";
|
|
22
22
|
const fs = require("node:fs"), path = require("node:path");
|
|
23
23
|
const options = JSON.parse(process.env.SPECTEST_NEXT_COVERAGE_OPTIONS || "{}");
|
|
24
|
-
|
|
24
|
+
// Preloads run again in spawned Node processes. Carry the original runtime
|
|
25
|
+
// root into children instead of interpreting appDir against each child's cwd.
|
|
26
|
+
const root = process.env.SPECTEST_COVERAGE_ROOT || process.cwd();
|
|
27
|
+
process.env.SPECTEST_COVERAGE_ROOT = root;
|
|
28
|
+
const app = path.resolve(root, options.appDir || ".");
|
|
25
29
|
const dist = path.resolve(app, options.distDir || ".next");
|
|
26
|
-
|
|
30
|
+
let build;
|
|
31
|
+
function manifest() { return build ||= JSON.parse(fs.readFileSync(path.join(dist, "required-server-files.json"), "utf8")); }
|
|
27
32
|
function launcher(m) {
|
|
28
33
|
const file = path.join(app, "server.js");
|
|
29
34
|
if (m.config.output !== "standalone" || !fs.existsSync(file)) return null;
|
|
30
35
|
const text = fs.readFileSync(file, "utf8");
|
|
31
36
|
if (!text.includes("__NEXT_PRIVATE_STANDALONE_CONFIG") || !text.includes("next/dist/server/lib/start-server")) return null;
|
|
32
|
-
return path.relative(
|
|
37
|
+
return path.relative(root, file);
|
|
33
38
|
}
|
|
34
39
|
try {
|
|
35
40
|
const file = launcher(manifest());
|
|
@@ -37,6 +42,55 @@ try {
|
|
|
37
42
|
...new Set([...JSON.parse(process.env.SPECTEST_COVERAGE_GENERATED_FILES || "[]"), file])
|
|
38
43
|
]);
|
|
39
44
|
} catch {} // Capture reports a useful error; do not change application startup.
|
|
45
|
+
|
|
46
|
+
// Next 16.4.3 gives its Edge chunker the offset from distDir to turbopack.root,
|
|
47
|
+
// although the chunker's output root is distDir/server/edge. Recover the exact
|
|
48
|
+
// serialized prefix from the build configuration, not from repository suffixes.
|
|
49
|
+
// See docs/validation/next-coverage-20261102.md for the unmodified-build repro
|
|
50
|
+
// and upstream implementation. Other versions/layouts keep normal map semantics.
|
|
51
|
+
let edgePolicy;
|
|
52
|
+
function edgeSourcePolicy() {
|
|
53
|
+
if (edgePolicy !== undefined) return edgePolicy;
|
|
54
|
+
edgePolicy = null;
|
|
55
|
+
try {
|
|
56
|
+
const version = JSON.parse(fs.readFileSync(require.resolve("next/package.json", {paths:[app]}), "utf8")).version;
|
|
57
|
+
if (version !== "16.4.3") return null;
|
|
58
|
+
const m = manifest(), config = m.config;
|
|
59
|
+
const buildRoot = config.turbopack && config.turbopack.root;
|
|
60
|
+
if (typeof m.appDir !== "string" || !path.isAbsolute(m.appDir) ||
|
|
61
|
+
typeof buildRoot !== "string" || !path.isAbsolute(buildRoot) ||
|
|
62
|
+
typeof config.distDir !== "string") return null;
|
|
63
|
+
const offset = path.relative(path.resolve(m.appDir, config.distDir), buildRoot);
|
|
64
|
+
if (!offset || !offset.split(path.sep).every(p => p === "..")) return null;
|
|
65
|
+
edgePolicy = {root:path.join(dist, "server/edge"), offset};
|
|
66
|
+
} catch {} // Unknown build metadata cannot authorize a source correction.
|
|
67
|
+
return edgePolicy;
|
|
68
|
+
}
|
|
69
|
+
exports.normalizeSourceMap = function(data, mapFile) {
|
|
70
|
+
const policy = edgeSourcePolicy();
|
|
71
|
+
if (!policy) return;
|
|
72
|
+
const relative = path.relative(policy.root, mapFile);
|
|
73
|
+
if (!relative.startsWith("chunks" + path.sep) || path.isAbsolute(relative) || relative.split(path.sep).includes("..")) return;
|
|
74
|
+
const prefix = path.relative(path.dirname(mapFile), policy.root).split(path.sep).join("/") + "/" + policy.offset.split(path.sep).join("/") + "/";
|
|
75
|
+
function visit(map, depth) {
|
|
76
|
+
if (!map || typeof map !== "object" || depth > 32 || map.sourceRoot) return;
|
|
77
|
+
if (Array.isArray(map.sections)) {
|
|
78
|
+
for (const section of map.sections) if (section && !section.url) visit(section.map, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(map.sources)) return;
|
|
81
|
+
map.sources = map.sources.map(source => {
|
|
82
|
+
if (typeof source !== "string" || !source.startsWith(prefix)) return source;
|
|
83
|
+
const suffix = source.slice(prefix.length);
|
|
84
|
+
let decoded;
|
|
85
|
+
try { decoded = decodeURIComponent(suffix); } catch { return source; }
|
|
86
|
+
// Correctly emitted paths have two extra '..' components. Never rewrite
|
|
87
|
+
// those, absolute/virtual sources, escapes or another root's sources.
|
|
88
|
+
if (/[\\:\0?#]/.test(decoded) || decoded.split("/").some(p => !p || p === "." || p === "..")) return source;
|
|
89
|
+
return "turbopack:///[project]/" + suffix;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
visit(data, 0);
|
|
93
|
+
};
|
|
40
94
|
if (require.main === module) {
|
|
41
95
|
try {
|
|
42
96
|
const m = manifest();
|
|
@@ -69,7 +123,9 @@ const GENERATED_SOURCES = [
|
|
|
69
123
|
"*/data:* <text/javascript>", // Turbopack virtual modules, including action proxies.
|
|
70
124
|
"*/.next-internal/server/app/*/actions.js (server actions loader)",
|
|
71
125
|
"*/app/*--route-entry.js",
|
|
72
|
-
"
|
|
126
|
+
"turbopack:///[project]/*.mjs (structured image object)",
|
|
127
|
+
"turbopack:///[project]/*.mjs (structured image object with data url)",
|
|
128
|
+
"*/app/*.mjs (structured image object)", // Older Next map identities.
|
|
73
129
|
];
|
|
74
130
|
|
|
75
131
|
/** Server + browser coverage for a built Next app. Build-time maps are required;
|
|
@@ -104,6 +160,7 @@ export function next(options: NextCoverageOptions = {}): CoverageAdapter {
|
|
|
104
160
|
NODE_OPTIONS: `--require ${NEXT_COVERAGE_HELPER_PATH} ${configured.env!.NODE_OPTIONS}`,
|
|
105
161
|
SPECTEST_NEXT_COVERAGE_OPTIONS: JSON.stringify(options),
|
|
106
162
|
SPECTEST_COVERAGE_SERVICE: info.key,
|
|
163
|
+
SPECTEST_COVERAGE_SOURCE_MAP_ADAPTER: NEXT_COVERAGE_HELPER_PATH,
|
|
107
164
|
},
|
|
108
165
|
files: [...configured.files!, { path: NEXT_COVERAGE_HELPER_PATH, content: NEXT_COVERAGE_HELPER }],
|
|
109
166
|
};
|
package/src/coverage.test.ts
CHANGED
|
@@ -246,7 +246,9 @@ const hasNode = await new Promise<boolean>((resolve) => {
|
|
|
246
246
|
p.on("exit", (code) => resolve(code === 0));
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
// Readiness must follow the first tick; otherwise a fast capture can win the
|
|
250
|
+
// race against the timer and correctly report that tick has not run yet.
|
|
251
|
+
const SERVER_JS = "function tick() { return 1 }\ntick(); setInterval(tick, 5); process.stdout.write('up\\n');\n";
|
|
250
252
|
// A wrapper that spawns the real server as a child node process and
|
|
251
253
|
// waits on it — what `pnpm exec`, `npm run` and the `tsx` binary do.
|
|
252
254
|
// It takes its child down with it on SIGTERM, so the test leaves no
|
|
@@ -334,7 +336,7 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
|
|
|
334
336
|
expect(fns.every((f) => f.some((_, i) => i % 3 === 2 && f[i]! > 0))).toBe(true);
|
|
335
337
|
const rec = (await records(dir)).get(`file://${path.join(dir, "server.js")}`)!;
|
|
336
338
|
expect(ranNames(dump, rec).sort()).toEqual(["", "tick"]);
|
|
337
|
-
expect(rec.lineLengths).toEqual([28,
|
|
339
|
+
expect(rec.lineLengths).toEqual([28, 59, 0]);
|
|
338
340
|
expect(rec.functions.map((f) => f[0])).toEqual(["", "tick"]);
|
|
339
341
|
expect(rec.sourceMap).toBeNull();
|
|
340
342
|
expect(dump.result[0]!.spectestScript).toBeDefined();
|
package/src/coverage.ts
CHANGED
|
@@ -517,9 +517,13 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
517
517
|
if (!bunRuntime) post("Debugger.enable", { maxScriptsCacheSize: 16 * 1024 * 1024 });
|
|
518
518
|
|
|
519
519
|
const GENERATED = new Map();
|
|
520
|
+
// Generated names are service-root-relative unit identities, not paths
|
|
521
|
+
// relative to a wrapper's child process. Preserve that root through execs.
|
|
522
|
+
const generatedRoot = process.env.SPECTEST_COVERAGE_ROOT || process.cwd();
|
|
523
|
+
process.env.SPECTEST_COVERAGE_ROOT = generatedRoot;
|
|
520
524
|
try {
|
|
521
525
|
for (const file of JSON.parse(process.env.SPECTEST_COVERAGE_GENERATED_FILES || "[]")) {
|
|
522
|
-
GENERATED.set(url.pathToFileURL(path.resolve(file)).href, file);
|
|
526
|
+
GENERATED.set(url.pathToFileURL(path.resolve(generatedRoot, file)).href, file);
|
|
523
527
|
}
|
|
524
528
|
} catch (e) {}
|
|
525
529
|
const generatedUnits = new Map();
|
|
@@ -527,6 +531,8 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
527
531
|
return "generated:" + process.env.SPECTEST_COVERAGE_SERVICE + ":" + file + "#" + crypto.createHash("sha256").update(text).digest("hex");
|
|
528
532
|
}
|
|
529
533
|
const SELF = url.pathToFileURL(__filename).href;
|
|
534
|
+
const SOURCE_MAP_ADAPTER = process.env.SPECTEST_COVERAGE_SOURCE_MAP_ADAPTER;
|
|
535
|
+
const SOURCE_MAP_ADAPTER_URL = SOURCE_MAP_ADAPTER ? url.pathToFileURL(path.resolve(SOURCE_MAP_ADAPTER)).href : null;
|
|
530
536
|
const SCRIPTS = path.join(dir, ${JSON.stringify(NODE_COVERAGE_SCRIPTS_SUBDIR)});
|
|
531
537
|
const MARKERS = path.join(dir, ${JSON.stringify(NODE_COVERAGE_MARKERS_SUBDIR)});
|
|
532
538
|
// V8 script ids identify a compiled script within this process. A URL
|
|
@@ -539,7 +545,7 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
539
545
|
fs.writeFileSync(path.join(dir, ".attached-" + (bunRuntime ? "bun" : "node")), "");
|
|
540
546
|
|
|
541
547
|
function isApp(u) {
|
|
542
|
-
return typeof u === "string" && u.startsWith("file://") && u.indexOf("/node_modules/") < 0 && u.indexOf("/.cache/") < 0 && u !== SELF;
|
|
548
|
+
return typeof u === "string" && u.startsWith("file://") && u.indexOf("/node_modules/") < 0 && u.indexOf("/.cache/") < 0 && u !== SELF && u !== SOURCE_MAP_ADAPTER_URL;
|
|
543
549
|
}
|
|
544
550
|
function isDep(u) {
|
|
545
551
|
return typeof u === "string" && u.startsWith("file://") && u.indexOf("/node_modules/") >= 0 && u !== SELF;
|
|
@@ -636,6 +642,10 @@ export const NODE_COVERAGE_HOOK = `"use strict";
|
|
|
636
642
|
} catch (e) {
|
|
637
643
|
return null;
|
|
638
644
|
}
|
|
645
|
+
// Framework policy runs before dependency attribution and before the final
|
|
646
|
+
// descriptor is hashed. The collector remains the sole owner of record IDs,
|
|
647
|
+
// deduplication, dump references and cleanup (including exit-time captures).
|
|
648
|
+
if (SOURCE_MAP_ADAPTER) require(SOURCE_MAP_ADAPTER).normalizeSourceMap(data, mapUrl ? url.fileURLToPath(mapUrl) : scriptPath);
|
|
639
649
|
// A source inside node_modules is a dependency: its unit id rides
|
|
640
650
|
// beside the map (\`sourceUnits[i]\`, null for the project's own), so
|
|
641
651
|
// the control plane never has to know what node_modules is.
|