@reventlessdev/reventless-gwt 1.0.0-alpha.74 → 1.0.0-alpha.76
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 +15 -0
- package/package.json +8 -8
- package/src/Bind.res.mjs +2 -2
- package/src/Cancellation.res +6 -0
- package/src/Cancellation.res.mjs +3 -2
- package/src/ChildProcess.res +19 -1
- package/src/ChildProcess.res.mjs +16 -1
- package/src/Cli.res +115 -9
- package/src/Cli.res.mjs +101 -17
- package/src/Collector.res +11 -0
- package/src/Collector.res.mjs +6 -3
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/Filter.res +11 -2
- package/src/Filter.res.mjs +10 -3
- package/src/FormatterVsCode.res +1 -1
- package/src/FormatterVsCode.res.mjs +2 -1
- package/src/JestBind.res +1 -1
- package/src/JestBind.res.mjs +2 -2
- package/src/PlatformRunner.res +27 -6
- package/src/PlatformRunner.res.mjs +27 -6
- package/src/ProcessManager.res +51 -7
- package/src/ProcessManager.res.mjs +43 -4
- package/src/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -0
- package/src/Watch.res +46 -30
- package/src/Watch.res.mjs +34 -32
- package/src/WatcherProbe.res +11 -3
- package/src/WatcherProbe.res.mjs +15 -2
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/DomainGraphTest.res +79 -2
- package/tests/DomainGraphTest.res.mjs +87 -6
- package/tests/FlowGwtTest.res.mjs +2 -1
- package/tests/InboundTranslationGwtTest.res.mjs +2 -1
- package/tests/OutboundTranslationGwtTest.res.mjs +2 -1
- package/tests/WatchDebounceTest.res +35 -7
- package/tests/WatchDebounceTest.res.mjs +37 -6
package/src/Filter.res.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function only() {
|
|
|
14
14
|
function xtest(param, name, _body) {
|
|
15
15
|
if (Collector$ReventlessGwt.isActive()) {
|
|
16
16
|
Collector$ReventlessGwt.markSkipNext();
|
|
17
|
-
return Collector$ReventlessGwt.push(undefined, undefined, name, () => Promise.resolve(Outcome$ReventlessGwt.pass));
|
|
17
|
+
return Collector$ReventlessGwt.push(undefined, undefined, undefined, name, () => Promise.resolve(Outcome$ReventlessGwt.pass));
|
|
18
18
|
} else {
|
|
19
19
|
globalThis.xtest(name, () => {});
|
|
20
20
|
return;
|
|
@@ -24,10 +24,17 @@ function xtest(param, name, _body) {
|
|
|
24
24
|
function xdescribe(label, body) {
|
|
25
25
|
if (Collector$ReventlessGwt.isActive()) {
|
|
26
26
|
Collector$ReventlessGwt.skipDepth.contents = Collector$ReventlessGwt.skipDepth.contents + 1 | 0;
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
try {
|
|
28
|
+
Collector$ReventlessGwt.pushDescribe(label, body);
|
|
29
|
+
Collector$ReventlessGwt.skipDepth.contents = Collector$ReventlessGwt.skipDepth.contents - 1 | 0;
|
|
30
|
+
return;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
Collector$ReventlessGwt.skipDepth.contents = Collector$ReventlessGwt.skipDepth.contents - 1 | 0;
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
29
35
|
} else {
|
|
30
36
|
globalThis.xdescribe(label, body);
|
|
37
|
+
return;
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
|
package/src/FormatterVsCode.res
CHANGED
|
@@ -262,7 +262,7 @@ let graph = (g: DomainGraph.graph) =>
|
|
|
262
262
|
label: n.label,
|
|
263
263
|
plugin: n.plugin,
|
|
264
264
|
}),
|
|
265
|
-
edges: g.edges->Array.map((e): P.graphEdge => {from: e.from, to_: e.to, kind: e.kind}),
|
|
265
|
+
edges: g.edges->Array.map((e): P.graphEdge => {from: e.from, to_: e.to, kind: e.kind, label: ?e.label}),
|
|
266
266
|
}),
|
|
267
267
|
)
|
|
268
268
|
|
package/src/JestBind.res
CHANGED
|
@@ -60,7 +60,7 @@ let testPromise = (
|
|
|
60
60
|
) =>
|
|
61
61
|
if Collector.isActive() {
|
|
62
62
|
let location = Collector.captureLocation(1)
|
|
63
|
-
Collector.push(~slice?, ~location?, name, body)
|
|
63
|
+
Collector.push(~slice?, ~location?, ~timeout?, name, body)
|
|
64
64
|
} else {
|
|
65
65
|
switch timeout {
|
|
66
66
|
| Some(t) =>
|
package/src/JestBind.res.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function todo(label) {
|
|
|
35
35
|
function test(slice, name, body) {
|
|
36
36
|
if (Collector$ReventlessGwt.isActive()) {
|
|
37
37
|
let location = Collector$ReventlessGwt.captureLocation(1);
|
|
38
|
-
return Collector$ReventlessGwt.push(slice, location, name, () => Promise.resolve(body()));
|
|
38
|
+
return Collector$ReventlessGwt.push(slice, location, undefined, name, () => Promise.resolve(body()));
|
|
39
39
|
}
|
|
40
40
|
globalThis.test(name, () => assertOutcome(slice, body()));
|
|
41
41
|
}
|
|
@@ -50,7 +50,7 @@ function testPromise(slice, name, timeout, body) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
let location = Collector$ReventlessGwt.captureLocation(1);
|
|
53
|
-
Collector$ReventlessGwt.push(slice, location, name, body);
|
|
53
|
+
Collector$ReventlessGwt.push(slice, location, timeout, name, body);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export {
|
package/src/PlatformRunner.res
CHANGED
|
@@ -60,6 +60,9 @@ let openEphemeral = (): promise<(netServer, int)> =>
|
|
|
60
60
|
s->_listen(0, "127.0.0.1", () => resolve((s, (s->_address)["port"])))
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
let closeServer = (s: netServer): promise<unit> =>
|
|
64
|
+
Promise.make((resolve, _reject) => s->_close(() => resolve()))
|
|
65
|
+
|
|
63
66
|
// Hold all n open while collecting ports so they're guaranteed distinct, then
|
|
64
67
|
// release them just before the child binds.
|
|
65
68
|
let allocFreePorts = async (n: int): array<int> => {
|
|
@@ -70,7 +73,10 @@ let allocFreePorts = async (n: int): array<int> => {
|
|
|
70
73
|
servers->Array.push(s)
|
|
71
74
|
ports->Array.push(p)
|
|
72
75
|
}
|
|
73
|
-
|
|
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))
|
|
74
80
|
ports
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -150,11 +156,26 @@ let run = async (
|
|
|
150
156
|
}
|
|
151
157
|
ChildProcess.onStdoutLine(proc, handle)
|
|
152
158
|
ChildProcess.onStderrLine(proc, handle)
|
|
153
|
-
ChildProcess.onClose(proc, code => callbacks.onStop(code))
|
|
154
159
|
|
|
155
|
-
// Keep the
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
159
180
|
}
|
|
160
181
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Nodenet from "node:net";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Cancellation$ReventlessGwt from "./Cancellation.res.mjs";
|
|
5
6
|
import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
|
|
6
7
|
import * as PlatformScan$ReventlessGwt from "./PlatformScan.res.mjs";
|
|
@@ -44,6 +45,12 @@ function openEphemeral() {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function closeServer(s) {
|
|
49
|
+
return new Promise((resolve, _reject) => {
|
|
50
|
+
s.close(() => resolve());
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
async function allocFreePorts(n) {
|
|
48
55
|
let servers = [];
|
|
49
56
|
let ports = [];
|
|
@@ -52,9 +59,7 @@ async function allocFreePorts(n) {
|
|
|
52
59
|
servers.push(match[0]);
|
|
53
60
|
ports.push(match[1]);
|
|
54
61
|
}
|
|
55
|
-
servers.
|
|
56
|
-
s.close(() => {});
|
|
57
|
-
});
|
|
62
|
+
await Promise.all(servers.map(closeServer));
|
|
58
63
|
return ports;
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -136,9 +141,24 @@ async function run(roots, backend, fixedPortsOpt, callbacks) {
|
|
|
136
141
|
};
|
|
137
142
|
ChildProcess$ReventlessGwt.onStdoutLine(proc, handle);
|
|
138
143
|
ChildProcess$ReventlessGwt.onStderrLine(proc, handle);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
142
162
|
}
|
|
143
163
|
callbacks.onLog("no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " + roots.join(", "));
|
|
144
164
|
return 1;
|
|
@@ -148,6 +168,7 @@ export {
|
|
|
148
168
|
tapSentinel,
|
|
149
169
|
classifyLine,
|
|
150
170
|
openEphemeral,
|
|
171
|
+
closeServer,
|
|
151
172
|
allocFreePorts,
|
|
152
173
|
fixedPlatformPorts,
|
|
153
174
|
run,
|
package/src/ProcessManager.res
CHANGED
|
@@ -10,6 +10,13 @@ type managed = {
|
|
|
10
10
|
|
|
11
11
|
let running: Dict.t<managed> = Dict.make()
|
|
12
12
|
|
|
13
|
+
// Per-dir count of unexpected watcher exits, so a build-watcher that crashes on
|
|
14
|
+
// startup (bad config, missing binary) is respawned a bounded number of times
|
|
15
|
+
// rather than either freezing the build status forever or spinning in a tight
|
|
16
|
+
// respawn loop.
|
|
17
|
+
let restarts: Dict.t<int> = Dict.make()
|
|
18
|
+
let maxRestarts = 3
|
|
19
|
+
|
|
13
20
|
let isManaging = (dir: string): bool =>
|
|
14
21
|
switch running->Dict.get(dir) {
|
|
15
22
|
| Some(_) => true
|
|
@@ -19,12 +26,33 @@ let isManaging = (dir: string): bool =>
|
|
|
19
26
|
// Spawn `pnpm run start` (the package's `rescript build -w`) in `pkg.dir`,
|
|
20
27
|
// unless one is already managed there. `onStdout` receives each output line so
|
|
21
28
|
// the caller can classify compiler markers (Phase 5).
|
|
22
|
-
let spawnWatcher = (pkg: PackageScan.pkg, ~onStdout: (string, string) => unit): unit =>
|
|
29
|
+
let rec spawnWatcher = (pkg: PackageScan.pkg, ~onStdout: (string, string) => unit): unit =>
|
|
23
30
|
switch running->Dict.get(pkg.dir) {
|
|
24
31
|
| Some(_) => ()
|
|
25
32
|
| None => {
|
|
26
33
|
let proc = ChildProcess.spawn("pnpm", ["run", "start"], ~cwd=pkg.dir)
|
|
27
34
|
ChildProcess.onStdoutLine(proc, line => onStdout(pkg.dir, line))
|
|
35
|
+
// stderr carries compiler/pnpm crash output; without this a watcher that
|
|
36
|
+
// dies on stderr was invisible until the caller's 120s watchdog.
|
|
37
|
+
ChildProcess.onStderrLine(proc, line => onStdout(pkg.dir, line))
|
|
38
|
+
// A spawn failure ("error") never reaches "close"; surface it as output.
|
|
39
|
+
ChildProcess.onError(proc, msg => onStdout(pkg.dir, msg))
|
|
40
|
+
// Detect an unexpected exit. We compare the stored proc identity: an
|
|
41
|
+
// intentional `stop` deletes the entry synchronously before "close" fires,
|
|
42
|
+
// so if we still own this exact proc, the watcher crashed — respawn it up
|
|
43
|
+
// to `maxRestarts` times so build status doesn't freeze.
|
|
44
|
+
ChildProcess.onClose(proc, _code =>
|
|
45
|
+
switch running->Dict.get(pkg.dir) {
|
|
46
|
+
| Some(m) if m.proc === proc =>
|
|
47
|
+
running->Dict.delete(pkg.dir)
|
|
48
|
+
let n = restarts->Dict.get(pkg.dir)->Option.getOr(0)
|
|
49
|
+
if n < maxRestarts {
|
|
50
|
+
restarts->Dict.set(pkg.dir, n + 1)
|
|
51
|
+
spawnWatcher(pkg, ~onStdout)
|
|
52
|
+
}
|
|
53
|
+
| _ => ()
|
|
54
|
+
}
|
|
55
|
+
)
|
|
28
56
|
running->Dict.set(pkg.dir, {dir: pkg.dir, proc})
|
|
29
57
|
}
|
|
30
58
|
}
|
|
@@ -58,20 +86,36 @@ let stop = (dir: string): bool =>
|
|
|
58
86
|
let cleanRebuild = (
|
|
59
87
|
pkg: PackageScan.pkg,
|
|
60
88
|
~onStdout: (string, string) => unit,
|
|
61
|
-
~onDone:
|
|
89
|
+
~onDone: bool => unit,
|
|
62
90
|
): unit => {
|
|
63
91
|
let wasManaged = stop(pkg.dir)
|
|
64
|
-
|
|
92
|
+
// `next` receives the step's exit code so the chain can tell a failed clean or
|
|
93
|
+
// build (non-zero / crash) from success — the previous shape discarded exit
|
|
94
|
+
// codes and always reported the rebuild as OK, masking compile failures.
|
|
95
|
+
let step = (cmd: string, args: array<string>, next: option<int> => unit): unit => {
|
|
65
96
|
let proc = ChildProcess.spawn(cmd, args, ~cwd=pkg.dir)
|
|
97
|
+
// "error" (spawn failure) and "close" can both fire; ensure the chain
|
|
98
|
+
// advances exactly once.
|
|
99
|
+
let settled = ref(false)
|
|
100
|
+
let settle = (code: option<int>) =>
|
|
101
|
+
if !settled.contents {
|
|
102
|
+
settled := true
|
|
103
|
+
next(code)
|
|
104
|
+
}
|
|
66
105
|
ChildProcess.onStdoutLine(proc, line => onStdout(pkg.dir, line))
|
|
67
|
-
ChildProcess.
|
|
106
|
+
ChildProcess.onStderrLine(proc, line => onStdout(pkg.dir, line))
|
|
107
|
+
ChildProcess.onError(proc, msg => {
|
|
108
|
+
onStdout(pkg.dir, msg)
|
|
109
|
+
settle(None)
|
|
110
|
+
})
|
|
111
|
+
ChildProcess.onClose(proc, code => settle(code))
|
|
68
112
|
}
|
|
69
|
-
step("pnpm", ["exec", "rescript", "clean"],
|
|
70
|
-
step("pnpm", ["exec", "rescript", "build"],
|
|
113
|
+
step("pnpm", ["exec", "rescript", "clean"], _cleanCode =>
|
|
114
|
+
step("pnpm", ["exec", "rescript", "build"], buildCode => {
|
|
71
115
|
if wasManaged {
|
|
72
116
|
spawnWatcher(pkg, ~onStdout)
|
|
73
117
|
}
|
|
74
|
-
onDone()
|
|
118
|
+
onDone(buildCode == Some(0))
|
|
75
119
|
})
|
|
76
120
|
)
|
|
77
121
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
4
6
|
import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
|
|
5
7
|
|
|
6
8
|
let running = {};
|
|
7
9
|
|
|
10
|
+
let restarts = {};
|
|
11
|
+
|
|
8
12
|
function isManaging(dir) {
|
|
9
13
|
return running[dir] !== undefined;
|
|
10
14
|
}
|
|
@@ -19,6 +23,23 @@ function spawnWatcher(pkg, onStdout) {
|
|
|
19
23
|
"start"
|
|
20
24
|
], pkg.dir, undefined);
|
|
21
25
|
ChildProcess$ReventlessGwt.onStdoutLine(proc, line => onStdout(pkg.dir, line));
|
|
26
|
+
ChildProcess$ReventlessGwt.onStderrLine(proc, line => onStdout(pkg.dir, line));
|
|
27
|
+
ChildProcess$ReventlessGwt.onError(proc, msg => onStdout(pkg.dir, msg));
|
|
28
|
+
ChildProcess$ReventlessGwt.onClose(proc, _code => {
|
|
29
|
+
let m = running[pkg.dir];
|
|
30
|
+
if (m === undefined) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (m.proc !== proc) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
Stdlib_Dict.$$delete(running, pkg.dir);
|
|
37
|
+
let n = Stdlib_Option.getOr(restarts[pkg.dir], 0);
|
|
38
|
+
if (n < 3) {
|
|
39
|
+
restarts[pkg.dir] = n + 1 | 0;
|
|
40
|
+
return spawnWatcher(pkg, onStdout);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
22
43
|
running[pkg.dir] = {
|
|
23
44
|
dir: pkg.dir,
|
|
24
45
|
proc: proc
|
|
@@ -40,22 +61,36 @@ function cleanRebuild(pkg, onStdout, onDone) {
|
|
|
40
61
|
let wasManaged = stop(pkg.dir);
|
|
41
62
|
let step = (cmd, args, next) => {
|
|
42
63
|
let proc = ChildProcess$ReventlessGwt.spawn(cmd, args, pkg.dir, undefined);
|
|
64
|
+
let settled = {
|
|
65
|
+
contents: false
|
|
66
|
+
};
|
|
67
|
+
let settle = code => {
|
|
68
|
+
if (!settled.contents) {
|
|
69
|
+
settled.contents = true;
|
|
70
|
+
return next(code);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
43
73
|
ChildProcess$ReventlessGwt.onStdoutLine(proc, line => onStdout(pkg.dir, line));
|
|
44
|
-
ChildProcess$ReventlessGwt.
|
|
74
|
+
ChildProcess$ReventlessGwt.onStderrLine(proc, line => onStdout(pkg.dir, line));
|
|
75
|
+
ChildProcess$ReventlessGwt.onError(proc, msg => {
|
|
76
|
+
onStdout(pkg.dir, msg);
|
|
77
|
+
settle(undefined);
|
|
78
|
+
});
|
|
79
|
+
ChildProcess$ReventlessGwt.onClose(proc, settle);
|
|
45
80
|
};
|
|
46
81
|
step("pnpm", [
|
|
47
82
|
"exec",
|
|
48
83
|
"rescript",
|
|
49
84
|
"clean"
|
|
50
|
-
],
|
|
85
|
+
], _cleanCode => step("pnpm", [
|
|
51
86
|
"exec",
|
|
52
87
|
"rescript",
|
|
53
88
|
"build"
|
|
54
|
-
],
|
|
89
|
+
], buildCode => {
|
|
55
90
|
if (wasManaged) {
|
|
56
91
|
spawnWatcher(pkg, onStdout);
|
|
57
92
|
}
|
|
58
|
-
onDone();
|
|
93
|
+
onDone(Primitive_object.equal(buildCode, 0));
|
|
59
94
|
}));
|
|
60
95
|
}
|
|
61
96
|
|
|
@@ -68,8 +103,12 @@ function managedCount() {
|
|
|
68
103
|
return Object.keys(running).length;
|
|
69
104
|
}
|
|
70
105
|
|
|
106
|
+
let maxRestarts = 3;
|
|
107
|
+
|
|
71
108
|
export {
|
|
72
109
|
running,
|
|
110
|
+
restarts,
|
|
111
|
+
maxRestarts,
|
|
73
112
|
isManaging,
|
|
74
113
|
spawnWatcher,
|
|
75
114
|
stop,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Worker entry for a single gwt watch re-run pass.
|
|
2
|
+
//
|
|
3
|
+
// Spawned by `Cli.runWatch` with the run `options` passed as `workerData`. Runs
|
|
4
|
+
// exactly one `runOnce` in this fresh worker (fresh ESM registry → recompiled
|
|
5
|
+
// implementation modules are re-imported, not served stale), emits its NDJSON to
|
|
6
|
+
// stdout (piped to the parent), then exits so the imported graph is reclaimed.
|
|
7
|
+
// The parent never reuses a worker — one per re-run.
|
|
8
|
+
|
|
9
|
+
@val @module("node:worker_threads") external workerData: Cli.options = "workerData"
|
|
10
|
+
@val external processExit: int => unit = "process.exit"
|
|
11
|
+
|
|
12
|
+
let () = {
|
|
13
|
+
let _ =
|
|
14
|
+
Cli.runOnce(workerData)
|
|
15
|
+
->Promise.then(code => {
|
|
16
|
+
processExit(code)
|
|
17
|
+
Promise.resolve()
|
|
18
|
+
})
|
|
19
|
+
->Promise.catch(e => {
|
|
20
|
+
Console.error2("gwt run worker failed:", e)
|
|
21
|
+
processExit(1)
|
|
22
|
+
Promise.resolve()
|
|
23
|
+
})
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
4
|
+
import * as Cli$ReventlessGwt from "./Cli.res.mjs";
|
|
5
|
+
import * as Nodeworker_threads from "node:worker_threads";
|
|
6
|
+
|
|
7
|
+
Stdlib_Promise.$$catch(Cli$ReventlessGwt.runOnce(Nodeworker_threads.workerData).then(code => {
|
|
8
|
+
process.exit(code);
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
}), e => {
|
|
11
|
+
console.error("gwt run worker failed:", e);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/* Not a pure module */
|
package/src/Watch.res
CHANGED
|
@@ -22,21 +22,6 @@ type event = Add | Change | Unlink
|
|
|
22
22
|
@val external setTimeout: (unit => unit, int) => float = "setTimeout"
|
|
23
23
|
@val external clearTimeout: float => unit = "clearTimeout"
|
|
24
24
|
|
|
25
|
-
// Rank events so a coalesced burst reports the *strongest structural* signal
|
|
26
|
-
// with a representative path. A single directory `mv` emits an interleaved
|
|
27
|
-
// burst — `unlink` of the `.res` AND the generated `.res.mjs`, then `add`s at
|
|
28
|
-
// the new path — inside one debounce window. The representative path must be a
|
|
29
|
-
// `.res` *source* unlink (the relocation signal), never a `.res.mjs` or a
|
|
30
|
-
// trailing add, or the consumer misclassifies it as an ordinary change. So an
|
|
31
|
-
// unlink of a `.res` source outranks everything; any other unlink/add outranks
|
|
32
|
-
// a plain edit.
|
|
33
|
-
let rank = (event: event, path: string): int =>
|
|
34
|
-
switch event {
|
|
35
|
-
| Unlink => String.endsWith(path, ".res") ? 2 : 1
|
|
36
|
-
| Add => 1
|
|
37
|
-
| Change => 0
|
|
38
|
-
}
|
|
39
|
-
|
|
40
25
|
// True when an event is the *structural* signal Phase 12 acts on: a source
|
|
41
26
|
// `.res` file leaving a directory (the move/delete half of a relocation).
|
|
42
27
|
// Generated `.res.mjs` outputs end in `.mjs`, so they never match — which is
|
|
@@ -45,17 +30,30 @@ let rank = (event: event, path: string): int =>
|
|
|
45
30
|
let isStructuralSource = (event: event, path: string): bool =>
|
|
46
31
|
event == Unlink && String.endsWith(path, ".res")
|
|
47
32
|
|
|
33
|
+
// Coalesce a burst of chokidar events into a single debounce window, then emit.
|
|
34
|
+
// A window may span *multiple* packages — a branch switch or a multi-package
|
|
35
|
+
// refactor emits `.res` unlinks under several package roots at once. The old
|
|
36
|
+
// shape kept only one "best" path, so it clean-rebuilt just one package and
|
|
37
|
+
// left the others with stale `.res.mjs`. Instead accumulate the *set* of
|
|
38
|
+
// distinct structural source paths and emit one `Unlink` callback per path
|
|
39
|
+
// (the consumer maps each to its owning package). If the window held no
|
|
40
|
+
// structural signal, emit a single representative event so the consumer does a
|
|
41
|
+
// plain re-run.
|
|
48
42
|
let debounce = (wait: int, fn: (event, string) => unit) => {
|
|
49
43
|
let pending: ref<option<float>> = ref(None)
|
|
50
|
-
let
|
|
51
|
-
let
|
|
52
|
-
let
|
|
44
|
+
let structural: ref<array<string>> = ref([])
|
|
45
|
+
let sawOther = ref(false)
|
|
46
|
+
let otherEvent = ref(Change)
|
|
47
|
+
let otherPath = ref("")
|
|
53
48
|
(event, path) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
if isStructuralSource(event, path) {
|
|
50
|
+
if !(structural.contents->Array.includes(path)) {
|
|
51
|
+
structural := Array.concat(structural.contents, [path])
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
sawOther := true
|
|
55
|
+
otherEvent := event
|
|
56
|
+
otherPath := path
|
|
59
57
|
}
|
|
60
58
|
switch pending.contents {
|
|
61
59
|
| Some(t) => clearTimeout(t)
|
|
@@ -65,12 +63,21 @@ let debounce = (wait: int, fn: (event, string) => unit) => {
|
|
|
65
63
|
Some(
|
|
66
64
|
setTimeout(() => {
|
|
67
65
|
pending := None
|
|
68
|
-
let
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
let paths = structural.contents
|
|
67
|
+
let other = sawOther.contents
|
|
68
|
+
let oe = otherEvent.contents
|
|
69
|
+
let op = otherPath.contents
|
|
70
|
+
structural := []
|
|
71
|
+
sawOther := false
|
|
72
|
+
otherEvent := Change
|
|
73
|
+
otherPath := ""
|
|
74
|
+
if paths->Array.length > 0 {
|
|
75
|
+
// One rebuild per distinct owning package; a plain edit alongside
|
|
76
|
+
// is covered by each structural rebuild's own re-run.
|
|
77
|
+
paths->Array.forEach(p => fn(Unlink, p))
|
|
78
|
+
} else if other {
|
|
79
|
+
fn(oe, op)
|
|
80
|
+
}
|
|
74
81
|
}, wait),
|
|
75
82
|
)
|
|
76
83
|
}
|
|
@@ -81,7 +88,16 @@ let start = (roots: array<string>, onChange: (event, string) => unit): watcher =
|
|
|
81
88
|
roots,
|
|
82
89
|
{
|
|
83
90
|
ignoreInitial: true,
|
|
84
|
-
|
|
91
|
+
// Keep in sync with Discovery's pruned dirs — writes under `dist/` or
|
|
92
|
+
// `.history/` are build/editor output, not source, and a `dist/` write
|
|
93
|
+
// could otherwise drive a re-run loop.
|
|
94
|
+
ignored: [
|
|
95
|
+
"**/node_modules/**",
|
|
96
|
+
"**/lib/**",
|
|
97
|
+
"**/.git/**",
|
|
98
|
+
"**/dist/**",
|
|
99
|
+
"**/.history/**",
|
|
100
|
+
],
|
|
85
101
|
},
|
|
86
102
|
)
|
|
87
103
|
let debounced = debounce(120, onChange)
|
package/src/Watch.res.mjs
CHANGED
|
@@ -2,21 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Chokidar from "chokidar";
|
|
4
4
|
|
|
5
|
-
function rank(event, path) {
|
|
6
|
-
switch (event) {
|
|
7
|
-
case "Add" :
|
|
8
|
-
return 1;
|
|
9
|
-
case "Change" :
|
|
10
|
-
return 0;
|
|
11
|
-
case "Unlink" :
|
|
12
|
-
if (path.endsWith(".res")) {
|
|
13
|
-
return 2;
|
|
14
|
-
} else {
|
|
15
|
-
return 1;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
5
|
function isStructuralSource(event, path) {
|
|
21
6
|
if (event === "Unlink") {
|
|
22
7
|
return path.endsWith(".res");
|
|
@@ -29,21 +14,27 @@ function debounce(wait, fn) {
|
|
|
29
14
|
let pending = {
|
|
30
15
|
contents: undefined
|
|
31
16
|
};
|
|
32
|
-
let
|
|
33
|
-
contents:
|
|
17
|
+
let structural = {
|
|
18
|
+
contents: []
|
|
34
19
|
};
|
|
35
|
-
let
|
|
20
|
+
let sawOther = {
|
|
21
|
+
contents: false
|
|
22
|
+
};
|
|
23
|
+
let otherEvent = {
|
|
36
24
|
contents: "Change"
|
|
37
25
|
};
|
|
38
|
-
let
|
|
26
|
+
let otherPath = {
|
|
39
27
|
contents: ""
|
|
40
28
|
};
|
|
41
29
|
return (event, path) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
if (isStructuralSource(event, path)) {
|
|
31
|
+
if (!structural.contents.includes(path)) {
|
|
32
|
+
structural.contents = structural.contents.concat([path]);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
sawOther.contents = true;
|
|
36
|
+
otherEvent.contents = event;
|
|
37
|
+
otherPath.contents = path;
|
|
47
38
|
}
|
|
48
39
|
let t = pending.contents;
|
|
49
40
|
if (t !== undefined) {
|
|
@@ -51,12 +42,22 @@ function debounce(wait, fn) {
|
|
|
51
42
|
}
|
|
52
43
|
pending.contents = setTimeout(() => {
|
|
53
44
|
pending.contents = undefined;
|
|
54
|
-
let
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
let paths = structural.contents;
|
|
46
|
+
let other = sawOther.contents;
|
|
47
|
+
let oe = otherEvent.contents;
|
|
48
|
+
let op = otherPath.contents;
|
|
49
|
+
structural.contents = [];
|
|
50
|
+
sawOther.contents = false;
|
|
51
|
+
otherEvent.contents = "Change";
|
|
52
|
+
otherPath.contents = "";
|
|
53
|
+
if (paths.length !== 0) {
|
|
54
|
+
paths.forEach(p => fn("Unlink", p));
|
|
55
|
+
return;
|
|
56
|
+
} else if (other) {
|
|
57
|
+
return fn(oe, op);
|
|
58
|
+
} else {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
60
61
|
}, wait);
|
|
61
62
|
};
|
|
62
63
|
}
|
|
@@ -67,7 +68,9 @@ function start(roots, onChange) {
|
|
|
67
68
|
ignored: [
|
|
68
69
|
"**/node_modules/**",
|
|
69
70
|
"**/lib/**",
|
|
70
|
-
"**/.git/**"
|
|
71
|
+
"**/.git/**",
|
|
72
|
+
"**/dist/**",
|
|
73
|
+
"**/.history/**"
|
|
71
74
|
]
|
|
72
75
|
});
|
|
73
76
|
let debounced = debounce(120, onChange);
|
|
@@ -75,7 +78,6 @@ function start(roots, onChange) {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
export {
|
|
78
|
-
rank,
|
|
79
81
|
isStructuralSource,
|
|
80
82
|
debounce,
|
|
81
83
|
start,
|
package/src/WatcherProbe.res
CHANGED
|
@@ -19,14 +19,22 @@
|
|
|
19
19
|
@module("node:path") external join: (string, string) => string = "join"
|
|
20
20
|
@val external _processKillSig: (int, int) => unit = "process.kill"
|
|
21
21
|
|
|
22
|
-
// `process.kill(pid, 0)` performs no signal delivery — it only probes existence
|
|
23
|
-
//
|
|
22
|
+
// `process.kill(pid, 0)` performs no signal delivery — it only probes existence.
|
|
23
|
+
// It throws `ESRCH` when the pid is gone (dead) and `EPERM` when the pid exists
|
|
24
|
+
// but is owned by another user (alive, just not ours). A bare catch-all treated
|
|
25
|
+
// EPERM as dead, which would spawn a duplicate watcher over a live foreign one;
|
|
26
|
+
// treat EPERM as alive.
|
|
27
|
+
let _errCode: JsExn.t => option<string> = %raw(`(e) => (e && typeof e.code === "string") ? e.code : undefined`)
|
|
24
28
|
let isAlive = (pid: int): bool =>
|
|
25
29
|
try {
|
|
26
30
|
_processKillSig(pid, 0)
|
|
27
31
|
true
|
|
28
32
|
} catch {
|
|
29
|
-
|
|
|
33
|
+
| exn =>
|
|
34
|
+
switch JsExn.fromException(exn) {
|
|
35
|
+
| Some(e) => _errCode(e) == Some("EPERM")
|
|
36
|
+
| None => false
|
|
37
|
+
}
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
let libLock = (dir: string, name: string): string => join(join(dir, "lib"), name)
|