@reventlessdev/reventless-local 3.0.0-alpha.244 → 3.0.0-alpha.246
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 +22 -0
- package/package.json +11 -11
- package/src/LocalPlatformRegistry.res +54 -63
- package/src/LocalPlatformRegistry.res.mjs +24 -5
- package/src/LocalPlatformStart.res +84 -0
- package/src/LocalPlatformStart.res.mjs +99 -0
- package/src/Platform.res +33 -0
- package/src/Platform.res.mjs +18 -4
- package/src/ShellConfig.res +20 -2
- package/src/ShellConfig.res.mjs +9 -4
- package/src/UiSlots.res +191 -0
- package/src/UiSlots.res.mjs +91 -0
- package/src/adapter/Api/LocalEvents_Server.res +18 -0
- package/src/adapter/Api/LocalEvents_Server.res.mjs +11 -0
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +50 -3
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res.mjs +62 -2
- package/src/adapter/LocalBus.res +30 -27
- package/src/adapter/LocalBus.res.mjs +14 -18
- package/src/adapter/LocalEventTap.res +139 -0
- package/src/adapter/LocalEventTap.res.mjs +177 -0
- package/tests/EventTapSocketFixtures.mjs +17 -0
- package/tests/LocalEventTapTest.res +154 -0
- package/tests/LocalEventTapTest.res.mjs +140 -0
- package/tests/LocalPlatformRegistryTest.res +61 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +41 -1
- package/tests/LocalPlatformStartTest.res +176 -0
- package/tests/LocalPlatformStartTest.res.mjs +210 -0
- package/tests/ShellConfigTest.res +41 -0
- package/tests/ShellConfigTest.res.mjs +40 -17
- package/tests/UiSlotsTest.res +197 -0
- package/tests/UiSlotsTest.res.mjs +196 -0
- package/tests/adapter/EventHistoryResolverTest.res +46 -0
- package/tests/adapter/EventHistoryResolverTest.res.mjs +41 -0
- package/tests/adapter/EventTapTest.res +16 -6
- package/tests/adapter/EventTapTest.res.mjs +5 -1
- package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -1
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +2 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +3 -3
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +1 -1
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +1 -1
- package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +1 -1
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +1 -1
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +1 -1
- package/tests/components/dcb/DcbFixtures.res.mjs +1 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +2 -2
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +1 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +1 -1
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodeos from "node:os";
|
|
5
|
+
import * as Nodepath from "node:path";
|
|
6
|
+
import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
|
|
7
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
8
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as LocalPlatformStart$ReventlessLocal from "../src/LocalPlatformStart.res.mjs";
|
|
12
|
+
import * as LocalPlatformRegistry$ReventlessLocal from "../src/LocalPlatformRegistry.res.mjs";
|
|
13
|
+
|
|
14
|
+
function tempRoot() {
|
|
15
|
+
return Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-start-"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sqliteStore(path) {
|
|
19
|
+
return {
|
|
20
|
+
kind: "sqlite",
|
|
21
|
+
path: path
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let memoryStore = {
|
|
26
|
+
kind: "memory",
|
|
27
|
+
path: undefined
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function writeAt(cwd, port, store, pidOpt) {
|
|
31
|
+
let pid = pidOpt !== undefined ? pidOpt : process.pid;
|
|
32
|
+
return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function withoutBypass(f) {
|
|
36
|
+
let previous = process.env[LocalPlatformStart$ReventlessLocal.bypassEnv];
|
|
37
|
+
Stdlib_Dict.$$delete(process.env, LocalPlatformStart$ReventlessLocal.bypassEnv);
|
|
38
|
+
let restore = () => {
|
|
39
|
+
if (previous !== undefined) {
|
|
40
|
+
process.env[LocalPlatformStart$ReventlessLocal.bypassEnv] = previous;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
f();
|
|
46
|
+
return restore();
|
|
47
|
+
} catch (e) {
|
|
48
|
+
restore();
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
globalThis.describe("LocalPlatformStart.guardReset", () => {
|
|
54
|
+
globalThis.test("refuses a reset of a store a live platform opened, naming it", () => {
|
|
55
|
+
let cwd = tempRoot();
|
|
56
|
+
let store = Nodepath.join(cwd, ".reventless", "local.db");
|
|
57
|
+
writeAt(cwd, 4000, {
|
|
58
|
+
kind: "sqlite",
|
|
59
|
+
path: store
|
|
60
|
+
}, undefined);
|
|
61
|
+
let val;
|
|
62
|
+
try {
|
|
63
|
+
val = LocalPlatformStart$ReventlessLocal.guardReset(store, cwd, undefined);
|
|
64
|
+
} catch (raw_e) {
|
|
65
|
+
let e = Primitive_exceptions.internalToException(raw_e);
|
|
66
|
+
if (e.RE_EXN_ID === "JsExn") {
|
|
67
|
+
let message = Stdlib_Option.getOr(Stdlib_JsExn.message(e._1), "");
|
|
68
|
+
globalThis.expect(message.includes(":4000")).toEqual(true);
|
|
69
|
+
globalThis.expect(message.includes(process.pid.toString())).toEqual(true);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
JestGlobals.fail("expected the reset to be refused");
|
|
75
|
+
});
|
|
76
|
+
globalThis.test("matches a relative configured path against the absolute registered one", () => {
|
|
77
|
+
let cwd = tempRoot();
|
|
78
|
+
let absolute = Nodepath.join(process.cwd(), ".reventless", "guard-probe.db");
|
|
79
|
+
writeAt(cwd, 4000, {
|
|
80
|
+
kind: "sqlite",
|
|
81
|
+
path: absolute
|
|
82
|
+
}, undefined);
|
|
83
|
+
globalThis.expect(Stdlib_Option.isSome(LocalPlatformStart$ReventlessLocal.servedBy("./.reventless/guard-probe.db", cwd, undefined))).toEqual(true);
|
|
84
|
+
});
|
|
85
|
+
globalThis.test("lets a reset through when nothing is serving that file", () => {
|
|
86
|
+
let cwd = tempRoot();
|
|
87
|
+
let path = Nodepath.join(cwd, "other.db");
|
|
88
|
+
writeAt(cwd, 4000, {
|
|
89
|
+
kind: "sqlite",
|
|
90
|
+
path: path
|
|
91
|
+
}, undefined);
|
|
92
|
+
globalThis.expect(LocalPlatformStart$ReventlessLocal.servedBy(Nodepath.join(cwd, "local.db"), cwd, undefined)).toEqual(undefined);
|
|
93
|
+
LocalPlatformStart$ReventlessLocal.guardReset(Nodepath.join(cwd, "local.db"), cwd, undefined);
|
|
94
|
+
});
|
|
95
|
+
globalThis.test("lets a reset through when the platform holding the store is gone", () => {
|
|
96
|
+
let cwd = tempRoot();
|
|
97
|
+
let store = Nodepath.join(cwd, "local.db");
|
|
98
|
+
writeAt(cwd, 4000, {
|
|
99
|
+
kind: "sqlite",
|
|
100
|
+
path: store
|
|
101
|
+
}, 2147483646);
|
|
102
|
+
LocalPlatformStart$ReventlessLocal.guardReset(store, cwd, undefined);
|
|
103
|
+
});
|
|
104
|
+
globalThis.test("ignores a platform that keeps its store in memory", () => {
|
|
105
|
+
let cwd = tempRoot();
|
|
106
|
+
writeAt(cwd, 4010, memoryStore, undefined);
|
|
107
|
+
LocalPlatformStart$ReventlessLocal.guardReset(Nodepath.join(cwd, "local.db"), cwd, undefined);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
globalThis.describe("LocalPlatformStart.orAddressRunning", () => {
|
|
112
|
+
globalThis.test("refuses a reset before it addresses the platform holding the store", () => {
|
|
113
|
+
let cwd = tempRoot();
|
|
114
|
+
let relative = "./.reventless/order-probe.db";
|
|
115
|
+
let path = Nodepath.resolve(relative);
|
|
116
|
+
writeAt(cwd, 4000, {
|
|
117
|
+
kind: "sqlite",
|
|
118
|
+
path: path
|
|
119
|
+
}, undefined);
|
|
120
|
+
let previousBackend = process.env["REVENTLESS_LOCAL_BACKEND"];
|
|
121
|
+
process.env["REVENTLESS_LOCAL_BACKEND"] = `sqlite:` + relative + `?reset`;
|
|
122
|
+
let outcome;
|
|
123
|
+
try {
|
|
124
|
+
LocalPlatformStart$ReventlessLocal.orAddressRunning(cwd, undefined);
|
|
125
|
+
outcome = "returned or exited";
|
|
126
|
+
} catch (raw_e) {
|
|
127
|
+
let e = Primitive_exceptions.internalToException(raw_e);
|
|
128
|
+
if (e.RE_EXN_ID === "JsExn") {
|
|
129
|
+
outcome = Stdlib_Option.getOr(Stdlib_JsExn.message(e._1), "");
|
|
130
|
+
} else {
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (previousBackend !== undefined) {
|
|
135
|
+
process.env["REVENTLESS_LOCAL_BACKEND"] = previousBackend;
|
|
136
|
+
} else {
|
|
137
|
+
Stdlib_Dict.$$delete(process.env, "REVENTLESS_LOCAL_BACKEND");
|
|
138
|
+
}
|
|
139
|
+
globalThis.expect(outcome.includes("refusing to reset")).toEqual(true);
|
|
140
|
+
globalThis.expect(outcome.includes(":4000")).toEqual(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
globalThis.describe("LocalPlatformStart.decide", () => {
|
|
145
|
+
globalThis.test("starts where nothing is running", () => withoutBypass(() => {
|
|
146
|
+
globalThis.expect(LocalPlatformStart$ReventlessLocal.decide(tempRoot(), undefined)).toEqual("Start");
|
|
147
|
+
}));
|
|
148
|
+
globalThis.test("addresses the one platform already serving this directory", () => withoutBypass(() => {
|
|
149
|
+
let cwd = tempRoot();
|
|
150
|
+
let path = Nodepath.join(cwd, "local.db");
|
|
151
|
+
writeAt(cwd, 4000, {
|
|
152
|
+
kind: "sqlite",
|
|
153
|
+
path: path
|
|
154
|
+
}, undefined);
|
|
155
|
+
let entries = LocalPlatformStart$ReventlessLocal.decide(cwd, undefined);
|
|
156
|
+
if (typeof entries !== "object") {
|
|
157
|
+
return JestGlobals.fail("expected the running platform to be addressed");
|
|
158
|
+
}
|
|
159
|
+
let entries$1 = entries._0;
|
|
160
|
+
globalThis.expect(entries$1.map(e => e.port)).toEqual([4000]);
|
|
161
|
+
let lines = LocalPlatformStart$ReventlessLocal.report(entries$1);
|
|
162
|
+
globalThis.expect(lines.length).toEqual(2);
|
|
163
|
+
globalThis.expect(lines.join("\n").includes(":4000")).toEqual(true);
|
|
164
|
+
}));
|
|
165
|
+
globalThis.test("names all of them when two are running", () => withoutBypass(() => {
|
|
166
|
+
let cwd = tempRoot();
|
|
167
|
+
let path = Nodepath.join(cwd, "local.db");
|
|
168
|
+
writeAt(cwd, 4000, {
|
|
169
|
+
kind: "sqlite",
|
|
170
|
+
path: path
|
|
171
|
+
}, undefined);
|
|
172
|
+
writeAt(cwd, 4010, memoryStore, undefined);
|
|
173
|
+
let entries = LocalPlatformStart$ReventlessLocal.decide(cwd, undefined);
|
|
174
|
+
if (typeof entries !== "object") {
|
|
175
|
+
return JestGlobals.fail("expected both running platforms to be reported");
|
|
176
|
+
}
|
|
177
|
+
let printed = LocalPlatformStart$ReventlessLocal.report(entries._0).join("\n");
|
|
178
|
+
globalThis.expect(printed.includes(":4000")).toEqual(true);
|
|
179
|
+
globalThis.expect(printed.includes(":4010")).toEqual(true);
|
|
180
|
+
}));
|
|
181
|
+
globalThis.test("starts anyway when REVENTLESS_DOMAIN_PORT names a port", () => {
|
|
182
|
+
let cwd = tempRoot();
|
|
183
|
+
let path = Nodepath.join(cwd, "local.db");
|
|
184
|
+
writeAt(cwd, 4000, {
|
|
185
|
+
kind: "sqlite",
|
|
186
|
+
path: path
|
|
187
|
+
}, undefined);
|
|
188
|
+
let previous = process.env[LocalPlatformStart$ReventlessLocal.bypassEnv];
|
|
189
|
+
process.env[LocalPlatformStart$ReventlessLocal.bypassEnv] = "4010";
|
|
190
|
+
let decision = LocalPlatformStart$ReventlessLocal.decide(cwd, undefined);
|
|
191
|
+
if (previous !== undefined) {
|
|
192
|
+
process.env[LocalPlatformStart$ReventlessLocal.bypassEnv] = previous;
|
|
193
|
+
} else {
|
|
194
|
+
Stdlib_Dict.$$delete(process.env, LocalPlatformStart$ReventlessLocal.bypassEnv);
|
|
195
|
+
}
|
|
196
|
+
globalThis.expect(decision).toEqual("Start");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let deadPid = 2147483646;
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
tempRoot,
|
|
204
|
+
deadPid,
|
|
205
|
+
sqliteStore,
|
|
206
|
+
memoryStore,
|
|
207
|
+
writeAt,
|
|
208
|
+
withoutBypass,
|
|
209
|
+
}
|
|
210
|
+
/* Not a pure module */
|
|
@@ -77,6 +77,47 @@ describe("ShellConfig.overlay", () => {
|
|
|
77
77
|
)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
// The key that makes the served module reachable. `UiSlots` writes the file and
|
|
81
|
+
// this writes its name, and neither alone is the feature: a file nothing imports
|
|
82
|
+
// and a key pointing at a 404 both leave every mode drawing its own regions,
|
|
83
|
+
// which is what an undeclared deployment already does. Nothing about the running
|
|
84
|
+
// app distinguishes the three.
|
|
85
|
+
describe("ShellConfig.overlay — uiSlotsFile", () => {
|
|
86
|
+
testSync("unset ⇒ no uiSlotsUrl", () => {
|
|
87
|
+
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
88
|
+
expect(out->Dict.get("uiSlotsUrl")->Option.isNone)->toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
testSync("declared ⇒ the URL the platform serves the module from", () => {
|
|
92
|
+
let out = ShellConfig.overlay(
|
|
93
|
+
~bakedManifest=None,
|
|
94
|
+
~uiSlotsFile="/src/storefront-slots.js",
|
|
95
|
+
~shellConfig=None,
|
|
96
|
+
)
|
|
97
|
+
expect(out->Dict.get("uiSlotsUrl"))->toEqual(Some(JSON.Encode.string("/ui-slots.js")))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Withdrawal has to clear the key as well as the file. The baseline is what
|
|
101
|
+
// does it — this pins that the overlay stops asserting the key, without which
|
|
102
|
+
// the shipped config could never come back.
|
|
103
|
+
testSync("withdrawn ⇒ the overlay stops naming it, so the baseline restores", () => {
|
|
104
|
+
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
105
|
+
expect(out->Dict.keysToArray->Array.includes("uiSlotsUrl"))->toBe(false)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
testSync("refuses a shellConfig key redirecting the computed module", () =>
|
|
109
|
+
expect(
|
|
110
|
+
threw(() =>
|
|
111
|
+
ShellConfig.overlay(
|
|
112
|
+
~bakedManifest=None,
|
|
113
|
+
~uiSlotsFile="/src/storefront-slots.js",
|
|
114
|
+
~shellConfig=Some(Dict.fromArray([("uiSlotsUrl", JSON.Encode.string("/elsewhere.js"))])),
|
|
115
|
+
)->ignore
|
|
116
|
+
),
|
|
117
|
+
)->toBe(true)
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
80
121
|
describe("ShellConfig.emit", () => {
|
|
81
122
|
let shipped = `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`
|
|
82
123
|
|
|
@@ -36,19 +36,19 @@ function threw(f) {
|
|
|
36
36
|
|
|
37
37
|
globalThis.describe("ShellConfig.overlay", () => {
|
|
38
38
|
globalThis.test("is empty when the deployment declares nothing", () => {
|
|
39
|
-
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined);
|
|
39
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
|
|
40
40
|
globalThis.expect(Object.keys(out)).toEqual([]);
|
|
41
41
|
});
|
|
42
42
|
globalThis.test("points manifestUrl at the bake's default key", () => {
|
|
43
|
-
let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined);
|
|
43
|
+
let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined);
|
|
44
44
|
globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
|
|
45
45
|
});
|
|
46
46
|
globalThis.test("follows a renamed bake", () => {
|
|
47
|
-
let out = ShellConfig$ReventlessLocal.overlay(manifest("storefront.json"), undefined);
|
|
47
|
+
let out = ShellConfig$ReventlessLocal.overlay(manifest("storefront.json"), undefined, undefined);
|
|
48
48
|
globalThis.expect(str(out, "manifestUrl")).toEqual("/storefront.json");
|
|
49
49
|
});
|
|
50
50
|
globalThis.test("passes the deployment's own keys through verbatim", () => {
|
|
51
|
-
let out = ShellConfig$ReventlessLocal.overlay(undefined, Object.fromEntries([
|
|
51
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, Object.fromEntries([
|
|
52
52
|
[
|
|
53
53
|
"appName",
|
|
54
54
|
"Online Shop"
|
|
@@ -63,7 +63,7 @@ globalThis.describe("ShellConfig.overlay", () => {
|
|
|
63
63
|
});
|
|
64
64
|
globalThis.test("refuses a shellConfig key the platform computes", () => {
|
|
65
65
|
globalThis.expect(threw(() => {
|
|
66
|
-
ShellConfig$ReventlessLocal.overlay(manifest(undefined), Object.fromEntries([[
|
|
66
|
+
ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, Object.fromEntries([[
|
|
67
67
|
"manifestUrl",
|
|
68
68
|
"/elsewhere.json"
|
|
69
69
|
]]));
|
|
@@ -71,6 +71,29 @@ globalThis.describe("ShellConfig.overlay", () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
globalThis.describe("ShellConfig.overlay — uiSlotsFile", () => {
|
|
75
|
+
globalThis.test("unset ⇒ no uiSlotsUrl", () => {
|
|
76
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
|
|
77
|
+
globalThis.expect(Stdlib_Option.isNone(out["uiSlotsUrl"])).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
globalThis.test("declared ⇒ the URL the platform serves the module from", () => {
|
|
80
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, "/src/storefront-slots.js", undefined);
|
|
81
|
+
globalThis.expect(out["uiSlotsUrl"]).toEqual("/ui-slots.js");
|
|
82
|
+
});
|
|
83
|
+
globalThis.test("withdrawn ⇒ the overlay stops naming it, so the baseline restores", () => {
|
|
84
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
|
|
85
|
+
globalThis.expect(Object.keys(out).includes("uiSlotsUrl")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
globalThis.test("refuses a shellConfig key redirecting the computed module", () => {
|
|
88
|
+
globalThis.expect(threw(() => {
|
|
89
|
+
ShellConfig$ReventlessLocal.overlay(undefined, "/src/storefront-slots.js", Object.fromEntries([[
|
|
90
|
+
"uiSlotsUrl",
|
|
91
|
+
"/elsewhere.js"
|
|
92
|
+
]]));
|
|
93
|
+
})).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
74
97
|
globalThis.describe("ShellConfig.emit", () => {
|
|
75
98
|
let shipped = `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`;
|
|
76
99
|
let distWithConfig = () => {
|
|
@@ -87,35 +110,35 @@ globalThis.describe("ShellConfig.emit", () => {
|
|
|
87
110
|
]]);
|
|
88
111
|
globalThis.test("merges the overlay onto the shipped file and keeps its other keys", () => {
|
|
89
112
|
let dir = distWithConfig();
|
|
90
|
-
ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, dir);
|
|
113
|
+
ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, dir);
|
|
91
114
|
let out = readConfig(dir);
|
|
92
115
|
globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
|
|
93
116
|
globalThis.expect(str(out, "apiEndpoint")).toEqual("/graphql");
|
|
94
117
|
});
|
|
95
118
|
globalThis.test("leaves a platform that declares nothing entirely alone", () => {
|
|
96
119
|
let dir = distWithConfig();
|
|
97
|
-
ShellConfig$ReventlessLocal.emit(undefined, undefined, dir);
|
|
120
|
+
ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
|
|
98
121
|
globalThis.expect(Nodefs.readFileSync(Nodepath.join(dir, "config.json"), "utf8")).toEqual(shipped);
|
|
99
122
|
globalThis.expect(Nodefs.existsSync(Nodepath.join(dir, "config.base.json"))).toBe(false);
|
|
100
123
|
});
|
|
101
124
|
globalThis.test("starts every boot from the shipped file, not the last output", () => {
|
|
102
125
|
let dir = distWithConfig();
|
|
103
|
-
ShellConfig$ReventlessLocal.emit(undefined, named("First"), dir);
|
|
104
|
-
ShellConfig$ReventlessLocal.emit(undefined, named("Second"), dir);
|
|
126
|
+
ShellConfig$ReventlessLocal.emit(undefined, undefined, named("First"), dir);
|
|
127
|
+
ShellConfig$ReventlessLocal.emit(undefined, undefined, named("Second"), dir);
|
|
105
128
|
globalThis.expect(str(readConfig(dir), "appName")).toEqual("Second");
|
|
106
129
|
globalThis.expect(str(readBaseline(dir), "appName")).toEqual("Shipped");
|
|
107
130
|
});
|
|
108
131
|
globalThis.test("restores the shipped file once the declaration is withdrawn", () => {
|
|
109
132
|
let dir = distWithConfig();
|
|
110
|
-
ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, dir);
|
|
111
|
-
ShellConfig$ReventlessLocal.emit(undefined, undefined, dir);
|
|
133
|
+
ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, dir);
|
|
134
|
+
ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
|
|
112
135
|
let out = readConfig(dir);
|
|
113
136
|
globalThis.expect(out["manifestUrl"]).toEqual(undefined);
|
|
114
137
|
globalThis.expect(str(out, "appName")).toEqual("Shipped");
|
|
115
138
|
});
|
|
116
139
|
globalThis.test("refuses to overlay a dist that ships no config.json", () => {
|
|
117
140
|
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
|
|
118
|
-
globalThis.expect(threw(() => ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, dir))).toBe(true);
|
|
141
|
+
globalThis.expect(threw(() => ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, undefined, dir))).toBe(true);
|
|
119
142
|
});
|
|
120
143
|
});
|
|
121
144
|
|
|
@@ -157,22 +180,22 @@ let fulfilment = {
|
|
|
157
180
|
|
|
158
181
|
globalThis.describe("ShellConfig.overlay — journeys", () => {
|
|
159
182
|
globalThis.test("a bake declaring no journeys writes no map", () => {
|
|
160
|
-
let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined);
|
|
183
|
+
let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined);
|
|
161
184
|
globalThis.expect(out["journeyManifestUrls"]).toEqual(undefined);
|
|
162
185
|
});
|
|
163
186
|
globalThis.test("an empty journeys array is the same as none", () => {
|
|
164
|
-
let out = ShellConfig$ReventlessLocal.overlay(withJourneys([]), undefined);
|
|
187
|
+
let out = ShellConfig$ReventlessLocal.overlay(withJourneys([]), undefined, undefined);
|
|
165
188
|
globalThis.expect(out["journeyManifestUrls"]).toEqual(undefined);
|
|
166
189
|
});
|
|
167
190
|
globalThis.test("keeps manifestUrl as the default journey", () => {
|
|
168
|
-
let out = ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined);
|
|
191
|
+
let out = ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined, undefined);
|
|
169
192
|
globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
|
|
170
193
|
});
|
|
171
194
|
globalThis.test("maps each declared group to its own file", () => {
|
|
172
195
|
let out = ShellConfig$ReventlessLocal.overlay(withJourneys([
|
|
173
196
|
shopper,
|
|
174
197
|
fulfilment
|
|
175
|
-
]), undefined);
|
|
198
|
+
]), undefined, undefined);
|
|
176
199
|
let map = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(out["journeyManifestUrls"], Stdlib_JSON.Decode.object), undefined);
|
|
177
200
|
globalThis.expect([
|
|
178
201
|
str(map, "Shopper"),
|
|
@@ -184,7 +207,7 @@ globalThis.describe("ShellConfig.overlay — journeys", () => {
|
|
|
184
207
|
});
|
|
185
208
|
globalThis.test("refuses a shellConfig that sets the journey map itself", () => {
|
|
186
209
|
globalThis.expect(threw(() => {
|
|
187
|
-
ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), Object.fromEntries([[
|
|
210
|
+
ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined, Object.fromEntries([[
|
|
188
211
|
"journeyManifestUrls",
|
|
189
212
|
{}
|
|
190
213
|
]]));
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The `ui-slots.js` the local platform serves. Two things are under test: that a
|
|
4
|
+
// declared module reaches the served dist/ unaltered, and that an undeclared
|
|
5
|
+
// platform serves *nothing* — which is where this parts company with the hints
|
|
6
|
+
// file, whose "nothing" means the host-shell package's own fallback.
|
|
7
|
+
//
|
|
8
|
+
// Every failure here is quiet in the worst way. Hints going unapplied read as a
|
|
9
|
+
// menu that is slightly wrong; renderers going unapplied read as a surface that
|
|
10
|
+
// was never customised, which is indistinguishable from one nobody customised.
|
|
11
|
+
|
|
12
|
+
let _ = TestRunner.setup()
|
|
13
|
+
|
|
14
|
+
// The shape the shell actually calls (`SlotModules.load`): one `register`
|
|
15
|
+
// export, handed `h` and the registry. Nothing here evaluates it — the bytes are
|
|
16
|
+
// what is under test — but a fixture that misstated the contract would be read
|
|
17
|
+
// as documentation of it.
|
|
18
|
+
let declared = `export function register({ h, slots }) {
|
|
19
|
+
slots.row("gallery.tile", ({ row }) => h("span", null, row.name))
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
let edited = `export function register({ h, slots }) {
|
|
23
|
+
slots.row("gallery.tile", ({ row }) => h("strong", null, row.name))
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
let tmpdir = prefix => NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), prefix]))
|
|
28
|
+
|
|
29
|
+
// No shipped counterpart, deliberately: the shell ships no slots module, and a
|
|
30
|
+
// fixture that invented one would test a fallback this seam must not have.
|
|
31
|
+
let emptyDist = () => tmpdir("reventless-uislots-dist-")
|
|
32
|
+
|
|
33
|
+
let declaredFile = (contents: string) => {
|
|
34
|
+
let path = NodePath.join([tmpdir("reventless-uislots-src-"), "storefront-slots.js"])
|
|
35
|
+
NodeFs.writeFileSync(path, contents)
|
|
36
|
+
path
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let servedPath = (dir: string) => NodePath.join([dir, "ui-slots.js"])
|
|
40
|
+
let served = (dir: string) => NodeFs.readFileSync(servedPath(dir))
|
|
41
|
+
let isServed = (dir: string) => NodeFs.existsSync(servedPath(dir))
|
|
42
|
+
|
|
43
|
+
let threw = (f: unit => unit): bool =>
|
|
44
|
+
try {
|
|
45
|
+
f()
|
|
46
|
+
false
|
|
47
|
+
} catch {
|
|
48
|
+
| _ => true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("UiSlots.emit", () => {
|
|
52
|
+
testSync("serves the declared module verbatim", () => {
|
|
53
|
+
let dir = emptyDist()
|
|
54
|
+
UiSlots.emit(~uiSlotsFile=Some(declaredFile(declared)), ~dir)
|
|
55
|
+
expect(served(dir))->toEqual(declared)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// An undeclared platform has to be byte-identical to one built before this
|
|
59
|
+
// module existed.
|
|
60
|
+
testSync("writes nothing when the platform declares no slots file", () => {
|
|
61
|
+
let dir = emptyDist()
|
|
62
|
+
UiSlots.emit(~uiSlotsFile=None, ~dir)
|
|
63
|
+
expect(isServed(dir))->toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// The counterpart to the hints baseline. There is no file to restore, so the
|
|
67
|
+
// state to return to is "no file" — leaving yesterday's renderers in place
|
|
68
|
+
// with nothing in the deployment to explain them is the failure a baseline
|
|
69
|
+
// exists to prevent, arriving by the other road.
|
|
70
|
+
testSync("removes the served module once the declaration is withdrawn", () => {
|
|
71
|
+
let dir = emptyDist()
|
|
72
|
+
UiSlots.emit(~uiSlotsFile=Some(declaredFile(declared)), ~dir)
|
|
73
|
+
UiSlots.emit(~uiSlotsFile=None, ~dir)
|
|
74
|
+
expect(isServed(dir))->toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
testSync("withdrawing twice is not an error", () => {
|
|
78
|
+
let dir = emptyDist()
|
|
79
|
+
UiSlots.emit(~uiSlotsFile=None, ~dir)
|
|
80
|
+
expect(threw(() => UiSlots.emit(~uiSlotsFile=None, ~dir)))->toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// The one failure this side can see, and it has to be the boot's problem: a
|
|
84
|
+
// declaration pointing nowhere registers no renderers and says nothing about
|
|
85
|
+
// why, which reads as a seam that does not work.
|
|
86
|
+
testSync("refuses a declaration naming a file that does not exist", () => {
|
|
87
|
+
let dir = emptyDist()
|
|
88
|
+
let missing = NodePath.join([tmpdir("reventless-uislots-src-"), "absent.js"])
|
|
89
|
+
expect(threw(() => UiSlots.emit(~uiSlotsFile=Some(missing), ~dir)))->toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Read before anything is touched: a bad declaration must not leave the served
|
|
93
|
+
// module half-replaced, which would be worse than either outcome.
|
|
94
|
+
testSync("leaves the last good module in place when the declaration is bad", () => {
|
|
95
|
+
let dir = emptyDist()
|
|
96
|
+
UiSlots.emit(~uiSlotsFile=Some(declaredFile(declared)), ~dir)
|
|
97
|
+
let missing = NodePath.join([tmpdir("reventless-uislots-src-"), "absent.js"])
|
|
98
|
+
let _ = threw(() => UiSlots.emit(~uiSlotsFile=Some(missing), ~dir))
|
|
99
|
+
expect(served(dir))->toEqual(declared)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// The deliberate non-check. There is no cheap way to know a module is good
|
|
103
|
+
// short of evaluating it, the deploy half cannot do it either, and a check
|
|
104
|
+
// that only ran locally would let a file pass the loop it was authored in and
|
|
105
|
+
// fail the one it ships to. The shell reports what it could not import.
|
|
106
|
+
testSync("serves a module it cannot vouch for rather than refusing it", () => {
|
|
107
|
+
let dir = emptyDist()
|
|
108
|
+
let broken = "export const register = (r) => {"
|
|
109
|
+
UiSlots.emit(~uiSlotsFile=Some(declaredFile(broken)), ~dir)
|
|
110
|
+
expect(served(dir))->toEqual(broken)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// The dev loop. Real `fs.watch` events rather than a stubbed clock, because what
|
|
115
|
+
// is being tested IS the plumbing — a debounce over a fake timer would pass with
|
|
116
|
+
// the watcher wired to nothing.
|
|
117
|
+
//
|
|
118
|
+
// The repeated edit is not belt-and-braces: `fs.watch` does not promise that a
|
|
119
|
+
// write landing near the watcher's creation is reported, and on macOS a write
|
|
120
|
+
// that beats the FSEvents stream up is not reported late but not at all. See the
|
|
121
|
+
// long note in `UiHintsTest` — the same reasoning and the same interval.
|
|
122
|
+
let retryEvery = 1000
|
|
123
|
+
|
|
124
|
+
let waitForReload = (~timeoutMs: int=4000, ~edit: unit => unit, ~uiSlotsFile, ~dir) =>
|
|
125
|
+
Promise.make((resolve, _) => {
|
|
126
|
+
let fired = ref(0)
|
|
127
|
+
let watcher = ref(None)
|
|
128
|
+
let deadline = ref(None)
|
|
129
|
+
let retry = ref(None)
|
|
130
|
+
// Resolves with a count rather than rejecting on the deadline: "nothing was
|
|
131
|
+
// re-served" is an expected answer here, and a rejection would make the
|
|
132
|
+
// assertion read as an infrastructure failure.
|
|
133
|
+
let finish = () => {
|
|
134
|
+
watcher.contents->Option.forEach(NodeFs.watcherClose)
|
|
135
|
+
deadline.contents->Option.forEach(clearTimeout)
|
|
136
|
+
retry.contents->Option.forEach(clearTimeout)
|
|
137
|
+
resolve(fired.contents)
|
|
138
|
+
}
|
|
139
|
+
let rec editUntilSeen = () => {
|
|
140
|
+
edit()
|
|
141
|
+
retry := Some(setTimeout(editUntilSeen, retryEvery))
|
|
142
|
+
}
|
|
143
|
+
watcher :=
|
|
144
|
+
UiSlots.watch(~uiSlotsFile, ~dir, ~onReload=() => {
|
|
145
|
+
fired := fired.contents + 1
|
|
146
|
+
finish()
|
|
147
|
+
})
|
|
148
|
+
deadline := Some(setTimeout(finish, timeoutMs))
|
|
149
|
+
editUntilSeen()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe("UiSlots.watch", () => {
|
|
153
|
+
testSync("watches nothing when the platform declares no slots file", () =>
|
|
154
|
+
expect(UiSlots.watch(~uiSlotsFile=None, ~onReload=() => ()))->toEqual(None)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
testSync("declines to watch a path whose directory is gone, without throwing", () => {
|
|
158
|
+
let missing = NodePath.join([tmpdir("reventless-uislots-gone-"), "nowhere", "slots.js"])
|
|
159
|
+
expect(UiSlots.watch(~uiSlotsFile=Some(missing), ~onReload=() => ()))->toEqual(None)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("re-serves the module when it changes, and says so", async () => {
|
|
163
|
+
let dir = emptyDist()
|
|
164
|
+
let path = declaredFile(declared)
|
|
165
|
+
UiSlots.emit(~uiSlotsFile=Some(path), ~dir)
|
|
166
|
+
let fired = await waitForReload(
|
|
167
|
+
~uiSlotsFile=Some(path),
|
|
168
|
+
~dir,
|
|
169
|
+
~edit=() => NodeFs.writeFileSync(path, edited),
|
|
170
|
+
)
|
|
171
|
+
expect((fired > 0, served(dir)))->toEqual((true, edited))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Mid-session the file going unreadable is almost always an editor between two
|
|
175
|
+
// writes, and killing a running dev server over a keystroke would make this
|
|
176
|
+
// worse than the restart it replaces. The previous copy stands, and the save
|
|
177
|
+
// completing is itself the next event, so recovery needs no second trigger.
|
|
178
|
+
test("keeps serving the last good copy when the file is momentarily unreadable", async () => {
|
|
179
|
+
let dir = emptyDist()
|
|
180
|
+
let path = declaredFile(declared)
|
|
181
|
+
UiSlots.emit(~uiSlotsFile=Some(path), ~dir)
|
|
182
|
+
let fired = await waitForReload(
|
|
183
|
+
~timeoutMs=1500,
|
|
184
|
+
~uiSlotsFile=Some(path),
|
|
185
|
+
~dir,
|
|
186
|
+
// Write-then-unlink rather than a bare unlink, so the edit stays repeatable
|
|
187
|
+
// for the retry above: it raises events every time and lands in the same
|
|
188
|
+
// absent state each time. It is also what a rename-based editor save
|
|
189
|
+
// actually does — the window this is about is the one between the two.
|
|
190
|
+
~edit=() => {
|
|
191
|
+
NodeFs.writeFileSync(path, edited)
|
|
192
|
+
NodeFs.unlinkSync(path)
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
expect((fired, served(dir)))->toEqual((0, declared))
|
|
196
|
+
})
|
|
197
|
+
})
|