Skip to content

Commit ddfd78b

Browse files
authored
Merge pull request #80 from probitas-test/feature/subprocess
Replace Web Worker with subprocess-based execution architecture
2 parents 75617c7 + 84504a4 commit ddfd78b

21 files changed

Lines changed: 1630 additions & 1290 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.DS_Store
22
.coverage
33
result
4+
dist/
5+
./src/cli/_embedded_templates.ts

assets/usage-list.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Options:
1717
--env <file> Load environment variables from file (default: .env)
1818
--no-env Skip loading .env file
1919
-r, --reload Reload dependencies before running
20+
--deno-<option>[=<value>] Pass options to deno subprocess
21+
Value options: --deno-config=custom.json, --deno-lock=custom.lock
22+
Boolean flags: --deno-no-lock, --deno-no-prompt
2023

2124
Selector Format:
2225
[!][type:]value

assets/usage-run.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Options:
2929
-v, --verbose Verbose output
3030
-d, --debug Debug output (maximum detail)
3131
--no-color Disable colored output
32+
--deno-<option>[=<value>] Pass options to deno subprocess
33+
Value options: --deno-config=custom.json, --deno-lock=custom.lock
34+
Boolean flags: --deno-no-lock, --deno-no-prompt
3235

3336
Selector Format:
3437
[!][type:]value

