@reventlessdev/reventless-gwt 1.0.0-alpha.100

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 (184) hide show
  1. package/CHANGELOG.md +916 -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 +569 -0
  10. package/src/Behavior_GWT.res.mjs +523 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +147 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +37 -0
  16. package/src/Cancellation.res.mjs +46 -0
  17. package/src/ChildProcess.res +102 -0
  18. package/src/ChildProcess.res.mjs +107 -0
  19. package/src/Cli.res +905 -0
  20. package/src/Cli.res.mjs +952 -0
  21. package/src/Collector.res +212 -0
  22. package/src/Collector.res.mjs +218 -0
  23. package/src/ComponentMeta.res +77 -0
  24. package/src/ComponentMeta.res.mjs +91 -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 +99 -0
  33. package/src/DomainDeadCode.res +98 -0
  34. package/src/DomainDeadCode.res.mjs +72 -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 +45 -0
  40. package/src/Filter.res.mjs +47 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +109 -0
  44. package/src/FormatterHuman.res.mjs +115 -0
  45. package/src/FormatterJson.res +264 -0
  46. package/src/FormatterJson.res.mjs +322 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +100 -0
  50. package/src/FormatterTap.res.mjs +131 -0
  51. package/src/FormatterVsCode.res +352 -0
  52. package/src/FormatterVsCode.res.mjs +546 -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 +119 -0
  62. package/src/LocalHost.res.mjs +99 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MismatchRender.res +72 -0
  66. package/src/MismatchRender.res.mjs +127 -0
  67. package/src/MultiSourceProjection_GWT.res +316 -0
  68. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  69. package/src/OutboundTranslation_GWT.res +238 -0
  70. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  71. package/src/Outcome.res +97 -0
  72. package/src/Outcome.res.mjs +93 -0
  73. package/src/PackageScan.res +80 -0
  74. package/src/PackageScan.res.mjs +81 -0
  75. package/src/PlatformRunner.res +181 -0
  76. package/src/PlatformRunner.res.mjs +176 -0
  77. package/src/PlatformScan.res +142 -0
  78. package/src/PlatformScan.res.mjs +132 -0
  79. package/src/ProcessManager.res +135 -0
  80. package/src/ProcessManager.res.mjs +119 -0
  81. package/src/Projection_GWT.res +312 -0
  82. package/src/Projection_GWT.res.mjs +332 -0
  83. package/src/Query_GWT.res +379 -0
  84. package/src/Query_GWT.res.mjs +367 -0
  85. package/src/RenderRescript.res +125 -0
  86. package/src/RenderRescript.res.mjs +141 -0
  87. package/src/RunWorker.res +24 -0
  88. package/src/RunWorker.res.mjs +16 -0
  89. package/src/RunnerTypes.res +68 -0
  90. package/src/RunnerTypes.res.mjs +60 -0
  91. package/src/SideEffect_GWT.res +157 -0
  92. package/src/SideEffect_GWT.res.mjs +113 -0
  93. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  94. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  95. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  96. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  97. package/src/StateChange/WithFixtures.res +22 -0
  98. package/src/StateChange/WithFixtures.res.mjs +31 -0
  99. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  100. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  101. package/src/StateChange/WithManualOpen.res +23 -0
  102. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  103. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  104. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  105. package/src/StubRuntime.res +54 -0
  106. package/src/StubRuntime.res.mjs +55 -0
  107. package/src/TestFixtures.res +16 -0
  108. package/src/TestFixtures.res.mjs +34 -0
  109. package/src/Watch.res +108 -0
  110. package/src/Watch.res.mjs +85 -0
  111. package/src/WatcherProbe.res +56 -0
  112. package/src/WatcherProbe.res.mjs +59 -0
  113. package/src/Worker.res +24 -0
  114. package/src/Worker.res.mjs +9 -0
  115. package/tests/AutomationGwtTest.res +71 -0
  116. package/tests/AutomationGwtTest.res.mjs +189 -0
  117. package/tests/BuildClassifierTest.res +133 -0
  118. package/tests/BuildClassifierTest.res.mjs +176 -0
  119. package/tests/CliRunEntryTest.res +57 -0
  120. package/tests/CliRunEntryTest.res.mjs +65 -0
  121. package/tests/CliWatchScopeTest.res +62 -0
  122. package/tests/CliWatchScopeTest.res.mjs +95 -0
  123. package/tests/ComponentMetaTest.res +67 -0
  124. package/tests/ComponentMetaTest.res.mjs +71 -0
  125. package/tests/DelegateGwtTest.res +487 -0
  126. package/tests/DelegateGwtTest.res.mjs +891 -0
  127. package/tests/DiscoveryTest.res +42 -0
  128. package/tests/DiscoveryTest.res.mjs +43 -0
  129. package/tests/DomainDeadCodeTest.res +119 -0
  130. package/tests/DomainDeadCodeTest.res.mjs +158 -0
  131. package/tests/DomainGraphTest.res +448 -0
  132. package/tests/DomainGraphTest.res.mjs +505 -0
  133. package/tests/FlowAggregateGwtTest.res +171 -0
  134. package/tests/FlowAggregateGwtTest.res.mjs +273 -0
  135. package/tests/FlowCrossPluginGwtTest.res +340 -0
  136. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  137. package/tests/FlowGwtTest.res +222 -0
  138. package/tests/FlowGwtTest.res.mjs +433 -0
  139. package/tests/FormatterGoldenTest.res +125 -0
  140. package/tests/FormatterGoldenTest.res.mjs +162 -0
  141. package/tests/FormatterVsCodeMessageTest.res +41 -0
  142. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  143. package/tests/InboundTranslationGwtTest.res +34 -0
  144. package/tests/InboundTranslationGwtTest.res.mjs +99 -0
  145. package/tests/LocalHostIntegration.res +70 -0
  146. package/tests/LocalHostIntegration.res.mjs +64 -0
  147. package/tests/LocalHostTest.res +71 -0
  148. package/tests/LocalHostTest.res.mjs +63 -0
  149. package/tests/MappingGwtTest.res +350 -0
  150. package/tests/MappingGwtTest.res.mjs +596 -0
  151. package/tests/OutboundTranslationGwtTest.res +59 -0
  152. package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
  153. package/tests/PlatformRunnerTest.res +66 -0
  154. package/tests/PlatformRunnerTest.res.mjs +85 -0
  155. package/tests/PlatformScanTest.res +61 -0
  156. package/tests/PlatformScanTest.res.mjs +56 -0
  157. package/tests/QueryGwtTest.res +161 -0
  158. package/tests/QueryGwtTest.res.mjs +314 -0
  159. package/tests/QueryResolverGwtTest.res +71 -0
  160. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  161. package/tests/RunnerUnitTest.res +219 -0
  162. package/tests/RunnerUnitTest.res.mjs +244 -0
  163. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  164. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  165. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  166. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  167. package/tests/SideEffect/MockEmail.res +18 -0
  168. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  169. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  170. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  171. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  172. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  173. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  174. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  175. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  176. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  177. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  178. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  179. package/tests/StateChangeSliceGwtTest.res +160 -0
  180. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  181. package/tests/StateViewSliceGwtTest.res +64 -0
  182. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  183. package/tests/WatchDebounceTest.res +106 -0
  184. package/tests/WatchDebounceTest.res.mjs +128 -0
