@systemfsoftware/stryker-js-cli 4.0.2 → 5.0.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,105 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
4
+ import { ChildProcessCrashedError, WorkerEntries, WorkerLauncher } from "@systemfsoftware/stryker-js-engine";
5
+ import * as FileSystem from "effect/FileSystem";
6
+ import * as Match from "effect/Match";
7
+ import * as Path from "effect/Path";
8
+ import { NodeFileSystem, NodePath, NodeSocket } from "@effect/platform-node";
9
+ import { Module } from "@systemfsoftware/stryker-js/Module";
10
+ import * as ChildProcess from "effect/unstable/process/ChildProcess";
11
+ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
12
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
13
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
14
+ //#region src/platform/node.ts
15
+ const makeModuleRequire = (nodeModule, filename) => {
16
+ const requireFrom = nodeModule.createRequire(filename);
17
+ const requireFn = (request) => requireFrom(request);
18
+ requireFn.resolve = (request, options) => {
19
+ if (options === void 0) return requireFrom.resolve(request);
20
+ return requireFrom.resolve(request, { paths: [...options.paths ?? []] });
21
+ };
22
+ return requireFn;
23
+ };
24
+ /**
25
+ * The Node implementation of the {@link Module} port: every call routes
26
+ * through the runtime's own `node:module` via `process.getBuiltinModule`, so
27
+ * this package imports no host builtins and the import ban holds here too.
28
+ */
29
+ const nodeModuleLayer = Layer.effect(Module, Effect.sync(() => {
30
+ const nodeModule = process.getBuiltinModule("node:module");
31
+ return {
32
+ createRequire: (filename) => makeModuleRequire(nodeModule, filename),
33
+ isBuiltin: (moduleName) => nodeModule.isBuiltin(moduleName)
34
+ };
35
+ }));
36
+ /**
37
+ * The worker entries this package's own build emits. The engine spawns by
38
+ * address; this process package knows its dist layout and hands the
39
+ * addresses in.
40
+ */
41
+ const workerEntriesLayer = Layer.succeed(WorkerEntries, {
42
+ checkerWorkerUrl: new URL("./workers/checker-worker.mjs", import.meta.url),
43
+ testRunnerWorkerUrl: new URL("./workers/child-process-test-runner-worker.mjs", import.meta.url)
44
+ });
45
+ /**
46
+ * The Node worker launcher: spawn a worker child with this runtime's
47
+ * executable, host the RPC server's address as a `net` `path` endpoint (a
48
+ * socket file in the worker directory on POSIX, a same-user named pipe on
49
+ * Windows), and connect the NDJSON protocol client over `NodeSocket`.
50
+ */
51
+ const nodeWorkerLauncherLayer = Layer.effect(WorkerLauncher, Effect.gen(function* () {
52
+ const fs = yield* FileSystem.FileSystem;
53
+ const path = yield* Path.Path;
54
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
55
+ return { spawn: (params) => Effect.gen(function* () {
56
+ const workerDir = yield* fs.makeTempDirectoryScoped({ prefix: params.tempDirPrefix });
57
+ const socketPath = Match.value(process.platform).pipe(Match.when("win32", () => `\\\\.\\pipe\\stryker-worker-${globalThis.crypto.randomUUID()}`), Match.orElse(() => path.join(workerDir, "worker.sock")));
58
+ yield* fs.writeFileString(path.join(workerDir, "options.json"), params.optionsJson);
59
+ const entryPath = yield* path.fromFileUrl(params.entryUrl);
60
+ const handle = yield* ChildProcess.make(process.execPath, [...params.execArgv, entryPath], {
61
+ cwd: params.workingDirectory,
62
+ extendEnv: true,
63
+ env: {
64
+ STRYKER_WORKER_DIR: workerDir,
65
+ STRYKER_SOCKET: socketPath
66
+ },
67
+ stderr: "inherit"
68
+ }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner));
69
+ const clientLayer = RpcClient.layerProtocolSocket({ retryTransientErrors: true }).pipe(Layer.provide(NodeSocket.layerNet({ path: socketPath })), Layer.provide(RpcSerialization.layerNdjson));
70
+ const exited = handle.exitCode.pipe(Effect.orDie, Effect.flatMap((exitCode) => Effect.fail(new ChildProcessCrashedError({
71
+ pid: Number(handle.pid),
72
+ exit: {
73
+ _tag: "Code",
74
+ code: exitCode
75
+ },
76
+ cause: "worker exited before it accepted the RPC connection"
77
+ }))));
78
+ return {
79
+ pid: Number(handle.pid),
80
+ clientLayer,
81
+ exited
82
+ };
83
+ }).pipe(Effect.catch((error) => {
84
+ if (error instanceof ChildProcessCrashedError) return Effect.fail(error);
85
+ return Effect.fail(new ChildProcessCrashedError({
86
+ pid: 0,
87
+ exit: {
88
+ _tag: "Code",
89
+ code: 1
90
+ },
91
+ cause: "worker spawn failed"
92
+ }));
93
+ })) };
94
+ }));
95
+ const nodeFsPathLayer = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
96
+ const nodeSpawnerLayer = NodeChildProcessSpawner.layer.pipe(Layer.provide(nodeFsPathLayer));
97
+ const nodeBase = Layer.merge(nodeFsPathLayer, nodeSpawnerLayer);
98
+ /**
99
+ * Every port the engine requires, provided from this runtime: the file
100
+ * system, the path service, the module loader, the child-process spawner,
101
+ * the worker launcher, and this build's worker entry addresses.
102
+ */
103
+ const nodePlatformLayer = Layer.mergeAll(nodeModuleLayer, workerEntriesLayer, nodeWorkerLauncherLayer.pipe(Layer.provide(nodeBase)), nodeBase);
104
+ //#endregion
105
+ export { nodePlatformLayer as n, nodeModuleLayer as t };
@@ -0,0 +1,20 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as Cause from "effect/Cause";
4
+ //#region src/workers/worker-runtime.ts
5
+ const workerSocketPath = (label) => {
6
+ const socketPath = process.env["STRYKER_SOCKET"];
7
+ if (socketPath === void 0) {
8
+ process.stderr.write(`${label}: STRYKER_SOCKET is not set\n`);
9
+ process.exit(1);
10
+ }
11
+ return socketPath;
12
+ };
13
+ const launchWorker = (main, label) => {
14
+ Effect.runFork(Layer.launch(main).pipe(Effect.tapCause((cause) => Effect.sync(() => {
15
+ process.stderr.write(`${label}: ${Cause.pretty(cause)}\n`);
16
+ process.exitCode = 1;
17
+ }))));
18
+ };
19
+ //#endregion
20
+ export { workerSocketPath as n, launchWorker as t };
@@ -0,0 +1,53 @@
1
+ import { t as nodeModuleLayer } from "../node-D-ynY_kk.mjs";
2
+ import { n as workerSocketPath, t as launchWorker } from "../worker-runtime-CT5LMMgY.mjs";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import * as FileSystem from "effect/FileSystem";
6
+ import * as Option from "effect/Option";
7
+ import * as Path from "effect/Path";
8
+ import { NodeFileSystem, NodePath, NodeSocketServer } from "@effect/platform-node";
9
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
10
+ import { Checker, CheckerFailed } from "@systemfsoftware/stryker-js/Checker";
11
+ import { RunConfiguration, SandboxDirectory } from "@systemfsoftware/stryker-js/Plugin";
12
+ import * as HashMap from "effect/HashMap";
13
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
14
+ import { CheckerRpcs, create, decodeWorkerOptions, loadPlugins } from "@systemfsoftware/stryker-js-engine/worker";
15
+ //#region src/workers/Checker.worker.ts
16
+ const buildChecker = (contribution, options) => Checker.pipe(Effect.provide(contribution.layer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(RunConfiguration, options), Layer.succeed(SandboxDirectory, process.cwd()), NodeFileSystem.layer, NodePath.layer, nodeModuleLayer)))));
17
+ const mutantIdsOf = (mutants) => mutants.map((mutant) => mutant.id);
18
+ const readWorkerOptions = Effect.gen(function* () {
19
+ const workerDir = process.env["STRYKER_WORKER_DIR"] ?? (yield* Effect.die(/* @__PURE__ */ new Error("STRYKER_WORKER_DIR is not set")));
20
+ const fs = yield* FileSystem.FileSystem;
21
+ const path = yield* Path.Path;
22
+ const raw = yield* fs.readFileString(path.join(workerDir, "options.json"));
23
+ return yield* decodeWorkerOptions(raw);
24
+ });
25
+ const CheckerHandlers = CheckerRpcs.toLayer(Effect.gen(function* () {
26
+ const options = yield* readWorkerOptions;
27
+ const loaded = yield* loadPlugins(options.plugins, process.cwd());
28
+ const checkers = HashMap.fromIterable(yield* Effect.forEach(options.checkers, (name) => Effect.gen(function* () {
29
+ const contribution = yield* create(loaded.pluginsByKind, "Checker", name);
30
+ const checker = yield* buildChecker(contribution, options);
31
+ yield* checker.init;
32
+ return [name, checker];
33
+ }), {
34
+ concurrency: "unbounded",
35
+ discard: false
36
+ }));
37
+ const resolve = (checkerName, mutants) => Option.match(HashMap.get(checkers, checkerName), {
38
+ onNone: () => Effect.fail(new CheckerFailed({
39
+ cause: `Checker ${checkerName} does not exist`,
40
+ checkerName,
41
+ mutantIds: mutantIdsOf(mutants)
42
+ })),
43
+ onSome: (checker) => Effect.succeed(checker)
44
+ });
45
+ return {
46
+ check: ({ checkerName, mutants }) => resolve(checkerName, mutants).pipe(Effect.flatMap((checker) => checker.check([...mutants])), Effect.map((resultMap) => Object.fromEntries(resultMap))),
47
+ group: ({ checkerName, mutants }) => resolve(checkerName, mutants).pipe(Effect.flatMap((checker) => checker.group([...mutants])))
48
+ };
49
+ })).pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
50
+ const MainLayer = RpcServer.layer(CheckerRpcs).pipe(Layer.provide(CheckerHandlers), Layer.provide(RpcServer.layerProtocolSocketServer), Layer.provide(RpcSerialization.layerNdjson), Layer.provide(NodeSocketServer.layer({ path: workerSocketPath("checker worker") })), Layer.provide(nodeModuleLayer));
51
+ launchWorker(MainLayer, "checker worker");
52
+ //#endregion
53
+ export {};
@@ -0,0 +1,67 @@
1
+ import { t as nodeModuleLayer } from "../node-D-ynY_kk.mjs";
2
+ import { n as workerSocketPath, t as launchWorker } from "../worker-runtime-CT5LMMgY.mjs";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import { errorToString } from "@systemfsoftware/stryker-js/Mutant";
6
+ import * as Cause from "effect/Cause";
7
+ import * as FileSystem from "effect/FileSystem";
8
+ import * as Path from "effect/Path";
9
+ import { NodeFileSystem, NodePath, NodeSocketServer } from "@effect/platform-node";
10
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
11
+ import { RunConfiguration, SandboxDirectory } from "@systemfsoftware/stryker-js/Plugin";
12
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
13
+ import { MutantCoverageSchema, TestRunnerRpcs, create, decodeWorkerOptions, loadPlugins } from "@systemfsoftware/stryker-js-engine/worker";
14
+ import { TestRunner, TestRunnerFailed } from "@systemfsoftware/stryker-js/TestRunner";
15
+ import { Schema } from "effect";
16
+ //#region src/workers/child-process-test-runner-worker.ts
17
+ const withCoverage = (result, options) => Effect.gen(function* () {
18
+ if (result.status === "error") return {
19
+ ...result,
20
+ errorMessage: errorToString(result.errorMessage)
21
+ };
22
+ if (result.status !== "complete") return result;
23
+ if (result.mutantCoverage !== void 0) return result;
24
+ if (options.coverageAnalysis === "off") return result;
25
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.optional(MutantCoverageSchema))(globalThis.__mutantCoverage__).pipe(Effect.orElseSucceed(() => void 0));
26
+ if (decoded === void 0) return result;
27
+ return {
28
+ ...result,
29
+ mutantCoverage: decoded
30
+ };
31
+ });
32
+ const normalizeMutantRun = (result) => {
33
+ if (result.status === "error") return {
34
+ ...result,
35
+ errorMessage: errorToString(result.errorMessage)
36
+ };
37
+ return result;
38
+ };
39
+ const readWorkerOptions = Effect.gen(function* () {
40
+ const workerDir = process.env["STRYKER_WORKER_DIR"] ?? (yield* Effect.die(/* @__PURE__ */ new Error("STRYKER_WORKER_DIR is not set")));
41
+ const fs = yield* FileSystem.FileSystem;
42
+ const path = yield* Path.Path;
43
+ const raw = yield* fs.readFileString(path.join(workerDir, "options.json"));
44
+ return yield* decodeWorkerOptions(raw);
45
+ });
46
+ const TestRunnerHandlers = TestRunnerRpcs.toLayer(Effect.gen(function* () {
47
+ const options = yield* readWorkerOptions;
48
+ const runnerName = options.testRunner;
49
+ const failed = (phase) => (cause) => Effect.fail(new TestRunnerFailed({
50
+ cause: Cause.pretty(cause),
51
+ phase,
52
+ runnerName
53
+ }));
54
+ const loaded = yield* loadPlugins(options.plugins, process.cwd()).pipe(Effect.catchCause(failed("init")));
55
+ const underlying = yield* create(loaded.pluginsByKind, "TestRunner", runnerName).pipe(Effect.flatMap((contribution) => TestRunner.pipe(Effect.provide(contribution.layer))), Effect.provide(Layer.merge(Layer.succeed(RunConfiguration, options), Layer.succeed(SandboxDirectory, process.cwd()))), Effect.catchCause(failed("init")));
56
+ yield* underlying.init.pipe(Effect.catchCause(failed("init")));
57
+ yield* Effect.addFinalizer(() => underlying.dispose.pipe(Effect.ignore));
58
+ return {
59
+ capabilities: () => underlying.capabilities.pipe(Effect.catchCause(failed("capabilities"))),
60
+ dryRun: ({ options: runOptions }) => underlying.dryRun(runOptions).pipe(Effect.flatMap((result) => withCoverage(result, runOptions)), Effect.catchCause(failed("dryRun"))),
61
+ mutantRun: ({ options: runOptions }) => underlying.mutantRun(runOptions).pipe(Effect.map(normalizeMutantRun), Effect.catchCause(failed("mutantRun")))
62
+ };
63
+ })).pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
64
+ const MainLayer = RpcServer.layer(TestRunnerRpcs).pipe(Layer.provide(TestRunnerHandlers), Layer.provide(RpcServer.layerProtocolSocketServer), Layer.provide(RpcSerialization.layerNdjson), Layer.provide(NodeSocketServer.layer({ path: workerSocketPath("test runner worker") })), Layer.provide(nodeModuleLayer));
65
+ launchWorker(MainLayer, "test runner worker");
66
+ //#endregion
67
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemfsoftware/stryker-js-cli",
3
- "version": "4.0.2",
3
+ "version": "5.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
@@ -18,32 +18,33 @@
18
18
  "@effect/platform-node-shared": "^4.0.0-rc.112",
