@reventlessdev/reventless-gwt 1.0.0-alpha.75 → 1.0.0-alpha.77
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 +16 -0
- package/package.json +8 -8
- package/src/Behavior_GWT.res +125 -159
- package/src/Behavior_GWT.res.mjs +156 -59
- package/src/Bind.res.mjs +2 -2
- package/src/BuildClassifier.res +15 -5
- package/src/BuildClassifier.res.mjs +3 -3
- 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 +260 -26
- package/src/Cli.res.mjs +227 -29
- package/src/Collector.res +13 -2
- package/src/Collector.res.mjs +8 -5
- package/src/ComponentMeta.res +16 -43
- package/src/ComponentMeta.res.mjs +12 -41
- package/src/Discovery.res +19 -19
- package/src/Discovery.res.mjs +5 -9
- 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/FormatterHuman.res +14 -43
- package/src/FormatterHuman.res.mjs +21 -28
- package/src/FormatterJson.res +12 -5
- package/src/FormatterJson.res.mjs +8 -6
- package/src/FormatterTap.res +13 -52
- package/src/FormatterTap.res.mjs +25 -43
- package/src/FormatterVsCode.res +4 -19
- package/src/FormatterVsCode.res.mjs +4 -23
- package/src/JestBind.res +1 -1
- package/src/JestBind.res.mjs +2 -2
- package/src/LocalHost.res +11 -27
- package/src/LocalHost.res.mjs +6 -34
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/Outcome.res +0 -64
- package/src/Outcome.res.mjs +0 -47
- package/src/PlatformRunner.res +27 -6
- package/src/PlatformRunner.res.mjs +27 -6
- package/src/PlatformScan.res +14 -10
- package/src/PlatformScan.res.mjs +14 -13
- 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/BuildClassifierTest.res +39 -0
- package/tests/BuildClassifierTest.res.mjs +44 -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 +15 -0
- package/tests/ComponentMetaTest.res.mjs +14 -0
- package/tests/DomainGraphTest.res +2 -2
- package/tests/FlowAggregateGwtTest.res +2 -0
- package/tests/FlowAggregateGwtTest.res.mjs +4 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/MappingGwtTest.res +2 -0
- package/tests/MappingGwtTest.res.mjs +4 -0
- package/tests/WatchDebounceTest.res +35 -7
- package/tests/WatchDebounceTest.res.mjs +37 -6
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)
|
package/src/WatcherProbe.res.mjs
CHANGED
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Nodepath from "node:path";
|
|
5
5
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
6
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
9
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
10
|
+
|
|
11
|
+
let _errCode = ((e) => (e && typeof e.code === "string") ? e.code : undefined);
|
|
6
12
|
|
|
7
13
|
function isAlive(pid) {
|
|
8
14
|
try {
|
|
9
15
|
process.kill(pid, 0);
|
|
10
16
|
return true;
|
|
11
|
-
} catch (
|
|
12
|
-
|
|
17
|
+
} catch (raw_exn) {
|
|
18
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
19
|
+
let e = Stdlib_JsExn.fromException(exn);
|
|
20
|
+
if (e !== undefined) {
|
|
21
|
+
return Primitive_object.equal(_errCode(Primitive_option.valFromOption(e)), "EPERM");
|
|
22
|
+
} else {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
13
25
|
}
|
|
14
26
|
}
|
|
15
27
|
|
|
@@ -38,6 +50,7 @@ function hasLiveWatcher(dir) {
|
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
export {
|
|
53
|
+
_errCode,
|
|
41
54
|
isAlive,
|
|
42
55
|
libLock,
|
|
43
56
|
lockHasLivePid,
|
package/src/Worker.res
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Minimal `node:worker_threads` binding for gwt watch re-runs.
|
|
2
|
+
//
|
|
3
|
+
// Node's ESM module registry is keyed by URL and never evicts, so re-importing a
|
|
4
|
+
// recompiled test file in the same process replays tests that close over the
|
|
5
|
+
// *old* instances of their statically-imported implementation modules (the
|
|
6
|
+
// Loader can only cache-bust the test file's own URL, not its transitive
|
|
7
|
+
// imports). Running each watch re-run in a short-lived Worker gives it a fresh
|
|
8
|
+
// registry — the recompiled modules are re-imported — and reclaims the whole
|
|
9
|
+
// imported graph on worker exit (fixing the monotonic-buster memory leak).
|
|
10
|
+
|
|
11
|
+
type t
|
|
12
|
+
type workerOptions<'a> = {workerData: 'a}
|
|
13
|
+
type url
|
|
14
|
+
|
|
15
|
+
@new @module("node:worker_threads")
|
|
16
|
+
external make: (url, workerOptions<'a>) => t = "Worker"
|
|
17
|
+
|
|
18
|
+
// A worker's stdout is piped to the parent's process.stdout by default, so the
|
|
19
|
+
// NDJSON the formatters write inside the worker reaches the client unchanged.
|
|
20
|
+
@send external on: (t, string, 'cb) => unit = "on"
|
|
21
|
+
@send external terminate: t => promise<int> = "terminate"
|
|
22
|
+
|
|
23
|
+
// The compiled worker entry lives next to this module in the in-source build.
|
|
24
|
+
let runWorkerUrl: url = %raw(`new URL("./RunWorker.res.mjs", import.meta.url)`)
|
|
@@ -80,6 +80,45 @@ describe("BuildClassifier", () => {
|
|
|
80
80
|
expect(fails.contents->Array.length)->toEqual(0)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
+
testPromise("errors emitted before the first Parsed line still settle to onFail (B3)", async () => {
|
|
84
|
+
// A crashed toolchain / pnpm resolution failure prints an error block with no
|
|
85
|
+
// preceding "Parsed …" line. The old `compiling && hasError` guard left this
|
|
86
|
+
// pinned forever; now `hasError` alone settles it.
|
|
87
|
+
let oks = ref(0)
|
|
88
|
+
let fails = ref([])
|
|
89
|
+
let feed = BuildClassifier.make({
|
|
90
|
+
onStart: () => (),
|
|
91
|
+
onOk: _ms => oks := oks.contents + 1,
|
|
92
|
+
onFail: msg => fails := Array.concat(fails.contents, [msg]),
|
|
93
|
+
})
|
|
94
|
+
feed("Syntax error: unexpected token")
|
|
95
|
+
feed(" at Foo.res:3:1")
|
|
96
|
+
await delay(550)
|
|
97
|
+
expect(oks.contents)->toEqual(0)
|
|
98
|
+
expect(fails.contents->Array.length)->toEqual(1)
|
|
99
|
+
expect((fails.contents->Array.getUnsafe(0))->String.includes("Syntax error"))->toEqual(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
testPromise("a multi-error build keeps error blocks past the old 20-line cap (B3)", async () => {
|
|
103
|
+
let fails = ref([])
|
|
104
|
+
let feed = BuildClassifier.make({
|
|
105
|
+
onStart: () => (),
|
|
106
|
+
onOk: _ms => (),
|
|
107
|
+
onFail: msg => fails := Array.concat(fails.contents, [msg]),
|
|
108
|
+
})
|
|
109
|
+
feed("Parsed 1 source files")
|
|
110
|
+
feed("We've found a bug for you!")
|
|
111
|
+
// 30 filler lines push a distinctive later error past the old 20-line window.
|
|
112
|
+
for i in 1 to 30 {
|
|
113
|
+
feed("noise line " ++ Int.toString(i))
|
|
114
|
+
}
|
|
115
|
+
feed("Found 2 errors")
|
|
116
|
+
await delay(550)
|
|
117
|
+
expect(fails.contents->Array.length)->toEqual(1)
|
|
118
|
+
// The late "Found 2 errors" line survives (dropped under the old cap of 20).
|
|
119
|
+
expect((fails.contents->Array.getUnsafe(0))->String.includes("Found 2 errors"))->toEqual(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
83
122
|
testPromise("strips ANSI colour codes before matching the finish line", async () => {
|
|
84
123
|
let oks = ref(0)
|
|
85
124
|
let feed = BuildClassifier.make({
|
|
@@ -109,6 +109,50 @@ globalThis.describe("BuildClassifier", () => {
|
|
|
109
109
|
globalThis.expect(oks.contents).toEqual(1);
|
|
110
110
|
globalThis.expect(fails.contents.length).toEqual(0);
|
|
111
111
|
});
|
|
112
|
+
globalThis.test("errors emitted before the first Parsed line still settle to onFail (B3)", async () => {
|
|
113
|
+
let oks = {
|
|
114
|
+
contents: 0
|
|
115
|
+
};
|
|
116
|
+
let fails = {
|
|
117
|
+
contents: []
|
|
118
|
+
};
|
|
119
|
+
let feed = BuildClassifier$ReventlessGwt.make(undefined, {
|
|
120
|
+
onStart: () => {},
|
|
121
|
+
onOk: _ms => {
|
|
122
|
+
oks.contents = oks.contents + 1 | 0;
|
|
123
|
+
},
|
|
124
|
+
onFail: msg => {
|
|
125
|
+
fails.contents = fails.contents.concat([msg]);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
feed("Syntax error: unexpected token");
|
|
129
|
+
feed(" at Foo.res:3:1");
|
|
130
|
+
await delay(550);
|
|
131
|
+
globalThis.expect(oks.contents).toEqual(0);
|
|
132
|
+
globalThis.expect(fails.contents.length).toEqual(1);
|
|
133
|
+
globalThis.expect(fails.contents[0].includes("Syntax error")).toEqual(true);
|
|
134
|
+
});
|
|
135
|
+
globalThis.test("a multi-error build keeps error blocks past the old 20-line cap (B3)", async () => {
|
|
136
|
+
let fails = {
|
|
137
|
+
contents: []
|
|
138
|
+
};
|
|
139
|
+
let feed = BuildClassifier$ReventlessGwt.make(undefined, {
|
|
140
|
+
onStart: () => {},
|
|
141
|
+
onOk: _ms => {},
|
|
142
|
+
onFail: msg => {
|
|
143
|
+
fails.contents = fails.contents.concat([msg]);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
feed("Parsed 1 source files");
|
|
147
|
+
feed("We've found a bug for you!");
|
|
148
|
+
for (let i = 1; i <= 30; ++i) {
|
|
149
|
+
feed("noise line " + i.toString());
|
|
150
|
+
}
|
|
151
|
+
feed("Found 2 errors");
|
|
152
|
+
await delay(550);
|
|
153
|
+
globalThis.expect(fails.contents.length).toEqual(1);
|
|
154
|
+
globalThis.expect(fails.contents[0].includes("Found 2 errors")).toEqual(true);
|
|
155
|
+
});
|
|
112
156
|
globalThis.test("strips ANSI colour codes before matching the finish line", async () => {
|
|
113
157
|
let oks = {
|
|
114
158
|
contents: 0
|