deno.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"exclude": [
4646
".coverage/**",
4747
".claude/**",
48-
".worktrees/**"
48+
".worktrees/**",
49+
"dist/**"
4950
],
5051
"tasks": {
5152
"probitas": "deno run -A --unstable-kv --lock --frozen ./src/cli.ts",
@@ -54,9 +55,12 @@
5455
"test:coverage": "deno task test --coverage=.coverage",
5556
"coverage": "deno coverage .coverage --include='/src/'",
5657
"verify": "deno fmt && deno lint && deno task check && deno task test",
57-
"release": "deno run -A .scripts/release.ts"
58+
"release": "deno run -A .scripts/release.ts",
59+
"compile": "deno run -A scripts/build_subprocess_templates.ts && deno compile -A --unstable-kv -o dist/probitas ./src/cli.ts"
5860
},
5961
"imports": {
62+
"@core/streamutil": "jsr:@core/streamutil@^1.0.0",
63+
"@deno/graph": "jsr:@deno/graph@^0.86.5",
6064
"@core/errorutil": "jsr:@core/errorutil@^1.2.1",
6165
"@core/unknownutil": "jsr:@core/unknownutil@^4.3.0",
6266
"@cspotcode/outdent": "jsr:@cspotcode/outdent@^0.8.0",
@@ -89,8 +93,10 @@
8993
"@std/collections": "jsr:@std/collections@^1.1.3",
9094
"@std/dotenv": "jsr:@std/dotenv@^0.225.6",
9195
"@std/fs": "jsr:@std/fs@^1.0.21",
96+
"@std/json": "jsr:@std/json@^1.0.2",
9297
"@std/jsonc": "jsr:@std/jsonc@^1.0.2",
9398
"@std/path": "jsr:@std/path@^1.1.4",
99+
"@std/streams": "jsr:@std/streams@^1.0.16",
94100
"@std/testing/bdd": "jsr:@std/testing@^1.0.16/bdd",
95101
"@std/testing/mock": "jsr:@std/testing@^1.0.16/mock",
96102
"@std/testing/time": "jsr:@std/testing@^1.0.16/time",

deno.lock

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env -S deno run -A
2+
/**
3+
* Build script to generate resolved subprocess templates
4+
*
5+
* Run before `deno compile`: deno run -A scripts/build_subprocess_templates.ts
6+
*
7+
* This script resolves bare specifiers in subprocess templates and embeds
8+
* them as string constants for use in compiled binaries.
9+
*
10+
* @module
11+
*/
12+
13+
import { resolveSubprocessTemplate } from "../src/cli/subprocess_template.ts";
14+
import type { EmbeddedTemplates } from "../src/cli/subprocess.ts";
15+
16+
const templates = [
17+
{
18+
name: "list",
19+
url: new URL("../src/cli/_templates/list.ts", import.meta.url),
20+
},
21+
{
22+
name: "run",
23+
url: new URL("../src/cli/_templates/run.ts", import.meta.url),
24+
},
25+
];
26+
27+
const outputs: EmbeddedTemplates = {};
28+
29+
for (const { name, url } of templates) {
30+
console.log(`Resolving template: ${name} (${url.href})`);
31+
try {
32+
const files = await resolveSubprocessTemplate(url);
33+
// Convert Map to Record for JSON serialization
34+
outputs[name] = Object.fromEntries(files);
35+
console.log(` ✓ Resolved ${files.size} file(s)`);
36+
} catch (error) {
37+
console.error(` ✗ Failed to resolve: ${error}`);
38+
Deno.exit(1);
39+
}
40+
}
41+
42+
// Generate embedded templates module
43+
const code = `// Auto-generated by scripts/build_subprocess_templates.ts
44+
// DO NOT EDIT MANUALLY
45+
46+
import type { EmbeddedTemplates } from "./subprocess.ts";
47+
48+
export const embeddedTemplates: EmbeddedTemplates = ${
49+
JSON.stringify(outputs, null, 2)
50+
};
51+
`;
52+
53+
const outputPath = new URL(
54+
"../src/cli/_embedded_templates.ts",
55+
import.meta.url,
56+
);
57+
await Deno.writeTextFile(outputPath, code);
58+
console.log(`\nGenerated: ${outputPath.pathname}`);

src/cli/_templates/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Subprocess Templates
2+
3+
Subprocess entry point templates executed in isolated Deno subprocesses.
4+
5+
## Files
6+
7+
| File | Description |
8+
| ------------------ | ------------------------------------- |
9+
| `run.ts` | Entry point for `probitas run` |
10+
| `list.ts` | Entry point for `probitas list` |
11+
| `run_protocol.ts` | Protocol types for run communication |
12+
| `list_protocol.ts` | Protocol types for list communication |
13+
| `utils.ts` | Shared utilities (TCP-based IPC) |
14+
15+
## Rules
16+
17+
- **Self-contained**: Only import from within this directory or JSR packages
18+
- **No direct imports**: Use `subprocess_template.ts` to resolve bare specifiers

src/cli/_templates/list.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Subprocess entry point for list command
3+
*
4+
* Loads scenarios and outputs metadata via TCP IPC.
5+
* Communication is via NDJSON over TCP (not stdin/stdout).
6+
*
7+
* @module
8+
* @internal
9+
*/
10+
11+
import { loadScenarios } from "@probitas/core/loader";
12+
import { applySelectors } from "@probitas/core/selector";
13+
import { toErrorObject } from "@core/errorutil/error-object";
14+
import type { ListInput, ListOutput, ScenarioMeta } from "./list_protocol.ts";
15+
import {
16+
closeIpc,
17+
connectIpc,
18+
parseIpcPort,
19+
readInput,
20+
writeOutput,
21+
} from "./utils.ts";
22+
23+
/**
24+
* Main entry point
25+
*/
26+
async function main(): Promise<void> {
27+
// Parse IPC port from command line arguments
28+
const port = parseIpcPort(Deno.args);
29+
30+
// Connect to parent process IPC server
31+
const ipc = await connectIpc(port);
32+
33+
try {
34+
// Read input from IPC (TCP connection establishes readiness)
35+
const input = await readInput(ipc) as ListInput;
36+
37+
// Load scenarios from files
38+
const scenarios = await loadScenarios(input.filePaths, {
39+
onImportError: (file, err) => {
40+
const m = err instanceof Error ? err.message : String(err);
41+
throw new Error(`Failed to load scenario from ${file}: ${m}`);
42+
},
43+
});
44+
45+
// Apply selectors to filter scenarios
46+
const filtered = input.selectors.length > 0
47+
? applySelectors(scenarios, input.selectors)
48+
: scenarios;
49+
50+
// Build output metadata
51+
const scenarioMetas: ScenarioMeta[] = filtered.map((s) => ({
52+
name: s.name,
53+
tags: s.tags,
54+
steps: s.steps.filter((e) => e.kind === "step").length,
55+
file: s.origin?.path || "unknown",
56+
}));
57+
58+
await writeOutput(
59+
ipc,
60+
{
61+
type: "result",
62+
scenarios: scenarioMetas,
63+
} satisfies ListOutput,
64+
);
65+
} catch (error) {
66+
const err = error instanceof Error ? error : new Error(String(error));
67+
try {
68+
await writeOutput(
69+
ipc,
70+
{
71+
type: "error",
72+
error: toErrorObject(err),
73+
} satisfies ListOutput,
74+
);
75+
} catch {
76+
// Failed to write error to IPC, log to console as fallback
77+
console.error("Subprocess error:", error);
78+
}
79+
} finally {
80+
closeIpc(ipc);
81+
}
82+
}
83+
84+
// Run main and exit explicitly to avoid async operations keeping process alive
85+
main().finally(() => {
86+
// Ensure process exits after output is flushed
87+
setTimeout(() => Deno.exit(0), 0);
88+
});

0 commit comments

Comments
 (0)