@@ -0,0 +1,81 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+
6
+ function readPackage(pkgJsonPath) {
7
+ try {
8
+ let json = JSON.parse(Nodefs.readFileSync(pkgJsonPath, "utf8"));
9
+ if (typeof json !== "object" || json === null || Array.isArray(json)) {
10
+ return;
11
+ }
12
+ let match = json["name"];
13
+ let name = typeof match === "string" ? match : "";
14
+ let match$1 = json["scripts"];
15
+ if (match$1 === undefined) {
16
+ return;
17
+ }
18
+ if (typeof match$1 !== "object" || match$1 === null || Array.isArray(match$1)) {
19
+ return;
20
+ }
21
+ let match$2 = match$1["start"];
22
+ if (typeof match$2 === "string") {
23
+ return [
24
+ name,
25
+ match$2
26
+ ];
27
+ } else {
28
+ return;
29
+ }
30
+ } catch (exn) {
31
+ return;
32
+ }
33
+ }
34
+
35
+ function findOwning(_dir) {
36
+ while (true) {
37
+ let dir = _dir;
38
+ let pkgJson = Nodepath.join(dir, "package.json");
39
+ let here = Nodefs.existsSync(pkgJson) ? readPackage(pkgJson) : undefined;
40
+ if (here !== undefined) {
41
+ return {
42
+ name: here[0],
43
+ dir: dir,
44
+ build: here[1]
45
+ };
46
+ }
47
+ let parent = Nodepath.dirname(dir);
48
+ if (parent === dir) {
49
+ return;
50
+ }
51
+ _dir = parent;
52
+ continue;
53
+ };
54
+ }
55
+
56
+ function scan(testFiles) {
57
+ let seen = {};
58
+ let out = [];
59
+ testFiles.forEach(file => {
60
+ let pkg = findOwning(Nodepath.dirname(file));
61
+ if (pkg === undefined) {
62
+ return;
63
+ }
64
+ let match = seen[pkg.dir];
65
+ if (match !== undefined) {
66
+ return;
67
+ } else {
68
+ seen[pkg.dir] = true;
69
+ out.push(pkg);
70
+ return;
71
+ }
72
+ });
73
+ return out;
74
+ }
75
+
76
+ export {
77
+ readPackage,
78
+ findOwning,
79
+ scan,
80
+ }
81
+ /* node:fs Not a pure module */
@@ -0,0 +1,181 @@
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
+ let closeServer = (s: netServer): promise<unit> =>
64
+ Promise.make((resolve, _reject) => s->_close(() => resolve()))
65
+
66
+ // Hold all n open while collecting ports so they're guaranteed distinct, then
67
+ // release them just before the child binds.
68
+ let allocFreePorts = async (n: int): array<int> => {
69
+ let servers = []
70
+ let ports = []
71
+ for _i in 1 to n {
72
+ let (s, p) = await openEphemeral()
73
+ servers->Array.push(s)
74
+ ports->Array.push(p)
75
+ }
76
+ // Await every close so the ports are actually released before the child binds
77
+ // them. Closing fire-and-forget returned the ports while the sockets were
78
+ // still bound, racing the child's bind into EADDRINUSE.
79
+ let _ = await Promise.all(servers->Array.map(closeServer))
80
+ ports
81
+ }
82
+
83
+ // The platform child's default ports (Platform.startServers): domain / platform /
84
+ // domain-MCP / platform-MCP. `~fixedPorts` pins to these instead of allocating
85
+ // ephemeral ones — the VS Code extension uses it when launching the host-shell UI,
86
+ // whose Vite proxy hardcodes localhost:4000/4001. The caller is responsible for
87
+ // ensuring they're free (it pre-checks and toasts on conflict).
88
+ let fixedPlatformPorts = [4000, 4001, 3001, 3002]
89
+
90
+ // Discover the platform package under `roots`, spawn it with the tap + per-session
91
+ // ports + in-memory backend, stream its events through `callbacks`, and stay alive
92
+ // until cancelled (the child is killed via its process group on SIGTERM/SIGINT).
93
+ let run = async (
94
+ ~roots: array<string>,
95
+ ~backend: string,
96
+ ~fixedPorts: bool=false,
97
+ ~callbacks: callbacks,
98
+ ): int => {
99
+ let pkgs = await PlatformScan.scan(roots)
100
+ switch pkgs->Array.get(0) {
101
+ | None =>
102
+ callbacks.onLog(
103
+ "no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " ++
104
+ roots->Array.join(", "),
105
+ )
106
+ 1
107
+ | Some(pkg) =>
108
+ let ports = fixedPorts ? fixedPlatformPorts : await allocFreePorts(4)
109
+ let dPort = ports->Array.getUnsafe(0)
110
+ let pPort = ports->Array.getUnsafe(1)
111
+ let dMcp = ports->Array.getUnsafe(2)
112
+ let pMcp = ports->Array.getUnsafe(3)
113
+ let env = Dict.fromArray([
114
+ ("REVENTLESS_EVENT_TAP", "ndjson"),
115
+ ("REVENTLESS_LOCAL_BACKEND", backend),
116
+ ("REVENTLESS_DOMAIN_PORT", Int.toString(dPort)),
117
+ ("REVENTLESS_PLATFORM_PORT", Int.toString(pPort)),
118
+ ("REVENTLESS_DOMAIN_MCP_PORT", Int.toString(dMcp)),
119
+ ("REVENTLESS_PLATFORM_MCP_PORT", Int.toString(pMcp)),
120
+ ("NODE_OPTIONS", "--disable-warning=ExperimentalWarning"),
121
+ // The gwt bin defaults LOG_LEVEL=silent so its OWN stdout stays pure NDJSON;
122
+ // the platform child is a separate process whose stdout we parse, so it needs
123
+ // real logs — the "listening on" line drives readiness and the rest becomes
124
+ // platformLog. Override the inherited silent.
125
+ ("LOG_LEVEL", "info"),
126
+ // The child's stdout is a pipe (we line-parse it), so the framework's sink
127
+ // detection would auto-pick JSON (non-TTY). But every consumer of `onLog`
128
+ // renders ANSI — the CLI forwards to the developer's terminal, the extension
129
+ // to an xterm Pseudoterminal — so force human text mode for bold/colour.
130
+ // (Tap event lines stay JSON regardless: they're sentinel-prefixed.)
131
+ ("REVENTLESS_LOG_FORMAT", "text"),
132
+ ])
133
+
134
+ callbacks.onStart(~package=pkg.name, ~dir=pkg.dir, ~domainPort=dPort, ~platformPort=pPort)
135
+
136
+ // Run the compiled entrypoint directly with `node`. `src/Main.res.mjs` is plain
137
+ // ESM, so it needs no tsx/loader, and spawning a single process (vs `pnpm run
138
+ // serve` → `pnpm run serve` → `npx tsx`) keeps the child's stdout piped straight
139
+ // to us — nested pnpm buffers a long-running grandchild's output, so the event
140
+ // tap never streams through it. cwd = the package dir so `PackageVersion.fromCwd`
141
+ // and relative paths resolve. We set the backend + tap + ports via env directly,
142
+ // so the package's own serve script (which would otherwise pick them) is bypassed.
143
+ let proc = ChildProcess.spawn("node", ["src/Main.res.mjs"], ~cwd=pkg.dir, ~env)
144
+ Cancellation.onCancel(() => ChildProcess.killTree(proc, "SIGTERM"))
145
+
146
+ let readyEmitted = ref(false)
147
+ let handle = line =>
148
+ switch classifyLine(line) {
149
+ | Domain(json) => callbacks.onDomainEvent(json)
150
+ | Ready =>
151
+ if !readyEmitted.contents {
152
+ readyEmitted := true
153
+ callbacks.onReady(~domainEndpoint="http://localhost:" ++ Int.toString(dPort) ++ "/graphql")
154
+ }
155
+ | Log(l) => callbacks.onLog(l)
156
+ }
157
+ ChildProcess.onStdoutLine(proc, handle)
158
+ ChildProcess.onStderrLine(proc, handle)
159
+
160
+ // Keep the parent alive until either the operator cancels or the child exits.
161
+ // The previous shape awaited cancellation *forever* and always returned 0, so
162
+ // a crashed platform child left a zombie parent that reported success. Resolve
163
+ // on child close (or a spawn error) too, and surface the child's exit code.
164
+ let exitCode = ref(0)
165
+ await Promise.make((resolve, _reject) => {
166
+ Cancellation.onCancel(() => resolve())
167
+ ChildProcess.onError(proc, msg => {
168
+ callbacks.onLog("platform spawn error: " ++ msg)
169
+ callbacks.onStop(None)
170
+ exitCode := 1
171
+ resolve()
172
+ })
173
+ ChildProcess.onClose(proc, code => {
174
+ callbacks.onStop(code)
175
+ exitCode := code->Option.getOr(0)
176
+ resolve()
177
+ })
178
+ })
179
+ exitCode.contents
180
+ }
181
+ }
@@ -0,0 +1,176 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodenet from "node:net";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Cancellation$ReventlessGwt from "./Cancellation.res.mjs";
6
+ import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
7
+ import * as PlatformScan$ReventlessGwt from "./PlatformScan.res.mjs";
8
+
9
+ let tapSentinel = "@@RVLESS_EVT@@ ";
10
+
11
+ function classifyLine(line) {
12
+ if (!line.startsWith(tapSentinel)) {
13
+ if (line.includes("GraphQL:Domain") && line.includes("listening on")) {
14
+ return "Ready";
15
+ } else {
16
+ return {
17
+ TAG: "Log",
18
+ _0: line
19
+ };
20
+ }
21
+ }
22
+ let jsonStr = line.slice(tapSentinel.length, line.length);
23
+ let json;
24
+ try {
25
+ json = JSON.parse(jsonStr);
26
+ } catch (exn) {
27
+ return {
28
+ TAG: "Log",
29
+ _0: line
30
+ };
31
+ }
32
+ return {
33
+ TAG: "Domain",
34
+ _0: json
35
+ };
36
+ }
37
+
38
+ function openEphemeral() {
39
+ return new Promise((resolve, _reject) => {
40
+ let s = Nodenet.createServer();
41
+ s.listen(0, "127.0.0.1", () => resolve([
42
+ s,
43
+ s.address().port
44
+ ]));
45
+ });
46
+ }
47
+
48
+ function closeServer(s) {
49
+ return new Promise((resolve, _reject) => {
50
+ s.close(() => resolve());
51
+ });
52
+ }
53
+
54
+ async function allocFreePorts(n) {
55
+ let servers = [];
56
+ let ports = [];
57
+ for (let _i = 1; _i <= n; ++_i) {
58
+ let match = await openEphemeral();
59
+ servers.push(match[0]);
60
+ ports.push(match[1]);
61
+ }
62
+ await Promise.all(servers.map(closeServer));
63
+ return ports;
64
+ }
65
+
66
+ let fixedPlatformPorts = [
67
+ 4000,
68
+ 4001,
69
+ 3001,
70
+ 3002
71
+ ];
72
+
73
+ async function run(roots, backend, fixedPortsOpt, callbacks) {
74
+ let fixedPorts = fixedPortsOpt !== undefined ? fixedPortsOpt : false;
75
+ let pkgs = await PlatformScan$ReventlessGwt.scan(roots);
76
+ let pkg = pkgs[0];
77
+ if (pkg !== undefined) {
78
+ let ports = fixedPorts ? fixedPlatformPorts : await allocFreePorts(4);
79
+ let dPort = ports[0];
80
+ let pPort = ports[1];
81
+ let dMcp = ports[2];
82
+ let pMcp = ports[3];
83
+ let env = Object.fromEntries([
84
+ [
85
+ "REVENTLESS_EVENT_TAP",
86
+ "ndjson"
87
+ ],
88
+ [
89
+ "REVENTLESS_LOCAL_BACKEND",
90
+ backend
91
+ ],
92
+ [
93
+ "REVENTLESS_DOMAIN_PORT",
94
+ dPort.toString()
95
+ ],
96
+ [
97
+ "REVENTLESS_PLATFORM_PORT",
98
+ pPort.toString()
99
+ ],
100
+ [
101
+ "REVENTLESS_DOMAIN_MCP_PORT",
102
+ dMcp.toString()
103
+ ],
104
+ [
105
+ "REVENTLESS_PLATFORM_MCP_PORT",
106
+ pMcp.toString()
107
+ ],
108
+ [
109
+ "NODE_OPTIONS",
110
+ "--disable-warning=ExperimentalWarning"
111
+ ],
112
+ [
113
+ "LOG_LEVEL",
114
+ "info"
115
+ ],
116
+ [
117
+ "REVENTLESS_LOG_FORMAT",
118
+ "text"
119
+ ]
120
+ ]);
121
+ callbacks.onStart(pkg.name, pkg.dir, dPort, pPort);
122
+ let proc = ChildProcess$ReventlessGwt.spawn("node", ["src/Main.res.mjs"], pkg.dir, env);
123
+ Cancellation$ReventlessGwt.onCancel(() => ChildProcess$ReventlessGwt.killTree(proc, "SIGTERM"));
124
+ let readyEmitted = {
125
+ contents: false
126
+ };
127
+ let handle = line => {
128
+ let json = classifyLine(line);
129
+ if (typeof json !== "object") {
130
+ if (!readyEmitted.contents) {
131
+ readyEmitted.contents = true;
132
+ return callbacks.onReady("http://localhost:" + dPort.toString() + "/graphql");
133
+ } else {
134
+ return;
135
+ }
136
+ } else if (json.TAG === "Domain") {
137
+ return callbacks.onDomainEvent(json._0);
138
+ } else {
139
+ return callbacks.onLog(json._0);
140
+ }
141
+ };
142
+ ChildProcess$ReventlessGwt.onStdoutLine(proc, handle);
143
+ ChildProcess$ReventlessGwt.onStderrLine(proc, handle);
144
+ let exitCode = {
145
+ contents: 0
146
+ };
147
+ await new Promise((resolve, _reject) => {
148
+ Cancellation$ReventlessGwt.onCancel(() => resolve());
149
+ ChildProcess$ReventlessGwt.onError(proc, msg => {
150
+ callbacks.onLog("platform spawn error: " + msg);
151
+ callbacks.onStop(undefined);
152
+ exitCode.contents = 1;
153
+ resolve();
154
+ });
155
+ ChildProcess$ReventlessGwt.onClose(proc, code => {
156
+ callbacks.onStop(code);
157
+ exitCode.contents = Stdlib_Option.getOr(code, 0);
158
+ resolve();
159
+ });
160
+ });
161
+ return exitCode.contents;
162
+ }
163
+ callbacks.onLog("no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " + roots.join(", "));
164
+ return 1;
165
+ }
166
+
167
+ export {
168
+ tapSentinel,
169
+ classifyLine,
170
+ openEphemeral,
171
+ closeServer,
172
+ allocFreePorts,
173
+ fixedPlatformPorts,
174
+ run,
175
+ }
176
+ /* node:net Not a pure module */
@@ -0,0 +1,142 @@
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
+ // A platform package — record it and stop: descending into its own subtree
109
+ // (src/, generated code, nested workspace copies) can only waste I/O or turn
110
+ // up spurious inner matches. This is what the "don't descend" comment always
111
+ // intended; the recursion below used to run regardless.
112
+ | Some(pkg) => acc->Array.push(pkg)
113
+ | None =>
114
+ let entries = try await _readdir(dir, {withFileTypes: true}) catch {
115
+ | _ => []
116
+ }
117
+ for i in 0 to entries->Array.length - 1 {
118
+ let entry = entries->Array.getUnsafe(i)
119
+ if entry._isDirectory() && !Array.includes(ignoreNames, entry.name) {
120
+ let _ = await walk(join(dir, entry.name), acc)
121
+ }
122
+ }
123
+ }
124
+ acc
125
+ }
126
+
127
+ let scan = async (roots: array<string>): array<platformPkg> => {
128
+ let seen = Dict.make()
129
+ let out = []
130
+ for i in 0 to roots->Array.length - 1 {
131
+ let found = await walk(roots->Array.getUnsafe(i), [])
132
+ found->Array.forEach(pkg =>
133
+ switch seen->Dict.get(pkg.dir) {
134
+ | Some(_) => ()
135
+ | None =>
136
+ seen->Dict.set(pkg.dir, true)
137
+ out->Array.push(pkg)
138
+ }
139
+ )
140
+ }
141
+ out
142
+ }