@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.
- package/CHANGELOG.md +19 -0
- package/package.json +10 -10
- package/src/BakedManifest.res +63 -51
- package/src/BakedManifest.res.mjs +44 -28
- package/src/HostShellDist.res +29 -0
- package/src/HostShellDist.res.mjs +20 -0
- package/src/Platform.res +30 -11
- package/src/Platform.res.mjs +6 -0
- package/src/ShellConfig.res +178 -0
- package/src/ShellConfig.res.mjs +114 -0
- package/src/UiHints.res +107 -0
- package/src/UiHints.res.mjs +61 -0
- package/src/adapter/Auth/Auth_GraphqlContext.res +4 -6
- package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +5 -0
- package/src/adapter/Auth/LocalAuth.res +136 -6
- package/src/adapter/Auth/LocalAuth.res.mjs +82 -3
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +23 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +19 -2
- package/src/adapter/DomainGraphQL_Server.res +67 -4
- package/src/adapter/DomainGraphQL_Server.res.mjs +49 -2
- package/src/adapter/GraphQL_CallerError.res +19 -0
- package/src/adapter/GraphQL_CallerError.res.mjs +16 -0
- package/src/adapter/GraphQL_Server.res.mjs +3 -0
- package/tests/BakedManifestFilesTest.res +68 -0
- package/tests/BakedManifestFilesTest.res.mjs +115 -0
- package/tests/ShellConfigTest.res +221 -0
- package/tests/ShellConfigTest.res.mjs +203 -0
- package/tests/UiHintsTest.res +94 -0
- package/tests/UiHintsTest.res.mjs +99 -0
- package/tests/adapter/CommandAuthorizationTest.res +86 -0
- package/tests/adapter/CommandAuthorizationTest.res.mjs +62 -0
- package/tests/adapter/LocalAuthLoginTest.res +214 -0
- package/tests/adapter/LocalAuthLoginTest.res.mjs +191 -8
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.218 (2026-08-13)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** bake the component manifest after every plugin stack is up ([2fff09a](https://github.com/ReventlessDev/reventless-core/commit/2fff09aa32b77cc8e33a65414c58c12026ca009e))
|
|
11
|
+
* **local:** bake one curated surface per audience ([3be6f63](https://github.com/ReventlessDev/reventless-core/commit/3be6f63e579cef8cd213a84b6ccded773b1abc9e))
|
|
12
|
+
* **local:** let a login act as one of the roles it holds ([61141ac](https://github.com/ReventlessDev/reventless-core/commit/61141ac783a71dd7f043e84e6e7732c67cfb921b))
|
|
13
|
+
* **local:** point the dev shell at the manifest it bakes ([e4d0f1c](https://github.com/ReventlessDev/reventless-core/commit/e4d0f1cef70f9a9ca6d13266d72db3f38d0cc2d9))
|
|
14
|
+
* **local:** serve the ui-hints file a deployment declares ([8258c85](https://github.com/ReventlessDev/reventless-core/commit/8258c858155fec760a772483f9bde208ffa57f61))
|
|
15
|
+
* **local:** switch role without logging in again ([4f82348](https://github.com/ReventlessDev/reventless-core/commit/4f82348f01b9117256fc1d7208355c3654875909))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.217 (2026-08-12)
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **command:** drop null arguments, and let a caller read why a payload was refused ([bfee0dc](https://github.com/ReventlessDev/reventless-core/commit/bfee0dc37755596ea8bcce1fe2cccdace7759f10))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.216 (2026-08-12)
|
|
7
26
|
|
|
8
27
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
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.218",
|
|
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": {
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"sury": "11.0.0-alpha.4",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
39
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
39
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
40
40
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
41
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
41
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.5",
|
|
42
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.230",
|
|
42
43
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
43
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.111"
|
|
44
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.78",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.177",
|
|
46
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.12",
|
|
47
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.94",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.112",
|
|
49
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.138"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
package/src/BakedManifest.res
CHANGED
|
@@ -1,72 +1,84 @@
|
|
|
1
1
|
// Writes the curated component manifest where the local host shell serves its
|
|
2
|
-
// static assets from, so `manifestUrl` discovery works in dev
|
|
3
|
-
// in a deployment.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// own `dist/` (that is where the shipped `config.json` and `ui-hints.json` come
|
|
7
|
-
// from), so that is where the file goes. Rewritten on every boot, which also
|
|
8
|
-
// makes it self-healing — a `pnpm install` that replaces the package directory
|
|
9
|
-
// costs one restart, not a debugging session.
|
|
2
|
+
// static assets from (`HostShellDist`), so `manifestUrl` discovery works in dev
|
|
3
|
+
// exactly as it does in a deployment. Rewritten on every boot, which also makes
|
|
4
|
+
// it self-healing — a `pnpm install` that replaces the package directory costs
|
|
5
|
+
// one restart, not a debugging session.
|
|
10
6
|
//
|
|
11
7
|
// The curation itself lives in `ReventlessCore.Platform_BakedManifest`, shared
|
|
12
8
|
// with any other platform that bakes: only the destination is local.
|
|
13
9
|
|
|
14
|
-
let hostShellPackage = "@reventlessdev/reventless-host-shell"
|
|
15
|
-
|
|
16
10
|
let log = ReventlessCore.Logger.fromEnv()
|
|
17
11
|
|
|
18
|
-
// Resolved from the running project rather than from this framework module: the
|
|
19
|
-
// bundle is a deploy input the project names in its own package.json, and a
|
|
20
|
-
// framework-rooted lookup would skip that pin (the same distinction
|
|
21
|
-
// `Util_Bundle.resolvePackageRoot(~fromPulumiProject)` draws on AWS).
|
|
22
|
-
let hostShellDistDir = (): option<string> =>
|
|
23
|
-
try {
|
|
24
|
-
Some(
|
|
25
|
-
NodePath.dirname(
|
|
26
|
-
NodeModule.createRequire(NodeProcess.cwd() ++ "/index.js")->NodeModule.requireResolve(
|
|
27
|
-
hostShellPackage ++ "/package.json",
|
|
28
|
-
),
|
|
29
|
-
) ++ "/dist",
|
|
30
|
-
)
|
|
31
|
-
} catch {
|
|
32
|
-
| _ => None
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let defaultKey = "component-manifest.json"
|
|
36
|
-
|
|
37
12
|
/**
|
|
38
13
|
A declared bake writes the file or fails loudly. Both failure modes it can hit
|
|
39
14
|
are the deployment's own mistake — a name that matches no component, or a shell
|
|
40
15
|
package that is not installed — and both produce the same symptom if swallowed:
|
|
41
16
|
a shop that renders nothing, with no line in the log saying why.
|
|
42
17
|
*/
|
|
18
|
+
let toSelections = (
|
|
19
|
+
components: array<ReventlessInfra.Platform.bakedManifestSelection>,
|
|
20
|
+
): array<ReventlessCore.Platform_BakedManifest.selection> =>
|
|
21
|
+
components->Array.map((s): ReventlessCore.Platform_BakedManifest.selection => {
|
|
22
|
+
plugin: s.plugin,
|
|
23
|
+
views: s.views,
|
|
24
|
+
commands: s.commands,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
Every file this declaration produces, as (key, selections) pairs.
|
|
29
|
+
|
|
30
|
+
The default journey first, then one per declared journey. Separate from the
|
|
31
|
+
write so the whole set is reachable from a test — the same reason
|
|
32
|
+
`ShellConfig.overlay` exists apart from its write.
|
|
33
|
+
*/
|
|
34
|
+
let files = (
|
|
35
|
+
~config: ReventlessInfra.Platform.bakedManifest,
|
|
36
|
+
): array<(string, array<ReventlessCore.Platform_BakedManifest.selection>)> => {
|
|
37
|
+
let base = [
|
|
38
|
+
(
|
|
39
|
+
config.key->Option.getOr(ReventlessCore.Platform_BakedManifest.defaultKey),
|
|
40
|
+
toSelections(config.components),
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
switch config.journeys {
|
|
44
|
+
| None => base
|
|
45
|
+
| Some(journeys) =>
|
|
46
|
+
base->Array.concat(
|
|
47
|
+
journeys->Array.map(j => (
|
|
48
|
+
j.key->Option.getOr(ReventlessCore.Platform_BakedManifest.journeyKey(~group=j.group)),
|
|
49
|
+
toSelections(j.components),
|
|
50
|
+
)),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
let emit = (
|
|
44
56
|
~structures: array<(string, Reventless.Plugin.pluginStructure)>,
|
|
45
57
|
~config: ReventlessInfra.Platform.bakedManifest,
|
|
46
58
|
) => {
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
let outputs = files(~config)
|
|
60
|
+
// Every file is curated before any is written. A declaration naming a
|
|
61
|
+
// component that does not exist fails the boot, and failing it halfway would
|
|
62
|
+
// leave a shell serving one audience's file and a stale copy of another's.
|
|
63
|
+
let curated = outputs->Array.map(((key, selections)) => (
|
|
64
|
+
key,
|
|
65
|
+
switch ReventlessCore.Platform_BakedManifest.curate(~structures, ~selections) {
|
|
66
|
+
| Error(e) => JsError.throwWithMessage(ReventlessCore.Platform_BakedManifest.describe(e))
|
|
67
|
+
| Ok(manifest) => manifest
|
|
68
|
+
},
|
|
69
|
+
))
|
|
70
|
+
switch HostShellDist.dir() {
|
|
71
|
+
| None =>
|
|
72
|
+
JsError.throwWithMessage(
|
|
73
|
+
`baked manifest: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
74
|
+
`the local shell serves the file from that package's dist/, so declaring a bake ` ++
|
|
75
|
+
`without the package installed would write nothing and render an empty shell.`,
|
|
76
|
+
)
|
|
77
|
+
| Some(dir) =>
|
|
78
|
+
curated->Array.forEach(((key, manifest)) => {
|
|
67
79
|
let path = NodePath.join([dir, key])
|
|
68
80
|
NodeFs.writeFileSync(path, JSON.stringify(manifest, ~space=2))
|
|
69
|
-
log.info(~comp="BakedManifest", `wrote ${key}
|
|
70
|
-
}
|
|
81
|
+
log.info(~comp="BakedManifest", `wrote ${key}: ${path}`)
|
|
82
|
+
})
|
|
71
83
|
}
|
|
72
84
|
}
|
|
@@ -2,51 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Nodepath from "node:path";
|
|
5
|
-
import * as Nodemodule from "node:module";
|
|
6
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
7
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
8
|
+
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
9
9
|
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
10
10
|
|
|
11
|
-
let hostShellPackage = "@reventlessdev/reventless-host-shell";
|
|
12
|
-
|
|
13
11
|
let log = Logger$ReventlessCore.fromEnv();
|
|
14
12
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
return Nodepath.dirname(Nodemodule.createRequire(process.cwd() + "/index.js").resolve(hostShellPackage + "/package.json")) + "/dist";
|
|
18
|
-
} catch (exn) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let defaultKey = "component-manifest.json";
|
|
24
|
-
|
|
25
|
-
function emit(structures, config) {
|
|
26
|
-
let selections = config.components.map(s => ({
|
|
13
|
+
function toSelections(components) {
|
|
14
|
+
return components.map(s => ({
|
|
27
15
|
plugin: s.plugin,
|
|
28
16
|
views: s.views,
|
|
29
17
|
commands: s.commands
|
|
30
18
|
}));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function files(config) {
|
|
22
|
+
let base = [[
|
|
23
|
+
Stdlib_Option.getOr(config.key, Platform_BakedManifest$ReventlessCore.defaultKey),
|
|
24
|
+
toSelections(config.components)
|
|
25
|
+
]];
|
|
26
|
+
let journeys = config.journeys;
|
|
27
|
+
if (journeys !== undefined) {
|
|
28
|
+
return base.concat(journeys.map(j => [
|
|
29
|
+
Stdlib_Option.getOr(j.key, Platform_BakedManifest$ReventlessCore.journeyKey(j.group)),
|
|
30
|
+
toSelections(j.components)
|
|
31
|
+
]));
|
|
32
|
+
} else {
|
|
33
|
+
return base;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function emit(structures, config) {
|
|
38
|
+
let outputs = files(config);
|
|
39
|
+
let curated = outputs.map(param => {
|
|
40
|
+
let e = Platform_BakedManifest$ReventlessCore.curate(structures, param[1]);
|
|
41
|
+
let tmp;
|
|
42
|
+
tmp = e.TAG === "Ok" ? e._0 : Stdlib_JsError.throwWithMessage(Platform_BakedManifest$ReventlessCore.describe(e._0));
|
|
43
|
+
return [
|
|
44
|
+
param[0],
|
|
45
|
+
tmp
|
|
46
|
+
];
|
|
47
|
+
});
|
|
48
|
+
let dir = HostShellDist$ReventlessLocal.dir();
|
|
49
|
+
if (dir !== undefined) {
|
|
50
|
+
curated.forEach(param => {
|
|
51
|
+
let key = param[0];
|
|
52
|
+
let path = Nodepath.join(dir, key);
|
|
53
|
+
Nodefs.writeFileSync(path, JSON.stringify(param[1], undefined, 2), "utf8");
|
|
54
|
+
log.info("BakedManifest", undefined, `wrote ` + key + `: ` + path);
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
} else {
|
|
58
|
+
return Stdlib_JsError.throwWithMessage(`baked manifest: cannot resolve ` + HostShellDist$ReventlessLocal.$$package + ` from ` + process.cwd() + ` — the local shell serves the file from that package's dist/, so declaring a bake without the package installed would write nothing and render an empty shell.`);
|
|
38
59
|
}
|
|
39
|
-
let key = Stdlib_Option.getOr(config.key, defaultKey);
|
|
40
|
-
let path = Nodepath.join(dir, key);
|
|
41
|
-
Nodefs.writeFileSync(path, JSON.stringify(e._0, undefined, 2), "utf8");
|
|
42
|
-
log.info("BakedManifest", undefined, `wrote ` + key + ` for ` + selections.length.toString() + ` plugin(s): ` + path);
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
export {
|
|
46
|
-
hostShellPackage,
|
|
47
63
|
log,
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
toSelections,
|
|
65
|
+
files,
|
|
50
66
|
emit,
|
|
51
67
|
}
|
|
52
68
|
/* log Not a pure module */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Where the local host shell serves its static assets from.
|
|
2
|
+
//
|
|
3
|
+
// Locally there is no deploy and no bucket: `reventless-host-shell` serves its
|
|
4
|
+
// own `dist/` (that is where the shipped `config.json` and `ui-hints.json` a dev
|
|
5
|
+
// sees come from), so that is where every generated asset the browser fetches
|
|
6
|
+
// has to land — the baked component manifest and the `config.json` overlay
|
|
7
|
+
// alike. Resolved in one place because those writers have to agree: a second
|
|
8
|
+
// opinion about the directory puts a manifest where nothing reads it, and the
|
|
9
|
+
// symptom is an empty shell rather than a missing file.
|
|
10
|
+
//
|
|
11
|
+
// Resolved from the running project rather than from this framework module: the
|
|
12
|
+
// bundle is a deploy input the project names in its own package.json, and a
|
|
13
|
+
// framework-rooted lookup would skip that pin (the same distinction
|
|
14
|
+
// `Util_Bundle.resolvePackageRoot(~fromPulumiProject)` draws on AWS).
|
|
15
|
+
|
|
16
|
+
let package = "@reventlessdev/reventless-host-shell"
|
|
17
|
+
|
|
18
|
+
let dir = (): option<string> =>
|
|
19
|
+
try {
|
|
20
|
+
Some(
|
|
21
|
+
NodePath.dirname(
|
|
22
|
+
NodeModule.createRequire(NodeProcess.cwd() ++ "/index.js")->NodeModule.requireResolve(
|
|
23
|
+
package ++ "/package.json",
|
|
24
|
+
),
|
|
25
|
+
) ++ "/dist",
|
|
26
|
+
)
|
|
27
|
+
} catch {
|
|
28
|
+
| _ => None
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodepath from "node:path";
|
|
4
|
+
import * as Nodemodule from "node:module";
|
|
5
|
+
|
|
6
|
+
let $$package = "@reventlessdev/reventless-host-shell";
|
|
7
|
+
|
|
8
|
+
function dir() {
|
|
9
|
+
try {
|
|
10
|
+
return Nodepath.dirname(Nodemodule.createRequire(process.cwd() + "/index.js").resolve($$package + "/package.json")) + "/dist";
|
|
11
|
+
} catch (exn) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
$$package,
|
|
18
|
+
dir,
|
|
19
|
+
}
|
|
20
|
+
/* node:path Not a pure module */
|
package/src/Platform.res
CHANGED
|
@@ -1428,28 +1428,33 @@ module MakeWithConfig = (
|
|
|
1428
1428
|
}
|
|
1429
1429
|
})
|
|
1430
1430
|
|
|
1431
|
-
// In-memory ignores
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1431
|
+
// In-memory ignores the infrastructure half of `~hostUiBundle` — the host shell
|
|
1432
|
+
// is served from the installed package's own `dist/`, not from a CDN. What a
|
|
1433
|
+
// deployment *says* is honoured, in the same three files the AWS deploy writes
|
|
1434
|
+
// beside each other: `bakedManifest` through `BakedManifest`, `shellConfig`
|
|
1435
|
+
// through `ShellConfig`, which overlays the served `config.json` the way the
|
|
1436
|
+
// AWS deploy composes the one it writes, and `uiHintsFile` through `UiHints`,
|
|
1437
|
+
// which replaces the served hints file the way the deploy uploads it.
|
|
1437
1438
|
type hostUiBundleConfig = {
|
|
1438
1439
|
assetsDir?: string,
|
|
1439
1440
|
bundleVersion?: string,
|
|
1441
|
+
// `makePlatform` serves the declared file in place of the host-shell
|
|
1442
|
+
// package's own dev-mode fallback, which an undeclared platform keeps.
|
|
1440
1443
|
uiHintsFile?: string,
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1444
|
+
// `makePlatform` writes the curated manifest where the local host-shell
|
|
1445
|
+
// serves its static assets from, and points `config.json` at it.
|
|
1443
1446
|
bakedManifest?: ReventlessInfra.Platform.bakedManifest,
|
|
1444
1447
|
// AWS host-ui deploy knobs — carried to satisfy the shared Platform.T
|
|
1445
1448
|
// signature; the in-memory platform provisions no infrastructure and
|
|
1446
1449
|
// ignores them.
|
|
1447
1450
|
geocoderPlaceIndex?: ReventlessInfra.Platform.geocoderIndex,
|
|
1448
1451
|
uploadBucket?: ReventlessInfra.Platform.objectStore,
|
|
1449
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
+
// Still ignored: a mode is a shell *build* input locally, turned on in the
|
|
1453
|
+
// host-shell package's own `public/config.json`, where the AWS deploy has to
|
|
1454
|
+
// write it into the config.json it hosts.
|
|
1452
1455
|
viewModes?: array<ReventlessInfra.Platform.viewMode>,
|
|
1456
|
+
// Merged into the served `config.json` verbatim, as on AWS — the keys the
|
|
1457
|
+
// shell owns and neither platform computes (`appName`, `elevatedGroups`, …).
|
|
1453
1458
|
shellConfig?: dict<JSON.t>,
|
|
1454
1459
|
}
|
|
1455
1460
|
let makePlatform = (
|
|
@@ -1610,6 +1615,20 @@ module MakeWithConfig = (
|
|
|
1610
1615
|
// offers, which is settled at composition and owes nothing to projection
|
|
1611
1616
|
// catch-up. Every plugin passed to this call is connected by construction
|
|
1612
1617
|
// locally, so there is no status or version dedup to apply.
|
|
1618
|
+
//
|
|
1619
|
+
// The config.json overlay is written first and synchronously: it depends on
|
|
1620
|
+
// the *declaration* only, where the bake has to wait for every plugin's
|
|
1621
|
+
// structure Output to land. Unconditional, unlike the bake, because it is
|
|
1622
|
+
// also what restores the shipped file for a platform that has stopped
|
|
1623
|
+
// declaring anything.
|
|
1624
|
+
ShellConfig.emit(
|
|
1625
|
+
~bakedManifest=hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest),
|
|
1626
|
+
~shellConfig=hostUiBundle->Option.flatMap(cfg => cfg.shellConfig),
|
|
1627
|
+
)
|
|
1628
|
+
// Unconditional for the same reason, and one file over: it is also what
|
|
1629
|
+
// restores the shell package's own hints for a platform that has stopped
|
|
1630
|
+
// declaring its own.
|
|
1631
|
+
UiHints.emit(~uiHintsFile=hostUiBundle->Option.flatMap(cfg => cfg.uiHintsFile))
|
|
1613
1632
|
switch hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest) {
|
|
1614
1633
|
| None => ()
|
|
1615
1634
|
| Some(cfg) => bakeManifest(~pluginComponents=plugins, ~config=cfg)
|
package/src/Platform.res.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
|
|
|
18
18
|
import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
|
|
19
19
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
20
20
|
import * as Backend$ReventlessLocal from "./adapter/Backend.res.mjs";
|
|
21
|
+
import * as UiHints$ReventlessLocal from "./UiHints.res.mjs";
|
|
21
22
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
22
23
|
import * as LocalBus$ReventlessLocal from "./adapter/LocalBus.res.mjs";
|
|
23
24
|
import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
|
|
@@ -27,6 +28,7 @@ import * as TestRunner$ReventlessLocal from "./test/TestRunner.res.mjs";
|
|
|
27
28
|
import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs";
|
|
28
29
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
29
30
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
31
|
+
import * as ShellConfig$ReventlessLocal from "./ShellConfig.res.mjs";
|
|
30
32
|
import * as BackendState$ReventlessLocal from "./adapter/BackendState.res.mjs";
|
|
31
33
|
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
32
34
|
import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
|
|
@@ -1285,6 +1287,8 @@ function MakeWithConfig(Config) {
|
|
|
1285
1287
|
adminResources: []
|
|
1286
1288
|
});
|
|
1287
1289
|
subscribeToPluginEvents();
|
|
1290
|
+
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), undefined);
|
|
1291
|
+
UiHints$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), undefined);
|
|
1288
1292
|
let cfg = Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest);
|
|
1289
1293
|
if (cfg !== undefined) {
|
|
1290
1294
|
bakeManifest(plugins$1, cfg);
|
|
@@ -3003,6 +3007,8 @@ function Make($star) {
|
|
|
3003
3007
|
adminResources: []
|
|
3004
3008
|
});
|
|
3005
3009
|
subscribeToPluginEvents();
|
|
3010
|
+
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), undefined);
|
|
3011
|
+
UiHints$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), undefined);
|
|
3006
3012
|
let cfg = Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest);
|
|
3007
3013
|
if (cfg !== undefined) {
|
|
3008
3014
|
bakeManifest(plugins$1, cfg);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Delivers a deployment's host-shell keys to local dev, so `config.json` says
|
|
2
|
+
// the same things it says on AWS.
|
|
3
|
+
//
|
|
4
|
+
// On AWS the deploy owns that file outright: `Util_ShellConfig.fields` joins the
|
|
5
|
+
// keys the deploy computes with the ones the deployment chose, and the result is
|
|
6
|
+
// written as a `BucketObject`. Locally there is no deploy and no bucket — the
|
|
7
|
+
// `config.json` a dev's browser fetches is the one `reventless-host-shell`
|
|
8
|
+
// shipped in its own `dist/`. That file is therefore the *baseline* here, not
|
|
9
|
+
// the output.
|
|
10
|
+
//
|
|
11
|
+
// Hence the copy. The first write puts the shipped file aside as
|
|
12
|
+
// `config.base.json` and every write since is baseline + overlay. Overlaying the
|
|
13
|
+
// previous output instead would make removal impossible: drop `bakedManifest`
|
|
14
|
+
// from the platform root and yesterday's `manifestUrl` stays, pointing the shell
|
|
15
|
+
// at a file nothing writes any more — an empty shop with nothing in the diff to
|
|
16
|
+
// explain it. Reading a baseline makes both directions self-healing, and a
|
|
17
|
+
// `pnpm install` that replaces the package directory costs one restart, because
|
|
18
|
+
// the baseline is re-seeded from whatever the new package ships.
|
|
19
|
+
//
|
|
20
|
+
// One key is computed (`manifestUrl`, from the bake declaration — local knows
|
|
21
|
+
// where it put the file); the rest are the deployment's own `shellConfig`,
|
|
22
|
+
// passed through verbatim. A `shellConfig` that names a computed key fails the
|
|
23
|
+
// same way it does on AWS: a passthrough cannot redirect a key the platform
|
|
24
|
+
// decides, and silently resolving it either way points the shell somewhere
|
|
25
|
+
// unintended.
|
|
26
|
+
|
|
27
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
28
|
+
|
|
29
|
+
let fileName = "config.json"
|
|
30
|
+
|
|
31
|
+
// Not `config.json.base` — the shell's dist is served as a static directory, and
|
|
32
|
+
// a name that keeps the `.json` extension is the one a dev can open in a browser
|
|
33
|
+
// to see what the overlay started from.
|
|
34
|
+
let baselineFileName = "config.base.json"
|
|
35
|
+
|
|
36
|
+
let manifestUrlOf = (config: ReventlessInfra.Platform.bakedManifest): string =>
|
|
37
|
+
ReventlessCore.Platform_BakedManifest.urlForKey(config.key)
|
|
38
|
+
|
|
39
|
+
// Where a caller acting as a given role discovers from. Beside `manifestUrl`
|
|
40
|
+
// rather than replacing it: that key stays the default journey, which is what a
|
|
41
|
+
// caller matching no declared group gets and what every existing deployment has.
|
|
42
|
+
let journeyManifestsKey = "journeyManifestUrls"
|
|
43
|
+
|
|
44
|
+
let computedKeys = ["manifestUrl", journeyManifestsKey]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
The overlay this platform puts on top of the shipped `config.json`.
|
|
48
|
+
|
|
49
|
+
Separate from the write so the whole key set is reachable from a test, the same
|
|
50
|
+
reason `Util_ShellConfig.fields` exists apart from `deployPlatform`'s
|
|
51
|
+
`Pulumi.Output.apply` on AWS.
|
|
52
|
+
*/
|
|
53
|
+
let overlay = (
|
|
54
|
+
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>,
|
|
55
|
+
~shellConfig: option<dict<JSON.t>>,
|
|
56
|
+
): dict<JSON.t> => {
|
|
57
|
+
let out = Dict.make()
|
|
58
|
+
|
|
59
|
+
bakedManifest->Option.forEach(config => {
|
|
60
|
+
out->Dict.set("manifestUrl", JSON.Encode.string(manifestUrlOf(config)))
|
|
61
|
+
// Omitted entirely when nothing is declared, so a single-audience deployment
|
|
62
|
+
// writes the same key set it always did and a shell that has never heard of
|
|
63
|
+
// journeys sees no new keys.
|
|
64
|
+
switch config.journeys {
|
|
65
|
+
| None => ()
|
|
66
|
+
| Some([]) => ()
|
|
67
|
+
| Some(journeys) =>
|
|
68
|
+
let map = Dict.make()
|
|
69
|
+
journeys->Array.forEach(j =>
|
|
70
|
+
map->Dict.set(
|
|
71
|
+
j.group,
|
|
72
|
+
JSON.Encode.string(
|
|
73
|
+
ReventlessCore.Platform_BakedManifest.urlForKey(
|
|
74
|
+
Some(
|
|
75
|
+
j.key->Option.getOr(
|
|
76
|
+
ReventlessCore.Platform_BakedManifest.journeyKey(~group=j.group),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
out->Dict.set(journeyManifestsKey, map->JSON.Encode.object)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
shellConfig->Option.forEach(extra => {
|
|
88
|
+
let collisions = extra->Dict.keysToArray->Array.filter(k => computedKeys->Array.includes(k))
|
|
89
|
+
if collisions->Array.length > 0 {
|
|
90
|
+
JsError.throwWithMessage(
|
|
91
|
+
"host UI config.json: shellConfig sets key(s) the platform already computes — " ++
|
|
92
|
+
collisions->Array.join(", ") ++
|
|
93
|
+
". Remove them from shellConfig; a passthrough cannot redirect a computed key.",
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
extra->Dict.forEachWithKey((v, k) => out->Dict.set(k, v))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let readObject = (~path: string, ~label: string): dict<JSON.t> =>
|
|
103
|
+
switch NodeFs.readFileSync(path)->JSON.parseOrThrow->JSON.Decode.object {
|
|
104
|
+
| Some(obj) => obj
|
|
105
|
+
| None => JsError.throwWithMessage(`host UI ${label}: ${path} is not a JSON object`)
|
|
106
|
+
| exception _ => JsError.throwWithMessage(`host UI ${label}: cannot read ${path} as JSON`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
Merge the overlay onto the shipped `config.json` in the host shell's `dist/`.
|
|
111
|
+
|
|
112
|
+
A no-op when there is nothing to say and nothing was said before — a platform
|
|
113
|
+
that declares neither a bake nor any `shellConfig` never touches the file, so
|
|
114
|
+
its dev shell is byte-identical to one built before this module existed. Once
|
|
115
|
+
either is declared the write happens or fails loudly, for the reason
|
|
116
|
+
`BakedManifest.emit` gives: both failure modes it can hit are the deployment's
|
|
117
|
+
own mistake, and both produce the same silent symptom if swallowed.
|
|
118
|
+
*/
|
|
119
|
+
let emit = (
|
|
120
|
+
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>,
|
|
121
|
+
~shellConfig: option<dict<JSON.t>>,
|
|
122
|
+
// Test seam. The baseline dance is the part of this module with state behind
|
|
123
|
+
// it — "boot twice and the second write still starts from the shipped file" is
|
|
124
|
+
// not a property a pure function can carry.
|
|
125
|
+
~dir: option<string>=?,
|
|
126
|
+
) => {
|
|
127
|
+
let overlay = overlay(~bakedManifest, ~shellConfig)
|
|
128
|
+
|
|
129
|
+
switch (
|
|
130
|
+
switch dir {
|
|
131
|
+
| Some(_) as given => given
|
|
132
|
+
| None => HostShellDist.dir()
|
|
133
|
+
}
|
|
134
|
+
) {
|
|
135
|
+
| None =>
|
|
136
|
+
// Nothing declared and no shell installed is the ordinary case for a
|
|
137
|
+
// platform nobody points a browser at; only a declaration makes the missing
|
|
138
|
+
// package an error, and then it is the same error the bake gives.
|
|
139
|
+
if overlay->Dict.keysToArray->Array.length > 0 {
|
|
140
|
+
JsError.throwWithMessage(
|
|
141
|
+
`host UI config.json: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
142
|
+
`the local shell reads its config from that package's dist/, so declaring shell ` ++
|
|
143
|
+
`config without the package installed would write nothing and leave the shell ` ++
|
|
144
|
+
`configured as it shipped.`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
| Some(dir) =>
|
|
148
|
+
let path = NodePath.join([dir, fileName])
|
|
149
|
+
let baselinePath = NodePath.join([dir, baselineFileName])
|
|
150
|
+
|
|
151
|
+
// Seeded only when there is an overlay to apply. A platform that declares
|
|
152
|
+
// nothing must not leave a `config.base.json` behind for the next one to
|
|
153
|
+
// read as authoritative.
|
|
154
|
+
if !NodeFs.existsSync(baselinePath) && overlay->Dict.keysToArray->Array.length > 0 {
|
|
155
|
+
if !NodeFs.existsSync(path) {
|
|
156
|
+
JsError.throwWithMessage(
|
|
157
|
+
`host UI config.json: ${HostShellDist.package} ships no ${fileName} at ${dir} — ` ++
|
|
158
|
+
`there is no baseline to overlay, so the shell would boot with only the keys ` ++
|
|
159
|
+
`declared here and none of the ones it expects.`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
NodeFs.writeFileSync(baselinePath, NodeFs.readFileSync(path))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if NodeFs.existsSync(baselinePath) {
|
|
166
|
+
let merged = readObject(~path=baselinePath, ~label="config baseline")
|
|
167
|
+
overlay->Dict.forEachWithKey((v, k) => merged->Dict.set(k, v))
|
|
168
|
+
NodeFs.writeFileSync(path, JSON.stringify(merged->JSON.Encode.object, ~space=2))
|
|
169
|
+
let keys = overlay->Dict.keysToArray
|
|
170
|
+
log.info(
|
|
171
|
+
~comp="ShellConfig",
|
|
172
|
+
keys->Array.length == 0
|
|
173
|
+
? `restored ${fileName} from ${baselineFileName}: ${path}`
|
|
174
|
+
: `wrote ${fileName} with ${keys->Array.join(", ")}: ${path}`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|