@reventlessdev/reventless-local 3.0.0-alpha.216 → 3.0.0-alpha.218

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 (34) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +10 -10
  3. package/src/BakedManifest.res +63 -51
  4. package/src/BakedManifest.res.mjs +44 -28
  5. package/src/HostShellDist.res +29 -0
  6. package/src/HostShellDist.res.mjs +20 -0
  7. package/src/Platform.res +30 -11
  8. package/src/Platform.res.mjs +6 -0
  9. package/src/ShellConfig.res +178 -0
  10. package/src/ShellConfig.res.mjs +114 -0
  11. package/src/UiHints.res +107 -0
  12. package/src/UiHints.res.mjs +61 -0
  13. package/src/adapter/Auth/Auth_GraphqlContext.res +4 -6
  14. package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +5 -0
  15. package/src/adapter/Auth/LocalAuth.res +136 -6
  16. package/src/adapter/Auth/LocalAuth.res.mjs +82 -3
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +23 -2
  18. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +19 -2
  19. package/src/adapter/DomainGraphQL_Server.res +67 -4
  20. package/src/adapter/DomainGraphQL_Server.res.mjs +49 -2
  21. package/src/adapter/GraphQL_CallerError.res +19 -0
  22. package/src/adapter/GraphQL_CallerError.res.mjs +16 -0
  23. package/src/adapter/GraphQL_Server.res.mjs +3 -0
  24. package/tests/BakedManifestFilesTest.res +68 -0
  25. package/tests/BakedManifestFilesTest.res.mjs +115 -0
  26. package/tests/ShellConfigTest.res +221 -0
  27. package/tests/ShellConfigTest.res.mjs +203 -0
  28. package/tests/UiHintsTest.res +94 -0
  29. package/tests/UiHintsTest.res.mjs +99 -0
  30. package/tests/adapter/CommandAuthorizationTest.res +86 -0
  31. package/tests/adapter/CommandAuthorizationTest.res.mjs +62 -0
  32. package/tests/adapter/LocalAuthLoginTest.res +214 -0
  33. package/tests/adapter/LocalAuthLoginTest.res.mjs +191 -8
  34. package/tests/adapter/LocalAuthUserStoreTest.res.mjs +2 -2
