@reventlessdev/reventless-gwt 1.0.0-alpha.74

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 (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +603 -0
  10. package/src/Behavior_GWT.res.mjs +426 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +137 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +31 -0
  16. package/src/Cancellation.res.mjs +45 -0
  17. package/src/ChildProcess.res +84 -0
  18. package/src/ChildProcess.res.mjs +92 -0
  19. package/src/Cli.res +671 -0
  20. package/src/Cli.res.mjs +754 -0
  21. package/src/Collector.res +201 -0
  22. package/src/Collector.res.mjs +215 -0
  23. package/src/ComponentMeta.res +104 -0
  24. package/src/ComponentMeta.res.mjs +120 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +103 -0
  33. package/src/DomainDeadCode.res +93 -0
  34. package/src/DomainDeadCode.res.mjs +69 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +36 -0
  40. package/src/Filter.res.mjs +40 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +138 -0
  44. package/src/FormatterHuman.res.mjs +122 -0
  45. package/src/FormatterJson.res +257 -0
  46. package/src/FormatterJson.res.mjs +320 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +139 -0
  50. package/src/FormatterTap.res.mjs +149 -0
  51. package/src/FormatterVsCode.res +362 -0
  52. package/src/FormatterVsCode.res.mjs +564 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +135 -0
  62. package/src/LocalHost.res.mjs +127 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MultiSourceProjection_GWT.res +316 -0
  66. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  67. package/src/OutboundTranslation_GWT.res +238 -0
  68. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  69. package/src/Outcome.res +161 -0
  70. package/src/Outcome.res.mjs +140 -0
  71. package/src/PackageScan.res +80 -0
  72. package/src/PackageScan.res.mjs +81 -0
  73. package/src/PlatformRunner.res +160 -0
  74. package/src/PlatformRunner.res.mjs +155 -0
  75. package/src/PlatformScan.res +138 -0
  76. package/src/PlatformScan.res.mjs +131 -0
  77. package/src/ProcessManager.res +91 -0
  78. package/src/ProcessManager.res.mjs +80 -0
  79. package/src/Projection_GWT.res +312 -0
  80. package/src/Projection_GWT.res.mjs +332 -0
  81. package/src/Query_GWT.res +379 -0
  82. package/src/Query_GWT.res.mjs +367 -0
  83. package/src/RenderRescript.res +125 -0
  84. package/src/RenderRescript.res.mjs +141 -0
  85. package/src/RunnerTypes.res +68 -0
  86. package/src/RunnerTypes.res.mjs +60 -0
  87. package/src/SideEffect_GWT.res +157 -0
  88. package/src/SideEffect_GWT.res.mjs +113 -0
  89. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  90. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  91. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  92. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  93. package/src/StateChange/WithFixtures.res +22 -0
  94. package/src/StateChange/WithFixtures.res.mjs +31 -0
  95. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  96. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  97. package/src/StateChange/WithManualOpen.res +23 -0
  98. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  99. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  100. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  101. package/src/StubRuntime.res +53 -0
  102. package/src/StubRuntime.res.mjs +54 -0
  103. package/src/TestFixtures.res +16 -0
  104. package/src/TestFixtures.res.mjs +34 -0
  105. package/src/Watch.res +92 -0
  106. package/src/Watch.res.mjs +83 -0
  107. package/src/WatcherProbe.res +48 -0
  108. package/src/WatcherProbe.res.mjs +46 -0
  109. package/tests/AutomationGwtTest.res +71 -0
  110. package/tests/AutomationGwtTest.res.mjs +189 -0
  111. package/tests/BuildClassifierTest.res +94 -0
  112. package/tests/BuildClassifierTest.res.mjs +132 -0
  113. package/tests/ComponentMetaTest.res +52 -0
  114. package/tests/ComponentMetaTest.res.mjs +57 -0
  115. package/tests/DelegateGwtTest.res +487 -0
  116. package/tests/DelegateGwtTest.res.mjs +891 -0
  117. package/tests/DiscoveryTest.res +42 -0
  118. package/tests/DiscoveryTest.res.mjs +43 -0
  119. package/tests/DomainDeadCodeTest.res +116 -0
  120. package/tests/DomainDeadCodeTest.res.mjs +155 -0
  121. package/tests/DomainGraphTest.res +365 -0
  122. package/tests/DomainGraphTest.res.mjs +413 -0
  123. package/tests/FlowAggregateGwtTest.res +169 -0
  124. package/tests/FlowAggregateGwtTest.res.mjs +269 -0
  125. package/tests/FlowCrossPluginGwtTest.res +340 -0
  126. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  127. package/tests/FlowGwtTest.res +222 -0
  128. package/tests/FlowGwtTest.res.mjs +432 -0
  129. package/tests/FormatterVsCodeMessageTest.res +41 -0
  130. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  131. package/tests/InboundTranslationGwtTest.res +34 -0
  132. package/tests/InboundTranslationGwtTest.res.mjs +98 -0
  133. package/tests/LocalHostIntegration.res +70 -0
  134. package/tests/LocalHostIntegration.res.mjs +64 -0
  135. package/tests/LocalHostTest.res +71 -0
  136. package/tests/LocalHostTest.res.mjs +63 -0
  137. package/tests/MappingGwtTest.res +348 -0
  138. package/tests/MappingGwtTest.res.mjs +592 -0
  139. package/tests/OutboundTranslationGwtTest.res +59 -0
  140. package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
  141. package/tests/PlatformRunnerTest.res +66 -0
  142. package/tests/PlatformRunnerTest.res.mjs +85 -0
  143. package/tests/PlatformScanTest.res +61 -0
  144. package/tests/PlatformScanTest.res.mjs +56 -0
  145. package/tests/QueryGwtTest.res +161 -0
  146. package/tests/QueryGwtTest.res.mjs +314 -0
  147. package/tests/QueryResolverGwtTest.res +71 -0
  148. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  149. package/tests/RunnerUnitTest.res +219 -0
  150. package/tests/RunnerUnitTest.res.mjs +244 -0
  151. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  152. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  153. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  154. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  155. package/tests/SideEffect/MockEmail.res +18 -0
  156. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  157. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  158. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  159. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  160. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  161. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  162. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  163. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  164. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  165. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  166. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  167. package/tests/StateChangeSliceGwtTest.res +160 -0
  168. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  169. package/tests/StateViewSliceGwtTest.res +64 -0
  170. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  171. package/tests/WatchDebounceTest.res +78 -0
  172. package/tests/WatchDebounceTest.res.mjs +97 -0
@@ -0,0 +1,160 @@
1
+ // Local platform runner (features plan Phase 9). Spawns an app's reventless-local
2
+ // platform as a managed child with the LocalBus NDJSON event tap enabled, on
3
+ // per-session free ports, classifies its stdout into structured lifecycle/event
4
+ // callbacks, and tears the child down on cancellation. The CLI (`reventless-gwt
5
+ // platform`) wires the callbacks to FormatterVsCode; tests drive `classifyLine`
6
+ // directly.
7
+ //
8
+ // Channels (Spike A/B): commands in / state out go to the platform's GraphQL
9
+ // server (the runner just reports its endpoint); the live event stream comes from
10
+ // the tap on stdout — one sentinel-prefixed JSON line per published event.
11
+
12
+ let tapSentinel = "@@RVLESS_EVT@@ "
13
+
14
+ type lineClass =
15
+ | Domain(JSON.t) // a tap line; the parsed `{event:"domainEvent",…}` payload
16
+ | Ready // the Domain GraphQL server's "listening on …" line
17
+ | Log(string) // any other stdout/stderr line (the platform's human logs)
18
+
19
+ // Pure line classifier — the unit-tested core. ANSI escapes around bracketed
20
+ // component tags survive substring checks, so readiness is detected structurally.
21
+ // The human-log payload is passed through **with its ANSI intact**: the extension
22
+ // renders it in a VS Code Pseudoterminal (xterm), which interprets colour/bold so
23
+ // the runner log reads exactly as on the command line. (Plain non-terminal sinks
24
+ // that want stripped text — CloudWatch etc. — are the framework's concern; see
25
+ // docs/plans/logging-output-optimization.md.)
26
+ let classifyLine = (line: string): lineClass =>
27
+ if line->String.startsWith(tapSentinel) {
28
+ let jsonStr = line->String.slice(~start=tapSentinel->String.length, ~end=line->String.length)
29
+ switch JSON.parseOrThrow(jsonStr) {
30
+ | exception _ => Log(line)
31
+ | json => Domain(json)
32
+ }
33
+ } else if line->String.includes("GraphQL:Domain") && line->String.includes("listening on") {
34
+ Ready
35
+ } else {
36
+ Log(line)
37
+ }
38
+
39
+ type callbacks = {
40
+ onStart: (~package: string, ~dir: string, ~domainPort: int, ~platformPort: int) => unit,
41
+ onReady: (~domainEndpoint: string) => unit,
42
+ onDomainEvent: JSON.t => unit,
43
+ onLog: string => unit,
44
+ onStop: option<int> => unit,
45
+ }
46
+
47
+ // ── Free-port allocation ────────────────────────────────────────────────────
48
+ // One platform child per app folder; default ports (4000/4001/3001/3002) collide
49
+ // with a developer's own `dev:full`, so the runner binds ephemeral ports and
50
+ // passes them to the child via the env overrides added in Phase 1.
51
+ type netServer
52
+ @module("node:net") external _createServer: unit => netServer = "createServer"
53
+ @send external _listen: (netServer, int, string, unit => unit) => unit = "listen"
54
+ @send external _address: netServer => {"port": int} = "address"
55
+ @send external _close: (netServer, unit => unit) => unit = "close"
56
+
57
+ let openEphemeral = (): promise<(netServer, int)> =>
58
+ Promise.make((resolve, _reject) => {
59
+ let s = _createServer()
60
+ s->_listen(0, "127.0.0.1", () => resolve((s, (s->_address)["port"])))
61
+ })
62
+
63
+ // Hold all n open while collecting ports so they're guaranteed distinct, then
64
+ // release them just before the child binds.
65
+ let allocFreePorts = async (n: int): array<int> => {
66
+ let servers = []
67
+ let ports = []
68
+ for _i in 1 to n {
69
+ let (s, p) = await openEphemeral()
70
+ servers->Array.push(s)
71
+ ports->Array.push(p)
72
+ }
73
+ servers->Array.forEach(s => s->_close(() => ()))
74
+ ports
75
+ }
76
+
77
+ // The platform child's default ports (Platform.startServers): domain / platform /
78
+ // domain-MCP / platform-MCP. `~fixedPorts` pins to these instead of allocating
79
+ // ephemeral ones — the VS Code extension uses it when launching the host-shell UI,
80
+ // whose Vite proxy hardcodes localhost:4000/4001. The caller is responsible for
81
+ // ensuring they're free (it pre-checks and toasts on conflict).
82
+ let fixedPlatformPorts = [4000, 4001, 3001, 3002]
83
+
84
+ // Discover the platform package under `roots`, spawn it with the tap + per-session
85
+ // ports + in-memory backend, stream its events through `callbacks`, and stay alive
86
+ // until cancelled (the child is killed via its process group on SIGTERM/SIGINT).
87
+ let run = async (
88
+ ~roots: array<string>,
89
+ ~backend: string,
90
+ ~fixedPorts: bool=false,
91
+ ~callbacks: callbacks,
92
+ ): int => {
93
+ let pkgs = await PlatformScan.scan(roots)
94
+ switch pkgs->Array.get(0) {
95
+ | None =>
96
+ callbacks.onLog(
97
+ "no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " ++
98
+ roots->Array.join(", "),
99
+ )
100
+ 1
101
+ | Some(pkg) =>
102
+ let ports = fixedPorts ? fixedPlatformPorts : await allocFreePorts(4)
103
+ let dPort = ports->Array.getUnsafe(0)
104
+ let pPort = ports->Array.getUnsafe(1)
105
+ let dMcp = ports->Array.getUnsafe(2)
106
+ let pMcp = ports->Array.getUnsafe(3)
107
+ let env = Dict.fromArray([
108
+ ("REVENTLESS_EVENT_TAP", "ndjson"),
109
+ ("REVENTLESS_LOCAL_BACKEND", backend),
110
+ ("REVENTLESS_DOMAIN_PORT", Int.toString(dPort)),
111
+ ("REVENTLESS_PLATFORM_PORT", Int.toString(pPort)),
112
+ ("REVENTLESS_DOMAIN_MCP_PORT", Int.toString(dMcp)),
113
+ ("REVENTLESS_PLATFORM_MCP_PORT", Int.toString(pMcp)),
114
+ ("NODE_OPTIONS", "--disable-warning=ExperimentalWarning"),
115
+ // The gwt bin defaults LOG_LEVEL=silent so its OWN stdout stays pure NDJSON;
116
+ // the platform child is a separate process whose stdout we parse, so it needs
117
+ // real logs — the "listening on" line drives readiness and the rest becomes
118
+ // platformLog. Override the inherited silent.
119
+ ("LOG_LEVEL", "info"),
120
+ // The child's stdout is a pipe (we line-parse it), so the framework's sink
121
+ // detection would auto-pick JSON (non-TTY). But every consumer of `onLog`
122
+ // renders ANSI — the CLI forwards to the developer's terminal, the extension
123
+ // to an xterm Pseudoterminal — so force human text mode for bold/colour.
124
+ // (Tap event lines stay JSON regardless: they're sentinel-prefixed.)
125
+ ("REVENTLESS_LOG_FORMAT", "text"),
126
+ ])
127
+
128
+ callbacks.onStart(~package=pkg.name, ~dir=pkg.dir, ~domainPort=dPort, ~platformPort=pPort)
129
+
130
+ // Run the compiled entrypoint directly with `node`. `src/Main.res.mjs` is plain
131
+ // ESM, so it needs no tsx/loader, and spawning a single process (vs `pnpm run
132
+ // serve` → `pnpm run serve` → `npx tsx`) keeps the child's stdout piped straight
133
+ // to us — nested pnpm buffers a long-running grandchild's output, so the event
134
+ // tap never streams through it. cwd = the package dir so `PackageVersion.fromCwd`
135
+ // and relative paths resolve. We set the backend + tap + ports via env directly,
136
+ // so the package's own serve script (which would otherwise pick them) is bypassed.
137
+ let proc = ChildProcess.spawn("node", ["src/Main.res.mjs"], ~cwd=pkg.dir, ~env)
138
+ Cancellation.onCancel(() => ChildProcess.killTree(proc, "SIGTERM"))
139
+
140
+ let readyEmitted = ref(false)
141
+ let handle = line =>
142
+ switch classifyLine(line) {
143
+ | Domain(json) => callbacks.onDomainEvent(json)
144
+ | Ready =>
145
+ if !readyEmitted.contents {
146
+ readyEmitted := true
147
+ callbacks.onReady(~domainEndpoint="http://localhost:" ++ Int.toString(dPort) ++ "/graphql")
148
+ }
149
+ | Log(l) => callbacks.onLog(l)
150
+ }
151
+ ChildProcess.onStdoutLine(proc, handle)
152
+ ChildProcess.onStderrLine(proc, handle)
153
+ ChildProcess.onClose(proc, code => callbacks.onStop(code))
154
+
155
+ // Keep the process alive until cancelled — like runWatch, returning here would
156
+ // let the bin wrapper process.exit and tear down the child immediately.
157
+ await Promise.make((resolve, _reject) => Cancellation.onCancel(() => resolve()))
158
+ 0
159
+ }
160
+ }
@@ -0,0 +1,155 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodenet from "node:net";
4
+ import * as Cancellation$ReventlessGwt from "./Cancellation.res.mjs";
5
+ import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
6
+ import * as PlatformScan$ReventlessGwt from "./PlatformScan.res.mjs";
7
+
8
+ let tapSentinel = "@@RVLESS_EVT@@ ";
9
+
10
+ function classifyLine(line) {
11
+ if (!line.startsWith(tapSentinel)) {
12
+ if (line.includes("GraphQL:Domain") && line.includes("listening on")) {
13
+ return "Ready";
14
+ } else {
15
+ return {
16
+ TAG: "Log",
17
+ _0: line
18
+ };
19
+ }
20
+ }
21
+ let jsonStr = line.slice(tapSentinel.length, line.length);
22
+ let json;
23
+ try {
24
+ json = JSON.parse(jsonStr);
25
+ } catch (exn) {
26
+ return {
27
+ TAG: "Log",
28
+ _0: line
29
+ };
30
+ }
31
+ return {
32
+ TAG: "Domain",
33
+ _0: json
34
+ };
35
+ }
36
+
37
+ function openEphemeral() {
38
+ return new Promise((resolve, _reject) => {
39
+ let s = Nodenet.createServer();
40
+ s.listen(0, "127.0.0.1", () => resolve([
41
+ s,
42
+ s.address().port
43
+ ]));
44
+ });
45
+ }
46
+
47
+ async function allocFreePorts(n) {
48
+ let servers = [];
49
+ let ports = [];
50
+ for (let _i = 1; _i <= n; ++_i) {
51
+ let match = await openEphemeral();
52
+ servers.push(match[0]);
53
+ ports.push(match[1]);
54
+ }
55
+ servers.forEach(s => {
56
+ s.close(() => {});
57
+ });
58
+ return ports;
59
+ }
60
+
61
+ let fixedPlatformPorts = [
62
+ 4000,
63
+ 4001,
64
+ 3001,
65
+ 3002
66
+ ];
67
+
68
+ async function run(roots, backend, fixedPortsOpt, callbacks) {
69
+ let fixedPorts = fixedPortsOpt !== undefined ? fixedPortsOpt : false;
70
+ let pkgs = await PlatformScan$ReventlessGwt.scan(roots);
71
+ let pkg = pkgs[0];
72
+ if (pkg !== undefined) {
73
+ let ports = fixedPorts ? fixedPlatformPorts : await allocFreePorts(4);
74
+ let dPort = ports[0];
75
+ let pPort = ports[1];
76
+ let dMcp = ports[2];
77
+ let pMcp = ports[3];
78
+ let env = Object.fromEntries([
79
+ [
80
+ "REVENTLESS_EVENT_TAP",
81
+ "ndjson"
82
+ ],
83
+ [
84
+ "REVENTLESS_LOCAL_BACKEND",
85
+ backend
86
+ ],
87
+ [
88
+ "REVENTLESS_DOMAIN_PORT",
89
+ dPort.toString()
90
+ ],
91
+ [
92
+ "REVENTLESS_PLATFORM_PORT",
93
+ pPort.toString()
94
+ ],
95
+ [
96
+ "REVENTLESS_DOMAIN_MCP_PORT",
97
+ dMcp.toString()
98
+ ],
99
+ [
100
+ "REVENTLESS_PLATFORM_MCP_PORT",
101
+ pMcp.toString()
102
+ ],
103
+ [
104
+ "NODE_OPTIONS",
105
+ "--disable-warning=ExperimentalWarning"
106
+ ],
107
+ [
108
+ "LOG_LEVEL",
109
+ "info"
110
+ ],
111
+ [
112
+ "REVENTLESS_LOG_FORMAT",
113
+ "text"
114
+ ]
115
+ ]);
116
+ callbacks.onStart(pkg.name, pkg.dir, dPort, pPort);
117
+ let proc = ChildProcess$ReventlessGwt.spawn("node", ["src/Main.res.mjs"], pkg.dir, env);
118
+ Cancellation$ReventlessGwt.onCancel(() => ChildProcess$ReventlessGwt.killTree(proc, "SIGTERM"));
119
+ let readyEmitted = {
120
+ contents: false
121
+ };
122
+ let handle = line => {
123
+ let json = classifyLine(line);
124
+ if (typeof json !== "object") {
125
+ if (!readyEmitted.contents) {
126
+ readyEmitted.contents = true;
127
+ return callbacks.onReady("http://localhost:" + dPort.toString() + "/graphql");
128
+ } else {
129
+ return;
130
+ }
131
+ } else if (json.TAG === "Domain") {
132
+ return callbacks.onDomainEvent(json._0);
133
+ } else {
134
+ return callbacks.onLog(json._0);
135
+ }
136
+ };
137
+ ChildProcess$ReventlessGwt.onStdoutLine(proc, handle);
138
+ ChildProcess$ReventlessGwt.onStderrLine(proc, handle);
139
+ ChildProcess$ReventlessGwt.onClose(proc, code => callbacks.onStop(code));
140
+ await new Promise((resolve, _reject) => Cancellation$ReventlessGwt.onCancel(() => resolve()));
141
+ return 0;
142
+ }
143
+ callbacks.onLog("no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " + roots.join(", "));
144
+ return 1;
145
+ }
146
+
147
+ export {
148
+ tapSentinel,
149
+ classifyLine,
150
+ openEphemeral,
151
+ allocFreePorts,
152
+ fixedPlatformPorts,
153
+ run,
154
+ }
155
+ /* node:net Not a pure module */
@@ -0,0 +1,138 @@
1
+ // Discovers the local-platform package(s) the runner can launch (features plan
2
+ // Phase 9). A platform package is one that (a) depends on
3
+ // `@reventlessdev/reventless-local` and (b) carries a compiled run entrypoint at
4
+ // `src/Main.res.mjs` (what `tsx`/`node` execute). This mirrors PackageScan's
5
+ // role for the watch session, but keys off the run entrypoint rather than test
6
+ // files. The matching predicate is pure so it can be unit-tested without a real
7
+ // workspace tree.
8
+
9
+ type platformPkg = {
10
+ // Package name from package.json (may be "" if absent).
11
+ name: string,
12
+ // Absolute package directory (the one containing package.json + src/).
13
+ dir: string,
14
+ // Absolute path to the compiled run entrypoint (src/Main.res.mjs).
15
+ mainPath: string,
16
+ // The npm-script name to launch, preferring an in-memory variant: "serve:memory"
17
+ // if declared, else "serve". None ⇒ no serve script (runner falls back to running
18
+ // src/Main.res.mjs via tsx directly).
19
+ serveScript: option<string>,
20
+ }
21
+
22
+ type dirent = {
23
+ name: string,
24
+ @as("isDirectory") _isDirectory: unit => bool,
25
+ }
26
+ type readdirOpts = {withFileTypes: bool}
27
+ @module("node:fs/promises")
28
+ external _readdir: (string, readdirOpts) => promise<array<dirent>> = "readdir"
29
+ @module("node:fs") external _existsSync: string => bool = "existsSync"
30
+ @module("node:fs") external _readFileSync: (string, string) => string = "readFileSync"
31
+ @module("node:path") external join: (string, string) => string = "join"
32
+
33
+ let localPlatformDep = "@reventlessdev/reventless-local"
34
+ let ignoreNames = ["node_modules", ".git", "dist", "lib", ".history"]
35
+
36
+ // Pure predicate: given a package.json's text and whether src/Main.res.mjs exists,
37
+ // decide whether this is a launchable platform package and surface its name +
38
+ // serve script. Depends on reventless-local appearing in any dependency block.
39
+ let matchPlatform = (
40
+ ~pkgJsonText: string,
41
+ ~mainExists: bool,
42
+ ): option<(string, option<string>)> => {
43
+ if !mainExists {
44
+ None
45
+ } else {
46
+ switch JSON.parseOrThrow(pkgJsonText) {
47
+ | exception _ => None
48
+ | json =>
49
+ switch json->JSON.Decode.object {
50
+ | None => None
51
+ | Some(obj) =>
52
+ let name = switch obj->Dict.get("name") {
53
+ | Some(String(n)) => n
54
+ | _ => ""
55
+ }
56
+ let depsHave = key =>
57
+ switch obj->Dict.get(key)->Option.flatMap(JSON.Decode.object) {
58
+ | Some(deps) => deps->Dict.get(localPlatformDep)->Option.isSome
59
+ | None => false
60
+ }
61
+ let dependsOnLocal =
62
+ depsHave("dependencies") ||
63
+ depsHave("devDependencies") ||
64
+ depsHave("optionalDependencies")
65
+ if dependsOnLocal {
66
+ let serveScript =
67
+ switch obj->Dict.get("scripts")->Option.flatMap(JSON.Decode.object) {
68
+ | Some(scripts) =>
69
+ if scripts->Dict.get("serve:memory")->Option.isSome {
70
+ Some("serve:memory")
71
+ } else if scripts->Dict.get("serve")->Option.isSome {
72
+ Some("serve")
73
+ } else {
74
+ None
75
+ }
76
+ | None => None
77
+ }
78
+ Some((name, serveScript))
79
+ } else {
80
+ None
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ // Inspect a single directory: is it a platform package?
88
+ let inspectDir = (dir: string): option<platformPkg> => {
89
+ let pkgJson = join(dir, "package.json")
90
+ let mainPath = join(join(dir, "src"), "Main.res.mjs")
91
+ if !_existsSync(pkgJson) {
92
+ None
93
+ } else {
94
+ let text = try _readFileSync(pkgJson, "utf8") catch {
95
+ | _ => ""
96
+ }
97
+ switch matchPlatform(~pkgJsonText=text, ~mainExists=_existsSync(mainPath)) {
98
+ | Some((name, serveScript)) => Some({name, dir, mainPath, serveScript})
99
+ | None => None
100
+ }
101
+ }
102
+ }
103
+
104
+ // Walk the roots, returning every platform package found (deduped by dir,
105
+ // declaration order preserved). Prunes node_modules/lib/.git like Discovery.
106
+ let rec walk = async (dir: string, acc: array<platformPkg>): array<platformPkg> => {
107
+ switch inspectDir(dir) {
108
+ | Some(pkg) => acc->Array.push(pkg) // a platform package — record, don't descend into its src
109
+ | None => ()
110
+ }
111
+ let entries = try await _readdir(dir, {withFileTypes: true}) catch {
112
+ | _ => []
113
+ }
114
+ for i in 0 to entries->Array.length - 1 {
115
+ let entry = entries->Array.getUnsafe(i)
116
+ if entry._isDirectory() && !Array.includes(ignoreNames, entry.name) {
117
+ let _ = await walk(join(dir, entry.name), acc)
118
+ }
119
+ }
120
+ acc
121
+ }
122
+
123
+ let scan = async (roots: array<string>): array<platformPkg> => {
124
+ let seen = Dict.make()
125
+ let out = []
126
+ for i in 0 to roots->Array.length - 1 {
127
+ let found = await walk(roots->Array.getUnsafe(i), [])
128
+ found->Array.forEach(pkg =>
129
+ switch seen->Dict.get(pkg.dir) {
130
+ | Some(_) => ()
131
+ | None =>
132
+ seen->Dict.set(pkg.dir, true)
133
+ out->Array.push(pkg)
134
+ }
135
+ )
136
+ }
137
+ out
138
+ }
@@ -0,0 +1,131 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Promises from "node:fs/promises";
8
+
9
+ let localPlatformDep = "@reventlessdev/reventless-local";
10
+
11
+ let ignoreNames = [
12
+ "node_modules",
13
+ ".git",
14
+ "dist",
15
+ "lib",
16
+ ".history"
17
+ ];
18
+
19
+ function matchPlatform(pkgJsonText, mainExists) {
20
+ if (!mainExists) {
21
+ return;
22
+ }
23
+ let json;
24
+ try {
25
+ json = JSON.parse(pkgJsonText);
26
+ } catch (exn) {
27
+ return;
28
+ }
29
+ let obj = Stdlib_JSON.Decode.object(json);
30
+ if (obj === undefined) {
31
+ return;
32
+ }
33
+ let match = obj["name"];
34
+ let name = typeof match === "string" ? match : "";
35
+ let depsHave = key => {
36
+ let deps = Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.object);
37
+ if (deps !== undefined) {
38
+ return Stdlib_Option.isSome(deps[localPlatformDep]);
39
+ } else {
40
+ return false;
41
+ }
42
+ };
43
+ let dependsOnLocal = depsHave("dependencies") || depsHave("devDependencies") || depsHave("optionalDependencies");
44
+ if (!dependsOnLocal) {
45
+ return;
46
+ }
47
+ let scripts = Stdlib_Option.flatMap(obj["scripts"], Stdlib_JSON.Decode.object);
48
+ let serveScript = scripts !== undefined ? (
49
+ Stdlib_Option.isSome(scripts["serve:memory"]) ? "serve:memory" : (
50
+ Stdlib_Option.isSome(scripts["serve"]) ? "serve" : undefined
51
+ )
52
+ ) : undefined;
53
+ return [
54
+ name,
55
+ serveScript
56
+ ];
57
+ }
58
+
59
+ function inspectDir(dir) {
60
+ let pkgJson = Nodepath.join(dir, "package.json");
61
+ let mainPath = Nodepath.join(Nodepath.join(dir, "src"), "Main.res.mjs");
62
+ if (!Nodefs.existsSync(pkgJson)) {
63
+ return;
64
+ }
65
+ let text;
66
+ try {
67
+ text = Nodefs.readFileSync(pkgJson, "utf8");
68
+ } catch (exn) {
69
+ text = "";
70
+ }
71
+ let match = matchPlatform(text, Nodefs.existsSync(mainPath));
72
+ if (match !== undefined) {
73
+ return {
74
+ name: match[0],
75
+ dir: dir,
76
+ mainPath: mainPath,
77
+ serveScript: match[1]
78
+ };
79
+ }
80
+ }
81
+
82
+ async function walk(dir, acc) {
83
+ let pkg = inspectDir(dir);
84
+ if (pkg !== undefined) {
85
+ acc.push(pkg);
86
+ }
87
+ let entries;
88
+ try {
89
+ entries = await Promises.readdir(dir, {
90
+ withFileTypes: true
91
+ });
92
+ } catch (exn) {
93
+ entries = [];
94
+ }
95
+ for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
96
+ let entry = entries[i];
97
+ if (entry.isDirectory() && !ignoreNames.includes(entry.name)) {
98
+ await walk(Nodepath.join(dir, entry.name), acc);
99
+ }
100
+ }
101
+ return acc;
102
+ }
103
+
104
+ async function scan(roots) {
105
+ let seen = {};
106
+ let out = [];
107
+ for (let i = 0, i_finish = roots.length; i < i_finish; ++i) {
108
+ let found = await walk(roots[i], []);
109
+ found.forEach(pkg => {
110
+ let match = seen[pkg.dir];
111
+ if (match !== undefined) {
112
+ return;
113
+ } else {
114
+ seen[pkg.dir] = true;
115
+ out.push(pkg);
116
+ return;
117
+ }
118
+ });
119
+ }
120
+ return out;
121
+ }
122
+
123
+ export {
124
+ localPlatformDep,
125
+ ignoreNames,
126
+ matchPlatform,
127
+ inspectDir,
128
+ walk,
129
+ scan,
130
+ }
131
+ /* node:fs Not a pure module */
@@ -0,0 +1,91 @@
1
+ // Owns the `rescript build -w` watchers a watch session spawns — one per
2
+ // package that owns tests (PackageScan), at most one per directory,
3
+ // reference-counted by `dir`. Registered with Cancellation so SIGINT/SIGTERM
4
+ // tears down every spawned watcher; never orphans a compiler.
5
+
6
+ type managed = {
7
+ dir: string,
8
+ proc: ChildProcess.t,
9
+ }
10
+
11
+ let running: Dict.t<managed> = Dict.make()
12
+
13
+ let isManaging = (dir: string): bool =>
14
+ switch running->Dict.get(dir) {
15
+ | Some(_) => true
16
+ | None => false
17
+ }
18
+
19
+ // Spawn `pnpm run start` (the package's `rescript build -w`) in `pkg.dir`,
20
+ // unless one is already managed there. `onStdout` receives each output line so
21
+ // the caller can classify compiler markers (Phase 5).
22
+ let spawnWatcher = (pkg: PackageScan.pkg, ~onStdout: (string, string) => unit): unit =>
23
+ switch running->Dict.get(pkg.dir) {
24
+ | Some(_) => ()
25
+ | None => {
26
+ let proc = ChildProcess.spawn("pnpm", ["run", "start"], ~cwd=pkg.dir)
27
+ ChildProcess.onStdoutLine(proc, line => onStdout(pkg.dir, line))
28
+ running->Dict.set(pkg.dir, {dir: pkg.dir, proc})
29
+ }
30
+ }
31
+
32
+ // Stop just the watcher managed for `dir` (if any), tearing down its process
33
+ // group. Returns true when one was running — i.e. the caller *owned* this
34
+ // package's build (vs. an adopted external watcher, which we can't stop).
35
+ let stop = (dir: string): bool =>
36
+ switch running->Dict.get(dir) {
37
+ | Some(m) => {
38
+ ChildProcess.killTree(m.proc, "SIGTERM")
39
+ running->Dict.delete(dir)
40
+ true
41
+ }
42
+ | None => false
43
+ }
44
+
45
+ // Force a full re-emit of `pkg` after a structural change — a module relocation
46
+ // that incremental compilation can't detect, since it keys invalidation on
47
+ // source + dependency-interface hashes, never on a dependency's filesystem
48
+ // location (see plan Phase 12). Drives a deterministic one-shot `rescript clean`
49
+ // → `rescript build` to completion (the `clean` is what forces every dependent
50
+ // `.res.mjs` to be re-emitted against the new layout; the `build` is essential
51
+ // even on the adopted path — an external watcher won't rebuild after a clean
52
+ // without a fresh source change, so a bare clean would strand the package with
53
+ // no outputs). If we *owned* the package's watcher we stop it first (no lock
54
+ // contention) and respawn it afterward to resume incremental watching; on the
55
+ // adopted path the developer's own watcher keeps running untouched. `onStdout`
56
+ // streams the clean/build output (compiler markers) to the caller; `onDone`
57
+ // fires once the build completes, gating the test re-run.
58
+ let cleanRebuild = (
59
+ pkg: PackageScan.pkg,
60
+ ~onStdout: (string, string) => unit,
61
+ ~onDone: unit => unit,
62
+ ): unit => {
63
+ let wasManaged = stop(pkg.dir)
64
+ let step = (cmd: string, args: array<string>, next: unit => unit): unit => {
65
+ let proc = ChildProcess.spawn(cmd, args, ~cwd=pkg.dir)
66
+ ChildProcess.onStdoutLine(proc, line => onStdout(pkg.dir, line))
67
+ ChildProcess.onClose(proc, _ => next())
68
+ }
69
+ step("pnpm", ["exec", "rescript", "clean"], () =>
70
+ step("pnpm", ["exec", "rescript", "build"], () => {
71
+ if wasManaged {
72
+ spawnWatcher(pkg, ~onStdout)
73
+ }
74
+ onDone()
75
+ })
76
+ )
77
+ }
78
+
79
+ // SIGTERM, not SIGINT: a POSIX shell sets background children to ignore SIGINT,
80
+ // so SIGINT can leak a watcher; SIGTERM tears down the whole group reliably
81
+ // (verified across foreground and background process-tree shapes).
82
+ let killAll = (): unit => {
83
+ running
84
+ ->Dict.valuesToArray
85
+ ->Array.forEach(m => ChildProcess.killTree(m.proc, "SIGTERM"))
86
+ running
87
+ ->Dict.keysToArray
88
+ ->Array.forEach(k => running->Dict.delete(k))
89
+ }
90
+
91
+ let managedCount = (): int => running->Dict.keysToArray->Array.length