@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/Behavior_GWT.res +125 -159
  4. package/src/Behavior_GWT.res.mjs +156 -59
  5. package/src/Bind.res.mjs +2 -2
  6. package/src/BuildClassifier.res +15 -5
  7. package/src/BuildClassifier.res.mjs +3 -3
  8. package/src/Cancellation.res +6 -0
  9. package/src/Cancellation.res.mjs +3 -2
  10. package/src/ChildProcess.res +19 -1
  11. package/src/ChildProcess.res.mjs +16 -1
  12. package/src/Cli.res +260 -26
  13. package/src/Cli.res.mjs +227 -29
  14. package/src/Collector.res +13 -2
  15. package/src/Collector.res.mjs +8 -5
  16. package/src/ComponentMeta.res +16 -43
  17. package/src/ComponentMeta.res.mjs +12 -41
  18. package/src/Discovery.res +19 -19
  19. package/src/Discovery.res.mjs +5 -9
  20. package/src/DomainGraph.res +0 -0
  21. package/src/DomainGraph.res.mjs +0 -0
  22. package/src/Filter.res +11 -2
  23. package/src/Filter.res.mjs +10 -3
  24. package/src/FormatterHuman.res +14 -43
  25. package/src/FormatterHuman.res.mjs +21 -28
  26. package/src/FormatterJson.res +12 -5
  27. package/src/FormatterJson.res.mjs +8 -6
  28. package/src/FormatterTap.res +13 -52
  29. package/src/FormatterTap.res.mjs +25 -43
  30. package/src/FormatterVsCode.res +4 -19
  31. package/src/FormatterVsCode.res.mjs +4 -23
  32. package/src/JestBind.res +1 -1
  33. package/src/JestBind.res.mjs +2 -2
  34. package/src/LocalHost.res +11 -27
  35. package/src/LocalHost.res.mjs +6 -34
  36. package/src/MismatchRender.res +72 -0
  37. package/src/MismatchRender.res.mjs +127 -0
  38. package/src/Outcome.res +0 -64
  39. package/src/Outcome.res.mjs +0 -47
  40. package/src/PlatformRunner.res +27 -6
  41. package/src/PlatformRunner.res.mjs +27 -6
  42. package/src/PlatformScan.res +14 -10
  43. package/src/PlatformScan.res.mjs +14 -13
  44. package/src/ProcessManager.res +51 -7
  45. package/src/ProcessManager.res.mjs +43 -4
  46. package/src/RunWorker.res +24 -0
  47. package/src/RunWorker.res.mjs +16 -0
  48. package/src/Watch.res +46 -30
  49. package/src/Watch.res.mjs +34 -32
  50. package/src/WatcherProbe.res +11 -3
  51. package/src/WatcherProbe.res.mjs +15 -2
  52. package/src/Worker.res +24 -0
  53. package/src/Worker.res.mjs +9 -0
  54. package/tests/BuildClassifierTest.res +39 -0
  55. package/tests/BuildClassifierTest.res.mjs +44 -0
  56. package/tests/CliRunEntryTest.res +57 -0
  57. package/tests/CliRunEntryTest.res.mjs +65 -0
  58. package/tests/CliWatchScopeTest.res +62 -0
  59. package/tests/CliWatchScopeTest.res.mjs +95 -0
  60. package/tests/ComponentMetaTest.res +15 -0
  61. package/tests/ComponentMetaTest.res.mjs +14 -0
  62. package/tests/DomainGraphTest.res +2 -2
  63. package/tests/FlowAggregateGwtTest.res +2 -0
  64. package/tests/FlowAggregateGwtTest.res.mjs +4 -0
  65. package/tests/FormatterGoldenTest.res +125 -0
  66. package/tests/FormatterGoldenTest.res.mjs +162 -0
  67. package/tests/MappingGwtTest.res +2 -0
  68. package/tests/MappingGwtTest.res.mjs +4 -0
  69. package/tests/WatchDebounceTest.res +35 -7
  70. package/tests/WatchDebounceTest.res.mjs +37 -6
@@ -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: unit => unit,
89
+ ~onDone: bool => unit,
62
90
  ): unit => {
63
91
  let wasManaged = stop(pkg.dir)
64
- let step = (cmd: string, args: array<string>, next: unit => unit): unit => {
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.onClose(proc, _ => next())
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.onClose(proc, param => next());
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
- ], () => step("pnpm", [
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 bestRank = ref(-1)
51
- let bestEvent = ref(Change)
52
- let bestPath = ref("")
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
- let r = rank(event, path)
55
- if r >= bestRank.contents {
56
- bestRank := r
57
- bestEvent := event
58
- bestPath := path
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 e = bestEvent.contents
69
- let p = bestPath.contents
70
- bestRank := -1
71
- bestEvent := Change
72
- bestPath := ""
73
- fn(e, p)
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
- ignored: ["**/node_modules/**", "**/lib/**", "**/.git/**"],
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 bestRank = {
33
- contents: -1
17
+ let structural = {
18
+ contents: []
34
19
  };
35
- let bestEvent = {
20
+ let sawOther = {
21
+ contents: false
22
+ };
23
+ let otherEvent = {
36
24
  contents: "Change"
37
25
  };
38
- let bestPath = {
26
+ let otherPath = {
39
27
  contents: ""
40
28
  };
41
29
  return (event, path) => {
42
- let r = rank(event, path);
43
- if (r >= bestRank.contents) {
44
- bestRank.contents = r;
45
- bestEvent.contents = event;
46
- bestPath.contents = path;
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 e = bestEvent.contents;
55
- let p = bestPath.contents;
56
- bestRank.contents = -1;
57
- bestEvent.contents = "Change";
58
- bestPath.contents = "";
59
- fn(e, p);
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,
@@ -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
- // (throws ESRCH when the pid is gone), which is exactly the liveness check.
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
- | _ => false
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)
@@ -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 (exn) {
12
- return false;
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)`)
@@ -0,0 +1,9 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let runWorkerUrl = (new URL("./RunWorker.res.mjs", import.meta.url));
5
+
6
+ export {
7
+ runWorkerUrl,
8
+ }
9
+ /* runWorkerUrl Not a pure module */
@@ -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