@specific.dev/spectest 0.119.7 → 0.120.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-javascript.d.ts +1 -0
- package/dist/coverage-javascript.js +11 -0
- package/dist/coverage-next.d.ts +2 -1
- package/dist/coverage-next.js +5 -5
- package/dist/coverage.d.ts +4 -2
- package/dist/coverage.js +21 -5
- package/dist/daemon.js +4 -1
- package/package.json +1 -6
- package/src/coverage-javascript.ts +11 -0
- package/src/coverage-next.test.ts +3 -48
- package/src/coverage-next.ts +5 -5
- package/src/coverage.test.ts +47 -1
- package/src/coverage.ts +22 -6
- package/src/daemon.ts +4 -1
- package/dist/next.d.ts +0 -4
- package/dist/next.js +0 -93
- package/src/next.ts +0 -85
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const JAVASCRIPT_GENERATED_SOURCES: readonly ["webpack/bootstrap", "*/webpack/bootstrap", "webpack/runtime/*", "*/webpack/runtime/*", "webpack://*/", "webpack://*/?*", "webpack://*/external *"];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Bundler-generated source identities shared by browser and Node coverage.
|
|
2
|
+
// Do not match arbitrary webpack:// sources: those include authored modules.
|
|
3
|
+
export const JAVASCRIPT_GENERATED_SOURCES = [
|
|
4
|
+
"webpack/bootstrap",
|
|
5
|
+
"*/webpack/bootstrap",
|
|
6
|
+
"webpack/runtime/*",
|
|
7
|
+
"*/webpack/runtime/*",
|
|
8
|
+
"webpack://*/", // Loader-only modules with an empty source identity.
|
|
9
|
+
"webpack://*/?*", // Loader-only modules without a resource path.
|
|
10
|
+
"webpack://*/external *", // Webpack external-module wrappers.
|
|
11
|
+
];
|
package/dist/coverage-next.d.ts
CHANGED
|
@@ -16,5 +16,6 @@ export interface NextBuildMetadata {
|
|
|
16
16
|
browserMaps: number;
|
|
17
17
|
}
|
|
18
18
|
/** Server + browser coverage for a built Next app. Build-time maps are required;
|
|
19
|
-
*
|
|
19
|
+
* enable native Next source maps and retain them in the runtime image.
|
|
20
|
+
* Configure this adapter only in Spectest; the application needs no SDK dependency. */
|
|
20
21
|
export declare function next(options?: NextCoverageOptions): CoverageAdapter;
|
package/dist/coverage-next.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Next policy lives in the SDK. The collector/indexer only see ordinary
|
|
2
2
|
// Node/browser reports and generic, report-scoped source metadata.
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
4
5
|
import { node, browser } from "./coverage.js";
|
|
5
6
|
export const NEXT_COVERAGE_HELPER_PATH = "/spectest/next-coverage.cjs";
|
|
6
7
|
// Also preloaded before the Node hook. Only an identified standalone launcher
|
|
@@ -40,22 +41,20 @@ if (require.main === module) {
|
|
|
40
41
|
}
|
|
41
42
|
walk(path.join(dist, "server"), true);
|
|
42
43
|
walk(path.join(dist, "static"), false);
|
|
43
|
-
if (!serverMaps || !browserMaps) throw new Error("Next coverage needs server and browser .js.map files in the runtime image. Enable
|
|
44
|
+
if (!serverMaps || !browserMaps) throw new Error("Next coverage needs server and browser .js.map files in the runtime image. Enable productionBrowserSourceMaps and experimental.serverSourceMaps in Next config, and copy .next/server and .next/static maps into the runtime image, including Edge maps.");
|
|
44
45
|
process.stdout.write(JSON.stringify({generated:launcher(m), serverMaps, browserMaps}));
|
|
45
46
|
} catch (e) { console.error("Next coverage: " + e.message); process.exitCode = 1; }
|
|
46
47
|
}
|
|
47
48
|
`;
|
|
48
49
|
const GENERATED_SOURCES = [
|
|
49
|
-
"webpack://*/", // Loader-only modules with an empty source identity.
|
|
50
|
-
"webpack://*/?*", // Loader-only modules without a resource path.
|
|
51
|
-
"webpack://*/external *", // Webpack external-module wrappers.
|
|
52
50
|
"*/data:* <text/javascript>", // Turbopack virtual modules, including action proxies.
|
|
53
51
|
"*/.next-internal/server/app/*/actions.js (server actions loader)",
|
|
54
52
|
"*/app/*--route-entry.js",
|
|
55
53
|
"*/app/*.mjs (structured image object)",
|
|
56
54
|
];
|
|
57
55
|
/** Server + browser coverage for a built Next app. Build-time maps are required;
|
|
58
|
-
*
|
|
56
|
+
* enable native Next source maps and retain them in the runtime image.
|
|
57
|
+
* Configure this adapter only in Spectest; the application needs no SDK dependency. */
|
|
59
58
|
export function next(options = {}) {
|
|
60
59
|
const sourceRoot = options.sourceRoot === undefined ? "." : options.sourceRoot;
|
|
61
60
|
if (typeof sourceRoot !== "string" || !sourceRoot || /[\\:\0\r\n]/.test(sourceRoot) || (sourceRoot !== "." && sourceRoot.split("/").some(p => !p || p === "." || p === "..")))
|
|
@@ -77,6 +76,7 @@ export function next(options = {}) {
|
|
|
77
76
|
return value;
|
|
78
77
|
};
|
|
79
78
|
return {
|
|
79
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
80
80
|
name: "next", reports: "delta", explains: server.explains,
|
|
81
81
|
ownsReport: name => server.ownsReport(name) || client.ownsReport(name),
|
|
82
82
|
configure(svc, info) {
|
package/dist/coverage.d.ts
CHANGED
|
@@ -88,7 +88,9 @@ export interface CoverageAdapter {
|
|
|
88
88
|
name: string;
|
|
89
89
|
/** Archived on this adapter's reports; other adapters retain their own policy. */
|
|
90
90
|
sourceOptions?: CoverageSourceOptions | ((ctx: CoverageSourceContext) => Promise<CoverageSourceOptions>);
|
|
91
|
-
/**
|
|
91
|
+
/** Generated scaffolding excluded in addition to explicit or legacy source policy. */
|
|
92
|
+
defaultExcludeSources?: readonly string[];
|
|
93
|
+
/** Required with sourceOptions/defaultExcludeSources, including runtime reports. */
|
|
92
94
|
ownsReport?(name: string): boolean;
|
|
93
95
|
/**
|
|
94
96
|
* How the reports this adapter writes (through `ctx.writeReport`)
|
|
@@ -172,7 +174,7 @@ export declare function coverageAdapters(cov: ServiceCoverage | undefined): read
|
|
|
172
174
|
export declare function applyCoverageAdapters<S extends ServiceConfig>(key: string, svc: S): S;
|
|
173
175
|
/** Resolve adapter policies once per capture. Invalid/overlapping ownership is
|
|
174
176
|
* an error, so one adapter cannot silently reinterpret another's reports. */
|
|
175
|
-
export declare function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext): Promise<{
|
|
177
|
+
export declare function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext, fallback?: CoverageSourceOptions): Promise<{
|
|
176
178
|
adapter: string;
|
|
177
179
|
ownsReport: (name: string) => boolean;
|
|
178
180
|
options: CoverageSourceOptions | undefined;
|
package/dist/coverage.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// import * as coverage from "@specific.dev/spectest/coverage";
|
|
28
28
|
// coverage: { adapters: [coverage.node(), coverage.browser()] }
|
|
29
29
|
import { createHash } from "node:crypto";
|
|
30
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
30
31
|
import { harvestAllBrowserCoverage, takeBrowserCoverageReport } from "./browser-coverage.js";
|
|
31
32
|
import { COVERAGE_CONTAINER_DIR } from "./harness/coverage.js";
|
|
32
33
|
export { COVERAGE_CONTAINER_DIR };
|
|
@@ -89,13 +90,17 @@ export function validateCoverage(service, cov) {
|
|
|
89
90
|
["configure", "load", "capture", "inventory", "ownsReport"].every((k) => a[k] === undefined ||
|
|
90
91
|
typeof a[k] === "function");
|
|
91
92
|
const sourceOptions = a?.sourceOptions;
|
|
93
|
+
const defaults = a?.defaultExcludeSources;
|
|
94
|
+
const defaultShape = defaults === undefined || typeof a.ownsReport === "function";
|
|
92
95
|
const sourceShape = sourceOptions === undefined || (typeof a.ownsReport === "function" &&
|
|
93
96
|
(typeof sourceOptions === "function" || (typeof sourceOptions === "object" && sourceOptions !== null && !Array.isArray(sourceOptions))));
|
|
94
|
-
if (!ok || !sourceShape) {
|
|
97
|
+
if (!ok || !sourceShape || !defaultShape) {
|
|
95
98
|
throw new Error(`service "${service}" has an invalid coverage adapter — an adapter is \`{ name, reports?, explains?, configure?, load?, capture?, inventory? }\` (see \`defineAdapter\` in @specific.dev/spectest/coverage)`);
|
|
96
99
|
}
|
|
97
100
|
if (sourceOptions !== undefined && typeof sourceOptions !== "function")
|
|
98
101
|
validateCoverageSourceOptions(service, sourceOptions);
|
|
102
|
+
if (defaults !== undefined)
|
|
103
|
+
validateCoverageSourceOptions(service, { excludeSources: defaults });
|
|
99
104
|
}
|
|
100
105
|
return;
|
|
101
106
|
}
|
|
@@ -146,13 +151,22 @@ function validateCoverageSourceOptions(service, options) {
|
|
|
146
151
|
}
|
|
147
152
|
/** Resolve adapter policies once per capture. Invalid/overlapping ownership is
|
|
148
153
|
* an error, so one adapter cannot silently reinterpret another's reports. */
|
|
149
|
-
export async function resolveCoverageSourceOptions(adapters, ctx) {
|
|
150
|
-
return Promise.all(adapters.filter(a => a.sourceOptions !== undefined || a.ownsReport).map(async (a) => {
|
|
154
|
+
export async function resolveCoverageSourceOptions(adapters, ctx, fallback) {
|
|
155
|
+
return Promise.all(adapters.filter(a => a.sourceOptions !== undefined || a.defaultExcludeSources !== undefined || a.ownsReport).map(async (a) => {
|
|
151
156
|
if (!a.ownsReport)
|
|
152
157
|
throw new Error(`coverage adapter "${a.name}" needs ownsReport with sourceOptions`);
|
|
153
|
-
|
|
158
|
+
let options = typeof a.sourceOptions === "function" ? await a.sourceOptions(ctx) : a.sourceOptions;
|
|
154
159
|
if (a.sourceOptions !== undefined)
|
|
155
160
|
validateCoverageSourceOptions(ctx.service, options);
|
|
161
|
+
if (a.defaultExcludeSources !== undefined) {
|
|
162
|
+
validateCoverageSourceOptions(ctx.service, { excludeSources: a.defaultExcludeSources });
|
|
163
|
+
// Defaults must not discard the source root/exclusions of older configs.
|
|
164
|
+
// Explicit adapter options still replace that service-level fallback.
|
|
165
|
+
options ??= fallback;
|
|
166
|
+
if (options !== undefined)
|
|
167
|
+
validateCoverageSourceOptions(ctx.service, options);
|
|
168
|
+
options = { ...options, excludeSources: [...new Set([...a.defaultExcludeSources, ...(options?.excludeSources ?? [])])] };
|
|
169
|
+
}
|
|
156
170
|
return { adapter: a.name, ownsReport: a.ownsReport, options };
|
|
157
171
|
}));
|
|
158
172
|
}
|
|
@@ -170,7 +184,7 @@ function withWireName(a) {
|
|
|
170
184
|
if (Object.prototype.hasOwnProperty.call(a, "toJSON"))
|
|
171
185
|
return a;
|
|
172
186
|
return Object.assign(Object.create(Object.getPrototypeOf(a)), a, {
|
|
173
|
-
toJSON: () => ({ adapter: a.name, ...(a.sourceOptions && typeof a.sourceOptions !== "function" ? { sourceOptions: a.sourceOptions } : {}) }),
|
|
187
|
+
toJSON: () => ({ adapter: a.name, ...(a.defaultExcludeSources ? { defaultExcludeSources: a.defaultExcludeSources } : {}), ...(a.sourceOptions && typeof a.sourceOptions !== "function" ? { sourceOptions: a.sourceOptions } : {}) }),
|
|
174
188
|
});
|
|
175
189
|
}
|
|
176
190
|
/** `env` with `value` appended to `name` (space-separated), or set. */
|
|
@@ -725,6 +739,7 @@ export function node(options = {}) {
|
|
|
725
739
|
}
|
|
726
740
|
return {
|
|
727
741
|
name: "node",
|
|
742
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
728
743
|
...(options.sourceRoot !== undefined || options.excludeSources !== undefined ? { sourceOptions: { sourceRoot: options.sourceRoot, excludeSources: options.excludeSources } } : {}),
|
|
729
744
|
ownsReport: name => isNodeDumpName(name) || name === NODE_COVERAGE_EMPTY_REPORT,
|
|
730
745
|
reports: "delta",
|
|
@@ -929,6 +944,7 @@ export async function collectNodeReports(dir) {
|
|
|
929
944
|
export function browser(options = {}) {
|
|
930
945
|
return {
|
|
931
946
|
name: "browser",
|
|
947
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
932
948
|
...(options.sourceRoot !== undefined || options.excludeSources !== undefined ? { sourceOptions: options } : {}),
|
|
933
949
|
ownsReport: name => name === "browser.lcov",
|
|
934
950
|
reports: "delta",
|
package/dist/daemon.js
CHANGED
|
@@ -3300,7 +3300,10 @@ async function captureServiceCoverage(baseline = false) {
|
|
|
3300
3300
|
return null;
|
|
3301
3301
|
}
|
|
3302
3302
|
},
|
|
3303
|
-
}
|
|
3303
|
+
}, typeof svc.coverage === "object" ? {
|
|
3304
|
+
sourceRoot: svc.coverage.sourceRoot,
|
|
3305
|
+
excludeSources: svc.coverage.excludeSources,
|
|
3306
|
+
} : undefined);
|
|
3304
3307
|
}
|
|
3305
3308
|
catch (err) {
|
|
3306
3309
|
return { service: svc.name, reports: [], error: `coverage source metadata: ${err instanceof Error ? err.message : String(err)}` };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specific.dev/spectest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.120.1",
|
|
4
4
|
"description": "Spectest SDK for defining test environments in TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,11 +21,6 @@
|
|
|
21
21
|
"bun": "./src/coverage.ts",
|
|
22
22
|
"default": "./dist/coverage.js"
|
|
23
23
|
},
|
|
24
|
-
"./next": {
|
|
25
|
-
"types": "./dist/next.d.ts",
|
|
26
|
-
"bun": "./src/next.ts",
|
|
27
|
-
"default": "./dist/next.js"
|
|
28
|
-
},
|
|
29
24
|
"./browser": {
|
|
30
25
|
"types": "./dist/browser.d.ts",
|
|
31
26
|
"bun": "./src/browser.ts",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Bundler-generated source identities shared by browser and Node coverage.
|
|
2
|
+
// Do not match arbitrary webpack:// sources: those include authored modules.
|
|
3
|
+
export const JAVASCRIPT_GENERATED_SOURCES = [
|
|
4
|
+
"webpack/bootstrap",
|
|
5
|
+
"*/webpack/bootstrap",
|
|
6
|
+
"webpack/runtime/*",
|
|
7
|
+
"*/webpack/runtime/*",
|
|
8
|
+
"webpack://*/", // Loader-only modules with an empty source identity.
|
|
9
|
+
"webpack://*/?*", // Loader-only modules without a resource path.
|
|
10
|
+
"webpack://*/external *", // Webpack external-module wrappers.
|
|
11
|
+
] as const;
|
|
@@ -1,7 +1,6 @@
|
|
|
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
3
|
import { applyCoverageAdapters, resolveCoverageSourceOptions, attachCoverageSourceOptions, node, browser, type CoverageSourceContext } from "./coverage.js";
|
|
4
|
-
import { withCoverage } from "./next.js";
|
|
5
4
|
import { applyCoverageDelta, encodeCoverageBundle } from "./harness/coverage.js";
|
|
6
5
|
import { gunzipSync } from "node:zlib";
|
|
7
6
|
import { promises as fs } from "node:fs";
|
|
@@ -30,6 +29,9 @@ describe("Next adapter", () => {
|
|
|
30
29
|
expect(browserLoaded).toBe(true);
|
|
31
30
|
const policies = await resolveCoverageSourceOptions([adapter],context());
|
|
32
31
|
expect(policies[0]!.options!.sourceRoot).toBe("packages");
|
|
32
|
+
expect(policies[0]!.options!.excludeSources).toContain("*/webpack/runtime/*");
|
|
33
|
+
expect(policies[0]!.options!.excludeSources).toContain("*/.next-internal/server/app/*/actions.js (server actions loader)");
|
|
34
|
+
expect(policies[0]!.options!.excludeSources!.some(p => p.includes("temporal"))).toBe(false);
|
|
33
35
|
const capture = attachCoverageSourceOptions({service:"web",reports:[
|
|
34
36
|
{name:"node-1.json",content:'{"result":[]}'}, {name:"browser.lcov",content:"TN:\n"},
|
|
35
37
|
{name:"other.lcov",content:"TN:\n"},
|
|
@@ -64,53 +66,6 @@ describe("Next adapter", () => {
|
|
|
64
66
|
expect(()=>attachCoverageSourceOptions({reports:[{name:"browser.lcov"}]},duplicate)).toThrow(/multiple adapters/);
|
|
65
67
|
expect(JSON.parse(JSON.stringify(applyCoverageAdapters("web",{image,coverage:{adapters}}))).coverage.adapters[1].sourceOptions.sourceRoot).toBe("client");
|
|
66
68
|
});
|
|
67
|
-
test("build helper preserves user settings and includes standalone maps", () => {
|
|
68
|
-
const c=withCoverage({output:"standalone",distDir:"build",experimental:{cpus:2},outputFileTracingIncludes:{"/*":["./assets/*"],"/api/*":["./data/*"]}}) as any;
|
|
69
|
-
expect(c.experimental).toEqual({cpus:2,serverSourceMaps:true});
|
|
70
|
-
expect(c.productionBrowserSourceMaps).toBe(true);
|
|
71
|
-
expect(c.outputFileTracingIncludes).toEqual({"/*":["./assets/*"],"/api/*":["./data/*"],"/**":["./build/server/**/*.map"]});
|
|
72
|
-
});
|
|
73
|
-
test("Webpack helper preserves hooks and disables dependency maps only", () => {
|
|
74
|
-
let called = false;
|
|
75
|
-
const config = withCoverage({ webpack(c: any) { called = true; return {...c, marker: true}; } });
|
|
76
|
-
const compiled = config.webpack({plugins: []});
|
|
77
|
-
expect(called).toBe(true);
|
|
78
|
-
expect(compiled.marker).toBe(true);
|
|
79
|
-
let loaderHook: (loader: any, module: any) => void;
|
|
80
|
-
compiled.plugins[0].apply({
|
|
81
|
-
hooks: {compilation: {tap(_name: string, f: Function) { f({}); }}},
|
|
82
|
-
webpack: {NormalModule: {getCompilationHooks() { return {
|
|
83
|
-
loader: {tap(_name: string, f: typeof loaderHook) { loaderHook = f; }},
|
|
84
|
-
}; }}},
|
|
85
|
-
});
|
|
86
|
-
for (const resource of ["/app/app/page.tsx", "/workspace/shared/index.ts", ""]) {
|
|
87
|
-
const loader = {sourceMap: true};
|
|
88
|
-
loaderHook!(loader, {resource});
|
|
89
|
-
expect(loader.sourceMap).toBe(true);
|
|
90
|
-
}
|
|
91
|
-
const dependency = {sourceMap: true};
|
|
92
|
-
loaderHook!(dependency, {resource: "/app/node_modules/next/dist/client/router.js"});
|
|
93
|
-
expect(dependency.sourceMap).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
test("build hook retains traced and untraced Edge maps and composes an existing compiler hook", async () => {
|
|
96
|
-
const dir=await fs.mkdtemp(path.join(os.tmpdir(),"next-trace-"));
|
|
97
|
-
try {
|
|
98
|
-
let called=false;
|
|
99
|
-
const config=withCoverage({compiler:{runAfterProductionCompile:async()=>{
|
|
100
|
-
called=true;
|
|
101
|
-
await fs.mkdir(path.join(dir,"server/edge"),{recursive:true});
|
|
102
|
-
await fs.writeFile(path.join(dir,"server/edge/middleware.js.map"),"{}");
|
|
103
|
-
await fs.writeFile(path.join(dir,"next-server.js.nft.json"),JSON.stringify({version:1,files:["runtime.js"]}));
|
|
104
|
-
await fs.writeFile(path.join(dir,"page.js.map"),"{}");
|
|
105
|
-
await fs.writeFile(path.join(dir,"chunk.js.map"),"{}");
|
|
106
|
-
await fs.writeFile(path.join(dir,"page.js.nft.json"),JSON.stringify({version:1,files:["chunk.js","missing.js"]}));
|
|
107
|
-
}}});
|
|
108
|
-
await (config.compiler.runAfterProductionCompile as any)({projectDir:dir,distDir:dir});
|
|
109
|
-
expect(called).toBe(true);
|
|
110
|
-
expect(JSON.parse(await fs.readFile(path.join(dir,"next-server.js.nft.json"),"utf8")).files).toEqual(["runtime.js","server/edge/middleware.js.map"]);
|
|
111
|
-
expect(JSON.parse(await fs.readFile(path.join(dir,"page.js.nft.json"),"utf8")).files).toEqual(["chunk.js","missing.js","page.js.map","chunk.js.map"]);
|
|
112
|
-
} finally {await fs.rm(dir,{recursive:true,force:true});}
|
|
113
|
-
});
|
|
114
69
|
test("runtime probe distinguishes generated launcher from authored custom server and checks maps", async () => {
|
|
115
70
|
const dir=await fs.mkdtemp(path.join(os.tmpdir(),"next-cov-"));
|
|
116
71
|
const helper=path.join(dir,"helper.cjs");
|
package/src/coverage-next.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Next policy lives in the SDK. The collector/indexer only see ordinary
|
|
2
2
|
// Node/browser reports and generic, report-scoped source metadata.
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
4
5
|
import { node, browser, type CoverageAdapter, type CoverageSourceContext, type CoverageSourceOptions } from "./coverage.js";
|
|
5
6
|
|
|
6
7
|
export interface NextCoverageOptions extends CoverageSourceOptions {
|
|
@@ -52,7 +53,7 @@ if (require.main === module) {
|
|
|
52
53
|
}
|
|
53
54
|
walk(path.join(dist, "server"), true);
|
|
54
55
|
walk(path.join(dist, "static"), false);
|
|
55
|
-
if (!serverMaps || !browserMaps) throw new Error("Next coverage needs server and browser .js.map files in the runtime image. Enable
|
|
56
|
+
if (!serverMaps || !browserMaps) throw new Error("Next coverage needs server and browser .js.map files in the runtime image. Enable productionBrowserSourceMaps and experimental.serverSourceMaps in Next config, and copy .next/server and .next/static maps into the runtime image, including Edge maps.");
|
|
56
57
|
process.stdout.write(JSON.stringify({generated:launcher(m), serverMaps, browserMaps}));
|
|
57
58
|
} catch (e) { console.error("Next coverage: " + e.message); process.exitCode = 1; }
|
|
58
59
|
}
|
|
@@ -65,9 +66,6 @@ export interface NextBuildMetadata {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const GENERATED_SOURCES = [
|
|
68
|
-
"webpack://*/", // Loader-only modules with an empty source identity.
|
|
69
|
-
"webpack://*/?*", // Loader-only modules without a resource path.
|
|
70
|
-
"webpack://*/external *", // Webpack external-module wrappers.
|
|
71
69
|
"*/data:* <text/javascript>", // Turbopack virtual modules, including action proxies.
|
|
72
70
|
"*/.next-internal/server/app/*/actions.js (server actions loader)",
|
|
73
71
|
"*/app/*--route-entry.js",
|
|
@@ -75,7 +73,8 @@ const GENERATED_SOURCES = [
|
|
|
75
73
|
];
|
|
76
74
|
|
|
77
75
|
/** Server + browser coverage for a built Next app. Build-time maps are required;
|
|
78
|
-
*
|
|
76
|
+
* enable native Next source maps and retain them in the runtime image.
|
|
77
|
+
* Configure this adapter only in Spectest; the application needs no SDK dependency. */
|
|
79
78
|
export function next(options: NextCoverageOptions = {}): CoverageAdapter {
|
|
80
79
|
const sourceRoot = options.sourceRoot === undefined ? "." : options.sourceRoot;
|
|
81
80
|
if (typeof sourceRoot !== "string" || !sourceRoot || /[\\:\0\r\n]/.test(sourceRoot) || (sourceRoot !== "." && sourceRoot.split("/").some(p => !p || p === "." || p === ".."))) throw new Error("coverage.next sourceRoot must be repository-relative (use \".\" for the repository root)");
|
|
@@ -95,6 +94,7 @@ export function next(options: NextCoverageOptions = {}): CoverageAdapter {
|
|
|
95
94
|
return value;
|
|
96
95
|
};
|
|
97
96
|
return {
|
|
97
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
98
98
|
name: "next", reports: "delta", explains: server.explains,
|
|
99
99
|
ownsReport: name => server.ownsReport!(name) || client.ownsReport!(name),
|
|
100
100
|
configure(svc, info) {
|
package/src/coverage.test.ts
CHANGED
|
@@ -22,9 +22,12 @@ import {
|
|
|
22
22
|
isNodeDumpName,
|
|
23
23
|
node,
|
|
24
24
|
resetNodeCoverageMemory,
|
|
25
|
+
resolveCoverageSourceOptions,
|
|
26
|
+
attachCoverageSourceOptions,
|
|
25
27
|
validateCoverage,
|
|
26
28
|
type CoverageCaptureContext,
|
|
27
29
|
} from "./coverage.js";
|
|
30
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
28
31
|
import type { ServiceConfig } from "./index.js";
|
|
29
32
|
import { foldBrowserCoverage } from "./browser-coverage.js";
|
|
30
33
|
|
|
@@ -109,7 +112,7 @@ describe("applyCoverageAdapters", () => {
|
|
|
109
112
|
expect(typeof list[0]?.capture).toBe("function");
|
|
110
113
|
expect(typeof list[1]?.load).toBe("function");
|
|
111
114
|
expect(JSON.parse(JSON.stringify(out)).coverage).toEqual({
|
|
112
|
-
adapters: [{ adapter: "node" }, { adapter: "browser" }],
|
|
115
|
+
adapters: [{ adapter: "node", defaultExcludeSources: [...JAVASCRIPT_GENERATED_SOURCES] }, { adapter: "browser", defaultExcludeSources: [...JAVASCRIPT_GENERATED_SOURCES] }],
|
|
113
116
|
explains: [...NODE_EXPLAINS],
|
|
114
117
|
});
|
|
115
118
|
});
|
|
@@ -614,3 +617,46 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
|
|
|
614
617
|
expect((await fs.readdir(dir)).filter((n) => n.endsWith(".json"))).toEqual([NODE_COVERAGE_EMPTY_REPORT]);
|
|
615
618
|
});
|
|
616
619
|
});
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
describe("JavaScript generated-source defaults", () => {
|
|
623
|
+
const ctx = {
|
|
624
|
+
service: "app", signal: new AbortController().signal,
|
|
625
|
+
exec: async () => ({stdout: "", stderr: ""}),
|
|
626
|
+
projectFiles: async () => [], readProjectFile: async () => null,
|
|
627
|
+
};
|
|
628
|
+
test("defaults are archived only on owned reports and preserve legacy policy", async () => {
|
|
629
|
+
const legacy = {sourceRoot: "packages", excludeSources: ["virtual:///generated/*"]};
|
|
630
|
+
const policies = await resolveCoverageSourceOptions([node(), browser()], ctx, legacy);
|
|
631
|
+
const captured = attachCoverageSourceOptions({reports: [
|
|
632
|
+
{name: "node-1.json"}, {name: "browser.lcov"}, {name: "other.lcov"},
|
|
633
|
+
]}, policies);
|
|
634
|
+
for (const report of captured.reports.slice(0, 2) as any[]) {
|
|
635
|
+
expect(report.sourceOptions.sourceRoot).toBe("packages");
|
|
636
|
+
expect(report.sourceOptions.excludeSources).toContain("*/webpack/bootstrap");
|
|
637
|
+
expect(report.sourceOptions.excludeSources).toContain("*/webpack/runtime/*");
|
|
638
|
+
expect(report.sourceOptions.excludeSources).toContain("virtual:///generated/*");
|
|
639
|
+
expect(report.sourceOptions.excludeSources.some((p: string) => p.includes("temporal"))).toBe(false);
|
|
640
|
+
expect(report.sourceOptions.excludeSources.some((p: string) => p.includes("server actions loader"))).toBe(false);
|
|
641
|
+
}
|
|
642
|
+
expect((captured.reports[2] as any).sourceOptions).toBeUndefined();
|
|
643
|
+
});
|
|
644
|
+
test("explicit user policy replaces legacy policy and extends defaults without duplicates", async () => {
|
|
645
|
+
const user = {sourceRoot: "workflows", excludeSources: [
|
|
646
|
+
"*/webpack/runtime/*", "*/temporal/workflows/index-autogenerated-entrypoint.cjs",
|
|
647
|
+
]};
|
|
648
|
+
for (const adapter of [node(user), browser(user)]) {
|
|
649
|
+
const [policy] = await resolveCoverageSourceOptions([adapter], ctx, {
|
|
650
|
+
sourceRoot: "old", excludeSources: ["old/*"],
|
|
651
|
+
});
|
|
652
|
+
expect(policy!.options!.sourceRoot).toBe("workflows");
|
|
653
|
+
expect(policy!.options!.excludeSources).toContain(user.excludeSources[1]!);
|
|
654
|
+
expect(policy!.options!.excludeSources).not.toContain("old/*");
|
|
655
|
+
expect(policy!.options!.excludeSources!.filter(p => p === "*/webpack/runtime/*")).toHaveLength(1);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
test("default policy requires report ownership and valid source patterns", () => {
|
|
659
|
+
expect(() => validateCoverage("app", {adapters: [{name: "bad", defaultExcludeSources: ["generated/*"]}]})).toThrow();
|
|
660
|
+
expect(() => validateCoverage("app", {adapters: [{name: "bad", ownsReport: () => true, defaultExcludeSources: [""]}]})).toThrow();
|
|
661
|
+
});
|
|
662
|
+
});
|
package/src/coverage.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// coverage: { adapters: [coverage.node(), coverage.browser()] }
|
|
29
29
|
|
|
30
30
|
import { createHash } from "node:crypto";
|
|
31
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
31
32
|
|
|
32
33
|
import type { ServiceConfig } from "./index.js";
|
|
33
34
|
import { harvestAllBrowserCoverage, takeBrowserCoverageReport } from "./browser-coverage.js";
|
|
@@ -126,7 +127,9 @@ export interface CoverageAdapter {
|
|
|
126
127
|
name: string;
|
|
127
128
|
/** Archived on this adapter's reports; other adapters retain their own policy. */
|
|
128
129
|
sourceOptions?: CoverageSourceOptions | ((ctx: CoverageSourceContext) => Promise<CoverageSourceOptions>);
|
|
129
|
-
/**
|
|
130
|
+
/** Generated scaffolding excluded in addition to explicit or legacy source policy. */
|
|
131
|
+
defaultExcludeSources?: readonly string[];
|
|
132
|
+
/** Required with sourceOptions/defaultExcludeSources, including runtime reports. */
|
|
130
133
|
ownsReport?(name: string): boolean;
|
|
131
134
|
/**
|
|
132
135
|
* How the reports this adapter writes (through `ctx.writeReport`)
|
|
@@ -248,14 +251,17 @@ export function validateCoverage(service: string, cov: unknown): void {
|
|
|
248
251
|
typeof (a as Record<string, unknown>)[k] === "function",
|
|
249
252
|
);
|
|
250
253
|
const sourceOptions = (a as CoverageAdapter | null)?.sourceOptions;
|
|
254
|
+
const defaults = (a as CoverageAdapter | null)?.defaultExcludeSources;
|
|
255
|
+
const defaultShape = defaults === undefined || typeof (a as CoverageAdapter).ownsReport === "function";
|
|
251
256
|
const sourceShape = sourceOptions === undefined || (typeof (a as CoverageAdapter).ownsReport === "function" &&
|
|
252
257
|
(typeof sourceOptions === "function" || (typeof sourceOptions === "object" && sourceOptions !== null && !Array.isArray(sourceOptions))));
|
|
253
|
-
if (!ok || !sourceShape) {
|
|
258
|
+
if (!ok || !sourceShape || !defaultShape) {
|
|
254
259
|
throw new Error(
|
|
255
260
|
`service "${service}" has an invalid coverage adapter — an adapter is \`{ name, reports?, explains?, configure?, load?, capture?, inventory? }\` (see \`defineAdapter\` in @specific.dev/spectest/coverage)`,
|
|
256
261
|
);
|
|
257
262
|
}
|
|
258
263
|
if (sourceOptions !== undefined && typeof sourceOptions !== "function") validateCoverageSourceOptions(service, sourceOptions);
|
|
264
|
+
if (defaults !== undefined) validateCoverageSourceOptions(service, { excludeSources: defaults });
|
|
259
265
|
}
|
|
260
266
|
return;
|
|
261
267
|
}
|
|
@@ -310,11 +316,19 @@ function validateCoverageSourceOptions(service: string, options: unknown): asser
|
|
|
310
316
|
|
|
311
317
|
/** Resolve adapter policies once per capture. Invalid/overlapping ownership is
|
|
312
318
|
* an error, so one adapter cannot silently reinterpret another's reports. */
|
|
313
|
-
export async function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext) {
|
|
314
|
-
return Promise.all(adapters.filter(a => a.sourceOptions !== undefined || a.ownsReport).map(async a => {
|
|
319
|
+
export async function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext, fallback?: CoverageSourceOptions) {
|
|
320
|
+
return Promise.all(adapters.filter(a => a.sourceOptions !== undefined || a.defaultExcludeSources !== undefined || a.ownsReport).map(async a => {
|
|
315
321
|
if (!a.ownsReport) throw new Error(`coverage adapter "${a.name}" needs ownsReport with sourceOptions`);
|
|
316
|
-
|
|
322
|
+
let options = typeof a.sourceOptions === "function" ? await a.sourceOptions(ctx) : a.sourceOptions;
|
|
317
323
|
if (a.sourceOptions !== undefined) validateCoverageSourceOptions(ctx.service, options);
|
|
324
|
+
if (a.defaultExcludeSources !== undefined) {
|
|
325
|
+
validateCoverageSourceOptions(ctx.service, { excludeSources: a.defaultExcludeSources });
|
|
326
|
+
// Defaults must not discard the source root/exclusions of older configs.
|
|
327
|
+
// Explicit adapter options still replace that service-level fallback.
|
|
328
|
+
options ??= fallback;
|
|
329
|
+
if (options !== undefined) validateCoverageSourceOptions(ctx.service, options);
|
|
330
|
+
options = { ...options, excludeSources: [...new Set([...a.defaultExcludeSources, ...(options?.excludeSources ?? [])])] };
|
|
331
|
+
}
|
|
318
332
|
return { adapter: a.name, ownsReport: a.ownsReport, options };
|
|
319
333
|
}));
|
|
320
334
|
}
|
|
@@ -333,7 +347,7 @@ export function attachCoverageSourceOptions<T extends { reports: { name: string;
|
|
|
333
347
|
function withWireName(a: CoverageAdapter): CoverageAdapter {
|
|
334
348
|
if (Object.prototype.hasOwnProperty.call(a, "toJSON")) return a;
|
|
335
349
|
return Object.assign(Object.create(Object.getPrototypeOf(a) as object | null), a, {
|
|
336
|
-
toJSON: () => ({ adapter: a.name, ...(a.sourceOptions && typeof a.sourceOptions !== "function" ? { sourceOptions: a.sourceOptions } : {}) }),
|
|
350
|
+
toJSON: () => ({ adapter: a.name, ...(a.defaultExcludeSources ? { defaultExcludeSources: a.defaultExcludeSources } : {}), ...(a.sourceOptions && typeof a.sourceOptions !== "function" ? { sourceOptions: a.sourceOptions } : {}) }),
|
|
337
351
|
}) as CoverageAdapter;
|
|
338
352
|
}
|
|
339
353
|
|
|
@@ -909,6 +923,7 @@ export function node(options: NodeCoverageOptions = {}): CoverageAdapter {
|
|
|
909
923
|
}
|
|
910
924
|
return {
|
|
911
925
|
name: "node",
|
|
926
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
912
927
|
...(options.sourceRoot !== undefined || options.excludeSources !== undefined ? { sourceOptions: { sourceRoot: options.sourceRoot, excludeSources: options.excludeSources } } : {}),
|
|
913
928
|
ownsReport: name => isNodeDumpName(name) || name === NODE_COVERAGE_EMPTY_REPORT,
|
|
914
929
|
reports: "delta",
|
|
@@ -1121,6 +1136,7 @@ export async function collectNodeReports(dir: string): Promise<NodeReportStats>
|
|
|
1121
1136
|
export function browser(options: CoverageSourceOptions = {}): CoverageAdapter {
|
|
1122
1137
|
return {
|
|
1123
1138
|
name: "browser",
|
|
1139
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
1124
1140
|
...(options.sourceRoot !== undefined || options.excludeSources !== undefined ? { sourceOptions: options } : {}),
|
|
1125
1141
|
ownsReport: name => name === "browser.lcov",
|
|
1126
1142
|
reports: "delta",
|
package/src/daemon.ts
CHANGED
|
@@ -3894,7 +3894,10 @@ async function captureServiceCoverage(baseline = false): Promise<ServiceCoverage
|
|
|
3894
3894
|
try { return await fs.readFile(resolveProjectPath(p, { record: false }), "utf8"); }
|
|
3895
3895
|
catch { return null; }
|
|
3896
3896
|
},
|
|
3897
|
-
}
|
|
3897
|
+
}, typeof svc.coverage === "object" ? {
|
|
3898
|
+
sourceRoot: svc.coverage.sourceRoot,
|
|
3899
|
+
excludeSources: svc.coverage.excludeSources,
|
|
3900
|
+
} : undefined);
|
|
3898
3901
|
} catch (err) {
|
|
3899
3902
|
return { service: svc.name, reports: [], error: `coverage source metadata: ${err instanceof Error ? err.message : String(err)}` };
|
|
3900
3903
|
}
|
package/dist/next.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Build-time companion for coverage.next(). Import this lightweight module in
|
|
2
|
-
* next.config; it does not load the Spectest harness or require Next at runtime.
|
|
3
|
-
* Apply only in the test build if production should not expose source maps. */
|
|
4
|
-
export declare function withCoverage<T extends object>(config: T): T;
|
package/dist/next.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
/** Keep each traced script's map alongside it in standalone output. Turbopack
|
|
4
|
-
* versions that do not apply outputFileTracingIncludes still emit these traces
|
|
5
|
-
* before runAfterProductionCompile. No source files or generated code change. */
|
|
6
|
-
async function includeTracedMaps(distDir) {
|
|
7
|
-
const traces = [];
|
|
8
|
-
const serverMaps = [];
|
|
9
|
-
async function walk(dir) {
|
|
10
|
-
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
11
|
-
const file = path.join(dir, entry.name);
|
|
12
|
-
if (entry.isDirectory() && entry.name !== "cache" && entry.name !== "standalone")
|
|
13
|
-
await walk(file);
|
|
14
|
-
else if (entry.isFile()) {
|
|
15
|
-
if (entry.name.endsWith(".nft.json"))
|
|
16
|
-
traces.push(file);
|
|
17
|
-
if (entry.name.endsWith(".js.map") && file.startsWith(path.join(distDir, "server") + path.sep))
|
|
18
|
-
serverMaps.push(file);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
await walk(distDir);
|
|
23
|
-
for (const file of traces) {
|
|
24
|
-
const dir = path.dirname(file);
|
|
25
|
-
const trace = JSON.parse(await fs.readFile(file, "utf8"));
|
|
26
|
-
if (!Array.isArray(trace.files))
|
|
27
|
-
throw new Error(`Invalid Next trace: ${file}`);
|
|
28
|
-
const scripts = [path.basename(file).slice(0, -".nft.json".length), ...trace.files];
|
|
29
|
-
// Edge bundles are copied through middleware-manifest, not NFT traces.
|
|
30
|
-
// The server trace is always copied into standalone output; retain
|
|
31
|
-
// their maps there without changing the middleware's executable files.
|
|
32
|
-
const maps = file === path.join(distDir, "next-server.js.nft.json")
|
|
33
|
-
? serverMaps.map(map => path.relative(dir, map)) : [];
|
|
34
|
-
for (const script of scripts) {
|
|
35
|
-
if (typeof script !== "string" || !script.endsWith(".js"))
|
|
36
|
-
continue;
|
|
37
|
-
const map = script + ".map";
|
|
38
|
-
try {
|
|
39
|
-
if ((await fs.stat(path.resolve(dir, map))).isFile())
|
|
40
|
-
maps.push(map);
|
|
41
|
-
}
|
|
42
|
-
catch (e) {
|
|
43
|
-
if (e.code !== "ENOENT")
|
|
44
|
-
throw e;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
trace.files = [...new Set([...trace.files, ...maps])];
|
|
48
|
-
await fs.writeFile(file, JSON.stringify(trace));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/** Build-time companion for coverage.next(). Import this lightweight module in
|
|
52
|
-
* next.config; it does not load the Spectest harness or require Next at runtime.
|
|
53
|
-
* Apply only in the test build if production should not expose source maps. */
|
|
54
|
-
export function withCoverage(config) {
|
|
55
|
-
const c = config;
|
|
56
|
-
const distDir = c.distDir ?? ".next";
|
|
57
|
-
const includes = c.outputFileTracingIncludes ?? {};
|
|
58
|
-
return {
|
|
59
|
-
...config,
|
|
60
|
-
productionBrowserSourceMaps: true,
|
|
61
|
-
turbopack: c.turbopack ?? {},
|
|
62
|
-
webpack(webpackConfig, context) {
|
|
63
|
-
const configured = c.webpack ? c.webpack(webpackConfig, context) : webpackConfig;
|
|
64
|
-
configured.plugins ??= [];
|
|
65
|
-
configured.plugins.push({
|
|
66
|
-
apply(compiler) {
|
|
67
|
-
compiler.hooks.compilation.tap("SpectestCoverage", (compilation) => {
|
|
68
|
-
compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap("SpectestCoverage", (loader, module) => {
|
|
69
|
-
// Keep dependencies identifiable as dependencies. Chaining a
|
|
70
|
-
// package's published maps can replace node_modules provenance
|
|
71
|
-
// with ambiguous paths such as ../../src/client/router.ts.
|
|
72
|
-
if (/[\\/]node_modules[\\/]/.test(module.resource ?? ""))
|
|
73
|
-
loader.sourceMap = false;
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
return configured;
|
|
79
|
-
},
|
|
80
|
-
experimental: { ...c.experimental, serverSourceMaps: true },
|
|
81
|
-
compiler: {
|
|
82
|
-
...c.compiler,
|
|
83
|
-
async runAfterProductionCompile(metadata) {
|
|
84
|
-
await c.compiler?.runAfterProductionCompile?.(metadata);
|
|
85
|
-
await includeTracedMaps(metadata.distDir);
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
outputFileTracingIncludes: {
|
|
89
|
-
...includes,
|
|
90
|
-
"/**": [...(includes["/**"] ?? []), `./${distDir}/server/**/*.map`],
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
package/src/next.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/** Keep each traced script's map alongside it in standalone output. Turbopack
|
|
5
|
-
* versions that do not apply outputFileTracingIncludes still emit these traces
|
|
6
|
-
* before runAfterProductionCompile. No source files or generated code change. */
|
|
7
|
-
async function includeTracedMaps(distDir: string): Promise<void> {
|
|
8
|
-
const traces: string[] = [];
|
|
9
|
-
const serverMaps: string[] = [];
|
|
10
|
-
async function walk(dir: string): Promise<void> {
|
|
11
|
-
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
12
|
-
const file = path.join(dir, entry.name);
|
|
13
|
-
if (entry.isDirectory() && entry.name !== "cache" && entry.name !== "standalone") await walk(file);
|
|
14
|
-
else if (entry.isFile()) {
|
|
15
|
-
if (entry.name.endsWith(".nft.json")) traces.push(file);
|
|
16
|
-
if (entry.name.endsWith(".js.map") && file.startsWith(path.join(distDir, "server") + path.sep)) serverMaps.push(file);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
await walk(distDir);
|
|
21
|
-
for (const file of traces) {
|
|
22
|
-
const dir = path.dirname(file);
|
|
23
|
-
const trace = JSON.parse(await fs.readFile(file, "utf8"));
|
|
24
|
-
if (!Array.isArray(trace.files)) throw new Error(`Invalid Next trace: ${file}`);
|
|
25
|
-
const scripts = [path.basename(file).slice(0, -".nft.json".length), ...trace.files];
|
|
26
|
-
// Edge bundles are copied through middleware-manifest, not NFT traces.
|
|
27
|
-
// The server trace is always copied into standalone output; retain
|
|
28
|
-
// their maps there without changing the middleware's executable files.
|
|
29
|
-
const maps = file === path.join(distDir, "next-server.js.nft.json")
|
|
30
|
-
? serverMaps.map(map => path.relative(dir, map)) : [];
|
|
31
|
-
for (const script of scripts) {
|
|
32
|
-
if (typeof script !== "string" || !script.endsWith(".js")) continue;
|
|
33
|
-
const map = script + ".map";
|
|
34
|
-
try { if ((await fs.stat(path.resolve(dir, map))).isFile()) maps.push(map); }
|
|
35
|
-
catch (e) { if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e; }
|
|
36
|
-
}
|
|
37
|
-
trace.files = [...new Set([...trace.files, ...maps])];
|
|
38
|
-
await fs.writeFile(file, JSON.stringify(trace));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build-time companion for coverage.next(). Import this lightweight module in
|
|
43
|
-
* next.config; it does not load the Spectest harness or require Next at runtime.
|
|
44
|
-
* Apply only in the test build if production should not expose source maps. */
|
|
45
|
-
export function withCoverage<T extends object>(config: T): T {
|
|
46
|
-
const c = config as Record<string, any>;
|
|
47
|
-
const distDir = c.distDir ?? ".next";
|
|
48
|
-
const includes = c.outputFileTracingIncludes ?? {};
|
|
49
|
-
return {
|
|
50
|
-
...config,
|
|
51
|
-
productionBrowserSourceMaps: true,
|
|
52
|
-
turbopack: c.turbopack ?? {},
|
|
53
|
-
webpack(webpackConfig: any, context: any) {
|
|
54
|
-
const configured = c.webpack ? c.webpack(webpackConfig, context) : webpackConfig;
|
|
55
|
-
configured.plugins ??= [];
|
|
56
|
-
configured.plugins.push({
|
|
57
|
-
apply(compiler: any) {
|
|
58
|
-
compiler.hooks.compilation.tap("SpectestCoverage", (compilation: any) => {
|
|
59
|
-
compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap(
|
|
60
|
-
"SpectestCoverage", (loader: any, module: any) => {
|
|
61
|
-
// Keep dependencies identifiable as dependencies. Chaining a
|
|
62
|
-
// package's published maps can replace node_modules provenance
|
|
63
|
-
// with ambiguous paths such as ../../src/client/router.ts.
|
|
64
|
-
if (/[\\/]node_modules[\\/]/.test(module.resource ?? "")) loader.sourceMap = false;
|
|
65
|
-
},
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
return configured;
|
|
71
|
-
},
|
|
72
|
-
experimental: { ...c.experimental, serverSourceMaps: true },
|
|
73
|
-
compiler: {
|
|
74
|
-
...c.compiler,
|
|
75
|
-
async runAfterProductionCompile(metadata: { projectDir: string; distDir: string }) {
|
|
76
|
-
await c.compiler?.runAfterProductionCompile?.(metadata);
|
|
77
|
-
await includeTracedMaps(metadata.distDir);
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
outputFileTracingIncludes: {
|
|
81
|
-
...includes,
|
|
82
|
-
"/**": [...(includes["/**"] ?? []), `./${distDir}/server/**/*.map`],
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|