19
19
  "@noble/hashes": "1.8.0",
20
20
  "effect": "^4.0.0-rc.112",
21
- "@systemfsoftware/effect-cell-types": "^5.0.2",
22
- "@systemfsoftware/stryker-js-html-reporter": "^0.1.1",
23
- "@systemfsoftware/stryker-js": "^0.1.1",
24
- "@systemfsoftware/stryker-js-platform-node": "^0.1.2"
21
+ "@systemfsoftware/effect-cell-types": "^6.0.0",
22
+ "@systemfsoftware/stryker-js-engine": "^0.2.0",
23
+ "@systemfsoftware/stryker-js": "^1.0.0",
24
+ "@systemfsoftware/stryker-js-html-reporter": "^1.0.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@systemfsoftware/arethetypeswrong-cli": "^1.1.1",
28
28
  "@types/node": "^24",
29
29
  "@vitest/coverage-v8": "^4",
30
+ "mutation-testing-report-schema": "3.7.3",
30
31
  "oxlint": "^1.77.0",
31
32
  "rimraf": "^6.1.3",
32
33
  "testcontainers": "^12.1.0",
33
34
  "tsdown": "^0.22.14",
34
35
  "vite-tsconfig-paths": "^6.1.1",
35
36
  "vitest": "^4",
