@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.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -2
- package/dist/main.mjs +349 -396
- package/dist/node-D-ynY_kk.mjs +105 -0
- package/dist/worker-runtime-CT5LMMgY.mjs +20 -0
- package/dist/workers/checker-worker.mjs +53 -0
- package/dist/workers/child-process-test-runner-worker.mjs +67 -0
- package/package.json +13 -12
|
@@ -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": "
|
|
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": "^
|
|
22
|
-
"@systemfsoftware/stryker-js-
|
|
23
|
-
"@systemfsoftware/stryker-js": "^0.
|
|
24
|
-
"@systemfsoftware/stryker-js-
|
|
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.
|
|
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": "^
|
|
41
|
-
"@systemfsoftware/stryker-js-vitest-runner": "^
|
|
42
|
-
"@systemfsoftware/stryker-
|
|
43
|
-
"@systemfsoftware/stryker-
|
|
44
|
-
"@systemfsoftware/
|
|
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/
|
|
47
|
+
"@systemfsoftware/oxlint-config": "^0.1.0"
|
|
47
48
|
},
|
|
48
49
|
"engines": {
|
|
49
50
|
"node": ">=20.0.0"
|