supercov 4.24.7 → 4.25.0
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/package.json +9 -9
- package/runtime/javascript/atomic.mjs +42 -2
- package/runtime/javascript/launchSupervisor.mjs +10 -0
- package/runtime/javascript/playwright.mjs +426 -179
- package/runtime/javascript/playwrightCacheThread.mjs +6 -0
- package/runtime/javascript/register.mjs +87 -31
- package/runtime/javascript/resolve-loader.mjs +49 -3
- package/runtime/javascript/runnerEvidence.mjs +16 -1
- package/runtime/javascript/runtime.mjs +223 -22
- package/runtime/javascript/snapshots.mjs +108 -0
- package/runtime/javascript/transport.mjs +36 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The thread Node runs module hooks in, where Playwright compiles ES modules
|
|
2
|
+
// and writes them into its cache: written whole there too (atomic.mjs says
|
|
3
|
+
// why). Supercov's own hooks run in the importing thread where Node can, so
|
|
4
|
+
// this goes to that thread on its own.
|
|
5
|
+
import { writePlaywrightCacheWhole } from "./atomic.mjs";
|
|
6
|
+
writePlaywrightCacheWhole();
|
|
@@ -13,28 +13,20 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervisor.mjs";
|
|
14
14
|
import { __supercovBindCapabilityWrapper } from "./capability.mjs";
|
|
15
15
|
import { adaptRunnerEntry, noteSkippedCoverageThresholds } from "./runnerEntry.mjs";
|
|
16
|
+
import { writePlaywrightCacheWhole } from "./atomic.mjs";
|
|
17
|
+
// Supercov runs Node with --enable-source-maps so a stack through an
|
|
18
|
+
// instrumented copy names the project's file. The flag also reads the maps
|
|
19
|
+
// every package ships, which a plain run never does: a third of the CPU a
|
|
20
|
+
// measured Vitest suite added. Where Node can, Supercov's flag maps the
|
|
21
|
+
// project's files alone, unless the command asked for maps itself: a second
|
|
22
|
+
// flag in NODE_OPTIONS, or one on its own command line.
|
|
23
|
+
if ((process.env.NODE_OPTIONS ?? "").split(/\s+/).filter((option) => option === "--enable-source-maps").length === 1 &&
|
|
24
|
+
!process.execArgv.includes("--enable-source-maps") &&
|
|
25
|
+
typeof Module.setSourceMapsSupport === "function")
|
|
26
|
+
Module.setSourceMapsSupport(true, { nodeModules: false, generatedCode: true });
|
|
16
27
|
installLaunchSupervisor();
|
|
17
|
-
// Playwright
|
|
18
|
-
|
|
19
|
-
// ran an empty module, and a config of no `default` failed at "Cannot add
|
|
20
|
-
// property metadata, object is not extensible". The cache a run gives it
|
|
21
|
-
// starts empty, so every run compiled; there each file is written whole, or
|
|
22
|
-
// is not there yet and is compiled again. A file written beside the cache's
|
|
23
|
-
// folders, not in one, is never taken for an old entry and removed.
|
|
24
|
-
const transformCache = process.env.PWTEST_CACHE_DIR;
|
|
25
|
-
if (transformCache) {
|
|
26
|
-
const cacheFolder = resolve(transformCache) + sep;
|
|
27
|
-
const writeFileSync = fs.writeFileSync;
|
|
28
|
-
let written = 0;
|
|
29
|
-
fs.writeFileSync = function (file, ...rest) {
|
|
30
|
-
if (typeof file !== "string" || !resolve(file).startsWith(cacheFolder))
|
|
31
|
-
return writeFileSync.call(this, file, ...rest);
|
|
32
|
-
const partial = resolve(transformCache, `.writing-${process.pid}-${written++}`);
|
|
33
|
-
writeFileSync.call(this, partial, ...rest);
|
|
34
|
-
fs.renameSync(partial, file);
|
|
35
|
-
};
|
|
36
|
-
syncBuiltinESMExports();
|
|
37
|
-
}
|
|
28
|
+
// Playwright's compile cache, written whole (atomic.mjs says why).
|
|
29
|
+
writePlaywrightCacheWhole();
|
|
38
30
|
// Instrumented sources import the browser-safe capability seam; bind the
|
|
39
31
|
// real supervisor implementation before any user module evaluates.
|
|
40
32
|
__supercovBindCapabilityWrapper(wrapImportedCapability);
|
|
@@ -213,8 +205,16 @@ const nodeAssertWrapper = new URL("./nodeAssert.mjs", import.meta.url).href;
|
|
|
213
205
|
const nodeAssertStrictWrapper = new URL("./nodeAssertStrict.mjs", import.meta.url).href;
|
|
214
206
|
const isPlaywrightEntrypoint = /\/(?:node_modules\/\.bin\/playwright|node_modules\/(?:@playwright\/test|playwright)\/(?:cli\.js|.*\/program\.js))$/.test(entrypoint);
|
|
215
207
|
const isJestEntrypoint = /\/node_modules\/(?:\.bin\/jest|(?:jest|jest-cli)\/bin\/jest\.js)$/.test(entrypoint);
|
|
216
|
-
if (generatedPlaywrightConfig && isPlaywrightEntrypoint)
|
|
208
|
+
if (generatedPlaywrightConfig && isPlaywrightEntrypoint) {
|
|
217
209
|
process.env.SUPERCOV_INSIDE_PLAYWRIGHT = "1";
|
|
210
|
+
// Every process the runner starts inherits SUPERCOV_INSIDE_PLAYWRIGHT, the
|
|
211
|
+
// app server and each mock server too, and loading the wrapper cost each of
|
|
212
|
+
// them 190 ms before its first line: a server Playwright waits on missed its
|
|
213
|
+
// first look, about 4 s a run. The runner's children are marked as not test
|
|
214
|
+
// workers, and the launch supervisor marks the ones it forks from its worker
|
|
215
|
+
// entry as workers, so only those load the wrapper.
|
|
216
|
+
process.env.SUPERCOV_PLAYWRIGHT_WORKER = "0";
|
|
217
|
+
}
|
|
218
218
|
// Linters, formatters and type checks read the project to judge it, not to
|
|
219
219
|
// run it. The workspace holds rewritten copies, and ESLint failed semver,
|
|
220
220
|
// js-yaml and picomatch on code nobody wrote ("Strings must use singlequote",
|
|
@@ -984,13 +984,41 @@ if (missingFile) {
|
|
|
984
984
|
}
|
|
985
985
|
};
|
|
986
986
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
987
|
+
const loaderData = {
|
|
988
|
+
typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS",
|
|
989
|
+
supportFile,
|
|
990
|
+
missingFile,
|
|
991
|
+
};
|
|
992
|
+
// Where Node can (22.15, 23.5), the hooks run in this thread. Off it, every
|
|
993
|
+
// Node process started a thread of its own and compiled a module loader
|
|
994
|
+
// there, and each import and require waited on a message to it: on a Next.js
|
|
995
|
+
// app's Vitest suite on one worker, a third of the time a measured run added.
|
|
996
|
+
if (typeof Module.registerHooks === "function") {
|
|
997
|
+
const hooks = await import("./resolve-loader.mjs");
|
|
998
|
+
hooks.initialize(loaderData);
|
|
999
|
+
Module.registerHooks({ resolve: hooks.resolveSync, load: hooks.loadSync });
|
|
1000
|
+
// Playwright compiles ES modules in the hooks thread its own loader
|
|
1001
|
+
// starts, which these hooks no longer reach: the cache patch goes to that
|
|
1002
|
+
// thread by itself, ahead of the first module anyone registers off this
|
|
1003
|
+
// one, which is what starts it. Supercov names the cache for every run,
|
|
1004
|
+
// and registering it unasked started the thread in every Node process --
|
|
1005
|
+
// a Vitest worker -- and sent each of its imports through it.
|
|
1006
|
+
if (process.env.PWTEST_CACHE_DIR) {
|
|
1007
|
+
const registerOffThread = Module.register;
|
|
1008
|
+
let cacheThreadRegistered = false;
|
|
1009
|
+
Module.register = function (...args) {
|
|
1010
|
+
if (!cacheThreadRegistered) {
|
|
1011
|
+
cacheThreadRegistered = true;
|
|
1012
|
+
registerOffThread.call(this, new URL("./playwrightCacheThread.mjs", import.meta.url));
|
|
1013
|
+
}
|
|
1014
|
+
return registerOffThread.apply(this, args);
|
|
1015
|
+
};
|
|
1016
|
+
syncBuiltinESMExports();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
register(new URL("./resolve-loader.mjs", import.meta.url), { data: loaderData });
|
|
1021
|
+
}
|
|
994
1022
|
if (process.env.SUPERCOV_DEBUG === "1") {
|
|
995
1023
|
console.error("[supercov] preload", { entrypoint });
|
|
996
1024
|
}
|
|
@@ -1056,9 +1084,21 @@ if (generatedPlaywrightConfig &&
|
|
|
1056
1084
|
}
|
|
1057
1085
|
process.argv.push("--config", generatedPlaywrightConfig);
|
|
1058
1086
|
}
|
|
1087
|
+
// A process the runner marked as not a test worker skips the wrapper, unless
|
|
1088
|
+
// it was started from Playwright's own files: a worker Playwright starts some
|
|
1089
|
+
// other way than its worker entry still has the wrapper, only later. The
|
|
1090
|
+
// runner itself has it too: it lists each test with the fixtures its spec's
|
|
1091
|
+
// `test` carries, and puts tests with the same worker fixtures in one worker.
|
|
1092
|
+
// A CommonJS spec it listed with Playwright's own `test` ran in the worker
|
|
1093
|
+
// with the wrapper's, beside a spec through a facade that has Playwright's
|
|
1094
|
+
// own in both, and the worker refused the pair: "Playwright detected
|
|
1095
|
+
// inconsistent test.use() options". An ES module spec already had the wrapper
|
|
1096
|
+
// in the runner, through the module hooks.
|
|
1097
|
+
const isPlaywrightProcess = /\/node_modules\/(?:@playwright\/test|playwright)\//.test(entrypoint);
|
|
1098
|
+
const mayBePlaywrightWorker = process.env.SUPERCOV_PLAYWRIGHT_WORKER !== "0" || isPlaywrightProcess;
|
|
1059
1099
|
if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
|
|
1060
1100
|
process.env.SUPERCOV_INSIDE_PLAYWRIGHT === "1" &&
|
|
1061
|
-
|
|
1101
|
+
(isPlaywrightEntrypoint || mayBePlaywrightWorker) &&
|
|
1062
1102
|
process.env.SUPERCOV_INSIDE_VITEST !== "1" &&
|
|
1063
1103
|
process.env.VITEST !== "true") {
|
|
1064
1104
|
const target = playwrightTarget ?? "@playwright/test";
|
|
@@ -1066,6 +1106,16 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
|
|
|
1066
1106
|
const originalPlaywrightConfig = process.env.SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG
|
|
1067
1107
|
?.replaceAll("\\", "/");
|
|
1068
1108
|
const wrapper = await import(__rewriteRelativeImportExtension(new URL("./playwright.mjs", import.meta.url)));
|
|
1109
|
+
// A spec transpiled from `import test from "@playwright/test"` reads the
|
|
1110
|
+
// module's `default` when it says it is an ES module, and the module
|
|
1111
|
+
// itself otherwise: the wrapper's namespace says nothing and is no
|
|
1112
|
+
// function, and a Next.js app's spec failed with `(0 , _test.default) is not
|
|
1113
|
+
// a function`. Its exports, marked as an ES module's, with `test` itself
|
|
1114
|
+
// as the default. Not a function that calls `test` (Playwright names a
|
|
1115
|
+
// test by where it was declared, read from the stack, and an extra frame
|
|
1116
|
+
// moved it), nor the exports assigned onto the wrapper's `test` (its
|
|
1117
|
+
// fixtures then differed between the runner and a worker).
|
|
1118
|
+
const commonjsWrapper = { ...wrapper, __esModule: true, default: wrapper.test };
|
|
1069
1119
|
const originalLoad = Module._load;
|
|
1070
1120
|
Module._load = function supercovLoad(request, parent, isMain) {
|
|
1071
1121
|
const parentFile = parent?.filename?.replaceAll("\\", "/");
|
|
@@ -1083,7 +1133,7 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
|
|
|
1083
1133
|
? parentFile.startsWith(`${normalizedRoot}/`)
|
|
1084
1134
|
: parentFile.includes("/tests/"));
|
|
1085
1135
|
if (request === target && belongsToProject)
|
|
1086
|
-
return
|
|
1136
|
+
return commonjsWrapper;
|
|
1087
1137
|
return originalLoad.call(this, request, parent, isMain);
|
|
1088
1138
|
};
|
|
1089
1139
|
}
|
|
@@ -1136,7 +1186,13 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1") {
|
|
|
1136
1186
|
}
|
|
1137
1187
|
}
|
|
1138
1188
|
const originalLoad = Module._load;
|
|
1189
|
+
const redirected = new Set(["test", "node:test", "assert", "node:assert", "assert/strict", "node:assert/strict"]);
|
|
1139
1190
|
Module._load = function supercovNodeTestLoad(request, parent, isMain) {
|
|
1191
|
+
// Every require comes here, cached or not, thousands in a test
|
|
1192
|
+
// worker: only these six names are redirected, and nothing else is
|
|
1193
|
+
// looked at for any other.
|
|
1194
|
+
if (!redirected.has(request))
|
|
1195
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
1140
1196
|
const parentFile = parent?.filename?.replaceAll("\\", "/");
|
|
1141
1197
|
const belongsToProject = Boolean(projectRoot &&
|
|
1142
1198
|
parentFile?.startsWith(`${projectRoot}/`) &&
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { writePlaywrightCacheWhole } from "./atomic.mjs";
|
|
5
|
+
// This thread is where Playwright compiles ES modules and writes them into
|
|
6
|
+
// its cache: written whole here too (atomic.mjs says why).
|
|
7
|
+
writePlaywrightCacheWhole();
|
|
4
8
|
const GENERATED_TARGET = "__SUPERCOV_PLAYWRIGHT_MODULE__";
|
|
5
9
|
const TARGET = process.env.SUPERCOV_PLAYWRIGHT_MODULE ??
|
|
6
10
|
(GENERATED_TARGET.startsWith("__") ? "@playwright/test" : GENERATED_TARGET);
|
|
@@ -111,7 +115,9 @@ function throughRequireHook(resolved) {
|
|
|
111
115
|
return resolved;
|
|
112
116
|
return { ...resolved, format: "commonjs" };
|
|
113
117
|
}
|
|
114
|
-
|
|
118
|
+
// A specifier Supercov answers itself (`url`), or sends on as another
|
|
119
|
+
// (`next`, answered as it is); undefined for every other.
|
|
120
|
+
function redirected(specifier, context) {
|
|
115
121
|
// Some transpilers preserve the source-relative runtime import while moving
|
|
116
122
|
// only the transformed application file to an output directory. The
|
|
117
123
|
// source-local copy keeps strict rootDir compilers happy; this fallback
|
|
@@ -157,8 +163,16 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
157
163
|
shortCircuit: true,
|
|
158
164
|
};
|
|
159
165
|
}
|
|
160
|
-
return
|
|
166
|
+
return { next: REPLACEMENT };
|
|
161
167
|
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
171
|
+
const redirect = redirected(specifier, context);
|
|
172
|
+
if (redirect?.url)
|
|
173
|
+
return redirect;
|
|
174
|
+
if (redirect?.next)
|
|
175
|
+
return nextResolve(redirect.next, context);
|
|
162
176
|
try {
|
|
163
177
|
return throughRequireHook(await nextResolve(specifier, context));
|
|
164
178
|
}
|
|
@@ -168,6 +182,28 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
168
182
|
throw error;
|
|
169
183
|
}
|
|
170
184
|
}
|
|
185
|
+
// The same hooks run in the loading thread (`module.registerHooks`), where
|
|
186
|
+
// the next hook answers at once. There they see `require` calls too, which
|
|
187
|
+
// the hooks off the thread never did, and the CommonJS loader routes itself:
|
|
188
|
+
// a TypeScript file it was given back as CommonJS was compiled as JavaScript,
|
|
189
|
+
// past the handler ts-node/register installed for it.
|
|
190
|
+
export function resolveSync(specifier, context, nextResolve) {
|
|
191
|
+
if (context.conditions?.includes("require"))
|
|
192
|
+
return nextResolve(specifier, context);
|
|
193
|
+
const redirect = redirected(specifier, context);
|
|
194
|
+
if (redirect?.url)
|
|
195
|
+
return redirect;
|
|
196
|
+
if (redirect?.next)
|
|
197
|
+
return nextResolve(redirect.next, context);
|
|
198
|
+
try {
|
|
199
|
+
return throughRequireHook(nextResolve(specifier, context));
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error?.code === "ERR_MODULE_NOT_FOUND")
|
|
203
|
+
noteMissing(specifier, context.parentURL);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
171
207
|
|
|
172
208
|
// Node's own test coverage filters files by `--test-coverage-include` and
|
|
173
209
|
// `--test-coverage-exclude` globs relative to the working directory, and the
|
|
@@ -200,7 +236,17 @@ function mapToCopy(source, filename) {
|
|
|
200
236
|
}
|
|
201
237
|
}
|
|
202
238
|
export async function load(url, context, nextLoad) {
|
|
203
|
-
|
|
239
|
+
return loaded(url, await nextLoad(url, context));
|
|
240
|
+
}
|
|
241
|
+
export function loadSync(url, context, nextLoad) {
|
|
242
|
+
// Every module this process loads, required or imported: a server's
|
|
243
|
+
// runtime rescans the module cache for new ones only when this moved.
|
|
244
|
+
globalThis.__SUPERCOV_MODULE_LOADS__ = (globalThis.__SUPERCOV_MODULE_LOADS__ ?? 0) + 1;
|
|
245
|
+
if (context.conditions?.includes("require"))
|
|
246
|
+
return nextLoad(url, context);
|
|
247
|
+
return loaded(url, nextLoad(url, context));
|
|
248
|
+
}
|
|
249
|
+
function loaded(url, result) {
|
|
204
250
|
noteLoaded(url);
|
|
205
251
|
if (!NODE_COVERAGE_FILTERS || result.source == null || !url.startsWith("file:"))
|
|
206
252
|
return result;
|
|
@@ -98,12 +98,27 @@ export function callerLocation(boundary = callerLocation) {
|
|
|
98
98
|
return {};
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A compact attempt file names its scope on a writer's first line only; each
|
|
103
|
+
* line without one is the attempt of the line before it.
|
|
104
|
+
*/
|
|
105
|
+
function inheritingScope() {
|
|
106
|
+
let current;
|
|
107
|
+
return (line) => {
|
|
108
|
+
const record = JSON.parse(line);
|
|
109
|
+
if (record.scope)
|
|
110
|
+
current = record.scope;
|
|
111
|
+
else if (current)
|
|
112
|
+
record.scope = current;
|
|
113
|
+
return record;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
101
116
|
export function readScopedServerEvidence(scope, evidencePath = serverEvidencePath(scope)) {
|
|
102
117
|
try {
|
|
103
118
|
return readFileSync(evidencePath, "utf8")
|
|
104
119
|
.split("\n")
|
|
105
120
|
.filter(Boolean)
|
|
106
|
-
.map((
|
|
121
|
+
.map(inheritingScope())
|
|
107
122
|
.filter((record) => record.scope?.attemptId === scope.attemptId);
|
|
108
123
|
}
|
|
109
124
|
catch {
|