@reventlessdev/reventless-local 3.0.0-alpha.290 → 3.0.0-alpha.292

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 (53) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +11 -11
  3. package/src/BakedManifest.res +7 -2
  4. package/src/BakedManifest.res.mjs +4 -4
  5. package/src/HostShellDist.res +11 -0
  6. package/src/HostShellDist.res.mjs +14 -1
  7. package/src/Platform.res +31 -19
  8. package/src/Platform.res.mjs +63 -60
  9. package/src/ShellConfig.res +27 -24
  10. package/src/ShellConfig.res.mjs +25 -13
  11. package/src/UiHints.res +4 -6
  12. package/src/UiSlots.res +2 -3
  13. package/src/adapter/Auth/LocalAuth.res +2 -2
  14. package/src/adapter/Backend.res +2 -2
  15. package/src/adapter/LocalBus.res +1 -1
  16. package/src/adapter/LocalUploadResolvers.res +1 -1
  17. package/src/components/Aggregate_Builder.res +2 -2
  18. package/src/components/Aggregate_Builder.res.mjs +4 -4
  19. package/src/components/DcbEventLog_Builder.res +1 -1
  20. package/src/components/StateChangeSlice_Builder.res +3 -3
  21. package/src/components/StateChangeSlice_Builder.res.mjs +3 -3
  22. package/src/reset/LocalSeedReset.res +1 -1
  23. package/tests/HostShellDistTest.res +70 -0
  24. package/tests/HostShellDistTest.res.mjs +75 -0
  25. package/tests/ShellConfigTest.res +45 -7
  26. package/tests/ShellConfigTest.res.mjs +45 -8
  27. package/tests/adapter/BackendParityTest.res +1 -1
  28. package/tests/adapter/EventHistoryResolverTest.res +2 -2
  29. package/tests/adapter/GraphQL_SchemaInspectorTest.res +1 -1
  30. package/tests/adapter/LocalAuthLoginTest.res +1 -1
  31. package/tests/adapter/QueryDbListResolverTest.res +1 -1
  32. package/tests/adapter/QueryDbResolvedFieldsTest.res +1 -1
  33. package/tests/components/aggregate/AggregateFixtures.res +6 -6
  34. package/tests/components/aggregate/AggregateFixtures.res.mjs +3 -3
  35. package/tests/components/aggregate/AggregateSelfMappingFixtures.res +2 -2
  36. package/tests/components/aggregate/AggregateSelfMappingFixtures.res.mjs +3 -3
  37. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +4 -4
  38. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +4 -4
  39. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +3 -3
  40. package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +4 -4
  41. package/tests/components/dcb/DcbCrossPartitionFixtures.res +2 -2
  42. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +2 -2
  43. package/tests/components/dcb/DcbFixtures.res +2 -2
  44. package/tests/components/dcb/DcbFixtures.res.mjs +2 -2
  45. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +1 -1
  46. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -2
  47. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +2 -2
  48. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +2 -2
  49. package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +2 -2
  50. package/tests/plugin/{PluginBehavior_GWT.res → PluginDecision_GWT.res} +2 -2
  51. package/tests/plugin/{PluginBehavior_GWT.res.mjs → PluginDecision_GWT.res.mjs} +8 -8
  52. package/tests/plugin/UiFragmentRegistry_GWT.res +1 -1
  53. package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +5 -5
