@reventlessdev/reventless-gwt 1.0.0-alpha.75 → 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 +8 -0
- package/package.json +6 -6
- 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/Filter.res +11 -2
- package/src/Filter.res.mjs +10 -3
- 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/WatchDebounceTest.res +35 -7
- package/tests/WatchDebounceTest.res.mjs +37 -6
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)`)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Regression tests for the CLI runner's per-test deadline (A2) and the
|
|
2
|
+
// Collector's skip-depth reset (A4). Before A2 a hung test body wedged the whole
|
|
3
|
+
// run — `runEntry` awaited it with no deadline. Before A4 a throwing `xdescribe`
|
|
4
|
+
// left `skipDepth` > 0, silently skipping every subsequently loaded file; the
|
|
5
|
+
// reset lives in `Collector.activate`.
|
|
6
|
+
|
|
7
|
+
open JestGlobals
|
|
8
|
+
|
|
9
|
+
@val external setTimeout: (unit => unit, int) => unit = "setTimeout"
|
|
10
|
+
|
|
11
|
+
let hangingEntry = (~timeout): Collector.entry => {
|
|
12
|
+
id: "hangs",
|
|
13
|
+
name: "hangs forever",
|
|
14
|
+
describePath: [],
|
|
15
|
+
slice: None,
|
|
16
|
+
// A body that never resolves — the pathological hung test.
|
|
17
|
+
body: () => Promise.make((_resolve, _reject) => ()),
|
|
18
|
+
status: Collector.Runnable,
|
|
19
|
+
location: None,
|
|
20
|
+
timeout,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Cli.runEntry deadline", () => {
|
|
24
|
+
testPromise("a hung body is reported as a timeout, not a wedge", async () => {
|
|
25
|
+
let r = await Cli.runEntry(hangingEntry(~timeout=Some(50)))
|
|
26
|
+
expect(r.status)->toEqual(RunnerTypes.Fail)
|
|
27
|
+
switch r.mismatch {
|
|
28
|
+
| Some(Outcome.Throw({error})) => expect(error->String.includes("timed out"))->toEqual(true)
|
|
29
|
+
| _ => JsError.throwWithMessage("expected a timeout Throw mismatch")
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
testPromise("a body that resolves within the deadline passes", async () => {
|
|
34
|
+
let entry: Collector.entry = {
|
|
35
|
+
id: "ok",
|
|
36
|
+
name: "resolves fast",
|
|
37
|
+
describePath: [],
|
|
38
|
+
slice: None,
|
|
39
|
+
body: () => Promise.resolve(Outcome.pass),
|
|
40
|
+
status: Collector.Runnable,
|
|
41
|
+
location: None,
|
|
42
|
+
timeout: Some(1000),
|
|
43
|
+
}
|
|
44
|
+
let r = await Cli.runEntry(entry)
|
|
45
|
+
expect(r.status)->toEqual(RunnerTypes.Pass)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("Collector.activate", () => {
|
|
50
|
+
testSync("resets a leaked skipDepth to zero", () => {
|
|
51
|
+
// Simulate the leak a throwing xdescribe body would leave behind.
|
|
52
|
+
Collector.skipDepth := 3
|
|
53
|
+
Collector.activate()
|
|
54
|
+
expect(Collector.skipDepth.contents)->toEqual(0)
|
|
55
|
+
Collector.deactivate()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
4
|
+
import * as Cli$ReventlessGwt from "../src/Cli.res.mjs";
|
|
5
|
+
import * as Outcome$ReventlessGwt from "../src/Outcome.res.mjs";
|
|
6
|
+
import * as Collector$ReventlessGwt from "../src/Collector.res.mjs";
|
|
7
|
+
|
|
8
|
+
function hangingEntry(timeout) {
|
|
9
|
+
return {
|
|
10
|
+
id: "hangs",
|
|
11
|
+
name: "hangs forever",
|
|
12
|
+
describePath: [],
|
|
13
|
+
slice: undefined,
|
|
14
|
+
body: () => new Promise((_resolve, _reject) => {}),
|
|
15
|
+
status: "Runnable",
|
|
16
|
+
location: undefined,
|
|
17
|
+
timeout: timeout
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
globalThis.describe("Cli.runEntry deadline", () => {
|
|
22
|
+
globalThis.test("a hung body is reported as a timeout, not a wedge", async () => {
|
|
23
|
+
let r = await Cli$ReventlessGwt.runEntry(hangingEntry(50));
|
|
24
|
+
globalThis.expect(r.status).toEqual("Fail");
|
|
25
|
+
let match = r.mismatch;
|
|
26
|
+
if (match === undefined) {
|
|
27
|
+
return Stdlib_JsError.throwWithMessage("expected a timeout Throw mismatch");
|
|
28
|
+
}
|
|
29
|
+
if (match.TAG !== "Throw") {
|
|
30
|
+
return Stdlib_JsError.throwWithMessage("expected a timeout Throw mismatch");
|
|
31
|
+
}
|
|
32
|
+
globalThis.expect(match.error.includes("timed out")).toEqual(true);
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("a body that resolves within the deadline passes", async () => {
|
|
35
|
+
let entry_describePath = [];
|
|
36
|
+
let entry_body = () => Promise.resolve(Outcome$ReventlessGwt.pass);
|
|
37
|
+
let entry_timeout = 1000;
|
|
38
|
+
let entry = {
|
|
39
|
+
id: "ok",
|
|
40
|
+
name: "resolves fast",
|
|
41
|
+
describePath: entry_describePath,
|
|
42
|
+
slice: undefined,
|
|
43
|
+
body: entry_body,
|
|
44
|
+
status: "Runnable",
|
|
45
|
+
location: undefined,
|
|
46
|
+
timeout: entry_timeout
|
|
47
|
+
};
|
|
48
|
+
let r = await Cli$ReventlessGwt.runEntry(entry);
|
|
49
|
+
globalThis.expect(r.status).toEqual("Pass");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
globalThis.describe("Collector.activate", () => {
|
|
54
|
+
globalThis.test("resets a leaked skipDepth to zero", () => {
|
|
55
|
+
Collector$ReventlessGwt.skipDepth.contents = 3;
|
|
56
|
+
Collector$ReventlessGwt.activate();
|
|
57
|
+
globalThis.expect(Collector$ReventlessGwt.skipDepth.contents).toEqual(0);
|
|
58
|
+
Collector$ReventlessGwt.deactivate();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
hangingEntry,
|
|
64
|
+
}
|
|
65
|
+
/* Not a pure module */
|
|
@@ -49,10 +49,11 @@ describe("Watch.debounce coalescing", () => {
|
|
|
49
49
|
expect(calls.contents)->toEqual([Watch.Unlink])
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
testPromise("interleaved .res.mjs unlinks
|
|
52
|
+
testPromise("interleaved .res.mjs unlinks never surface; each distinct .res source does", async () => {
|
|
53
53
|
// The real directory-mv burst chokidar emits: source AND generated files
|
|
54
|
-
// unlink interleaved.
|
|
55
|
-
//
|
|
54
|
+
// unlink interleaved. Only `.res` sources are emitted (never a `.res.mjs`),
|
|
55
|
+
// and every *distinct* source is emitted — two moved modules must both drive
|
|
56
|
+
// a rebuild, not collapse to one representative.
|
|
56
57
|
let calls = ref([])
|
|
57
58
|
let fire = Watch.debounce(40, (e, p) => calls := Array.concat(calls.contents, [(e, p)]))
|
|
58
59
|
fire(Watch.Unlink, "/p/src/old/A.res")
|
|
@@ -62,10 +63,37 @@ describe("Watch.debounce coalescing", () => {
|
|
|
62
63
|
fire(Watch.Add, "/p/src/new/A.res")
|
|
63
64
|
fire(Watch.Add, "/p/src/new/A.res.mjs")
|
|
64
65
|
await delay(120)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(
|
|
68
|
-
expect(
|
|
66
|
+
let paths = calls.contents->Array.map(((_, p)) => p)
|
|
67
|
+
expect(calls.contents->Array.every(((e, p)) => Watch.isStructuralSource(e, p)))->toEqual(true)
|
|
68
|
+
expect(paths->Array.includes("/p/src/old/A.res"))->toEqual(true)
|
|
69
|
+
expect(paths->Array.includes("/p/src/old/A_Behavior.res"))->toEqual(true)
|
|
70
|
+
expect(paths->Array.some(p => p->String.endsWith(".mjs")))->toEqual(false)
|
|
71
|
+
expect(calls.contents->Array.length)->toEqual(2)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
testPromise("a burst spanning two packages emits an unlink per package (A4.1)", async () => {
|
|
75
|
+
// Regression: the old single-`bestPath` debounce clean-rebuilt only one
|
|
76
|
+
// package on a multi-package burst, stranding the other with stale .res.mjs.
|
|
77
|
+
let calls = ref([])
|
|
78
|
+
let fire = Watch.debounce(40, (e, p) => calls := Array.concat(calls.contents, [(e, p)]))
|
|
79
|
+
fire(Watch.Unlink, "/pkgA/src/Foo.res")
|
|
80
|
+
fire(Watch.Unlink, "/pkgB/src/Bar.res")
|
|
81
|
+
await delay(120)
|
|
82
|
+
let paths = calls.contents->Array.map(((_, p)) => p)
|
|
83
|
+
expect(paths->Array.includes("/pkgA/src/Foo.res"))->toEqual(true)
|
|
84
|
+
expect(paths->Array.includes("/pkgB/src/Bar.res"))->toEqual(true)
|
|
85
|
+
expect(calls.contents->Array.length)->toEqual(2)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
testPromise("a structural signal suppresses a co-occurring plain change", async () => {
|
|
89
|
+
// A structural rebuild does its own re-run, so a plain edit in the same
|
|
90
|
+
// window must not also emit a separate Change (which would double-run).
|
|
91
|
+
let calls = ref([])
|
|
92
|
+
let fire = Watch.debounce(40, (e, _p) => calls := Array.concat(calls.contents, [e]))
|
|
93
|
+
fire(Watch.Change, "/p/src/Edited.res")
|
|
94
|
+
fire(Watch.Unlink, "/p/src/old/Moved.res")
|
|
95
|
+
await delay(120)
|
|
96
|
+
expect(calls.contents)->toEqual([Watch.Unlink])
|
|
69
97
|
})
|
|
70
98
|
|
|
71
99
|
testPromise("a lone change stays a change (incremental re-run)", async () => {
|
|
@@ -55,7 +55,7 @@ globalThis.describe("Watch.debounce coalescing", () => {
|
|
|
55
55
|
await delay(120);
|
|
56
56
|
globalThis.expect(calls.contents).toEqual(["Unlink"]);
|
|
57
57
|
});
|
|
58
|
-
globalThis.test("interleaved .res.mjs unlinks
|
|
58
|
+
globalThis.test("interleaved .res.mjs unlinks never surface; each distinct .res source does", async () => {
|
|
59
59
|
let calls = {
|
|
60
60
|
contents: []
|
|
61
61
|
};
|
|
@@ -72,11 +72,42 @@ globalThis.describe("Watch.debounce coalescing", () => {
|
|
|
72
72
|
fire("Add", "/p/src/new/A.res");
|
|
73
73
|
fire("Add", "/p/src/new/A.res.mjs");
|
|
74
74
|
await delay(120);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
globalThis.expect(
|
|
79
|
-
globalThis.expect(
|
|
75
|
+
let paths = calls.contents.map(param => param[1]);
|
|
76
|
+
globalThis.expect(calls.contents.every(param => Watch$ReventlessGwt.isStructuralSource(param[0], param[1]))).toEqual(true);
|
|
77
|
+
globalThis.expect(paths.includes("/p/src/old/A.res")).toEqual(true);
|
|
78
|
+
globalThis.expect(paths.includes("/p/src/old/A_Behavior.res")).toEqual(true);
|
|
79
|
+
globalThis.expect(paths.some(p => p.endsWith(".mjs"))).toEqual(false);
|
|
80
|
+
globalThis.expect(calls.contents.length).toEqual(2);
|
|
81
|
+
});
|
|
82
|
+
globalThis.test("a burst spanning two packages emits an unlink per package (A4.1)", async () => {
|
|
83
|
+
let calls = {
|
|
84
|
+
contents: []
|
|
85
|
+
};
|
|
86
|
+
let fire = Watch$ReventlessGwt.debounce(40, (e, p) => {
|
|
87
|
+
calls.contents = calls.contents.concat([[
|
|
88
|
+
e,
|
|
89
|
+
p
|
|
90
|
+
]]);
|
|
91
|
+
});
|
|
92
|
+
fire("Unlink", "/pkgA/src/Foo.res");
|
|
93
|
+
fire("Unlink", "/pkgB/src/Bar.res");
|
|
94
|
+
await delay(120);
|
|
95
|
+
let paths = calls.contents.map(param => param[1]);
|
|
96
|
+
globalThis.expect(paths.includes("/pkgA/src/Foo.res")).toEqual(true);
|
|
97
|
+
globalThis.expect(paths.includes("/pkgB/src/Bar.res")).toEqual(true);
|
|
98
|
+
globalThis.expect(calls.contents.length).toEqual(2);
|
|
99
|
+
});
|
|
100
|
+
globalThis.test("a structural signal suppresses a co-occurring plain change", async () => {
|
|
101
|
+
let calls = {
|
|
102
|
+
contents: []
|
|
103
|
+
};
|
|
104
|
+
let fire = Watch$ReventlessGwt.debounce(40, (e, _p) => {
|
|
105
|
+
calls.contents = calls.contents.concat([e]);
|
|
106
|
+
});
|
|
107
|
+
fire("Change", "/p/src/Edited.res");
|
|
108
|
+
fire("Unlink", "/p/src/old/Moved.res");
|
|
109
|
+
await delay(120);
|
|
110
|
+
globalThis.expect(calls.contents).toEqual(["Unlink"]);
|
|
80
111
|
});
|
|
81
112
|
globalThis.test("a lone change stays a change (incremental re-run)", async () => {
|
|
82
113
|
let calls = {
|