@@ -0,0 +1,221 @@
1
+ open JestGlobals
2
+
3
+ // The `config.json` overlay the local platform puts on the host shell's shipped
4
+ // file. Two things are under test and they fail differently: the key set (what a
5
+ // deployment's declaration turns into) and the baseline (what makes a second
6
+ // boot, and a *withdrawn* declaration, land where they should). Every failure
7
+ // here is silent at the point it happens and loud a long way away — a shell that
8
+ // boots with the wrong surface, or none.
9
+
10
+ let _ = TestRunner.setup()
11
+
12
+ let manifest = (~key: option<string>=?): ReventlessInfra.Platform.bakedManifest => {
13
+ components: [{plugin: "Catalog", views: ["Products"], commands: []}],
14
+ key: ?key,
15
+ }
16
+
17
+ let str = (d: dict<JSON.t>, k: string): option<string> =>
18
+ d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
19
+
20
+ let threw = (f: unit => unit): bool =>
21
+ try {
22
+ f()
23
+ false
24
+ } catch {
25
+ | _ => true
26
+ }
27
+
28
+ describe("ShellConfig.overlay", () => {
29
+ testSync("is empty when the deployment declares nothing", () => {
30
+ let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
31
+ expect(out->Dict.keysToArray)->toEqual([])
32
+ })
33
+
34
+ testSync("points manifestUrl at the bake's default key", () => {
35
+ let out = ShellConfig.overlay(~bakedManifest=Some(manifest()), ~shellConfig=None)
36
+ expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
37
+ })
38
+
39
+ // The key and the URL are one string: the bake writes to the dist root, which
40
+ // is also the shell's URL root. A renamed file that kept the default URL would
41
+ // 404 on a shell that has no admin API to fall back to.
42
+ testSync("follows a renamed bake", () => {
43
+ let out = ShellConfig.overlay(
44
+ ~bakedManifest=Some(manifest(~key="storefront.json")),
45
+ ~shellConfig=None,
46
+ )
47
+ expect(out->str("manifestUrl"))->toEqual(Some("/storefront.json"))
48
+ })
49
+
50
+ testSync("passes the deployment's own keys through verbatim", () => {
51
+ let out = ShellConfig.overlay(
52
+ ~bakedManifest=None,
53
+ ~shellConfig=Some(
54
+ Dict.fromArray([
55
+ ("appName", JSON.Encode.string("Online Shop")),
56
+ ("elevatedGroups", ["Admin"]->Array.map(JSON.Encode.string)->JSON.Encode.array),
57
+ ]),
58
+ ),
59
+ )
60
+ expect(out->str("appName"))->toEqual(Some("Online Shop"))
61
+ expect(out->Dict.get("elevatedGroups")->Option.isSome)->toBe(true)
62
+ })
63
+
64
+ // Silently resolving it either way points the shell at a manifest the platform
65
+ // did not write, with nothing in the diff to say so.
66
+ testSync("refuses a shellConfig key the platform computes", () =>
67
+ expect(
68
+ threw(() =>
69
+ ShellConfig.overlay(
70
+ ~bakedManifest=Some(manifest()),
71
+ ~shellConfig=Some(
72
+ Dict.fromArray([("manifestUrl", JSON.Encode.string("/elsewhere.json"))]),
73
+ ),
74
+ )->ignore
75
+ ),
76
+ )->toBe(true)
77
+ )
78
+ })
79
+
80
+ describe("ShellConfig.emit", () => {
81
+ let shipped = `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`
82
+
83
+ let distWithConfig = () => {
84
+ let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-shellconfig-"]))
85
+ NodeFs.writeFileSync(NodePath.join([dir, "config.json"]), shipped)
86
+ dir
87
+ }
88
+
89
+ let readJson = (path: string): dict<JSON.t> =>
90
+ NodeFs.readFileSync(path)->JSON.parseOrThrow->JSON.Decode.object->Option.getOrThrow
91
+
92
+ let readConfig = (dir: string) => readJson(NodePath.join([dir, "config.json"]))
93
+ let readBaseline = (dir: string) => readJson(NodePath.join([dir, "config.base.json"]))
94
+
95
+ let named = name => Some(Dict.fromArray([("appName", JSON.Encode.string(name))]))
96
+
97
+ testSync("merges the overlay onto the shipped file and keeps its other keys", () => {
98
+ let dir = distWithConfig()
99
+ ShellConfig.emit(~bakedManifest=Some(manifest()), ~shellConfig=None, ~dir)
100
+ let out = readConfig(dir)
101
+ expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
102
+ expect(out->str("apiEndpoint"))->toEqual(Some("/graphql"))
103
+ })
104
+
105
+ testSync("leaves a platform that declares nothing entirely alone", () => {
106
+ let dir = distWithConfig()
107
+ ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
108
+ expect(NodeFs.readFileSync(NodePath.join([dir, "config.json"])))->toEqual(shipped)
109
+ expect(NodeFs.existsSync(NodePath.join([dir, "config.base.json"])))->toBe(false)
110
+ })
111
+
112
+ // The whole reason a baseline is kept: boot 2 overlaying boot 1's output would
113
+ // read `appName: "First"` as shipped and carry it forever.
114
+ testSync("starts every boot from the shipped file, not the last output", () => {
115
+ let dir = distWithConfig()
116
+ ShellConfig.emit(~bakedManifest=None, ~shellConfig=named("First"), ~dir)
117
+ ShellConfig.emit(~bakedManifest=None, ~shellConfig=named("Second"), ~dir)
118
+ expect(readConfig(dir)->str("appName"))->toEqual(Some("Second"))
119
+ expect(readBaseline(dir)->str("appName"))->toEqual(Some("Shipped"))
120
+ })
121
+
122
+ // Withdrawing a declaration has to be as reachable as making it: a lingering
123
+ // manifestUrl points the shell at a file nothing writes any more, and the
124
+ // symptom is an empty shop.
125
+ testSync("restores the shipped file once the declaration is withdrawn", () => {
126
+ let dir = distWithConfig()
127
+ ShellConfig.emit(~bakedManifest=Some(manifest()), ~shellConfig=None, ~dir)
128
+ ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
129
+ let out = readConfig(dir)
130
+ expect(out->Dict.get("manifestUrl"))->toEqual(None)
131
+ expect(out->str("appName"))->toEqual(Some("Shipped"))
132
+ })
133
+
134
+ // Writing only the declared keys would boot a shell with no apiEndpoint, and
135
+ // that failure surfaces nowhere near here.
136
+ testSync("refuses to overlay a dist that ships no config.json", () => {
137
+ let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-shellconfig-"]))
138
+ expect(
139
+ threw(() => ShellConfig.emit(~bakedManifest=Some(manifest()), ~shellConfig=None, ~dir)),
140
+ )->toBe(true)
141
+ })
142
+ })
143
+
144
+ // ── Journeys ──────────────────────────────────────────────────────────────
145
+ //
146
+ // One curated surface per audience, beside the default one. The property under
147
+ // test throughout is that a deployment declaring none is untouched: journeys are
148
+ // what a shop with several audiences opts into, and every existing deployment
149
+ // has exactly one.
150
+
151
+ let withJourneys = (
152
+ ~journeys: array<ReventlessInfra.Platform.bakedJourney>,
153
+ ): ReventlessInfra.Platform.bakedManifest => {
154
+ components: [{plugin: "Catalog", views: ["Products"], commands: []}],
155
+ journeys,
156
+ }
157
+
158
+ let shopper: ReventlessInfra.Platform.bakedJourney = {
159
+ group: "Shopper",
160
+ components: [{plugin: "Catalog", views: ["Products"], commands: []}],
161
+ }
162
+
163
+ let fulfilment: ReventlessInfra.Platform.bakedJourney = {
164
+ group: "Fulfilment",
165
+ components: [{plugin: "Ordering", views: ["Orders"], commands: ["ShipOrder"]}],
166
+ key: "fulfilment.json",
167
+ }
168
+
169
+ describe("ShellConfig.overlay — journeys", () => {
170
+ open Expect
171
+
172
+ // The regression line. A shell that has never heard of journeys must not
173
+ // suddenly find a key it does not know.
174
+ testSync("a bake declaring no journeys writes no map", () => {
175
+ let out = ShellConfig.overlay(~bakedManifest=Some(manifest()), ~shellConfig=None)
176
+ expect(out->Dict.get("journeyManifestUrls"))->toEqual(None)
177
+ })
178
+
179
+ testSync("an empty journeys array is the same as none", () => {
180
+ let out = ShellConfig.overlay(~bakedManifest=Some(withJourneys(~journeys=[])), ~shellConfig=None)
181
+ expect(out->Dict.get("journeyManifestUrls"))->toEqual(None)
182
+ })
183
+
184
+ // The default journey keeps `manifestUrl`, so a caller matching no declared
185
+ // group lands where every caller landed before.
186
+ testSync("keeps manifestUrl as the default journey", () => {
187
+ let out = ShellConfig.overlay(
188
+ ~bakedManifest=Some(withJourneys(~journeys=[shopper])),
189
+ ~shellConfig=None,
190
+ )
191
+ expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
192
+ })
193
+
194
+ testSync("maps each declared group to its own file", () => {
195
+ let out = ShellConfig.overlay(
196
+ ~bakedManifest=Some(withJourneys(~journeys=[shopper, fulfilment])),
197
+ ~shellConfig=None,
198
+ )
199
+ let map =
200
+ out->Dict.get("journeyManifestUrls")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
201
+ expect((map->str("Shopper"), map->str("Fulfilment")))->toEqual((
202
+ // Derived from the group, lower-cased, because a key is part of a URL.
203
+ Some("/component-manifest-shopper.json"),
204
+ // Named explicitly, and the declaration wins.
205
+ Some("/fulfilment.json"),
206
+ ))
207
+ })
208
+
209
+ // A passthrough cannot redirect a key the platform computes — the same rule
210
+ // `manifestUrl` already carries, extended to the map it now writes beside it.
211
+ testSync("refuses a shellConfig that sets the journey map itself", () =>
212
+ expect(
213
+ threw(() =>
214
+ ShellConfig.overlay(
215
+ ~bakedManifest=Some(withJourneys(~journeys=[shopper])),
216
+ ~shellConfig=Some(Dict.fromArray([("journeyManifestUrls", JSON.Encode.object(Dict.make()))])),
217
+ )->ignore
218
+ ),
219
+ )->toBe(true)
220
+ )
221
+ })
@@ -0,0 +1,203 @@
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 Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
9
+ import * as ShellConfig$ReventlessLocal from "../src/ShellConfig.res.mjs";
10
+
11
+ TestRunner$ReventlessLocal.setup();
12
+
13
+ function manifest(key) {
14
+ return {
15
+ components: [{
16
+ plugin: "Catalog",
17
+ views: ["Products"],
18
+ commands: []
19
+ }],
20
+ key: key
21
+ };
22
+ }
23
+
24
+ function str(d, k) {
25
+ return Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
26
+ }
27
+
28
+ function threw(f) {
29
+ try {
30
+ f();
31
+ return false;
32
+ } catch (exn) {
33
+ return true;
34
+ }
35
+ }
36
+
37
+ globalThis.describe("ShellConfig.overlay", () => {
38
+ globalThis.test("is empty when the deployment declares nothing", () => {
39
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined);
40
+ globalThis.expect(Object.keys(out)).toEqual([]);
41
+ });
42
+ globalThis.test("points manifestUrl at the bake's default key", () => {
43
+ let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined);
44
+ globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
45
+ });
46
+ globalThis.test("follows a renamed bake", () => {
47
+ let out = ShellConfig$ReventlessLocal.overlay(manifest("storefront.json"), undefined);
48
+ globalThis.expect(str(out, "manifestUrl")).toEqual("/storefront.json");
49
+ });
50
+ globalThis.test("passes the deployment's own keys through verbatim", () => {
51
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, Object.fromEntries([
52
+ [
53
+ "appName",
54
+ "Online Shop"
55
+ ],
56
+ [
57
+ "elevatedGroups",
58
+ ["Admin"].map(prim => prim)
59
+ ]
60
+ ]));
61
+ globalThis.expect(str(out, "appName")).toEqual("Online Shop");
62
+ globalThis.expect(Stdlib_Option.isSome(out["elevatedGroups"])).toBe(true);
63
+ });
64
+ globalThis.test("refuses a shellConfig key the platform computes", () => {
65
+ globalThis.expect(threw(() => {
66
+ ShellConfig$ReventlessLocal.overlay(manifest(undefined), Object.fromEntries([[
67
+ "manifestUrl",
68
+ "/elsewhere.json"
69
+ ]]));
70
+ })).toBe(true);
71
+ });
72
+ });
73
+
74
+ globalThis.describe("ShellConfig.emit", () => {
75
+ let shipped = `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`;
76
+ let distWithConfig = () => {
77
+ let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
78
+ Nodefs.writeFileSync(Nodepath.join(dir, "config.json"), shipped, "utf8");
79
+ return dir;
80
+ };
81
+ let readJson = path => Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(path, "utf8"))), undefined);
82
+ let readConfig = dir => readJson(Nodepath.join(dir, "config.json"));
83
+ let readBaseline = dir => readJson(Nodepath.join(dir, "config.base.json"));
84
+ let named = name => Object.fromEntries([[
85
+ "appName",
86
+ name
87
+ ]]);
88
+ globalThis.test("merges the overlay onto the shipped file and keeps its other keys", () => {
89
+ let dir = distWithConfig();
90
+ ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, dir);
91
+ let out = readConfig(dir);
92
+ globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
93
+ globalThis.expect(str(out, "apiEndpoint")).toEqual("/graphql");
94
+ });
95
+ globalThis.test("leaves a platform that declares nothing entirely alone", () => {
96
+ let dir = distWithConfig();
97
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, dir);
98
+ globalThis.expect(Nodefs.readFileSync(Nodepath.join(dir, "config.json"), "utf8")).toEqual(shipped);
99
+ globalThis.expect(Nodefs.existsSync(Nodepath.join(dir, "config.base.json"))).toBe(false);
100
+ });
101
+ globalThis.test("starts every boot from the shipped file, not the last output", () => {
102
+ let dir = distWithConfig();
103
+ ShellConfig$ReventlessLocal.emit(undefined, named("First"), dir);
104
+ ShellConfig$ReventlessLocal.emit(undefined, named("Second"), dir);
105
+ globalThis.expect(str(readConfig(dir), "appName")).toEqual("Second");
106
+ globalThis.expect(str(readBaseline(dir), "appName")).toEqual("Shipped");
107
+ });
108
+ globalThis.test("restores the shipped file once the declaration is withdrawn", () => {
109
+ let dir = distWithConfig();
110
+ ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, dir);
111
+ ShellConfig$ReventlessLocal.emit(undefined, undefined, dir);
112
+ let out = readConfig(dir);
113
+ globalThis.expect(out["manifestUrl"]).toEqual(undefined);
114
+ globalThis.expect(str(out, "appName")).toEqual("Shipped");
115
+ });
116
+ globalThis.test("refuses to overlay a dist that ships no config.json", () => {
117
+ let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
118
+ globalThis.expect(threw(() => ShellConfig$ReventlessLocal.emit(manifest(undefined), undefined, dir))).toBe(true);
119
+ });
120
+ });
121
+
122
+ function withJourneys(journeys) {
123
+ return {
124
+ components: [{
125
+ plugin: "Catalog",
126
+ views: ["Products"],
127
+ commands: []
128
+ }],
129
+ journeys: journeys
130
+ };
131
+ }
132
+
133
+ let shopper_components = [{
134
+ plugin: "Catalog",
135
+ views: ["Products"],
136
+ commands: []
137
+ }];
138
+
139
+ let shopper = {
140
+ group: "Shopper",
141
+ components: shopper_components
142
+ };
143
+
144
+ let fulfilment_components = [{
145
+ plugin: "Ordering",
146
+ views: ["Orders"],
147
+ commands: ["ShipOrder"]
148
+ }];
149
+
150
+ let fulfilment_key = "fulfilment.json";
151
+
152
+ let fulfilment = {
153
+ group: "Fulfilment",
154
+ components: fulfilment_components,
155
+ key: fulfilment_key
156
+ };
157
+
158
+ globalThis.describe("ShellConfig.overlay — journeys", () => {
159
+ globalThis.test("a bake declaring no journeys writes no map", () => {
160
+ let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined);
161
+ globalThis.expect(out["journeyManifestUrls"]).toEqual(undefined);
162
+ });
163
+ globalThis.test("an empty journeys array is the same as none", () => {
164
+ let out = ShellConfig$ReventlessLocal.overlay(withJourneys([]), undefined);
165
+ globalThis.expect(out["journeyManifestUrls"]).toEqual(undefined);
166
+ });
167
+ globalThis.test("keeps manifestUrl as the default journey", () => {
168
+ let out = ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), undefined);
169
+ globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
170
+ });
171
+ globalThis.test("maps each declared group to its own file", () => {
172
+ let out = ShellConfig$ReventlessLocal.overlay(withJourneys([
173
+ shopper,
174
+ fulfilment
175
+ ]), undefined);
176
+ let map = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(out["journeyManifestUrls"], Stdlib_JSON.Decode.object), undefined);
177
+ globalThis.expect([
178
+ str(map, "Shopper"),
179
+ str(map, "Fulfilment")
180
+ ]).toEqual([
181
+ "/component-manifest-shopper.json",
182
+ "/fulfilment.json"
183
+ ]);
184
+ });
185
+ globalThis.test("refuses a shellConfig that sets the journey map itself", () => {
186
+ globalThis.expect(threw(() => {
187
+ ShellConfig$ReventlessLocal.overlay(withJourneys([shopper]), Object.fromEntries([[
188
+ "journeyManifestUrls",
189
+ {}
190
+ ]]));
191
+ })).toBe(true);
192
+ });
193
+ });
194
+
195
+ export {
196
+ manifest,
197
+ str,
198
+ threw,
199
+ withJourneys,
200
+ shopper,
201
+ fulfilment,
202
+ }
203
+ /* Not a pure module */
@@ -0,0 +1,94 @@
1
+ open JestGlobals
2
+
3
+ // The `ui-hints.json` the local platform serves. Two things are under test:
4
+ // which file wins (the declaration or the host-shell package's own dev-mode
5
+ // fallback), and the baseline that makes a second boot — and a *withdrawn*
6
+ // declaration — land where they should. Every failure here is quiet: hints are
7
+ // presentation, so the symptom is a menu that reads slightly wrong.
8
+
9
+ let _ = TestRunner.setup()
10
+
11
+ let shipped = `{"Catalog": {"views": {"Products": {"nav": {"label": "Shipped"}}}}}`
12
+ let declared = `{"Catalog": {"views": {"Products": {"nav": {"label": "Declared"}}}}}`
13
+
14
+ let tmpdir = prefix => NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), prefix]))
15
+
16
+ let distWithHints = () => {
17
+ let dir = tmpdir("reventless-uihints-dist-")
18
+ NodeFs.writeFileSync(NodePath.join([dir, "ui-hints.json"]), shipped)
19
+ dir
20
+ }
21
+
22
+ let declaredFile = (contents: string) => {
23
+ let path = NodePath.join([tmpdir("reventless-uihints-src-"), "ui-hints.json"])
24
+ NodeFs.writeFileSync(path, contents)
25
+ path
26
+ }
27
+
28
+ let served = (dir: string) => NodeFs.readFileSync(NodePath.join([dir, "ui-hints.json"]))
29
+ let baselinePath = (dir: string) => NodePath.join([dir, "ui-hints.base.json"])
30
+
31
+ let threw = (f: unit => unit): bool =>
32
+ try {
33
+ f()
34
+ false
35
+ } catch {
36
+ | _ => true
37
+ }
38
+
39
+ describe("UiHints.emit", () => {
40
+ testSync("serves the declared file verbatim", () => {
41
+ let dir = distWithHints()
42
+ UiHints.emit(~uiHintsFile=Some(declaredFile(declared)), ~dir)
43
+ expect(served(dir))->toEqual(declared)
44
+ })
45
+
46
+ // An undeclared platform has to be byte-identical to one built before this
47
+ // module existed, or every shell repo demonstration breaks to fix a problem
48
+ // it does not have.
49
+ testSync("leaves a platform that declares nothing entirely alone", () => {
50
+ let dir = distWithHints()
51
+ UiHints.emit(~uiHintsFile=None, ~dir)
52
+ expect(served(dir))->toEqual(shipped)
53
+ expect(NodeFs.existsSync(baselinePath(dir)))->toBe(false)
54
+ })
55
+
56
+ // Boot 2 seeding its baseline from boot 1's output would freeze the first
57
+ // declaration in as "shipped" and make the withdrawal below unreachable.
58
+ testSync("starts every boot from the shipped file, not the last output", () => {
59
+ let dir = distWithHints()
60
+ UiHints.emit(~uiHintsFile=Some(declaredFile(declared)), ~dir)
61
+ UiHints.emit(~uiHintsFile=Some(declaredFile(shipped)), ~dir)
62
+ expect(NodeFs.readFileSync(baselinePath(dir)))->toEqual(shipped)
63
+ })
64
+
65
+ testSync("restores the shipped file once the declaration is withdrawn", () => {
66
+ let dir = distWithHints()
67
+ UiHints.emit(~uiHintsFile=Some(declaredFile(declared)), ~dir)
68
+ UiHints.emit(~uiHintsFile=None, ~dir)
69
+ expect(served(dir))->toEqual(shipped)
70
+ })
71
+
72
+ // All three failures below produce the same symptom if swallowed — hints that
73
+ // quietly are not applied — so each has to be the boot's problem, not the
74
+ // reader's.
75
+ testSync("refuses a declaration naming a file that does not exist", () => {
76
+ let dir = distWithHints()
77
+ let missing = NodePath.join([tmpdir("reventless-uihints-src-"), "absent.json"])
78
+ expect(threw(() => UiHints.emit(~uiHintsFile=Some(missing), ~dir)))->toBe(true)
79
+ })
80
+
81
+ testSync("refuses a declared file that is not JSON", () => {
82
+ let dir = distWithHints()
83
+ expect(threw(() => UiHints.emit(~uiHintsFile=Some(declaredFile("not json")), ~dir)))->toBe(true)
84
+ })
85
+
86
+ // Read before anything is touched: a bad declaration must not leave the
87
+ // served file half-replaced, which would be worse than either outcome.
88
+ testSync("leaves the served file untouched when the declaration is bad", () => {
89
+ let dir = distWithHints()
90
+ let _ = threw(() => UiHints.emit(~uiHintsFile=Some(declaredFile("not json")), ~dir))
91
+ expect(served(dir))->toEqual(shipped)
92
+ expect(NodeFs.existsSync(baselinePath(dir)))->toBe(false)
93
+ })
94
+ })
@@ -0,0 +1,99 @@
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 UiHints$ReventlessLocal from "../src/UiHints.res.mjs";
7
+ import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
8
+
9
+ TestRunner$ReventlessLocal.setup();
10
+
11
+ let shipped = `{"Catalog": {"views": {"Products": {"nav": {"label": "Shipped"}}}}}`;
12
+
13
+ let declared = `{"Catalog": {"views": {"Products": {"nav": {"label": "Declared"}}}}}`;
14
+
15
+ function tmpdir(prefix) {
16
+ return Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), prefix));
17
+ }
18
+
19
+ function distWithHints() {
20
+ let dir = tmpdir("reventless-uihints-dist-");
21
+ Nodefs.writeFileSync(Nodepath.join(dir, "ui-hints.json"), shipped, "utf8");
22
+ return dir;
23
+ }
24
+
25
+ function declaredFile(contents) {
26
+ let path = Nodepath.join(tmpdir("reventless-uihints-src-"), "ui-hints.json");
27
+ Nodefs.writeFileSync(path, contents, "utf8");
28
+ return path;
29
+ }
30
+
31
+ function served(dir) {
32
+ return Nodefs.readFileSync(Nodepath.join(dir, "ui-hints.json"), "utf8");
33
+ }
34
+
35
+ function baselinePath(dir) {
36
+ return Nodepath.join(dir, "ui-hints.base.json");
37
+ }
38
+
39
+ function threw(f) {
40
+ try {
41
+ f();
42
+ return false;
43
+ } catch (exn) {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ globalThis.describe("UiHints.emit", () => {
49
+ globalThis.test("serves the declared file verbatim", () => {
50
+ let dir = distWithHints();
51
+ UiHints$ReventlessLocal.emit(declaredFile(declared), dir);
52
+ globalThis.expect(served(dir)).toEqual(declared);
53
+ });
54
+ globalThis.test("leaves a platform that declares nothing entirely alone", () => {
55
+ let dir = distWithHints();
56
+ UiHints$ReventlessLocal.emit(undefined, dir);
57
+ globalThis.expect(served(dir)).toEqual(shipped);
58
+ globalThis.expect(Nodefs.existsSync(Nodepath.join(dir, "ui-hints.base.json"))).toBe(false);
59
+ });
60
+ globalThis.test("starts every boot from the shipped file, not the last output", () => {
61
+ let dir = distWithHints();
62
+ UiHints$ReventlessLocal.emit(declaredFile(declared), dir);
63
+ UiHints$ReventlessLocal.emit(declaredFile(shipped), dir);
64
+ globalThis.expect(Nodefs.readFileSync(Nodepath.join(dir, "ui-hints.base.json"), "utf8")).toEqual(shipped);
65
+ });
66
+ globalThis.test("restores the shipped file once the declaration is withdrawn", () => {
67
+ let dir = distWithHints();
68
+ UiHints$ReventlessLocal.emit(declaredFile(declared), dir);
69
+ UiHints$ReventlessLocal.emit(undefined, dir);
70
+ globalThis.expect(served(dir)).toEqual(shipped);
71
+ });
72
+ globalThis.test("refuses a declaration naming a file that does not exist", () => {
73
+ let dir = distWithHints();
74
+ let missing = Nodepath.join(tmpdir("reventless-uihints-src-"), "absent.json");
75
+ globalThis.expect(threw(() => UiHints$ReventlessLocal.emit(missing, dir))).toBe(true);
76
+ });
77
+ globalThis.test("refuses a declared file that is not JSON", () => {
78
+ let dir = distWithHints();
79
+ globalThis.expect(threw(() => UiHints$ReventlessLocal.emit(declaredFile("not json"), dir))).toBe(true);
80
+ });
81
+ globalThis.test("leaves the served file untouched when the declaration is bad", () => {
82
+ let dir = distWithHints();
83
+ threw(() => UiHints$ReventlessLocal.emit(declaredFile("not json"), dir));
84
+ globalThis.expect(served(dir)).toEqual(shipped);
85
+ globalThis.expect(Nodefs.existsSync(Nodepath.join(dir, "ui-hints.base.json"))).toBe(false);
86
+ });
87
+ });
88
+
89
+ export {
90
+ shipped,
91
+ declared,
92
+ tmpdir,
93
+ distWithHints,
94
+ declaredFile,
95
+ served,
96
+ baselinePath,
97
+ threw,
98
+ }
99
+ /* Not a pure module */