@reventlessdev/reventless-gwt 1.0.0-alpha.115 → 1.0.0-alpha.117

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +9 -16
  3. package/rescript.json +0 -1
  4. package/bin/reventless-dev.mjs +0 -21
  5. package/src/Bind.res +0 -29
  6. package/src/Bind.res.mjs +0 -22
  7. package/src/BuildClassifier.res +0 -147
  8. package/src/BuildClassifier.res.mjs +0 -136
  9. package/src/Cancellation.res +0 -37
  10. package/src/Cancellation.res.mjs +0 -46
  11. package/src/ChildProcess.res +0 -102
  12. package/src/ChildProcess.res.mjs +0 -107
  13. package/src/Cli.res +0 -889
  14. package/src/Cli.res.mjs +0 -944
  15. package/src/Collector.res +0 -222
  16. package/src/Collector.res.mjs +0 -228
  17. package/src/ComponentMeta.res +0 -77
  18. package/src/ComponentMeta.res.mjs +0 -91
  19. package/src/ComponentScan.res +0 -101
  20. package/src/ComponentScan.res.mjs +0 -92
  21. package/src/Discovery.res +0 -102
  22. package/src/Discovery.res.mjs +0 -86
  23. package/src/DomainDeadCode.res +0 -98
  24. package/src/DomainDeadCode.res.mjs +0 -72
  25. package/src/DomainGraph.res +0 -0
  26. package/src/DomainGraph.res.mjs +0 -0
  27. package/src/ExnMessage.res +0 -14
  28. package/src/ExnMessage.res.mjs +0 -17
  29. package/src/ExnReflect.mjs +0 -24
  30. package/src/Filter.res +0 -45
  31. package/src/Filter.res.mjs +0 -47
  32. package/src/FormatterHuman.res +0 -109
  33. package/src/FormatterHuman.res.mjs +0 -115
  34. package/src/FormatterJson.res +0 -264
  35. package/src/FormatterJson.res.mjs +0 -322
  36. package/src/FormatterJunit.res +0 -93
  37. package/src/FormatterJunit.res.mjs +0 -90
  38. package/src/FormatterTap.res +0 -100
  39. package/src/FormatterTap.res.mjs +0 -131
  40. package/src/FormatterVsCode.res +0 -352
  41. package/src/FormatterVsCode.res.mjs +0 -546
  42. package/src/Loader.res +0 -24
  43. package/src/Loader.res.mjs +0 -23
  44. package/src/PackageScan.res +0 -80
  45. package/src/PackageScan.res.mjs +0 -81
  46. package/src/PlatformRunner.res +0 -191
  47. package/src/PlatformRunner.res.mjs +0 -178
  48. package/src/PlatformScan.res +0 -141
  49. package/src/PlatformScan.res.mjs +0 -124
  50. package/src/ProcessManager.res +0 -135
  51. package/src/ProcessManager.res.mjs +0 -119
  52. package/src/RunWorker.res +0 -24
  53. package/src/RunWorker.res.mjs +0 -16
  54. package/src/RunnerTypes.res +0 -68
  55. package/src/RunnerTypes.res.mjs +0 -60
  56. package/src/ScanIgnore.res +0 -11
  57. package/src/ScanIgnore.res.mjs +0 -23
  58. package/src/Watch.res +0 -102
  59. package/src/Watch.res.mjs +0 -80
  60. package/src/WatcherProbe.res +0 -56
  61. package/src/WatcherProbe.res.mjs +0 -59
  62. package/src/Worker.res +0 -24
  63. package/src/Worker.res.mjs +0 -9
  64. package/tests/BuildClassifierTest.res +0 -133
  65. package/tests/BuildClassifierTest.res.mjs +0 -176
  66. package/tests/CliRunEntryTest.res +0 -57
  67. package/tests/CliRunEntryTest.res.mjs +0 -65
  68. package/tests/CliWatchScopeTest.res +0 -62
  69. package/tests/CliWatchScopeTest.res.mjs +0 -95
  70. package/tests/ComponentMetaTest.res +0 -67
  71. package/tests/ComponentMetaTest.res.mjs +0 -71
  72. package/tests/DiscoveryTest.res +0 -42
  73. package/tests/DiscoveryTest.res.mjs +0 -43
  74. package/tests/DomainDeadCodeTest.res +0 -119
  75. package/tests/DomainDeadCodeTest.res.mjs +0 -158
  76. package/tests/DomainGraphTest.res +0 -448
  77. package/tests/DomainGraphTest.res.mjs +0 -505
  78. package/tests/ExnMessageTest.res +0 -42
  79. package/tests/ExnMessageTest.res.mjs +0 -64
  80. package/tests/FormatterGoldenTest.res +0 -125
  81. package/tests/FormatterGoldenTest.res.mjs +0 -162
  82. package/tests/FormatterVsCodeMessageTest.res +0 -41
  83. package/tests/FormatterVsCodeMessageTest.res.mjs +0 -42
  84. package/tests/PlatformRunnerTest.res +0 -66
  85. package/tests/PlatformRunnerTest.res.mjs +0 -85
  86. package/tests/PlatformScanTest.res +0 -61
  87. package/tests/PlatformScanTest.res.mjs +0 -56
  88. package/tests/RunnerUnitTest.res +0 -219
  89. package/tests/RunnerUnitTest.res.mjs +0 -244
  90. package/tests/WatchDebounceTest.res +0 -106
  91. package/tests/WatchDebounceTest.res.mjs +0 -128
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.0.0-alpha.117 (2026-07-16)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-gwt
9
+
10
+
11
+
12
+
13
+
14
+ # 1.0.0-alpha.116 (2026-07-16)
15
+
16
+ * refactor(gwt)!: focus the package on the GWT test harness library ([925e944](https://github.com/ReventlessDev/reventless-core/commit/925e944e996613f4fb894564696ea8e1ff6b7489))
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ * the reventless-dev / reventless-gwt bin entries and the
21
+ runner/formatter modules are no longer part of this package.
22
+
23
+
24
+
6
25
  # 1.0.0-alpha.115 (2026-07-15)
7
26
 
8
27
  **Note:** Version bump only for package @reventlessdev/reventless-gwt
package/package.json CHANGED
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.115",
4
- "description": "Given-When-Then DSLs and CLI runner for Reventless slice testing",
3
+ "version": "1.0.0-alpha.117",
4
+ "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
- "bin": {
7
- "reventless-dev": "./bin/reventless-dev.mjs",
8
- "reventless-gwt": "./bin/reventless-dev.mjs"
9
- },
10
6
  "jest": {
11
7
  "testMatch": [
12
8
  "<rootDir>/tests/**/*Test.res.mjs"
@@ -17,23 +13,20 @@
17
13
  ]
18
14
  },
19
15
  "dependencies": {
20
- "chokidar": "^3.6.0",
21
- "picocolors": "^1.1.1",
22
16
  "sury": "11.0.0-alpha.4",
23
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
24
- "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
25
- "@reventlessdev/reventless-core": "3.0.0-alpha.169",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.100",
27
- "@reventlessdev/reventless-domain-protocol": "1.0.0-alpha.20",
28
- "@reventlessdev/reventless-spec": "3.0.0-alpha.77"
17
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.28",
18
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.8",
19
+ "@reventlessdev/reventless-core": "3.0.0-alpha.170",
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.78",
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.101"
29
22
  },
30
23
  "devDependencies": {
31
- "rescript": "^12.3.0",
24
+ "rescript": "12.3.0",
32
25
  "sury-ppx": "11.0.0-alpha.2",
33
26
  "@reventlessdev/reventless-ppx": "1.0.0-alpha.53"
34
27
  },
35
28
  "peerDependencies": {
36
- "rescript": "^12.3.0"
29
+ "rescript": "12.3.0"
37
30
  },
38
31
  "publishConfig": {
39
32
  "registry": "https://registry.npmjs.org"
package/rescript.json CHANGED
@@ -23,7 +23,6 @@
23
23
  }
24
24
  ],
25
25
  "dependencies": [
26
- "@reventlessdev/reventless-domain-protocol",
27
26
  "sury",
28
27
  "@reventlessdev/rescript-jest",
29
28
  "@reventlessdev/rescript-effect",
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env node
2
- // Thin launcher for the reventless-dev CLI (formerly reventless-gwt). The heavy lifting lives in
3
- // Cli.res — this file only resolves the compiled module and exits with the
4
- // code Cli.main returns.
5
- //
6
- // `LOG_LEVEL=silent` is the default so framework Info/Debug logs don't
7
- // interleave with NDJSON / TAP / JUnit streams. Set `LOG_LEVEL=info` (or
8
- // debug/warn/error) explicitly to re-enable. We use a dynamic `import()` so
9
- // the env is applied BEFORE any ReScript module runs its top-level
10
- // `Logger.fromEnv()` call.
11
-
12
- if (!process.env.LOG_LEVEL) process.env.LOG_LEVEL = "silent";
13
-
14
- const { main } = await import("../src/Cli.res.mjs");
15
-
16
- main()
17
- .then((code) => process.exit(typeof code === "number" ? code : 0))
18
- .catch((err) => {
19
- console.error(err && err.stack ? err.stack : err);
20
- process.exit(1);
21
- });
package/src/Bind.res DELETED
@@ -1,29 +0,0 @@
1
- // CLI-mode binding for test files.
2
- //
3
- // Mirrors the shape of `JestBind` (`describe`, `test`, `testPromise`) but
4
- // pushes directly into `Collector` instead of forwarding to Jest globals.
5
- // Test files can either:
6
- //
7
- // 1. Rely on the DSL default (every `*_GWT.res` module uses `JestBind`
8
- // internally, which routes to `Collector` when the CLI has activated it),
9
- // or
10
- // 2. Explicitly `open ReventlessGwt.Bind` to get these functions bound at
11
- // compile time — useful for ad-hoc harnesses or tests that want to
12
- // bypass the JestBind fallback entirely.
13
- //
14
- // Either way, the runtime contract is identical: each call pushes one entry
15
- // into `Collector.entries`. The CLI drains the list after dynamic-import
16
- // resolves.
17
-
18
- let describe = (label: string, body: unit => unit) =>
19
- Collector.pushDescribe(label, body)
20
-
21
- let test = (~slice=?, name: string, body: unit => Outcome.outcome) => {
22
- let location = Collector.captureLocation(1)
23
- Collector.push(~slice?, ~location?, name, () => Promise.resolve(body()))
24
- }
25
-
26
- let testPromise = (~slice=?, name: string, body: unit => promise<Outcome.outcome>) => {
27
- let location = Collector.captureLocation(1)
28
- Collector.push(~slice?, ~location?, name, body)
29
- }
package/src/Bind.res.mjs DELETED
@@ -1,22 +0,0 @@
1
- // Generated by ReScript, PLEASE EDIT WITH CARE
2
-
3
- import * as Collector$ReventlessGwt from "./Collector.res.mjs";
4
-
5
- let describe = Collector$ReventlessGwt.pushDescribe;
6
-
7
- function test(slice, name, body) {
8
- let location = Collector$ReventlessGwt.captureLocation(1);
9
- Collector$ReventlessGwt.push(slice, location, undefined, name, () => Promise.resolve(body()));
10
- }
11
-
12
- function testPromise(slice, name, body) {
13
- let location = Collector$ReventlessGwt.captureLocation(1);
14
- Collector$ReventlessGwt.push(slice, location, undefined, name, body);
15
- }
16
-
17
- export {
18
- describe,
19
- test,
20
- testPromise,
21
- }
22
- /* No side effect */
@@ -1,147 +0,0 @@
1
- // Turns a ReScript v12 `rescript build -w` stdout line-stream into build status
2
- // callbacks. One classifier per managed package.
3
- //
4
- // The watch output cycle is:
5
- // Parsed N source files ← a compile pass begins
6
- // Compiled N modules
7
- // Finished incremental compilation ← success terminator
8
- // On a type/syntax error there is NO terminator — rescript prints a
9
- // "We've found a bug for you!" block and waits. So success is detected on the
10
- // "Finished … compilation" line; failure is detected by an error marker and
11
- // flushed after the output settles (debounced), since there is no end line.
12
- //
13
- // Backstop: a build that prints "Parsed …" but then emits neither a terminator
14
- // nor a recognised error marker — a crashed/hung watcher, or an error in a form
15
- // the markers miss — would otherwise pin the consumer (the VS Code status bar)
16
- // at "building" forever. A generous wall-clock watchdog fails such a build so
17
- // the UI recovers.
18
-
19
- @val external now: unit => float = "performance.now"
20
- @val external setTimeout: (unit => unit, int) => float = "setTimeout"
21
- @val external clearTimeout: float => unit = "clearTimeout"
22
-
23
- let stripAnsi: string => string = %raw(`s => s.replace(/\x1b\[[0-9;]*m/g, "")`)
24
-
25
- type callbacks = {
26
- onStart: unit => unit,
27
- onOk: float => unit,
28
- onFail: string => unit,
29
- }
30
-
31
- let isStartLine = (l: string) =>
32
- String.startsWith(l, "Parsed ") && String.endsWith(l, "source files")
33
-
34
- let isFinishLine = (l: string) =>
35
- String.includes(l, "Finished") && String.includes(l, "compilation")
36
-
37
- let isErrorMarker = (l: string) =>
38
- String.includes(l, "We've found a bug for you!") ||
39
- String.includes(l, "Syntax error") ||
40
- (String.startsWith(l, "Found ") && String.includes(l, "error"))
41
-
42
- // Returns a `feed` function: hand it each raw stdout line. `watchdogMs` bounds
43
- // how long a started-but-unterminated compile may stay "building" before it is
44
- // reported failed (see the backstop note above).
45
- let make = (~watchdogMs: int=120_000, cb: callbacks): (string => unit) => {
46
- let compiling = ref(false)
47
- let hasError = ref(false)
48
- let startTime = ref(0.0)
49
- let buffer = ref([])
50
- let timer: ref<option<float>> = ref(None)
51
- let watchdog: ref<option<float>> = ref(None)
52
-
53
- let clearTimer = () =>
54
- switch timer.contents {
55
- | Some(t) => {
56
- clearTimeout(t)
57
- timer := None
58
- }
59
- | None => ()
60
- }
61
-
62
- let clearWatchdog = () =>
63
- switch watchdog.contents {
64
- | Some(t) => {
65
- clearTimeout(t)
66
- watchdog := None
67
- }
68
- | None => ()
69
- }
70
-
71
- let reset = () => {
72
- compiling := false
73
- hasError := false
74
- buffer := []
75
- }
76
-
77
- // Cap the failure message so a pathological build can't emit an unbounded
78
- // line. 20 was too tight — a multi-error build has several "We've found a bug"
79
- // blocks and the tail (later errors) was silently dropped; keep enough to
80
- // carry a handful of blocks.
81
- let maxMessageLines = 200
82
-
83
- // After output settles, a build that saw an error (and thus no "Finished"
84
- // terminator) is reported failed, capturing a concise message. The condition
85
- // is `hasError` alone — not `compiling && hasError` — so an error emitted
86
- // *before* the first "Parsed …" line (a crashed toolchain, a pnpm/resolution
87
- // failure) still settles into an `onFail` instead of leaving the consumer
88
- // pinned at its previous state forever.
89
- let settle = () =>
90
- if hasError.contents {
91
- let msg = buffer.contents->Array.slice(~start=0, ~end=maxMessageLines)->Array.join("\n")
92
- clearWatchdog()
93
- cb.onFail(msg)
94
- reset()
95
- }
96
-
97
- let arm = () => {
98
- clearTimer()
99
- timer := Some(setTimeout(settle, 400))
100
- }
101
-
102
- // Last-resort timer: fail a build still compiling after `watchdogMs` with no
103
- // terminator and no recognised error, so the consumer never spins forever.
104
- let armWatchdog = () => {
105
- clearWatchdog()
106
- watchdog :=
107
- Some(
108
- setTimeout(() =>
109
- if compiling.contents {
110
- clearTimer()
111
- cb.onFail("build watchdog: no completion within " ++ Int.toString(watchdogMs / 1000) ++ "s")
112
- reset()
113
- }
114
- , watchdogMs),
115
- )
116
- }
117
-
118
- line => {
119
- let l = stripAnsi(line)->String.trim
120
- if isStartLine(l) {
121
- if !compiling.contents {
122
- cb.onStart()
123
- }
124
- compiling := true
125
- hasError := false
126
- buffer := []
127
- startTime := now()
128
- arm()
129
- armWatchdog()
130
- } else if isFinishLine(l) {
131
- clearTimer()
132
- clearWatchdog()
133
- if compiling.contents {
134
- cb.onOk(now() -. startTime.contents)
135
- reset()
136
- }
137
- } else {
138
- if String.length(l) > 0 {
139
- buffer.contents->Array.push(l)
140
- }
141
- if isErrorMarker(l) {
142
- hasError := true
143
- }
144
- arm()
145
- }
146
- }
147
- }
@@ -1,136 +0,0 @@
1
- // Generated by ReScript, PLEASE EDIT WITH CARE
2
-
3
-
4
- let stripAnsi = (s => s.replace(/\x1b\[[0-9;]*m/g, ""));
5
-
6
- function isStartLine(l) {
7
- if (l.startsWith("Parsed ")) {
8
- return l.endsWith("source files");
9
- } else {
10
- return false;
11
- }
12
- }
13
-
14
- function isFinishLine(l) {
15
- if (l.includes("Finished")) {
16
- return l.includes("compilation");
17
- } else {
18
- return false;
19
- }
20
- }
21
-
22
- function isErrorMarker(l) {
23
- if (l.includes("We've found a bug for you!") || l.includes("Syntax error")) {
24
- return true;
25
- } else if (l.startsWith("Found ")) {
26
- return l.includes("error");
27
- } else {
28
- return false;
29
- }
30
- }
31
-
32
- function make(watchdogMsOpt, cb) {
33
- let watchdogMs = watchdogMsOpt !== undefined ? watchdogMsOpt : 120000;
34
- let compiling = {
35
- contents: false
36
- };
37
- let hasError = {
38
- contents: false
39
- };
40
- let startTime = {
41
- contents: 0.0
42
- };
43
- let buffer = {
44
- contents: []
45
- };
46
- let timer = {
47
- contents: undefined
48
- };
49
- let watchdog = {
50
- contents: undefined
51
- };
52
- let clearTimer = () => {
53
- let t = timer.contents;
54
- if (t !== undefined) {
55
- clearTimeout(t);
56
- timer.contents = undefined;
57
- return;
58
- }
59
- };
60
- let clearWatchdog = () => {
61
- let t = watchdog.contents;
62
- if (t !== undefined) {
63
- clearTimeout(t);
64
- watchdog.contents = undefined;
65
- return;
66
- }
67
- };
68
- let reset = () => {
69
- compiling.contents = false;
70
- hasError.contents = false;
71
- buffer.contents = [];
72
- };
73
- let settle = () => {
74
- if (!hasError.contents) {
75
- return;
76
- }
77
- let msg = buffer.contents.slice(0, 200).join("\n");
78
- clearWatchdog();
79
- cb.onFail(msg);
80
- reset();
81
- };
82
- let arm = () => {
83
- clearTimer();
84
- timer.contents = setTimeout(settle, 400);
85
- };
86
- let armWatchdog = () => {
87
- clearWatchdog();
88
- watchdog.contents = setTimeout(() => {
89
- if (compiling.contents) {
90
- clearTimer();
91
- cb.onFail("build watchdog: no completion within " + (watchdogMs / 1000 | 0).toString() + "s");
92
- return reset();
93
- }
94
- }, watchdogMs);
95
- };
96
- return line => {
97
- let l = stripAnsi(line).trim();
98
- if (isStartLine(l)) {
99
- if (!compiling.contents) {
100
- cb.onStart();
101
- }
102
- compiling.contents = true;
103
- hasError.contents = false;
104
- buffer.contents = [];
105
- startTime.contents = performance.now();
106
- arm();
107
- return armWatchdog();
108
- } else if (isFinishLine(l)) {
109
- clearTimer();
110
- clearWatchdog();
111
- if (compiling.contents) {
112
- cb.onOk(performance.now() - startTime.contents);
113
- return reset();
114
- } else {
115
- return;
116
- }
117
- } else {
118
- if (l.length > 0) {
119
- buffer.contents.push(l);
120
- }
121
- if (isErrorMarker(l)) {
122
- hasError.contents = true;
123
- }
124
- return arm();
125
- }
126
- };
127
- }
128
-
129
- export {
130
- stripAnsi,
131
- isStartLine,
132
- isFinishLine,
133
- isErrorMarker,
134
- make,
135
- }
136
- /* No side effect */
@@ -1,37 +0,0 @@
1
- // SIGINT / SIGTERM trap. Sets a module-level flag that the main loop
2
- // polls between tests. The VS Code and JSON streaming formatters flush a
3
- // `runEnd` event with the skipped remainder before the process exits.
4
-
5
- type process
6
- @val external process: process = "process"
7
- @send external onSignal: (process, string, unit => unit) => unit = "on"
8
- @send external exit: (process, int) => unit = "exit"
9
-
10
- let cancelled = ref(false)
11
- let handlers: ref<array<unit => unit>> = ref([])
12
-
13
- let isCancelled = () => cancelled.contents
14
-
15
- let onCancel = (handler: unit => unit) =>
16
- handlers := Array.concat(handlers.contents, [handler])
17
-
18
- let install = () => {
19
- let handler = () => {
20
- if !cancelled.contents {
21
- cancelled := true
22
- handlers.contents->Array.forEach(h =>
23
- try h() catch {
24
- | _ => ()
25
- }
26
- )
27
- } else {
28
- // Second signal: the first requested a graceful drain, but a hung await
29
- // can keep the process alive indefinitely. Honour the operator's repeat
30
- // SIGINT/SIGTERM as a hard exit (130 = terminated by SIGINT) so only the
31
- // one signal — not SIGKILL — is needed to break out.
32
- process->exit(130)
33
- }
34
- }
35
- process->onSignal("SIGINT", handler)
36
- process->onSignal("SIGTERM", handler)
37
- }
@@ -1,46 +0,0 @@
1
- // Generated by ReScript, PLEASE EDIT WITH CARE
2
-
3
-
4
- let cancelled = {
5
- contents: false
6
- };
7
-
8
- let handlers = {
9
- contents: []
10
- };
11
-
12
- function isCancelled() {
13
- return cancelled.contents;
14
- }
15
-
16
- function onCancel(handler) {
17
- handlers.contents = handlers.contents.concat([handler]);
18
- }
19
-
20
- function install() {
21
- let handler = () => {
22
- if (cancelled.contents) {
23
- process.exit(130);
24
- } else {
25
- cancelled.contents = true;
26
- handlers.contents.forEach(h => {
27
- try {
28
- return h();
29
- } catch (exn) {
30
- return;
31
- }
32
- });
33
- }
34
- };
35
- process.on("SIGINT", handler);
36
- process.on("SIGTERM", handler);
37
- }
38
-
39
- export {
40
- cancelled,
41
- handlers,
42
- isCancelled,
43
- onCancel,
44
- install,
45
- }
46
- /* No side effect */
@@ -1,102 +0,0 @@
1
- // Minimal `node:child_process` binding for the watch-mode build manager. The
2
- // only consumer is ProcessManager, which spawns one `pnpm run start`
3
- // (`rescript build -w`) per package. Children are spawned **detached** so each
4
- // gets its own process group; killing the negative pid then tears down the whole
5
- // tree (pnpm + the rescript watcher it spawns) — no orphaned compilers.
6
-
7
- type t
8
- type stream
9
-
10
- type spawnOptions = {
11
- cwd: string,
12
- detached: bool,
13
- env: Dict.t<string>,
14
- }
15
-
16
- @module("node:child_process")
17
- external _spawn: (string, array<string>, spawnOptions) => t = "spawn"
18
-
19
- @val external _processEnv: Dict.t<string> = "process.env"
20
-
21
- // Merge the current process env with optional overrides into a fresh dict — node's
22
- // `spawn` replaces the whole env, so callers that pass `~env` still inherit PATH etc.
23
- let mergeEnv = (overrides: option<Dict.t<string>>): Dict.t<string> => {
24
- let m = Dict.make()
25
- _processEnv->Dict.toArray->Array.forEach(((k, v)) => m->Dict.set(k, v))
26
- switch overrides {
27
- | Some(o) => o->Dict.toArray->Array.forEach(((k, v)) => m->Dict.set(k, v))
28
- | None => ()
29
- }
30
- m
31
- }
32
-
33
- @get external _pid: t => Nullable.t<int> = "pid"
34
- @get external _stdout: t => stream = "stdout"
35
- @get external _stderr: t => stream = "stderr"
36
- @send external _streamSetEncoding: (stream, string) => unit = "setEncoding"
37
- @send external _streamOn: (stream, string, string => unit) => unit = "on"
38
- @send external _streamOnEnd: (stream, string, unit => unit) => unit = "on"
39
- @send external _procOnClose: (t, string, Nullable.t<int> => unit) => unit = "on"
40
- @send external _procOnError: (t, string, JsExn.t => unit) => unit = "on"
41
- @send external _kill: (t, string) => bool = "kill"
42
- @val external _processKill: (int, string) => unit = "process.kill"
43
-
44
- let spawn = (cmd: string, args: array<string>, ~cwd: string, ~env: option<Dict.t<string>>=?): t =>
45
- _spawn(cmd, args, {cwd, detached: true, env: mergeEnv(env)})
46
-
47
- let pid = (proc: t): option<int> => proc->_pid->Nullable.toOption
48
-
49
- // Subscribe to a stream line-by-line (stdout or stderr), buffering partial
50
- // chunks. Used to classify ReScript compiler markers in Phase 5. On stream
51
- // `end` the final unterminated line is flushed — crash/panic messages
52
- // typically lack a trailing newline, so without this the last (often most
53
- // diagnostic) line was silently dropped.
54
- let onLines = (s: stream, cb: string => unit): unit => {
55
- _streamSetEncoding(s, "utf8")
56
- let buf = ref("")
57
- _streamOn(s, "data", chunk => {
58
- buf := buf.contents ++ chunk
59
- let rec drain = () =>
60
- switch String.indexOf(buf.contents, "\n") {
61
- | -1 => ()
62
- | idx => {
63
- let line = String.slice(buf.contents, ~start=0, ~end=idx)
64
- buf := String.slice(buf.contents, ~start=idx + 1, ~end=String.length(buf.contents))
65
- cb(line)
66
- drain()
67
- }
68
- }
69
- drain()
70
- })
71
- _streamOnEnd(s, "end", () =>
72
- if buf.contents != "" {
73
- let last = buf.contents
74
- buf := ""
75
- cb(last)
76
- }
77
- )
78
- }
79
-
80
- let onStdoutLine = (proc: t, cb: string => unit): unit => onLines(proc->_stdout, cb)
81
- let onStderrLine = (proc: t, cb: string => unit): unit => onLines(proc->_stderr, cb)
82
- let onClose = (proc: t, cb: option<int> => unit): unit =>
83
- _procOnClose(proc, "close", code => cb(code->Nullable.toOption))
84
-
85
- // A spawn failure (ENOENT for a missing binary, EACCES, …) emits `"error"` on
86
- // the child, not `"close"`. Without a listener Node re-throws it as an uncaught
87
- // exception and crashes the whole CLI. Route the message to the caller instead.
88
- let onError = (proc: t, cb: string => unit): unit =>
89
- _procOnError(proc, "error", e => cb(e->JsExn.message->Option.getOr("spawn failed")))
90
-
91
- // Kill the child's whole process group (negative pid) so the rescript watcher
92
- // pnpm spawned dies too. Falls back to a direct kill if the pid is gone.
93
- let killTree = (proc: t, signal: string): unit =>
94
- switch proc->pid {
95
- | Some(p) =>
96
- try {
97
- _processKill(-p, signal)
98
- } catch {
99
- | _ => ignore(proc->_kill(signal))
100
- }
101
- | None => ignore(proc->_kill(signal))
102
- }