@reventlessdev/reventless-local 3.0.0-alpha.289 → 3.0.0-alpha.291
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 +15 -0
- package/package.json +11 -11
- package/src/BakedManifest.res +7 -2
- package/src/BakedManifest.res.mjs +4 -4
- package/src/HostShellDist.res +11 -0
- package/src/HostShellDist.res.mjs +14 -1
- package/src/Platform.res +18 -6
- package/src/Platform.res.mjs +19 -16
- package/src/ShellConfig.res +27 -24
- package/src/ShellConfig.res.mjs +25 -13
- package/src/UiHints.res +3 -5
- package/src/UiSlots.res +2 -3
- package/tests/HostShellDistTest.res +70 -0
- package/tests/HostShellDistTest.res.mjs +75 -0
- package/tests/ShellConfigTest.res +45 -7
- package/tests/ShellConfigTest.res.mjs +45 -8
- package/tests/components/automationslice/AutomationRescheduleFixtures.res +0 -1
- package/tests/components/automationslice/AutomationRescheduleFixtures.res.mjs +0 -1
- package/tests/components/automationslice/AutomationScheduleFixtures.res +0 -1
- package/tests/components/automationslice/AutomationScheduleFixtures.res.mjs +0 -5
- package/tests/components/automationslice/AutomationSliceFixtures.res +0 -2
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +0 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +0 -1
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +0 -1
- package/tests/components/automationslice/AutomationSweepFixtures.res +0 -1
- package/tests/components/automationslice/AutomationSweepFixtures.res.mjs +0 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +0 -1
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +0 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +0 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +0 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +0 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +0 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.291 (2026-10-19)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **spec,aws,local:** tell the host shell which environment it runs in, and write local host UI files into a chosen assetsDir ([4f896d1](https://github.com/ReventlessDev/reventless-core/commit/4f896d13ae9ec0d211c33dbd9271e7197cfebc41))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.290 (2026-10-17)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.289 (2026-10-16)
|
|
7
22
|
|
|
8
23
|
* feat!(inbound): a translation slice can declare a signed webhook URL ([0ad6994](https://github.com/ReventlessDev/reventless-core/commit/0ad69949fcc91c2756c60ca8cb120dd468841d8b))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.291",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -34,24 +34,24 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
37
38
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
38
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
39
40
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
40
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
41
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
42
41
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/
|
|
44
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.
|
|
45
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
42
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.148",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.248",
|
|
45
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.164",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.197",
|
|
47
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.169",
|
|
48
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.300",
|
|
49
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.34"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
53
53
|
"sury-ppx": "11.0.0",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.103"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "^12.3.1"
|
package/src/BakedManifest.res
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Writes the curated component manifest where the local host shell serves its
|
|
2
|
-
// static assets from (`HostShellDist`), so `manifestUrl` discovery works in dev
|
|
2
|
+
// static assets from (the chosen `assetsDir`, else `HostShellDist`), so `manifestUrl` discovery works in dev
|
|
3
3
|
// exactly as it does in a deployment. Rewritten on every boot, which also makes
|
|
4
4
|
// it self-healing — a `pnpm install` that replaces the package directory costs
|
|
5
5
|
// one restart, not a debugging session.
|
|
@@ -30,6 +30,8 @@ let files = (~config: ReventlessInfra.Platform.bakedManifest): array<(
|
|
|
30
30
|
let emit = (
|
|
31
31
|
~structures: array<(string, Reventless.Plugin.pluginStructure)>,
|
|
32
32
|
~config: ReventlessInfra.Platform.bakedManifest,
|
|
33
|
+
// The platform's chosen `assetsDir`; unset, the host-shell package's `dist/`.
|
|
34
|
+
~dir: option<string>=?,
|
|
33
35
|
) => {
|
|
34
36
|
let outputs = files(~config)
|
|
35
37
|
// Every file is curated before any is written. A declaration naming a
|
|
@@ -42,7 +44,10 @@ let emit = (
|
|
|
42
44
|
| Ok(manifest) => manifest
|
|
43
45
|
},
|
|
44
46
|
))
|
|
45
|
-
switch
|
|
47
|
+
switch switch dir {
|
|
48
|
+
| Some(_) as given => given
|
|
49
|
+
| None => HostShellDist.dir()
|
|
50
|
+
} {
|
|
46
51
|
| None =>
|
|
47
52
|
JsError.throwWithMessage(
|
|
48
53
|
`baked manifest: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
@@ -11,7 +11,7 @@ let log = Logger$ReventlessCore.fromEnv();
|
|
|
11
11
|
|
|
12
12
|
let files = Platform_BakedManifest$ReventlessCore.files;
|
|
13
13
|
|
|
14
|
-
function emit(structures, config) {
|
|
14
|
+
function emit(structures, config, dir) {
|
|
15
15
|
let outputs = Platform_BakedManifest$ReventlessCore.files(config);
|
|
16
16
|
let curated = outputs.map(param => {
|
|
17
17
|
let e = Platform_BakedManifest$ReventlessCore.curate(structures, param[1]);
|
|
@@ -22,11 +22,11 @@ function emit(structures, config) {
|
|
|
22
22
|
tmp
|
|
23
23
|
];
|
|
24
24
|
});
|
|
25
|
-
let dir = HostShellDist$ReventlessLocal.dir();
|
|
26
|
-
if (dir !== undefined) {
|
|
25
|
+
let dir$1 = dir !== undefined ? dir : HostShellDist$ReventlessLocal.dir();
|
|
26
|
+
if (dir$1 !== undefined) {
|
|
27
27
|
curated.forEach(param => {
|
|
28
28
|
let key = param[0];
|
|
29
|
-
let path = Nodepath.join(dir, key);
|
|
29
|
+
let path = Nodepath.join(dir$1, key);
|
|
30
30
|
Nodefs.writeFileSync(path, JSON.stringify(param[1], undefined, 2), "utf8");
|
|
31
31
|
log.info("BakedManifest", undefined, `wrote ` + key + `: ` + path);
|
|
32
32
|
});
|
package/src/HostShellDist.res
CHANGED
|
@@ -27,3 +27,14 @@ let dir = (): option<string> =>
|
|
|
27
27
|
} catch {
|
|
28
28
|
| _ => None
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/** The directory the local writers write into: the app's chosen `assetsDir`,
|
|
32
|
+
created if missing, else the package's `dist/`. */
|
|
33
|
+
let resolve = (~assetsDir: option<string>): option<string> =>
|
|
34
|
+
switch assetsDir {
|
|
35
|
+
| Some(chosen) =>
|
|
36
|
+
let absolute = NodePath.resolve([NodeProcess.cwd(), chosen])
|
|
37
|
+
NodeFs.mkdirSync(absolute, {recursive: true})
|
|
38
|
+
Some(absolute)
|
|
39
|
+
| None => dir()
|
|
40
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
3
4
|
import * as Nodepath from "node:path";
|
|
4
5
|
import * as Nodemodule from "node:module";
|
|
5
6
|
|
|
@@ -13,8 +14,20 @@ function dir() {
|
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function resolve(assetsDir) {
|
|
18
|
+
if (assetsDir === undefined) {
|
|
19
|
+
return dir();
|
|
20
|
+
}
|
|
21
|
+
let absolute = Nodepath.resolve(process.cwd(), assetsDir);
|
|
22
|
+
Nodefs.mkdirSync(absolute, {
|
|
23
|
+
recursive: true
|
|
24
|
+
});
|
|
25
|
+
return absolute;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export {
|
|
17
29
|
$$package,
|
|
18
30
|
dir,
|
|
31
|
+
resolve,
|
|
19
32
|
}
|
|
20
|
-
/* node:
|
|
33
|
+
/* node:fs Not a pure module */
|
package/src/Platform.res
CHANGED
|
@@ -928,6 +928,7 @@ module MakeWithConfig = (
|
|
|
928
928
|
let bakeManifest = (
|
|
929
929
|
~pluginComponents: array<ReventlessCore.Plugin.component>,
|
|
930
930
|
~config: ReventlessInfra.Platform.bakedManifest,
|
|
931
|
+
~dir: option<string>,
|
|
931
932
|
) => {
|
|
932
933
|
let resolved: array<(string, Reventless.Plugin.pluginStructure)> = []
|
|
933
934
|
let expected = pluginComponents->Array.length
|
|
@@ -942,7 +943,7 @@ module MakeWithConfig = (
|
|
|
942
943
|
| None => ()
|
|
943
944
|
}
|
|
944
945
|
if resolved->Array.length === expected {
|
|
945
|
-
BakedManifest.emit(~structures=resolved, ~config)
|
|
946
|
+
BakedManifest.emit(~structures=resolved, ~config, ~dir?)
|
|
946
947
|
}
|
|
947
948
|
})
|
|
948
949
|
})
|
|
@@ -1518,13 +1519,15 @@ module MakeWithConfig = (
|
|
|
1518
1519
|
})
|
|
1519
1520
|
|
|
1520
1521
|
// In-memory ignores the infrastructure half of `~hostUiBundle` — the host shell
|
|
1521
|
-
// is served from
|
|
1522
|
-
// deployment *says* is
|
|
1522
|
+
// is served from a local directory (`assetsDir`, else the installed package's
|
|
1523
|
+
// own `dist/`), not from a CDN. What a deployment *says* is honored, in the same files the AWS deploy writes
|
|
1523
1524
|
// beside each other: `bakedManifest` through `BakedManifest`, `shellConfig`
|
|
1524
1525
|
// through `ShellConfig`, which overlays the served `config.json` the way the
|
|
1525
1526
|
// AWS deploy composes the one it writes, and `uiHintsFile` through `UiHints`,
|
|
1526
1527
|
// which replaces the served hints file the way the deploy uploads it.
|
|
1527
1528
|
type hostUiBundleConfig = {
|
|
1529
|
+
// Where every file below is written, as on AWS. Unset: the host-shell
|
|
1530
|
+
// package's `dist/`.
|
|
1528
1531
|
assetsDir?: string,
|
|
1529
1532
|
bundleVersion?: string,
|
|
1530
1533
|
// `makePlatform` serves the declared file in place of the host-shell
|
|
@@ -1736,7 +1739,14 @@ module MakeWithConfig = (
|
|
|
1736
1739
|
// structure Output to land. Unconditional, unlike the bake, because it is
|
|
1737
1740
|
// also what restores the shipped file for a platform that has stopped
|
|
1738
1741
|
// declaring anything.
|
|
1742
|
+
//
|
|
1743
|
+
// All four writers share one directory, resolved once: a second opinion puts
|
|
1744
|
+
// a file where nothing reads it.
|
|
1745
|
+
let hostUiDir = HostShellDist.resolve(
|
|
1746
|
+
~assetsDir=hostUiBundle->Option.flatMap(cfg => cfg.assetsDir),
|
|
1747
|
+
)
|
|
1739
1748
|
ShellConfig.emit(
|
|
1749
|
+
~dir=?hostUiDir,
|
|
1740
1750
|
~bakedManifest=hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest),
|
|
1741
1751
|
// Names the module `UiSlots.emit` serves below. Both or neither: a key
|
|
1742
1752
|
// without the file is a 404 the shell survives quietly, and a file without
|
|
@@ -1747,7 +1757,7 @@ module MakeWithConfig = (
|
|
|
1747
1757
|
// Unconditional for the same reason, and one file over: it is also what
|
|
1748
1758
|
// restores the shell package's own hints for a platform that has stopped
|
|
1749
1759
|
// declaring its own.
|
|
1750
|
-
UiHints.emit(~uiHintsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiHintsFile))
|
|
1760
|
+
UiHints.emit(~uiHintsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiHintsFile), ~dir=?hostUiDir)
|
|
1751
1761
|
// The dev loop for the file just written. Editing hints is presentation
|
|
1752
1762
|
// work — you change a label, you want to see the label — and routing that
|
|
1753
1763
|
// through a platform restart puts fifteen seconds between the two. The
|
|
@@ -1759,20 +1769,22 @@ module MakeWithConfig = (
|
|
|
1759
1769
|
// would imply a shutdown path this platform does not have.
|
|
1760
1770
|
let _ = UiHints.watch(
|
|
1761
1771
|
~uiHintsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiHintsFile),
|
|
1772
|
+
~dir=?hostUiDir,
|
|
1762
1773
|
~onReload=LocalEvents_Server.broadcastUiHintsChanged,
|
|
1763
1774
|
)
|
|
1764
1775
|
// The same pair for the module that draws what a hint cannot say. Also
|
|
1765
1776
|
// unconditional, and for a reason the hints file does not have: this is what
|
|
1766
1777
|
// removes a module a platform has stopped declaring, and leaving one behind
|
|
1767
1778
|
// would go on drawing regions with nothing in the deployment to explain it.
|
|
1768
|
-
UiSlots.emit(~uiSlotsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiSlotsFile))
|
|
1779
|
+
UiSlots.emit(~uiSlotsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiSlotsFile), ~dir=?hostUiDir)
|
|
1769
1780
|
let _ = UiSlots.watch(
|
|
1770
1781
|
~uiSlotsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiSlotsFile),
|
|
1782
|
+
~dir=?hostUiDir,
|
|
1771
1783
|
~onReload=LocalEvents_Server.broadcastUiSlotsChanged,
|
|
1772
1784
|
)
|
|
1773
1785
|
switch hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest) {
|
|
1774
1786
|
| None => ()
|
|
1775
|
-
| Some(cfg) => bakeManifest(~pluginComponents=plugins, ~config=cfg)
|
|
1787
|
+
| Some(cfg) => bakeManifest(~pluginComponents=plugins, ~config=cfg, ~dir=hostUiDir)
|
|
1776
1788
|
}
|
|
1777
1789
|
switch (projectionCatchup, pgProjectionCatchup) {
|
|
1778
1790
|
| (Some((db, upperBound, dcbUpperBound)), _) =>
|
package/src/Platform.res.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
|
|
|
38
38
|
import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
|
|
39
39
|
import * as Task_Builder$ReventlessLocal from "./components/Task_Builder.res.mjs";
|
|
40
40
|
import * as BakedManifest$ReventlessLocal from "./BakedManifest.res.mjs";
|
|
41
|
+
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
41
42
|
import * as LocalEventTap$ReventlessLocal from "./adapter/LocalEventTap.res.mjs";
|
|
42
43
|
import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
|
|
43
44
|
import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginBehavior.res.mjs";
|
|
@@ -860,7 +861,7 @@ function MakeWithConfig(Config) {
|
|
|
860
861
|
let pluginStructuresStore = {
|
|
861
862
|
contents: {}
|
|
862
863
|
};
|
|
863
|
-
let bakeManifest = (pluginComponents, config) => {
|
|
864
|
+
let bakeManifest = (pluginComponents, config, dir) => {
|
|
864
865
|
let resolved = [];
|
|
865
866
|
let expected = pluginComponents.length;
|
|
866
867
|
pluginComponents.forEach(plugin => {
|
|
@@ -877,7 +878,7 @@ function MakeWithConfig(Config) {
|
|
|
877
878
|
]);
|
|
878
879
|
}
|
|
879
880
|
if (resolved.length === expected) {
|
|
880
|
-
return BakedManifest$ReventlessLocal.emit(resolved, config);
|
|
881
|
+
return BakedManifest$ReventlessLocal.emit(resolved, config, dir);
|
|
881
882
|
}
|
|
882
883
|
});
|
|
883
884
|
});
|
|
@@ -1330,14 +1331,15 @@ function MakeWithConfig(Config) {
|
|
|
1330
1331
|
adminResources: []
|
|
1331
1332
|
});
|
|
1332
1333
|
subscribeToPluginEvents();
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
UiHints$ReventlessLocal.
|
|
1336
|
-
|
|
1337
|
-
UiSlots$ReventlessLocal.
|
|
1334
|
+
let hostUiDir = HostShellDist$ReventlessLocal.resolve(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.assetsDir));
|
|
1335
|
+
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), hostUiDir);
|
|
1336
|
+
UiHints$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir);
|
|
1337
|
+
UiHints$ReventlessLocal.watch(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir, LocalEvents_Server$ReventlessLocal.broadcastUiHintsChanged);
|
|
1338
|
+
UiSlots$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), hostUiDir);
|
|
1339
|
+
UiSlots$ReventlessLocal.watch(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), hostUiDir, LocalEvents_Server$ReventlessLocal.broadcastUiSlotsChanged);
|
|
1338
1340
|
let cfg = Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest);
|
|
1339
1341
|
if (cfg !== undefined) {
|
|
1340
|
-
bakeManifest(plugins$1, cfg);
|
|
1342
|
+
bakeManifest(plugins$1, cfg, hostUiDir);
|
|
1341
1343
|
}
|
|
1342
1344
|
if (projectionCatchup !== undefined) {
|
|
1343
1345
|
Stdlib_Promise.$$catch(ProjectionCheckpoint$ReventlessLocal.startupCatchup(projectionCatchup[0], projectionCatchup[1], projectionCatchup[2], () => Bus.projectionCatchupHandlers()), e => {
|
|
@@ -2620,7 +2622,7 @@ function Make($star) {
|
|
|
2620
2622
|
let pluginStructuresStore = {
|
|
2621
2623
|
contents: {}
|
|
2622
2624
|
};
|
|
2623
|
-
let bakeManifest = (pluginComponents, config) => {
|
|
2625
|
+
let bakeManifest = (pluginComponents, config, dir) => {
|
|
2624
2626
|
let resolved = [];
|
|
2625
2627
|
let expected = pluginComponents.length;
|
|
2626
2628
|
pluginComponents.forEach(plugin => {
|
|
@@ -2637,7 +2639,7 @@ function Make($star) {
|
|
|
2637
2639
|
]);
|
|
2638
2640
|
}
|
|
2639
2641
|
if (resolved.length === expected) {
|
|
2640
|
-
return BakedManifest$ReventlessLocal.emit(resolved, config);
|
|
2642
|
+
return BakedManifest$ReventlessLocal.emit(resolved, config, dir);
|
|
2641
2643
|
}
|
|
2642
2644
|
});
|
|
2643
2645
|
});
|
|
@@ -3085,14 +3087,15 @@ function Make($star) {
|
|
|
3085
3087
|
adminResources: []
|
|
3086
3088
|
});
|
|
3087
3089
|
subscribeToPluginEvents();
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
UiHints$ReventlessLocal.
|
|
3091
|
-
|
|
3092
|
-
UiSlots$ReventlessLocal.
|
|
3090
|
+
let hostUiDir = HostShellDist$ReventlessLocal.resolve(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.assetsDir));
|
|
3091
|
+
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), hostUiDir);
|
|
3092
|
+
UiHints$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir);
|
|
3093
|
+
UiHints$ReventlessLocal.watch(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir, LocalEvents_Server$ReventlessLocal.broadcastUiHintsChanged);
|
|
3094
|
+
UiSlots$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), hostUiDir);
|
|
3095
|
+
UiSlots$ReventlessLocal.watch(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), hostUiDir, LocalEvents_Server$ReventlessLocal.broadcastUiSlotsChanged);
|
|
3093
3096
|
let cfg = Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest);
|
|
3094
3097
|
if (cfg !== undefined) {
|
|
3095
|
-
bakeManifest(plugins$1, cfg);
|
|
3098
|
+
bakeManifest(plugins$1, cfg, hostUiDir);
|
|
3096
3099
|
}
|
|
3097
3100
|
if (projectionCatchup !== undefined) {
|
|
3098
3101
|
Stdlib_Promise.$$catch(ProjectionCheckpoint$ReventlessLocal.startupCatchup(projectionCatchup[0], projectionCatchup[1], projectionCatchup[2], () => Bus.projectionCatchupHandlers()), e => {
|
package/src/ShellConfig.res
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
// `pnpm install` that replaces the package directory costs one restart, because
|
|
18
18
|
// the baseline is re-seeded from whatever the new package ships.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// The computed keys are the environment (always `local`), `manifestUrl` and the
|
|
21
|
+
// journey map (from the bake declaration — local knows where it put the file),
|
|
22
|
+
// `uiSlotsUrl` and `adminGroup`; the rest are the deployment's own `shellConfig`,
|
|
22
23
|
// passed through verbatim. A `shellConfig` that names a computed key fails the
|
|
23
24
|
// same way it does on AWS: a passthrough cannot redirect a key the platform
|
|
24
25
|
// decides, and silently resolving it either way points the shell somewhere
|
|
@@ -41,7 +42,10 @@ let manifestUrlOf = (config: ReventlessInfra.Platform.bakedManifest): string =>
|
|
|
41
42
|
// caller matching no declared group gets and what every existing deployment has.
|
|
42
43
|
let journeyManifestsKey = "journeyManifestUrls"
|
|
43
44
|
|
|
45
|
+
let environmentKeys = [Reventless.ShellEnvironment.nameKey, Reventless.ShellEnvironment.purposeKey]
|
|
46
|
+
|
|
44
47
|
let computedKeys = [
|
|
48
|
+
...environmentKeys,
|
|
45
49
|
"manifestUrl",
|
|
46
50
|
journeyManifestsKey,
|
|
47
51
|
ReventlessCore.Platform_UiSlots.configKey,
|
|
@@ -60,7 +64,10 @@ let overlay = (
|
|
|
60
64
|
~uiSlotsFile: option<string>=?,
|
|
61
65
|
~shellConfig: option<dict<JSON.t>>,
|
|
62
66
|
): dict<JSON.t> => {
|
|
63
|
-
let out =
|
|
67
|
+
let out =
|
|
68
|
+
Reventless.ShellEnvironment.local
|
|
69
|
+
->Reventless.ShellEnvironment.configFields
|
|
70
|
+
->Dict.fromArray
|
|
64
71
|
|
|
65
72
|
// `UiSlots` serves the module; this is what tells the shell to import it. The
|
|
66
73
|
// two are separate writes to separate files, which is exactly why the name
|
|
@@ -120,23 +127,22 @@ let readObject = (~path: string, ~label: string): dict<JSON.t> =>
|
|
|
120
127
|
/**
|
|
121
128
|
Merge the overlay onto the shipped `config.json` in the host shell's `dist/`.
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
The environment keys are always written, so the shell can say it is local;
|
|
131
|
+
without the package they are dropped silently, since a platform nobody points a
|
|
132
|
+
browser at has nothing to badge. Anything declared beyond them is written or
|
|
133
|
+
fails loudly, for the reason `BakedManifest.emit` gives: both failure modes it
|
|
134
|
+
can hit are the deployment's own mistake, and both produce the same silent
|
|
135
|
+
symptom if swallowed.
|
|
129
136
|
*/
|
|
130
137
|
let emit = (
|
|
131
138
|
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>,
|
|
132
139
|
~uiSlotsFile: option<string>=?,
|
|
133
140
|
~shellConfig: option<dict<JSON.t>>,
|
|
134
|
-
//
|
|
135
|
-
// it — "boot twice and the second write still starts from the shipped file" is
|
|
136
|
-
// not a property a pure function can carry.
|
|
141
|
+
// The platform's chosen `assetsDir`; unset, the host-shell package's `dist/`.
|
|
137
142
|
~dir: option<string>=?,
|
|
138
143
|
) => {
|
|
139
144
|
let overlay = overlay(~bakedManifest, ~uiSlotsFile?, ~shellConfig)
|
|
145
|
+
let declared = overlay->Dict.keysToArray->Array.filter(k => !(environmentKeys->Array.includes(k)))
|
|
140
146
|
|
|
141
147
|
switch switch dir {
|
|
142
148
|
| Some(_) as given => given
|
|
@@ -146,7 +152,7 @@ let emit = (
|
|
|
146
152
|
// Nothing declared and no shell installed is the ordinary case for a
|
|
147
153
|
// platform nobody points a browser at; only a declaration makes the missing
|
|
148
154
|
// package an error, and then it is the same error the bake gives.
|
|
149
|
-
if
|
|
155
|
+
if declared->Array.length > 0 {
|
|
150
156
|
JsError.throwWithMessage(
|
|
151
157
|
`host UI config.json: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
152
158
|
`the local shell reads its config from that package's dist/, so declaring shell ` ++
|
|
@@ -157,29 +163,26 @@ let emit = (
|
|
|
157
163
|
let path = NodePath.join([dir, fileName])
|
|
158
164
|
let baselinePath = NodePath.join([dir, baselineFileName])
|
|
159
165
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
// The baseline is whatever `config.json` the directory held first: the
|
|
167
|
+
// shell's shipped file, or the app's own in a chosen `assetsDir`.
|
|
168
|
+
if !NodeFs.existsSync(baselinePath) {
|
|
169
|
+
if NodeFs.existsSync(path) {
|
|
170
|
+
NodeFs.writeFileSync(baselinePath, NodeFs.readFileSync(path))
|
|
171
|
+
} else if declared->Array.length > 0 {
|
|
165
172
|
JsError.throwWithMessage(
|
|
166
|
-
`host UI config.json:
|
|
173
|
+
`host UI config.json: there is no ${fileName} at ${dir} — ` ++
|
|
167
174
|
`there is no baseline to overlay, so the shell would boot with only the keys ` ++ `declared here and none of the ones it expects.`,
|
|
168
175
|
)
|
|
169
176
|
}
|
|
170
|
-
NodeFs.writeFileSync(baselinePath, NodeFs.readFileSync(path))
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
if NodeFs.existsSync(baselinePath) {
|
|
174
180
|
let merged = readObject(~path=baselinePath, ~label="config baseline")
|
|
175
181
|
overlay->Dict.forEachWithKey((v, k) => merged->Dict.set(k, v))
|
|
176
182
|
NodeFs.writeFileSync(path, JSON.stringify(merged->JSON.Encode.object, ~space=2))
|
|
177
|
-
let keys = overlay->Dict.keysToArray
|
|
178
183
|
log.info(
|
|
179
184
|
~comp="ShellConfig",
|
|
180
|
-
|
|
181
|
-
? `restored ${fileName} from ${baselineFileName}: ${path}`
|
|
182
|
-
: `wrote ${fileName} with ${keys->Array.join(", ")}: ${path}`,
|
|
185
|
+
`wrote ${fileName} with ${overlay->Dict.keysToArray->Array.join(", ")}: ${path}`,
|
|
183
186
|
)
|
|
184
187
|
}
|
|
185
188
|
}
|
package/src/ShellConfig.res.mjs
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
|
|
5
6
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
6
7
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
7
8
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
9
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
10
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
11
|
+
import * as ShellEnvironment$Reventless from "@reventlessdev/reventless-spec/src/types/ShellEnvironment.res.mjs";
|
|
10
12
|
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
11
13
|
import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
|
|
12
14
|
import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
|
|
@@ -24,15 +26,23 @@ function manifestUrlOf(config) {
|
|
|
24
26
|
|
|
25
27
|
let journeyManifestsKey = "journeyManifestUrls";
|
|
26
28
|
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Platform_UiSlots$ReventlessCore.configKey,
|
|
31
|
-
Platform_AdminGroup$ReventlessCore.configKey
|
|
29
|
+
let environmentKeys = [
|
|
30
|
+
ShellEnvironment$Reventless.nameKey,
|
|
31
|
+
ShellEnvironment$Reventless.purposeKey
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
let computedKeys = Belt_Array.concatMany([
|
|
35
|
+
environmentKeys,
|
|
36
|
+
[
|
|
37
|
+
"manifestUrl",
|
|
38
|
+
journeyManifestsKey,
|
|
39
|
+
Platform_UiSlots$ReventlessCore.configKey,
|
|
40
|
+
Platform_AdminGroup$ReventlessCore.configKey
|
|
41
|
+
]
|
|
42
|
+
]);
|
|
43
|
+
|
|
34
44
|
function overlay(bakedManifest, uiSlotsFile, shellConfig) {
|
|
35
|
-
let out =
|
|
45
|
+
let out = Object.fromEntries(ShellEnvironment$Reventless.configFields(ShellEnvironment$Reventless.local));
|
|
36
46
|
Stdlib_Option.forEach(uiSlotsFile, param => {
|
|
37
47
|
out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
|
|
38
48
|
});
|
|
@@ -78,9 +88,10 @@ function readObject(path, label) {
|
|
|
78
88
|
|
|
79
89
|
function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
80
90
|
let overlay$1 = overlay(bakedManifest, uiSlotsFile, shellConfig);
|
|
91
|
+
let declared = Object.keys(overlay$1).filter(k => !environmentKeys.includes(k));
|
|
81
92
|
let dir$1 = dir !== undefined ? dir : HostShellDist$ReventlessLocal.dir();
|
|
82
93
|
if (dir$1 === undefined) {
|
|
83
|
-
if (
|
|
94
|
+
if (declared.length !== 0) {
|
|
84
95
|
return Stdlib_JsError.throwWithMessage(`host UI config.json: cannot resolve ` + HostShellDist$ReventlessLocal.$$package + ` from ` + process.cwd() + ` — the local shell reads its config from that package's dist/, so declaring shell config without the package installed would write nothing and leave the shell configured as it shipped.`);
|
|
85
96
|
} else {
|
|
86
97
|
return;
|
|
@@ -88,11 +99,12 @@ function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
|
88
99
|
}
|
|
89
100
|
let path = Nodepath.join(dir$1, fileName);
|
|
90
101
|
let baselinePath = Nodepath.join(dir$1, baselineFileName);
|
|
91
|
-
if (!Nodefs.existsSync(baselinePath)
|
|
92
|
-
if (
|
|
93
|
-
|
|
102
|
+
if (!Nodefs.existsSync(baselinePath)) {
|
|
103
|
+
if (Nodefs.existsSync(path)) {
|
|
104
|
+
Nodefs.writeFileSync(baselinePath, Nodefs.readFileSync(path, "utf8"), "utf8");
|
|
105
|
+
} else if (declared.length !== 0) {
|
|
106
|
+
Stdlib_JsError.throwWithMessage(`host UI config.json: there is no ` + fileName + ` at ` + dir$1 + ` — there is no baseline to overlay, so the shell would boot with only the keys declared here and none of the ones it expects.`);
|
|
94
107
|
}
|
|
95
|
-
Nodefs.writeFileSync(baselinePath, Nodefs.readFileSync(path, "utf8"), "utf8");
|
|
96
108
|
}
|
|
97
109
|
if (!Nodefs.existsSync(baselinePath)) {
|
|
98
110
|
return;
|
|
@@ -102,8 +114,7 @@ function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
|
102
114
|
merged[k] = v;
|
|
103
115
|
});
|
|
104
116
|
Nodefs.writeFileSync(path, JSON.stringify(merged, undefined, 2), "utf8");
|
|
105
|
-
|
|
106
|
-
log.info("ShellConfig", undefined, keys.length === 0 ? `restored ` + fileName + ` from ` + baselineFileName + `: ` + path : `wrote ` + fileName + ` with ` + keys.join(", ") + `: ` + path);
|
|
117
|
+
log.info("ShellConfig", undefined, `wrote ` + fileName + ` with ` + Object.keys(overlay$1).join(", ") + `: ` + path);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
export {
|
|
@@ -112,6 +123,7 @@ export {
|
|
|
112
123
|
baselineFileName,
|
|
113
124
|
manifestUrlOf,
|
|
114
125
|
journeyManifestsKey,
|
|
126
|
+
environmentKeys,
|
|
115
127
|
computedKeys,
|
|
116
128
|
overlay,
|
|
117
129
|
readObject,
|
package/src/UiHints.res
CHANGED
|
@@ -37,9 +37,8 @@ let baselineFileName = "ui-hints.base.json"
|
|
|
37
37
|
*/
|
|
38
38
|
let emit = (
|
|
39
39
|
~uiHintsFile: option<string>,
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// shipped file" is not a property a pure function can carry.
|
|
40
|
+
// Where the file is served from: the platform's chosen `assetsDir`; unset, the
|
|
41
|
+
// host-shell package's `dist/`.
|
|
43
42
|
~dir: option<string>=?,
|
|
44
43
|
) => {
|
|
45
44
|
// Read before anything is touched, so a bad declaration cannot leave the
|
|
@@ -125,8 +124,7 @@ let emit = (
|
|
|
125
124
|
*/
|
|
126
125
|
let watch = (
|
|
127
126
|
~uiHintsFile: option<string>,
|
|
128
|
-
// The
|
|
129
|
-
// watch a file it wrote into a temp dir without a host-shell package on disk.
|
|
127
|
+
// The directory `emit` takes, threaded to it.
|
|
130
128
|
~dir: option<string>=?,
|
|
131
129
|
~onReload: unit => unit,
|
|
132
130
|
): option<NodeFs.watcher> =>
|
package/src/UiSlots.res
CHANGED
|
@@ -53,9 +53,8 @@ let fileName = ReventlessCore.Platform_UiSlots.fileName
|
|
|
53
53
|
*/
|
|
54
54
|
let emit = (
|
|
55
55
|
~uiSlotsFile: option<string>,
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// property a pure function can carry.
|
|
56
|
+
// Where the module is served from: the platform's chosen `assetsDir`; unset,
|
|
57
|
+
// the host-shell package's `dist/`.
|
|
59
58
|
~dir: option<string>=?,
|
|
60
59
|
) => {
|
|
61
60
|
// Read before anything is touched, so a bad declaration cannot leave the
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The one directory every local host-UI writer writes into. An app that composes
|
|
4
|
+
// its own UI bundle names it as `assetsDir`, as on AWS; a writer that took a
|
|
5
|
+
// second opinion would put its file where nothing serves it.
|
|
6
|
+
|
|
7
|
+
let _ = TestRunner.setup()
|
|
8
|
+
|
|
9
|
+
let tmpdir = prefix => NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), prefix]))
|
|
10
|
+
|
|
11
|
+
// What the package's dist/ holds, so a test can say it was not touched. Empty
|
|
12
|
+
// when the package is not installed where the test runs.
|
|
13
|
+
let snapshot = () =>
|
|
14
|
+
HostShellDist.dir()
|
|
15
|
+
->Option.filter(NodeFs.existsSync)
|
|
16
|
+
->Option.mapOr([], dir =>
|
|
17
|
+
NodeFs.readdirSyncNames(dir, {})
|
|
18
|
+
->Array.toSorted(String.compare)
|
|
19
|
+
->Array.map(name => {
|
|
20
|
+
let path = NodePath.join([dir, name])
|
|
21
|
+
(name, NodeFs.statSync(path)->NodeFs.mtimeMs)
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
describe("HostShellDist.resolve", () => {
|
|
26
|
+
testSync("is the package's dist/ when no directory is chosen", () =>
|
|
27
|
+
expect(HostShellDist.resolve(~assetsDir=None))->toEqual(HostShellDist.dir())
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
testSync("is the chosen directory, created when missing", () => {
|
|
31
|
+
let chosen = NodePath.join([tmpdir("reventless-assetsdir-"), "app", "dist"])
|
|
32
|
+
expect(HostShellDist.resolve(~assetsDir=Some(chosen)))->toEqual(Some(chosen))
|
|
33
|
+
expect(NodeFs.existsSync(chosen))->toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("the local writers in a chosen assetsDir", () => {
|
|
38
|
+
testSync("all four write there and leave the package's dist/ alone", () => {
|
|
39
|
+
let before = snapshot()
|
|
40
|
+
let dir =
|
|
41
|
+
HostShellDist.resolve(
|
|
42
|
+
~assetsDir=Some(NodePath.join([tmpdir("reventless-assetsdir-"), "ui"])),
|
|
43
|
+
)->Option.getOrThrow
|
|
44
|
+
// The app's own config.json is the baseline the overlay goes on.
|
|
45
|
+
NodeFs.writeFileSync(NodePath.join([dir, "config.json"]), `{"appName": "App"}`)
|
|
46
|
+
let source = name => {
|
|
47
|
+
let path = NodePath.join([tmpdir("reventless-assetsdir-src-"), name])
|
|
48
|
+
NodeFs.writeFileSync(
|
|
49
|
+
path,
|
|
50
|
+
name->String.endsWith(".json") ? "{}" : "export function register() {}",
|
|
51
|
+
)
|
|
52
|
+
path
|
|
53
|
+
}
|
|
54
|
+
let bake: ReventlessInfra.Platform.bakedManifest = {components: []}
|
|
55
|
+
|
|
56
|
+
ShellConfig.emit(~bakedManifest=Some(bake), ~shellConfig=None, ~dir)
|
|
57
|
+
UiHints.emit(~uiHintsFile=Some(source("ui-hints.json")), ~dir)
|
|
58
|
+
UiSlots.emit(~uiSlotsFile=Some(source("slots.js")), ~dir)
|
|
59
|
+
BakedManifest.emit(~structures=[], ~config=bake, ~dir)
|
|
60
|
+
|
|
61
|
+
expect(NodeFs.readdirSyncNames(dir, {})->Array.toSorted(String.compare))->toEqual([
|
|
62
|
+
"component-manifest.json",
|
|
63
|
+
"config.base.json",
|
|
64
|
+
"config.json",
|
|
65
|
+
"ui-hints.json",
|
|
66
|
+
"ui-slots.js",
|
|
67
|
+
])
|
|
68
|
+
expect(snapshot())->toEqual(before)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
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_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
|
+
import * as UiHints$ReventlessLocal from "../src/UiHints.res.mjs";
|
|
9
|
+
import * as UiSlots$ReventlessLocal from "../src/UiSlots.res.mjs";
|
|
10
|
+
import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
|
|
11
|
+
import * as ShellConfig$ReventlessLocal from "../src/ShellConfig.res.mjs";
|
|
12
|
+
import * as BakedManifest$ReventlessLocal from "../src/BakedManifest.res.mjs";
|
|
13
|
+
import * as HostShellDist$ReventlessLocal from "../src/HostShellDist.res.mjs";
|
|
14
|
+
|
|
15
|
+
TestRunner$ReventlessLocal.setup();
|
|
16
|
+
|
|
17
|
+
function tmpdir(prefix) {
|
|
18
|
+
return Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), prefix));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function snapshot() {
|
|
22
|
+
return Stdlib_Option.mapOr(Stdlib_Option.filter(HostShellDist$ReventlessLocal.dir(), prim => Nodefs.existsSync(prim)), [], dir => Nodefs.readdirSync(dir, {}).toSorted(Primitive_string.compare).map(name => {
|
|
23
|
+
let path = Nodepath.join(dir, name);
|
|
24
|
+
return [
|
|
25
|
+
name,
|
|
26
|
+
Nodefs.statSync(path).mtimeMs
|
|
27
|
+
];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
globalThis.describe("HostShellDist.resolve", () => {
|
|
32
|
+
globalThis.test("is the package's dist/ when no directory is chosen", () => {
|
|
33
|
+
globalThis.expect(HostShellDist$ReventlessLocal.resolve(undefined)).toEqual(HostShellDist$ReventlessLocal.dir());
|
|
34
|
+
});
|
|
35
|
+
globalThis.test("is the chosen directory, created when missing", () => {
|
|
36
|
+
let chosen = Nodepath.join(tmpdir("reventless-assetsdir-"), "app", "dist");
|
|
37
|
+
globalThis.expect(HostShellDist$ReventlessLocal.resolve(chosen)).toEqual(chosen);
|
|
38
|
+
globalThis.expect(Nodefs.existsSync(chosen)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
globalThis.describe("the local writers in a chosen assetsDir", () => {
|
|
43
|
+
globalThis.test("all four write there and leave the package's dist/ alone", () => {
|
|
44
|
+
let before = snapshot();
|
|
45
|
+
let dir = Stdlib_Option.getOrThrow(HostShellDist$ReventlessLocal.resolve(Nodepath.join(tmpdir("reventless-assetsdir-"), "ui")), undefined);
|
|
46
|
+
Nodefs.writeFileSync(Nodepath.join(dir, "config.json"), `{"appName": "App"}`, "utf8");
|
|
47
|
+
let source = name => {
|
|
48
|
+
let path = Nodepath.join(tmpdir("reventless-assetsdir-src-"), name);
|
|
49
|
+
Nodefs.writeFileSync(path, name.endsWith(".json") ? "{}" : "export function register() {}", "utf8");
|
|
50
|
+
return path;
|
|
51
|
+
};
|
|
52
|
+
let bake_components = [];
|
|
53
|
+
let bake = {
|
|
54
|
+
components: bake_components
|
|
55
|
+
};
|
|
56
|
+
ShellConfig$ReventlessLocal.emit(bake, undefined, undefined, dir);
|
|
57
|
+
UiHints$ReventlessLocal.emit(source("ui-hints.json"), dir);
|
|
58
|
+
UiSlots$ReventlessLocal.emit(source("slots.js"), dir);
|
|
59
|
+
BakedManifest$ReventlessLocal.emit([], bake, dir);
|
|
60
|
+
globalThis.expect(Nodefs.readdirSync(dir, {}).toSorted(Primitive_string.compare)).toEqual([
|
|
61
|
+
"component-manifest.json",
|
|
62
|
+
"config.base.json",
|
|
63
|
+
"config.json",
|
|
64
|
+
"ui-hints.json",
|
|
65
|
+
"ui-slots.js"
|
|
66
|
+
]);
|
|
67
|
+
globalThis.expect(snapshot()).toEqual(before);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
tmpdir,
|
|
73
|
+
snapshot,
|
|
74
|
+
}
|
|
75
|
+
/* Not a pure module */
|
|
@@ -26,11 +26,35 @@ let threw = (f: unit => unit): bool =>
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe("ShellConfig.overlay", () => {
|
|
29
|
-
testSync("
|
|
29
|
+
testSync("carries only the environment when the deployment declares nothing", () => {
|
|
30
30
|
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
31
|
-
expect(out->Dict.keysToArray)->toEqual([])
|
|
31
|
+
expect(out->Dict.keysToArray)->toEqual(["environment", "environmentPurpose"])
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
+
// The shell badges everything that is not production; locally it says so.
|
|
35
|
+
testSync("names the environment and its purpose local", () => {
|
|
36
|
+
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
37
|
+
expect((out->str("environment"), out->str("environmentPurpose")))->toEqual((
|
|
38
|
+
Some("local"),
|
|
39
|
+
Some("local"),
|
|
40
|
+
))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
testSync("refuses a shellConfig naming either environment key", () =>
|
|
44
|
+
expect(
|
|
45
|
+
["environment", "environmentPurpose"]->Array.map(
|
|
46
|
+
key =>
|
|
47
|
+
threw(
|
|
48
|
+
() =>
|
|
49
|
+
ShellConfig.overlay(
|
|
50
|
+
~bakedManifest=None,
|
|
51
|
+
~shellConfig=Some(Dict.fromArray([(key, JSON.Encode.string("prod"))])),
|
|
52
|
+
)->ignore,
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
)->toEqual([true, true])
|
|
56
|
+
)
|
|
57
|
+
|
|
34
58
|
testSync("points manifestUrl at the bake's default key", () => {
|
|
35
59
|
let out = ShellConfig.overlay(~bakedManifest=Some(manifest()), ~shellConfig=None)
|
|
36
60
|
expect(out->str("manifestUrl"))->toEqual(Some("/component-manifest.json"))
|
|
@@ -147,11 +171,25 @@ describe("ShellConfig.emit", () => {
|
|
|
147
171
|
expect(out->str("apiEndpoint"))->toEqual(Some("/graphql"))
|
|
148
172
|
})
|
|
149
173
|
|
|
150
|
-
testSync("
|
|
174
|
+
testSync("adds only the environment for a platform that declares nothing", () => {
|
|
151
175
|
let dir = distWithConfig()
|
|
152
176
|
ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
|
|
153
|
-
|
|
154
|
-
expect(
|
|
177
|
+
let out = readConfig(dir)
|
|
178
|
+
expect(out->Dict.keysToArray)->toEqual([
|
|
179
|
+
"apiEndpoint",
|
|
180
|
+
"appName",
|
|
181
|
+
"environment",
|
|
182
|
+
"environmentPurpose",
|
|
183
|
+
])
|
|
184
|
+
expect(readBaseline(dir)->str("appName"))->toEqual(Some("Shipped"))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// The environment alone is no declaration: a directory with no config.json to
|
|
188
|
+
// overlay is left as it is rather than refused.
|
|
189
|
+
testSync("writes nothing and refuses nothing when only the environment has no baseline", () => {
|
|
190
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-shellconfig-"]))
|
|
191
|
+
ShellConfig.emit(~bakedManifest=None, ~shellConfig=None, ~dir)
|
|
192
|
+
expect(NodeFs.readdirSyncNames(dir, {}))->toEqual([])
|
|
155
193
|
})
|
|
156
194
|
|
|
157
195
|
// The whole reason a baseline is kept: boot 2 overlaying boot 1's output would
|
|
@@ -273,8 +311,8 @@ describe("ShellConfig.overlay — adminGroup", () => {
|
|
|
273
311
|
testSync("is absent while the administrator role keeps its own name", () => {
|
|
274
312
|
Reventless.Role.clearGroups()
|
|
275
313
|
expect(
|
|
276
|
-
ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)->Dict.
|
|
277
|
-
)->toEqual(
|
|
314
|
+
ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)->Dict.get("adminGroup"),
|
|
315
|
+
)->toEqual(None)
|
|
278
316
|
})
|
|
279
317
|
|
|
280
318
|
testSync("carries the group a renamed administrator role maps to", () => {
|
|
@@ -36,9 +36,36 @@ function threw(f) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
globalThis.describe("ShellConfig.overlay", () => {
|
|
39
|
-
globalThis.test("
|
|
39
|
+
globalThis.test("carries only the environment when the deployment declares nothing", () => {
|
|
40
40
|
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
|
|
41
|
-
globalThis.expect(Object.keys(out)).toEqual([
|
|
41
|
+
globalThis.expect(Object.keys(out)).toEqual([
|
|
42
|
+
"environment",
|
|
43
|
+
"environmentPurpose"
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
globalThis.test("names the environment and its purpose local", () => {
|
|
47
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
|
|
48
|
+
globalThis.expect([
|
|
49
|
+
str(out, "environment"),
|
|
50
|
+
str(out, "environmentPurpose")
|
|
51
|
+
]).toEqual([
|
|
52
|
+
"local",
|
|
53
|
+
"local"
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
globalThis.test("refuses a shellConfig naming either environment key", () => {
|
|
57
|
+
globalThis.expect([
|
|
58
|
+
"environment",
|
|
59
|
+
"environmentPurpose"
|
|
60
|
+
].map(key => threw(() => {
|
|
61
|
+
ShellConfig$ReventlessLocal.overlay(undefined, undefined, Object.fromEntries([[
|
|
62
|
+
key,
|
|
63
|
+
"prod"
|
|
64
|
+
]]));
|
|
65
|
+
}))).toEqual([
|
|
66
|
+
true,
|
|
67
|
+
true
|
|
68
|
+
]);
|
|
42
69
|
});
|
|
43
70
|
globalThis.test("points manifestUrl at the bake's default key", () => {
|
|
44
71
|
let out = ShellConfig$ReventlessLocal.overlay(manifest(undefined), undefined, undefined);
|
|
@@ -96,10 +123,9 @@ globalThis.describe("ShellConfig.overlay — uiSlotsFile", () => {
|
|
|
96
123
|
});
|
|
97
124
|
|
|
98
125
|
globalThis.describe("ShellConfig.emit", () => {
|
|
99
|
-
let shipped = `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`;
|
|
100
126
|
let distWithConfig = () => {
|
|
101
127
|
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
|
|
102
|
-
Nodefs.writeFileSync(Nodepath.join(dir, "config.json"),
|
|
128
|
+
Nodefs.writeFileSync(Nodepath.join(dir, "config.json"), `{\n "apiEndpoint": "/graphql",\n "appName": "Shipped"\n}`, "utf8");
|
|
103
129
|
return dir;
|
|
104
130
|
};
|
|
105
131
|
let readJson = path => Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(path, "utf8"))), undefined);
|
|
@@ -116,11 +142,22 @@ globalThis.describe("ShellConfig.emit", () => {
|
|
|
116
142
|
globalThis.expect(str(out, "manifestUrl")).toEqual("/component-manifest.json");
|
|
117
143
|
globalThis.expect(str(out, "apiEndpoint")).toEqual("/graphql");
|
|
118
144
|
});
|
|
119
|
-
globalThis.test("
|
|
145
|
+
globalThis.test("adds only the environment for a platform that declares nothing", () => {
|
|
120
146
|
let dir = distWithConfig();
|
|
121
147
|
ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
|
|
122
|
-
|
|
123
|
-
globalThis.expect(
|
|
148
|
+
let out = readConfig(dir);
|
|
149
|
+
globalThis.expect(Object.keys(out)).toEqual([
|
|
150
|
+
"apiEndpoint",
|
|
151
|
+
"appName",
|
|
152
|
+
"environment",
|
|
153
|
+
"environmentPurpose"
|
|
154
|
+
]);
|
|
155
|
+
globalThis.expect(str(readBaseline(dir), "appName")).toEqual("Shipped");
|
|
156
|
+
});
|
|
157
|
+
globalThis.test("writes nothing and refuses nothing when only the environment has no baseline", () => {
|
|
158
|
+
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-shellconfig-"));
|
|
159
|
+
ShellConfig$ReventlessLocal.emit(undefined, undefined, undefined, dir);
|
|
160
|
+
globalThis.expect(Nodefs.readdirSync(dir, {})).toEqual([]);
|
|
124
161
|
});
|
|
125
162
|
globalThis.test("starts every boot from the shipped file, not the last output", () => {
|
|
126
163
|
let dir = distWithConfig();
|
|
@@ -219,7 +256,7 @@ globalThis.describe("ShellConfig.overlay — journeys", () => {
|
|
|
219
256
|
globalThis.describe("ShellConfig.overlay — adminGroup", () => {
|
|
220
257
|
globalThis.test("is absent while the administrator role keeps its own name", () => {
|
|
221
258
|
Role$Reventless.clearGroups();
|
|
222
|
-
globalThis.expect(
|
|
259
|
+
globalThis.expect(ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined)["adminGroup"]).toEqual(undefined);
|
|
223
260
|
});
|
|
224
261
|
globalThis.test("carries the group a renamed administrator role maps to", () => {
|
|
225
262
|
Role$Reventless.clearGroups();
|
|
@@ -54,7 +54,6 @@ let DemandReportSpec = {
|
|
|
54
54
|
todoItemSchema: todoItemSchema,
|
|
55
55
|
commandSchema: commandSchema,
|
|
56
56
|
maxRetries: 2,
|
|
57
|
-
heartbeatInterval: 60,
|
|
58
57
|
targetName: targetName,
|
|
59
58
|
schedule: DemandReportSpec_schedule,
|
|
60
59
|
authorizationOf: authorizationOf,
|
|
@@ -124,7 +123,6 @@ let UnscheduledSpec = {
|
|
|
124
123
|
todoItemSchema: todoItemSchema,
|
|
125
124
|
commandSchema: commandSchema,
|
|
126
125
|
maxRetries: 2,
|
|
127
|
-
heartbeatInterval: 60,
|
|
128
126
|
targetName: targetName,
|
|
129
127
|
authorizationOf: authorizationOf,
|
|
130
128
|
commandTransition: commandTransition,
|
|
@@ -155,7 +153,6 @@ let FromClockUnscheduled = AutomationSlice$Reventless.Mapping.Make({
|
|
|
155
153
|
todoItemSchema: todoItemSchema,
|
|
156
154
|
commandSchema: commandSchema,
|
|
157
155
|
maxRetries: 2,
|
|
158
|
-
heartbeatInterval: 60,
|
|
159
156
|
targetName: targetName,
|
|
160
157
|
schedule: undefined
|
|
161
158
|
})({
|
|
@@ -180,7 +177,6 @@ AutomationSlice$Reventless.Mappings.Make({
|
|
|
180
177
|
todoItemSchema: todoItemSchema,
|
|
181
178
|
commandSchema: commandSchema,
|
|
182
179
|
maxRetries: 2,
|
|
183
|
-
heartbeatInterval: 60,
|
|
184
180
|
targetName: targetName,
|
|
185
181
|
schedule: undefined
|
|
186
182
|
});
|
|
@@ -208,7 +204,6 @@ let Unscheduled = AutomationSliceMaker.Make({
|
|
|
208
204
|
todoItemSchema: todoItemSchema,
|
|
209
205
|
commandSchema: commandSchema,
|
|
210
206
|
maxRetries: 2,
|
|
211
|
-
heartbeatInterval: 60,
|
|
212
207
|
targetName: targetName,
|
|
213
208
|
schedule: undefined
|
|
214
209
|
})(UnscheduledAutomation);
|
|
@@ -39,7 +39,6 @@ module ShipOrderSpec = {
|
|
|
39
39
|
type command = CreateShipment({orderId: @s.matches(Reventless.DcbTag.string) string})
|
|
40
40
|
|
|
41
41
|
let maxRetries = 3
|
|
42
|
-
let heartbeatInterval = 60
|
|
43
42
|
let targetName = "CreateShipment"
|
|
44
43
|
let schedule = None
|
|
45
44
|
}
|
|
@@ -55,7 +54,6 @@ module SkipProcessSpec = {
|
|
|
55
54
|
type command = Noop
|
|
56
55
|
|
|
57
56
|
let maxRetries = 0
|
|
58
|
-
let heartbeatInterval = 60
|
|
59
57
|
let targetName = "Noop"
|
|
60
58
|
let schedule = None
|
|
61
59
|
}
|
|
@@ -75,7 +75,6 @@ let ShipOrderSpec = {
|
|
|
75
75
|
todoItemSchema: todoItemSchema,
|
|
76
76
|
commandSchema: commandSchema,
|
|
77
77
|
maxRetries: 3,
|
|
78
|
-
heartbeatInterval: 60,
|
|
79
78
|
targetName: "CreateShipment",
|
|
80
79
|
schedule: undefined,
|
|
81
80
|
authorizationOf: authorizationOf,
|
|
@@ -107,7 +106,6 @@ let SkipProcessSpec = {
|
|
|
107
106
|
todoItemSchema: todoItemSchema$1,
|
|
108
107
|
commandSchema: commandSchema$1,
|
|
109
108
|
maxRetries: 0,
|
|
110
|
-
heartbeatInterval: 60,
|
|
111
109
|
targetName: "Noop",
|
|
112
110
|
schedule: undefined,
|
|
113
111
|
authorizationOf: authorizationOf$1,
|
package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs
CHANGED
|
@@ -44,7 +44,6 @@ let FireForgetCallback = OutboundTranslationSlice_Callback$ReventlessCore.Make({
|
|
|
44
44
|
outboundItemSchema: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.outboundItemSchema,
|
|
45
45
|
inboundCommandSchema: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.inboundCommandSchema,
|
|
46
46
|
maxRetries: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.maxRetries,
|
|
47
|
-
heartbeatInterval: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.heartbeatInterval,
|
|
48
47
|
targetName: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.targetName,
|
|
49
48
|
sourceNames: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.sourceNames,
|
|
50
49
|
externalSystem: OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.externalSystem,
|
|
@@ -59,7 +58,6 @@ let CommandBackCallback = OutboundTranslationSlice_Callback$ReventlessCore.Make(
|
|
|
59
58
|
outboundItemSchema: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.outboundItemSchema,
|
|
60
59
|
inboundCommandSchema: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.inboundCommandSchema,
|
|
61
60
|
maxRetries: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.maxRetries,
|
|
62
|
-
heartbeatInterval: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.heartbeatInterval,
|
|
63
61
|
targetName: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.targetName,
|
|
64
62
|
sourceNames: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.sourceNames,
|
|
65
63
|
externalSystem: OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.externalSystem,
|
|
@@ -38,7 +38,6 @@ module SendTrackingEmailSpec = {
|
|
|
38
38
|
let onExhausted = (_id, _item, ~lastError as _) => None
|
|
39
39
|
|
|
40
40
|
let maxRetries = 3
|
|
41
|
-
let heartbeatInterval = 60
|
|
42
41
|
let targetName = None
|
|
43
42
|
let sourceNames: array<string> = []
|
|
44
43
|
let externalSystem = None
|
|
@@ -86,7 +85,6 @@ module ProcessPaymentSpec = {
|
|
|
86
85
|
let onExhausted = (id, item, ~lastError) => onExhaustedFn.contents(id, item, lastError)
|
|
87
86
|
|
|
88
87
|
let maxRetries = 2
|
|
89
|
-
let heartbeatInterval = 30
|
|
90
88
|
let targetName = Some("ConfirmPayment")
|
|
91
89
|
let sourceNames: array<string> = []
|
|
92
90
|
let externalSystem = None
|
|
@@ -76,7 +76,6 @@ let SendTrackingEmailSpec = {
|
|
|
76
76
|
translate: translate,
|
|
77
77
|
onExhausted: onExhausted,
|
|
78
78
|
maxRetries: 3,
|
|
79
|
-
heartbeatInterval: 60,
|
|
80
79
|
targetName: undefined,
|
|
81
80
|
sourceNames: sourceNames,
|
|
82
81
|
externalSystem: undefined,
|
|
@@ -158,7 +157,6 @@ let ProcessPaymentSpec = {
|
|
|
158
157
|
onExhaustedFn: onExhaustedFn,
|
|
159
158
|
onExhausted: onExhausted$1,
|
|
160
159
|
maxRetries: 2,
|
|
161
|
-
heartbeatInterval: 30,
|
|
162
160
|
targetName: ProcessPaymentSpec_targetName,
|
|
163
161
|
sourceNames: sourceNames$1,
|
|
164
162
|
externalSystem: undefined,
|
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs
CHANGED
|
@@ -121,7 +121,6 @@ let SendConfirmSpec = {
|
|
|
121
121
|
outboundItemSchema: outboundItemSchema,
|
|
122
122
|
inboundCommandSchema: inboundCommandSchema,
|
|
123
123
|
maxRetries: 3,
|
|
124
|
-
heartbeatInterval: 60,
|
|
125
124
|
targetName: undefined,
|
|
126
125
|
sourceNames: sourceNames,
|
|
127
126
|
externalSystem: SendConfirmSpec_externalSystem,
|