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.
@@ -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 1.59 writes each file it compiles into its cache in place and
18
- // reads it back unchecked: a process that read one while another wrote it
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
- register(new URL("./resolve-loader.mjs", import.meta.url), {
988
- data: {
989
- typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS",
990
- supportFile,
991
- missingFile,
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
- !isPlaywrightEntrypoint &&
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 wrapper;
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
- export async function resolve(specifier, context, nextResolve) {
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 nextResolve(REPLACEMENT, context);
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
- const result = await nextLoad(url, context);
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((line) => JSON.parse(line))
121
+ .map(inheritingScope())
107
122
  .filter((record) => record.scope?.attemptId === scope.attemptId);
108
123
  }
109
124
  catch {