@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.
- package/CHANGELOG.md +916 -0
- package/LICENSE +202 -0
- package/bin/reventless-dev.mjs +21 -0
- package/package.json +54 -0
- package/rescript.json +36 -0
- package/src/.gwtignore +10 -0
- package/src/Automation_GWT.res +207 -0
- package/src/Automation_GWT.res.mjs +160 -0
- package/src/Behavior_GWT.res +569 -0
- package/src/Behavior_GWT.res.mjs +523 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +147 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +37 -0
- package/src/Cancellation.res.mjs +46 -0
- package/src/ChildProcess.res +102 -0
- package/src/ChildProcess.res.mjs +107 -0
- package/src/Cli.res +905 -0
- package/src/Cli.res.mjs +952 -0
- package/src/Collector.res +212 -0
- package/src/Collector.res.mjs +218 -0
- package/src/ComponentMeta.res +77 -0
- package/src/ComponentMeta.res.mjs +91 -0
- package/src/ComponentScan.res +103 -0
- package/src/ComponentScan.res.mjs +105 -0
- package/src/Delegate_GWT.res +303 -0
- package/src/Delegate_GWT.res.mjs +372 -0
- package/src/Diff.res +91 -0
- package/src/Diff.res.mjs +98 -0
- package/src/Discovery.res +105 -0
- package/src/Discovery.res.mjs +99 -0
- package/src/DomainDeadCode.res +98 -0
- package/src/DomainDeadCode.res.mjs +72 -0
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/EventMapping_GWT.res +29 -0
- package/src/EventMapping_GWT.res.mjs +44 -0
- package/src/Filter.res +45 -0
- package/src/Filter.res.mjs +47 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +109 -0
- package/src/FormatterHuman.res.mjs +115 -0
- package/src/FormatterJson.res +264 -0
- package/src/FormatterJson.res.mjs +322 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +100 -0
- package/src/FormatterTap.res.mjs +131 -0
- package/src/FormatterVsCode.res +352 -0
- package/src/FormatterVsCode.res.mjs +546 -0
- package/src/Hint.res +119 -0
- package/src/Hint.res.mjs +124 -0
- package/src/InboundTranslation_GWT.res +83 -0
- package/src/InboundTranslation_GWT.res.mjs +94 -0
- package/src/JestBind.res +70 -0
- package/src/JestBind.res.mjs +63 -0
- package/src/Loader.res +24 -0
- package/src/Loader.res.mjs +23 -0
- package/src/LocalHost.res +119 -0
- package/src/LocalHost.res.mjs +99 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/MultiSourceProjection_GWT.res +316 -0
- package/src/MultiSourceProjection_GWT.res.mjs +354 -0
- package/src/OutboundTranslation_GWT.res +238 -0
- package/src/OutboundTranslation_GWT.res.mjs +186 -0
- package/src/Outcome.res +97 -0
- package/src/Outcome.res.mjs +93 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +181 -0
- package/src/PlatformRunner.res.mjs +176 -0
- package/src/PlatformScan.res +142 -0
- package/src/PlatformScan.res.mjs +132 -0
- package/src/ProcessManager.res +135 -0
- package/src/ProcessManager.res.mjs +119 -0
- package/src/Projection_GWT.res +312 -0
- package/src/Projection_GWT.res.mjs +332 -0
- package/src/Query_GWT.res +379 -0
- package/src/Query_GWT.res.mjs +367 -0
- package/src/RenderRescript.res +125 -0
- package/src/RenderRescript.res.mjs +141 -0
- package/src/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -0
- package/src/RunnerTypes.res +68 -0
- package/src/RunnerTypes.res.mjs +60 -0
- package/src/SideEffect_GWT.res +157 -0
- package/src/SideEffect_GWT.res.mjs +113 -0
- package/src/StateChange/ExternalAddCategorySlice.res +25 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
- package/src/StateChange/WithFixtures.res +22 -0
- package/src/StateChange/WithFixtures.res.mjs +31 -0
- package/src/StateChange/WithFixtures_Behavior.res +17 -0
- package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
- package/src/StateChange/WithManualOpen.res +23 -0
- package/src/StateChange/WithManualOpen.res.mjs +31 -0
- package/src/StateChange/WithManualOpen_Behavior.res +17 -0
- package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
- package/src/StubRuntime.res +54 -0
- package/src/StubRuntime.res.mjs +55 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +108 -0
- package/src/Watch.res.mjs +85 -0
- package/src/WatcherProbe.res +56 -0
- package/src/WatcherProbe.res.mjs +59 -0
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +133 -0
- package/tests/BuildClassifierTest.res.mjs +176 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +67 -0
- package/tests/ComponentMetaTest.res.mjs +71 -0
- package/tests/DelegateGwtTest.res +487 -0
- package/tests/DelegateGwtTest.res.mjs +891 -0
- package/tests/DiscoveryTest.res +42 -0
- package/tests/DiscoveryTest.res.mjs +43 -0
- package/tests/DomainDeadCodeTest.res +119 -0
- package/tests/DomainDeadCodeTest.res.mjs +158 -0
- package/tests/DomainGraphTest.res +448 -0
- package/tests/DomainGraphTest.res.mjs +505 -0
- package/tests/FlowAggregateGwtTest.res +171 -0
- package/tests/FlowAggregateGwtTest.res.mjs +273 -0
- package/tests/FlowCrossPluginGwtTest.res +340 -0
- package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
- package/tests/FlowGwtTest.res +222 -0
- package/tests/FlowGwtTest.res.mjs +433 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/FormatterVsCodeMessageTest.res +41 -0
- package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
- package/tests/InboundTranslationGwtTest.res +34 -0
- package/tests/InboundTranslationGwtTest.res.mjs +99 -0
- package/tests/LocalHostIntegration.res +70 -0
- package/tests/LocalHostIntegration.res.mjs +64 -0
- package/tests/LocalHostTest.res +71 -0
- package/tests/LocalHostTest.res.mjs +63 -0
- package/tests/MappingGwtTest.res +350 -0
- package/tests/MappingGwtTest.res.mjs +596 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
- package/tests/PlatformRunnerTest.res +66 -0
- package/tests/PlatformRunnerTest.res.mjs +85 -0
- package/tests/PlatformScanTest.res +61 -0
- package/tests/PlatformScanTest.res.mjs +56 -0
- package/tests/QueryGwtTest.res +161 -0
- package/tests/QueryGwtTest.res.mjs +314 -0
- package/tests/QueryResolverGwtTest.res +71 -0
- package/tests/QueryResolverGwtTest.res.mjs +136 -0
- package/tests/RunnerUnitTest.res +219 -0
- package/tests/RunnerUnitTest.res.mjs +244 -0
- package/tests/SideEffect/FakeOrderNotification.res +23 -0
- package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
- package/tests/SideEffect/MockEmail.res +18 -0
- package/tests/SideEffect/MockEmail.res.mjs +47 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
- package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
- package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithFixtures_GWT.res +23 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithManualOpen_GWT.res +17 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
- package/tests/StateChangeSliceGwtTest.res +160 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
- package/tests/StateViewSliceGwtTest.res +64 -0
- package/tests/StateViewSliceGwtTest.res.mjs +161 -0
- package/tests/WatchDebounceTest.res +106 -0
- 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
|
+
}
|