@@ -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
- // 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`,
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 = Dict.make()
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
- A no-op when there is nothing to say and nothing was said before — a platform
124
- that declares neither a bake nor any `shellConfig` never touches the file, so
125
- its dev shell is byte-identical to one built before this module existed. Once
126
- either is declared the write happens or fails loudly, for the reason
127
- `BakedManifest.emit` gives: both failure modes it can hit are the deployment's
128
- own mistake, and both produce the same silent symptom if swallowed.
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
- // Test seam. The baseline dance is the part of this module with state behind
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 overlay->Dict.keysToArray->Array.length > 0 {
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
- // Seeded only when there is an overlay to apply. A platform that declares
161
- // nothing must not leave a `config.base.json` behind for the next one to
162
- // read as authoritative.
163
- if !NodeFs.existsSync(baselinePath) && overlay->Dict.keysToArray->Array.length > 0 {
164
- if !NodeFs.existsSync(path) {
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: ${HostShellDist.package} ships no ${fileName} at ${dir} — ` ++
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
- keys->Array.length == 0
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
  }
@@ -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 computedKeys = [
28
- "manifestUrl",
29
- journeyManifestsKey,
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 (Object.keys(overlay$1).length !== 0) {
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) && Object.keys(overlay$1).length !== 0) {
92
- if (!Nodefs.existsSync(path)) {
93
- Stdlib_JsError.throwWithMessage(`host UI config.json: ` + HostShellDist$ReventlessLocal.$$package + ` ships 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.`);
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
- let keys = Object.keys(overlay$1);
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
@@ -12,7 +12,7 @@
12
12
  // withdrawn declaration leaves yesterday's hints in place with nothing in the
13
13
  // diff to explain them. Unlike `ShellConfig` there is no merge — AWS writes the
14
14
  // declared file verbatim, and layering a deployment's hints over the shell
15
- // package's demonstration hints would invent a third behaviour neither platform
15
+ // package's demonstration hints would invent a third behavior neither platform
16
16
  // has.
17
17
 
18
18
  let log = ReventlessCore.Logger.fromEnv()
@@ -37,9 +37,8 @@ let baselineFileName = "ui-hints.base.json"
37
37
  */
38
38
  let emit = (
39
39
  ~uiHintsFile: option<string>,
40
- // Test seam, as in `ShellConfig.emit`: the baseline dance is the part with
41
- // state behind it, and "boot twice and the second write still starts from the
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 same seam `emit` takes, and threaded to it: a test has to be able to
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
- // Test seam, as in `UiHints.emit`: the removal on withdrawal is the part with
57
- // state behind it, and "declare, then withdraw, and the file is gone" is not a
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
@@ -113,7 +113,7 @@ module Login = {
113
113
  // every ReScript rebuild, so a per-process secret logs the developer out
114
114
  // several times an hour, mid-task, with a "reconnecting" spinner as the
115
115
  // only explanation.
116
- // 3. A random 32 bytes, per process — today's behaviour, kept for the run
116
+ // 3. A random 32 bytes, per process — today's behavior, kept for the run
117
117
  // that has no `.reventless/` to write to.
118
118
  //
119
119
  // The directory is used but never created. Its presence is what distinguishes
@@ -389,7 +389,7 @@ let authenticate = async (ctx: ReventlessCore.Auth_Adapter.requestContext): Iden
389
389
  | None =>
390
390
  // No headers: in-memory mode defaults to defaultUser so local dev
391
391
  // against an AllowAuthenticated-defaulted backend "just works" without
392
- // setting headers. Anonymous behaviour is reachable only by building
392
+ // setting headers. Anonymous behavior is reachable only by building
393
393
  // an Identity.anonymous explicitly in code.
394
394
  Authenticated(defaultUser)
395
395
  | Some(g) =>
@@ -1,6 +1,6 @@
1
1
  // Selects which storage backend the in-memory Platform uses.
2
2
  //
3
- // Memory — pure in-memory dicts/refs, current default behaviour, wiped on restart.
3
+ // Memory — pure in-memory dicts/refs, current default behavior, wiped on restart.
4
4
  // Sqlite — opt-in on-disk SQLite file via SqliteDriver. Survives process restart.
5
5
  // resetOnStart=true wipes the file when the Platform is constructed.
6
6
 
@@ -41,7 +41,7 @@ let removeFileIfExists = (path: string) =>
41
41
  | _ => ()
42
42
  }
43
43
 
44
- // Parses REVENTLESS_LOCAL_BACKEND. Recognised forms:
44
+ // Parses REVENTLESS_LOCAL_BACKEND. Recognized forms:
45
45
  // sqlite:./local.db — Sqlite backend, no reset on start
46
46
  // sqlite:./local.db?reset — Sqlite backend, wipes file at Platform.Make time
47
47
  // memory — explicit Memory selector
@@ -117,7 +117,7 @@ module type T = {
117
117
  let registerQueryDbScan: (string, unit => array<JSON.t>) => unit
118
118
  let getQueryDbScan: string => option<unit => array<JSON.t>>
119
119
  // Stream variant: lazily creates a Stream from current storage contents.
120
- // Used by LocalQueryEngine to honour ~limit via Stream.take without loading all items.
120
+ // Used by LocalQueryEngine to honor ~limit via Stream.take without loading all items.
121
121
  let registerQueryDbStream: (string, unit => Stream.t<JSON.t, string, unit>) => unit
122
122
  let getQueryDbStream: string => option<unit => Stream.t<JSON.t, string, unit>>
123
123
  // Indexed equality lookup: (field, value) → matching items. The SQLite backend
@@ -1,6 +1,6 @@
1
1
  // Local upload resolver logic (route B), backed by `LocalObjectStore`. Shared by the
2
2
  // dev platform (`Platform.res` registers these on the domain server) and
3
- // `ServedBucketHttpTest`, so the mint/release behaviour has one definition.
3
+ // `ServedBucketHttpTest`, so the mint/release behavior has one definition.
4
4
  //
5
5
  // The dev store has no identities and no clock, so release enforces only the *shape*
6
6
  // of the rule (key under a served prefix), not the identity/age conditions AWS applies
@@ -28,12 +28,12 @@ module MakeWithHooks = (
28
28
 
29
29
  module Make = (
30
30
  Spec: Reventless.Aggregate.Spec,
31
- Behavior: Reventless.Behavior.T with module Spec := Spec,
31
+ Decision: Reventless.Decision.T with module Spec := Spec,
32
32
  EventMappings: ReventlessInfra.EventMapper.Mappings with module Target := Spec,
33
33
  ) => {
34
34
  include ReventlessCore.Aggregate_Builder.Make(
35
35
  Spec,
36
- Behavior,
36
+ Decision,
37
37
  EventMappings,
38
38
  RuntimeEnvironment,
39
39
  CommandGeneratorResolvers_GraphQL,
@@ -56,8 +56,8 @@ function MakeWithHooks(Bus) {
56
56
  let AggregateRuntimeBuilder = LocalAggregateRuntime_Builder$ReventlessLocal.Make(Bus)({
57
57
  make: CommandTopicChannel.make
58
58
  })(EventCollectorChannel);
59
- let Make = Spec => (Behavior => (EventMappings => {
60
- let include = Aggregate_Builder$ReventlessCore.Make(Spec)(Behavior)({
59
+ let Make = Spec => (Decision => (EventMappings => {
60
+ let include = Aggregate_Builder$ReventlessCore.Make(Spec)(Decision)({
61
61
  mappings: EventMappings.mappings,
62
62
  counter: EventMappings.counter
63
63
  })({
@@ -105,8 +105,8 @@ function Make(Bus) {
105
105
  let AggregateRuntimeBuilder = LocalAggregateRuntime_Builder$ReventlessLocal.Make(Bus)({
106
106
  make: CommandTopicChannel.make
107
107
  })(EventCollectorChannel);
108
- let Make$1 = Spec => (Behavior => (EventMappings => {
109
- let include = Aggregate_Builder$ReventlessCore.Make(Spec)(Behavior)({
108
+ let Make$1 = Spec => (Decision => (EventMappings => {
109
+ let include = Aggregate_Builder$ReventlessCore.Make(Spec)(Decision)({
110
110
  mappings: EventMappings.mappings,
111
111
  counter: EventMappings.counter
112
112
  })({
@@ -4,7 +4,7 @@ module Make = (Bus: LocalBus.T) => {
4
4
  module EventTopicPublisher = LocalEventTopicPublisher.Make(Bus)
5
5
 
6
6
  // Use the backend-aware storage functor (as Platform does), not the plain
7
- // module: the functor consults BackendState so a standalone DCB log honours
7
+ // module: the functor consults BackendState so a standalone DCB log honors
8
8
  // REVENTLESS_LOCAL_BACKEND=sqlite, and it registers the read with the Bus so
9
9
  // `Bus.getDcbEventLogRead` can see it. The plain module did neither — the log
10
10
  // silently stayed in memory under SQLite and was invisible to bus readers.
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Make = (
4
4
  Spec: Reventless.StateChangeSlice.Spec,
5
- Behavior: Reventless.StateChangeSlice.Behavior with module Spec := Spec,
5
+ Decision: Reventless.StateChangeSlice.Decision with module Spec := Spec,
6
6
  ) => {
7
- module Inner = ReventlessCore.StateChangeSlice_Builder.Make(Spec, Behavior)
7
+ module Inner = ReventlessCore.StateChangeSlice_Builder.Make(Spec, Decision)
8
8
  module Spec = Spec
9
- module Behavior = Behavior
9
+ module Decision = Decision
10
10
  let isAsync = Inner.isAsync
11
11
  type component = Inner.component
12
12
  let make: (
@@ -3,12 +3,12 @@
3
3
  import * as StateChangeSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs";
4
4
 
5
5
  function Make(Spec) {
6
- return Behavior => {
7
- let Inner = StateChangeSlice_Builder$ReventlessCore.Make(Spec)(Behavior);
6
+ return Decision => {
7
+ let Inner = StateChangeSlice_Builder$ReventlessCore.Make(Spec)(Decision);
8
8
  return {
9
9
  Inner: Inner,
10
10
  Spec: Spec,
11
- Behavior: Behavior,
11
+ Decision: Decision,
12
12
  isAsync: Inner.isAsync,
13
13
  make: Inner.make
14
14
  };
@@ -552,7 +552,7 @@ let run = (~dbPath: option<string>=?): unit => {
552
552
  Console.log("Nothing to do.")
553
553
  } else {
554
554
  // Set at all ⇒ it is the answer, affirmative or not. Falling through to
555
- // the prompt on an unrecognised value would ask for a TTY the runs this
555
+ // the prompt on an unrecognized value would ask for a TTY the runs this
556
556
  // variable exists for do not have, so `SEED_RESET_CONFIRM=no` would
557
557
  // throw where it plainly means "don't". Both arms read the reply
558
558
  // through one predicate, which is what keeps the typed `y` and 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("is empty when the deployment declares nothing", () => {
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("leaves a platform that declares nothing entirely alone", () => {
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
- expect(NodeFs.readFileSync(NodePath.join([dir, "config.json"])))->toEqual(shipped)
154
- expect(NodeFs.existsSync(NodePath.join([dir, "config.base.json"])))->toBe(false)
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.keysToArray,
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", () => {