36
- "@systemfsoftware/all": "^1.0.2",
37
+ "@systemfsoftware/all": "^1.1.2",
37
38
  "@systemfsoftware/effect-gherkin-spec": "^4.0.0",
38
39
  "@systemfsoftware/effect-schema-law": "^2.0.1",
39
40
  "@systemfsoftware/effect-schema-vite": "^2.0.2",
40
- "@systemfsoftware/stryker-js-typescript-checker": "^3.0.2",
41
- "@systemfsoftware/stryker-js-vitest-runner": "^2.0.2",
42
- "@systemfsoftware/stryker-test-contribution": "^1.1.1",
43
- "@systemfsoftware/stryker-plugins": "^2.0.0",
44
- "@systemfsoftware/oxlint-config": "^0.1.0",
41
+ "@systemfsoftware/stryker-js-typescript-checker": "^4.0.0",
42
+ "@systemfsoftware/stryker-js-vitest-runner": "^3.0.0",
43
+ "@systemfsoftware/stryker-plugins": "^2.0.2",
44
+ "@systemfsoftware/stryker-test-contribution": "^1.1.3",
45
+ "@systemfsoftware/vitest-config": "^0.1.0",
45
46
  "@systemfsoftware/tsconfig": "^1.3.3",
46
- "@systemfsoftware/vitest-config": "^0.1.0"
47
+ "@systemfsoftware/oxlint-config": "^0.1.0"
47
48
  },
48
49
  "engines": {
49
50
  "node": ">=20.0.0"