@reventlessdev/reventless-spec 3.0.0-alpha.84 → 3.0.0-alpha.85

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 CHANGED
@@ -3,6 +3,14 @@
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.85 (2026-07-30)
7
+
8
+ ### Features
9
+
10
+ * **spec,core,local:** emit capabilities.json from the plugin build ([57a3276](https://github.com/ReventlessDev/reventless-core/commit/57a3276a6b02af93490a8b460e3aa158b7e0e0f8))
11
+ * **spec,core:** derive the platform capability list — inference and generation (stage 3) ([8ab4d25](https://github.com/ReventlessDev/reventless-core/commit/8ab4d2504bd717487fbc87a0f5a053024c8258f9))
12
+
13
+
6
14
  # 3.0.0-alpha.84 (2026-07-29)
7
15
 
8
16
  ### Features
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.84",
3
+ "version": "3.0.0-alpha.85",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
7
- "generate-plugin": "./run-generator.mjs"
7
+ "generate-plugin": "./run-generator.mjs",
8
+ "generate-platform": "./run-platform-generator.mjs"
8
9
  },
9
10
  "jest": {
10
11
  "testMatch": [
@@ -18,7 +19,8 @@
18
19
  "dependencies": {
19
20
  "jsonschema2graphql": "1.1.1",
20
21
  "sury": "11.0.0-alpha.4",
21
- "sury-ppx": "11.0.0-alpha.2"
22
+ "sury-ppx": "11.0.0-alpha.2",
23
+ "yaml": "^2.8.3"
22
24
  },
23
25
  "devDependencies": {
24
26
  "rescript": "12.3.0",
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./src/generator/PlatformGenerator.res.mjs"
@@ -0,0 +1,67 @@
1
+ /**
2
+ The per-plugin capability manifest — `capabilities.json`, written beside the
3
+ generated `Plugin.res` by the plugin package's build.
4
+
5
+ Each entry states one capability the plugin's fields declare they need, keyed
6
+ by the store's qualified `{plugin}.{store}` identity, with the declaring
7
+ `(component, field)` sites as provenance. The keys are taken verbatim from
8
+ `pluginStructure.requiredStores` — the manifest is a rendering of the
9
+ structure, never a second scan of the sources, so the two cannot spell one
10
+ fact differently.
11
+
12
+ The platform generator unions these files across a deployment's plugins and
13
+ emits the platform's capability list from them.
14
+ */
15
+
16
+ @schema
17
+ type kind = ObjectStore
18
+
19
+ /** The declaration site: the component's spec name and the field carrying the
20
+ `@storageRef` annotation. */
21
+ @schema
22
+ type provenance = {component: string, field: string}
23
+
24
+ @schema
25
+ type entry = {
26
+ kind: kind,
27
+ key: string,
28
+ declaredBy: array<provenance>,
29
+ }
30
+
31
+ @schema
32
+ type t = {capabilities: array<entry>}
33
+
34
+ /**
35
+ Build the manifest from a plugin's structure.
36
+
37
+ Keys iterate `requiredStores` itself, so a manifest's key set is byte-identical
38
+ to what the deployed plugin reports at runtime — the contract the deploy-time
39
+ coverage assertion checks against. A structure with no declarations yields an
40
+ empty `capabilities` list, not an absent file: "declares nothing" is a
41
+ statement, and the generator reading the manifests must be able to tell it
42
+ apart from "was never built".
43
+ */
44
+ let fromStructure = (structure: Plugin.pluginStructure): t => {
45
+ let declarations = structure.requiredStoreDeclarations->Option.getOr([])
46
+ {
47
+ capabilities: structure.requiredStores
48
+ ->Option.getOr([])
49
+ ->Array.map(key => {
50
+ kind: ObjectStore,
51
+ key,
52
+ declaredBy: declarations->Array.filterMap(d =>
53
+ d.store == key ? Some({component: d.component, field: d.field}) : None
54
+ ),
55
+ }),
56
+ }
57
+ }
58
+
59
+ /** Deterministic rendering: 2-space indent, trailing newline. Rebuilding with
60
+ no source change must produce a byte-identical file, so the committed
61
+ manifest never churns. */
62
+ let render = (manifest: t): string =>
63
+ JSON.stringify(manifest->S.reverseConvertToJsonOrThrow(schema), ~space=2) ++ "\n"
64
+
65
+ /** One call for the emit CLI: structure in, `capabilities.json` content out. */
66
+ let renderForStructure = (structure: Plugin.pluginStructure): string =>
67
+ structure->fromStructure->render
@@ -0,0 +1,59 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+
7
+ let kindSchema = S.literal("ObjectStore");
8
+
9
+ let provenanceSchema = S.schema(s => ({
10
+ component: s.m(S.string),
11
+ field: s.m(S.string)
12
+ }));
13
+
14
+ let entrySchema = S.schema(s => ({
15
+ kind: s.m(kindSchema),
16
+ key: s.m(S.string),
17
+ declaredBy: s.m(S.array(provenanceSchema))
18
+ }));
19
+
20
+ let schema = S.schema(s => ({
21
+ capabilities: s.m(S.array(entrySchema))
22
+ }));
23
+
24
+ function fromStructure(structure) {
25
+ let declarations = Stdlib_Option.getOr(structure.requiredStoreDeclarations, []);
26
+ return {
27
+ capabilities: Stdlib_Option.getOr(structure.requiredStores, []).map(key => ({
28
+ kind: "ObjectStore",
29
+ key: key,
30
+ declaredBy: Stdlib_Array.filterMap(declarations, d => {
31
+ if (d.store === key) {
32
+ return {
33
+ component: d.component,
34
+ field: d.field
35
+ };
36
+ }
37
+ })
38
+ }))
39
+ };
40
+ }
41
+
42
+ function render(manifest) {
43
+ return JSON.stringify(S.reverseConvertToJsonOrThrow(manifest, schema), undefined, 2) + "\n";
44
+ }
45
+
46
+ function renderForStructure(structure) {
47
+ return render(fromStructure(structure));
48
+ }
49
+
50
+ export {
51
+ kindSchema,
52
+ provenanceSchema,
53
+ entrySchema,
54
+ schema,
55
+ fromStructure,
56
+ render,
57
+ renderForStructure,
58
+ }
59
+ /* kindSchema Not a pure module */
@@ -379,6 +379,26 @@ type extensionPointDef = {
379
379
  // jsonableValidation inside the pluginStructure union variant payload.
380
380
  let extensionPointDefArrayOptionSchema = _jsNullable(S.array(extensionPointDefSchema), ())
381
381
 
382
+ /**
383
+ One field's store requirement, with its provenance.
384
+
385
+ `store` is the same qualified `{plugin}.{store}` string `requiredStores`
386
+ carries; `component` and `field` name the declaration site. The site matters
387
+ because a requirement only ever changes by editing a field — when a rename
388
+ removes a store from the manifest, the diff has to say which field caused it.
389
+ */
390
+ @schema
391
+ type requiredStoreDeclaration = {
392
+ store: string,
393
+ component: string,
394
+ field: string,
395
+ }
396
+
397
+ let requiredStoreDeclarationArrayOptionSchema = _jsNullable(
398
+ S.array(requiredStoreDeclarationSchema),
399
+ (),
400
+ )
401
+
382
402
  @schema
383
403
  type pluginStructure = {
384
404
  readModels: array<queryableDef>,
@@ -409,6 +429,14 @@ type pluginStructure = {
409
429
  existed still decode (absent → None, read as []).
410
430
  */
411
431
  requiredStores: @s.matches(stringArrayOptionSchema) option<array<string>>,
432
+ /**
433
+ Provenance for `requiredStores`: one entry per declaring `(component, field)`
434
+ site, with `store` matching the qualified key above. `requiredStores` is
435
+ derived from this list, so the two cannot disagree. Optional and js_nullable
436
+ for the same reason as `extensionPoints`.
437
+ */
438
+ requiredStoreDeclarations: @s.matches(requiredStoreDeclarationArrayOptionSchema)
439
+ option<array<requiredStoreDeclaration>>,
412
440
  }
413
441
 
414
442
  let pluginStructureOptionSchema = _jsNullable(pluginStructureSchema, ())
@@ -178,6 +178,14 @@ let extensionPointDefSchema = S.schema(s => ({
178
178
 
179
179
  let extensionPointDefArrayOptionSchema = SuryResMjs.js_nullable(S.array(extensionPointDefSchema));
180
180
 
181
+ let requiredStoreDeclarationSchema = S.schema(s => ({
182
+ store: s.m(S.string),
183
+ component: s.m(S.string),
184
+ field: s.m(S.string)
185
+ }));
186
+
187
+ let requiredStoreDeclarationArrayOptionSchema = SuryResMjs.js_nullable(S.array(requiredStoreDeclarationSchema));
188
+
181
189
  let pluginStructureSchema = S.schema(s => ({
182
190
  readModels: s.m(S.array(queryableDefSchema)),
183
191
  stateViewSlices: s.m(S.array(queryableDefSchema)),
@@ -188,7 +196,8 @@ let pluginStructureSchema = S.schema(s => ({
188
196
  inboundTranslationSlices: s.m(S.array(inboundTranslationSliceDefSchema)),
189
197
  extensions: s.m(S.array(extensionDefSchema)),
190
198
  extensionPoints: s.m(extensionPointDefArrayOptionSchema),
191
- requiredStores: s.m(stringArrayOptionSchema)
199
+ requiredStores: s.m(stringArrayOptionSchema),
200
+ requiredStoreDeclarations: s.m(requiredStoreDeclarationArrayOptionSchema)
192
201
  }));
193
202
 
194
203
  let pluginStructureOptionSchema = SuryResMjs.js_nullable(pluginStructureSchema);
@@ -244,6 +253,8 @@ export {
244
253
  extensionDefSchema,
245
254
  extensionPointDefSchema,
246
255
  extensionPointDefArrayOptionSchema,
256
+ requiredStoreDeclarationSchema,
257
+ requiredStoreDeclarationArrayOptionSchema,
247
258
  pluginStructureSchema,
248
259
  pluginStructureOptionSchema,
249
260
  pluginDefinitionSchema,
@@ -0,0 +1,139 @@
1
+ // Pure rendering for the platform generator: union the plugins' capability
2
+ // manifests and emit the committed `PlatformCapabilities.res`.
3
+ //
4
+ // Only the derived list is generated — the platform root stays hand-written
5
+ // and reads `PlatformCapabilities.capabilities`. That keeps a requirement
6
+ // change small and entirely about capabilities in review, and it keeps the
7
+ // generator out of the business of expressing everything a real root does
8
+ // (bootstrap calls, capability handles, stack references).
9
+ //
10
+ // I/O-free so the exact bytes are testable; `PlatformGenerator` owns the file
11
+ // system.
12
+
13
+ /** One plugin's manifest, tagged with the name `deploy-manifest.yaml` lists it
14
+ under — the name provenance comments cite. */
15
+ type pluginManifest = {
16
+ pluginName: string,
17
+ manifest: CapabilityManifest.t,
18
+ }
19
+
20
+ /** One declaring site, keeping which plugin's manifest carried it. */
21
+ type provenance = {
22
+ pluginName: string,
23
+ component: string,
24
+ field: string,
25
+ }
26
+
27
+ /** One capability the platform must provision: the qualified `{plugin}.{store}`
28
+ key with every declaring site across every plugin. Identity is the key —
29
+ many fields, in several plugins, legitimately name one store. */
30
+ type unionEntry = {
31
+ kind: CapabilityManifest.kind,
32
+ key: string,
33
+ declaredBy: array<provenance>,
34
+ }
35
+
36
+ /** Union the manifests: entries with one key collapse to one capability, their
37
+ provenance concatenated in manifest order. Sorted by key so the rendered
38
+ file is deterministic. */
39
+ let union = (manifests: array<pluginManifest>): array<unionEntry> => {
40
+ let byKey: dict<unionEntry> = Dict.make()
41
+ manifests->Array.forEach(({pluginName, manifest}) =>
42
+ manifest.capabilities->Array.forEach(entry => {
43
+ let sites =
44
+ entry.declaredBy->Array.map(site => {
45
+ pluginName,
46
+ component: site.component,
47
+ field: site.field,
48
+ })
49
+ switch byKey->Dict.get(entry.key) {
50
+ | Some(existing) =>
51
+ byKey->Dict.set(entry.key, {...existing, declaredBy: existing.declaredBy->Array.concat(sites)})
52
+ | None => byKey->Dict.set(entry.key, {kind: entry.kind, key: entry.key, declaredBy: sites})
53
+ }
54
+ })
55
+ )
56
+ byKey
57
+ ->Dict.valuesToArray
58
+ ->Array.toSorted((a, b) => String.compare(a.key, b.key))
59
+ }
60
+
61
+ let quote = (s: string): string => s->JSON.Encode.string->JSON.stringify
62
+
63
+ // A key is `{plugin}.{store}` by construction (`Plugin_Structure` qualifies
64
+ // every declaration). The split is at the first dot: the plugin's registered
65
+ // name cannot contain one, the store name could in principle.
66
+ let splitKey = (key: string): option<(string, string)> =>
67
+ key
68
+ ->String.indexOfOpt(".")
69
+ ->Option.map(i => (
70
+ key->String.slice(~start=0, ~end=i),
71
+ key->String.slice(~start=i + 1),
72
+ ))
73
+
74
+ // The provenance comment quotes the annotation as its author wrote it: a store
75
+ // declared by its own plugin is unqualified, a foreign store keeps the
76
+ // qualified form. The manifest carries only the deploy-manifest plugin name
77
+ // (lowercase by convention) while the key carries the registered name, so the
78
+ // comparison is case-insensitive — and it only shapes a comment.
79
+ let annotationStore = (~pluginName, ~key): string =>
80
+ switch splitKey(key) {
81
+ | Some((keyPlugin, store)) if keyPlugin->String.toLowerCase == pluginName->String.toLowerCase => store
82
+ | _ => key
83
+ }
84
+
85
+ let renderEntry = (entry: unionEntry): result<array<string>, string> =>
86
+ switch splitKey(entry.key) {
87
+ | None =>
88
+ Error(
89
+ `malformed capability key ${quote(entry.key)} — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`,
90
+ )
91
+ | Some((plugin, store)) => {
92
+ let comments =
93
+ entry.declaredBy->Array.map(site =>
94
+ ` // ${site.pluginName}: ${site.component}.${site.field} @storageRef(${quote(
95
+ annotationStore(~pluginName=site.pluginName, ~key=entry.key),
96
+ )})`
97
+ )
98
+ let line = switch entry.kind {
99
+ | ObjectStore => ` ObjectStore({plugin: ${quote(plugin)}, store: ${quote(store)}}),`
100
+ }
101
+ Ok(comments->Array.concat([line]))
102
+ }
103
+ }
104
+
105
+ let header = [
106
+ "// AUTO-GENERATED — do not edit. Run `pnpm run generate:platform` to update.",
107
+ "//",
108
+ "// The platform's capability list, unioned from the committed",
109
+ "// `capabilities.json` manifests of the plugins deploy-manifest.yaml names.",
110
+ "// Each entry keeps its declaring fields as provenance, so when a capability",
111
+ "// disappears from this list, the diff says which field's change removed it.",
112
+ "",
113
+ ]
114
+
115
+ /** Render the complete `PlatformCapabilities.res`. Deterministic and
116
+ newline-terminated: regenerating with no manifest change is byte-identical,
117
+ so the committed file never churns. */
118
+ let render = (manifests: array<pluginManifest>): result<string, string> => {
119
+ let entries = union(manifests)
120
+ let rendered = entries->Array.map(renderEntry)
121
+ switch rendered->Array.findMap(r =>
122
+ switch r {
123
+ | Error(e) => Some(e)
124
+ | Ok(_) => None
125
+ }
126
+ ) {
127
+ | Some(e) => Error(e)
128
+ | None => {
129
+ let body = if entries->Array.length == 0 {
130
+ ["let capabilities: array<ReventlessInfra.Platform.capability> = []"]
131
+ } else {
132
+ ["let capabilities: array<ReventlessInfra.Platform.capability> = ["]
133
+ ->Array.concat(rendered->Array.flatMap(r => r->Result.getOr([])))
134
+ ->Array.concat(["]"])
135
+ }
136
+ Ok(header->Array.concat(body)->Array.join("\n") ++ "\n")
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,116 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
7
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
8
+
9
+ function union(manifests) {
10
+ let byKey = {};
11
+ manifests.forEach(param => {
12
+ let pluginName = param.pluginName;
13
+ param.manifest.capabilities.forEach(entry => {
14
+ let sites = entry.declaredBy.map(site => ({
15
+ pluginName: pluginName,
16
+ component: site.component,
17
+ field: site.field
18
+ }));
19
+ let existing = byKey[entry.key];
20
+ if (existing !== undefined) {
21
+ byKey[entry.key] = {
22
+ kind: existing.kind,
23
+ key: existing.key,
24
+ declaredBy: existing.declaredBy.concat(sites)
25
+ };
26
+ } else {
27
+ byKey[entry.key] = {
28
+ kind: entry.kind,
29
+ key: entry.key,
30
+ declaredBy: sites
31
+ };
32
+ }
33
+ });
34
+ });
35
+ return Object.values(byKey).toSorted((a, b) => Primitive_string.compare(a.key, b.key));
36
+ }
37
+
38
+ function quote(s) {
39
+ return JSON.stringify(s);
40
+ }
41
+
42
+ function splitKey(key) {
43
+ return Stdlib_Option.map(Stdlib_String.indexOfOpt(key, "."), i => [
44
+ key.slice(0, i),
45
+ key.slice(i + 1 | 0)
46
+ ]);
47
+ }
48
+
49
+ function annotationStore(pluginName, key) {
50
+ let match = splitKey(key);
51
+ if (match !== undefined && match[0].toLowerCase() === pluginName.toLowerCase()) {
52
+ return match[1];
53
+ } else {
54
+ return key;
55
+ }
56
+ }
57
+
58
+ function renderEntry(entry) {
59
+ let match = splitKey(entry.key);
60
+ if (match === undefined) {
61
+ return {
62
+ TAG: "Error",
63
+ _0: `malformed capability key ` + JSON.stringify(entry.key) + ` — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`
64
+ };
65
+ }
66
+ let comments = entry.declaredBy.map(site => ` // ` + site.pluginName + `: ` + site.component + `.` + site.field + ` @storageRef(` + JSON.stringify(annotationStore(site.pluginName, entry.key)) + `)`);
67
+ let line = ` ObjectStore({plugin: ` + JSON.stringify(match[0]) + `, store: ` + JSON.stringify(match[1]) + `}),`;
68
+ return {
69
+ TAG: "Ok",
70
+ _0: comments.concat([line])
71
+ };
72
+ }
73
+
74
+ let header = [
75
+ "// AUTO-GENERATED — do not edit. Run `pnpm run generate:platform` to update.",
76
+ "//",
77
+ "// The platform's capability list, unioned from the committed",
78
+ "// `capabilities.json` manifests of the plugins deploy-manifest.yaml names.",
79
+ "// Each entry keeps its declaring fields as provenance, so when a capability",
80
+ "// disappears from this list, the diff says which field's change removed it.",
81
+ ""
82
+ ];
83
+
84
+ function render(manifests) {
85
+ let entries = union(manifests);
86
+ let rendered = entries.map(renderEntry);
87
+ let e = Stdlib_Array.findMap(rendered, r => {
88
+ if (r.TAG === "Ok") {
89
+ return;
90
+ } else {
91
+ return r._0;
92
+ }
93
+ });
94
+ if (e !== undefined) {
95
+ return {
96
+ TAG: "Error",
97
+ _0: e
98
+ };
99
+ }
100
+ let body = entries.length === 0 ? ["let capabilities: array<ReventlessInfra.Platform.capability> = []"] : ["let capabilities: array<ReventlessInfra.Platform.capability> = ["].concat(rendered.flatMap(r => Stdlib_Result.getOr(r, []))).concat(["]"]);
101
+ return {
102
+ TAG: "Ok",
103
+ _0: header.concat(body).join("\n") + "\n"
104
+ };
105
+ }
106
+
107
+ export {
108
+ union,
109
+ quote,
110
+ splitKey,
111
+ annotationStore,
112
+ renderEntry,
113
+ header,
114
+ render,
115
+ }
116
+ /* No side effect */
@@ -0,0 +1,148 @@
1
+ // Entry point for the platform generator.
2
+ // Usage: generate-platform <deploy-manifest.yaml>
3
+ //
4
+ // Reads the deploy manifest for the plugin list, unions the plugins' committed
5
+ // `capabilities.json` manifests, and writes the platform's committed
6
+ // `PlatformCapabilities.res` (see PlatformCodegen). The platform root stays
7
+ // hand-written and reads `PlatformCapabilities.capabilities`.
8
+ //
9
+ // A plugin path may be the composition package itself (its `src/` carries the
10
+ // manifest) or an `-aws` deploy root that delegates to one. The delegation's
11
+ // single existing record is the root's own `generate` script —
12
+ // `generate-plugin --aws <Namespace> <srcDir>` — so the composition `src/` is
13
+ // read from there rather than from a second, hand-typed spelling of the path.
14
+
15
+ @val external processExit: int => unit = "process.exit"
16
+ @module("yaml") external parseYaml: string => JSON.t = "parse"
17
+
18
+ let fail = (message: string) => {
19
+ Console.error("generate-platform: " ++ message)
20
+ processExit(1)
21
+ }
22
+
23
+ let asObject = (json: JSON.t): option<dict<JSON.t>> => json->JSON.Decode.object
24
+ let stringAt = (obj: dict<JSON.t>, field: string): option<string> =>
25
+ obj->Dict.get(field)->Option.flatMap(JSON.Decode.string)
26
+
27
+ // Where a plugin keeps its capability manifest. The direct arm covers a plugin
28
+ // path that is itself a composition package; the delegated arm follows the
29
+ // `-aws` root's `generate-plugin --aws <Namespace> <srcDir>` script to the
30
+ // composition `src/` it is generated from.
31
+ let manifestPathFor = (~pluginDir: string): option<string> => {
32
+ let direct = Generator_Node.join([pluginDir, "src", "capabilities.json"])
33
+ if Generator_Node.existsSync(direct) {
34
+ Some(direct)
35
+ } else {
36
+ let packageJsonPath = Generator_Node.join([pluginDir, "package.json"])
37
+ if !Generator_Node.existsSync(packageJsonPath) {
38
+ None
39
+ } else {
40
+ let generateScript =
41
+ try Generator_Node.readFileSync(packageJsonPath)
42
+ ->JSON.parseOrThrow
43
+ ->asObject
44
+ ->Option.flatMap(pkg => pkg->Dict.get("scripts"))
45
+ ->Option.flatMap(asObject)
46
+ ->Option.flatMap(scripts => scripts->stringAt("generate")) catch {
47
+ | _ => None
48
+ }
49
+ generateScript
50
+ ->Option.flatMap(script => {
51
+ let tokens = script->String.split(" ")->Array.filter(t => t != "")
52
+ switch tokens {
53
+ | ["generate-plugin", "--aws", _namespace, srcDir] =>
54
+ Some(Generator_Node.resolve([pluginDir, srcDir, "capabilities.json"]))
55
+ | _ => None
56
+ }
57
+ })
58
+ ->Option.filter(Generator_Node.existsSync)
59
+ }
60
+ }
61
+ }
62
+
63
+ let () = {
64
+ let manifestArg = Generator_Node.argv->Array.get(2)->Option.getOr("")
65
+ if manifestArg == "" {
66
+ Console.error("Usage: generate-platform <deploy-manifest.yaml>")
67
+ processExit(1)
68
+ } else {
69
+ let manifestPath = Generator_Node.resolve([manifestArg])
70
+ if !Generator_Node.existsSync(manifestPath) {
71
+ fail(`${manifestPath} not found`)
72
+ }
73
+ let manifestDir = Generator_Node.dirname(manifestPath)
74
+ let deployManifest = try Generator_Node.readFileSync(manifestPath)->parseYaml catch {
75
+ | _ => {
76
+ fail(`could not parse ${manifestPath} as YAML`)
77
+ JSON.Null
78
+ }
79
+ }
80
+
81
+ let platformPath =
82
+ deployManifest
83
+ ->asObject
84
+ ->Option.flatMap(m => m->Dict.get("platform"))
85
+ ->Option.flatMap(asObject)
86
+ ->Option.flatMap(p => p->stringAt("path"))
87
+ let plugins =
88
+ deployManifest
89
+ ->asObject
90
+ ->Option.flatMap(m => m->Dict.get("plugins"))
91
+ ->Option.flatMap(JSON.Decode.array)
92
+ ->Option.map(entries =>
93
+ entries->Array.filterMap(entry => {
94
+ let obj = entry->asObject
95
+ switch (
96
+ obj->Option.flatMap(o => o->stringAt("name")),
97
+ obj->Option.flatMap(o => o->stringAt("path")),
98
+ ) {
99
+ | (Some(name), Some(path)) => Some((name, path))
100
+ | _ => None
101
+ }
102
+ })
103
+ )
104
+
105
+ switch (platformPath, plugins) {
106
+ | (Some(platformPath), Some(plugins)) => {
107
+ let pluginManifests = plugins->Array.map(((pluginName, pluginPath)) => {
108
+ let pluginDir = Generator_Node.resolve([manifestDir, pluginPath])
109
+ switch manifestPathFor(~pluginDir) {
110
+ | None => {
111
+ fail(
112
+ `no capabilities.json found for plugin "${pluginName}" (${pluginDir}) — ` ++
113
+ `build the plugin first; its build emits src/capabilities.json beside Plugin.res`,
114
+ )
115
+ ({pluginName, manifest: {capabilities: []}}: PlatformCodegen.pluginManifest)
116
+ }
117
+ | Some(capabilitiesPath) => {
118
+ let manifest = try Generator_Node.readFileSync(capabilitiesPath)
119
+ ->JSON.parseOrThrow
120
+ ->S.parseOrThrow(CapabilityManifest.schema) catch {
121
+ | _ => {
122
+ fail(`could not parse ${capabilitiesPath} as a capability manifest`)
123
+ ({capabilities: []}: CapabilityManifest.t)
124
+ }
125
+ }
126
+ ({pluginName, manifest}: PlatformCodegen.pluginManifest)
127
+ }
128
+ }
129
+ })
130
+
131
+ switch PlatformCodegen.render(pluginManifests) {
132
+ | Error(message) => fail(message)
133
+ | Ok(source) => {
134
+ let outputPath = Generator_Node.resolve([
135
+ manifestDir,
136
+ platformPath,
137
+ "src",
138
+ "PlatformCapabilities.res",
139
+ ])
140
+ Generator_Node.writeFileSync(outputPath, source)
141
+ Console.log("Generated: " ++ outputPath)
142
+ }
143
+ }
144
+ }
145
+ | _ => fail(`${manifestPath} needs a \`platform.path\` and a \`plugins\` list with name + path`)
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,140 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Yaml from "yaml";
5
+ import * as Nodefs from "node:fs";
6
+ import * as Nodepath from "node:path";
7
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
8
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as PlatformCodegen$Reventless from "./PlatformCodegen.res.mjs";
11
+ import * as CapabilityManifest$Reventless from "../components/CapabilityManifest.res.mjs";
12
+
13
+ function fail(message) {
14
+ console.error("generate-platform: " + message);
15
+ process.exit(1);
16
+ }
17
+
18
+ function asObject(json) {
19
+ return Stdlib_JSON.Decode.object(json);
20
+ }
21
+
22
+ function stringAt(obj, field) {
23
+ return Stdlib_Option.flatMap(obj[field], Stdlib_JSON.Decode.string);
24
+ }
25
+
26
+ function manifestPathFor(pluginDir) {
27
+ let direct = Nodepath.join(pluginDir, "src", "capabilities.json");
28
+ if (Nodefs.existsSync(direct)) {
29
+ return direct;
30
+ }
31
+ let packageJsonPath = Nodepath.join(pluginDir, "package.json");
32
+ if (!Nodefs.existsSync(packageJsonPath)) {
33
+ return;
34
+ }
35
+ let generateScript;
36
+ try {
37
+ generateScript = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(packageJsonPath, "utf8"))), pkg => pkg["scripts"]), asObject), scripts => stringAt(scripts, "generate"));
38
+ } catch (exn) {
39
+ generateScript = undefined;
40
+ }
41
+ return Stdlib_Option.filter(Stdlib_Option.flatMap(generateScript, script => {
42
+ let tokens = script.split(" ").filter(t => t !== "");
43
+ if (tokens.length !== 4) {
44
+ return;
45
+ }
46
+ let match = tokens[0];
47
+ if (match !== "generate-plugin") {
48
+ return;
49
+ }
50
+ let match$1 = tokens[1];
51
+ if (match$1 !== "--aws") {
52
+ return;
53
+ }
54
+ let srcDir = tokens[3];
55
+ return Nodepath.resolve(pluginDir, srcDir, "capabilities.json");
56
+ }), prim => Nodefs.existsSync(prim));
57
+ }
58
+
59
+ let manifestArg = Stdlib_Option.getOr(process.argv[2], "");
60
+
61
+ if (manifestArg === "") {
62
+ console.error("Usage: generate-platform <deploy-manifest.yaml>");
63
+ process.exit(1);
64
+ } else {
65
+ let manifestPath = Nodepath.resolve(manifestArg);
66
+ if (!Nodefs.existsSync(manifestPath)) {
67
+ fail(manifestPath + ` not found`);
68
+ }
69
+ let manifestDir = Nodepath.dirname(manifestPath);
70
+ let deployManifest;
71
+ try {
72
+ deployManifest = Yaml.parse(Nodefs.readFileSync(manifestPath, "utf8"));
73
+ } catch (exn) {
74
+ fail(`could not parse ` + manifestPath + ` as YAML`);
75
+ deployManifest = null;
76
+ }
77
+ let platformPath = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(deployManifest), m => m["platform"]), asObject), p => stringAt(p, "path"));
78
+ let plugins = Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(deployManifest), m => m["plugins"]), Stdlib_JSON.Decode.array), entries => Stdlib_Array.filterMap(entries, entry => {
79
+ let obj = Stdlib_JSON.Decode.object(entry);
80
+ let match = Stdlib_Option.flatMap(obj, o => stringAt(o, "name"));
81
+ let match$1 = Stdlib_Option.flatMap(obj, o => stringAt(o, "path"));
82
+ if (match !== undefined && match$1 !== undefined) {
83
+ return [
84
+ match,
85
+ match$1
86
+ ];
87
+ }
88
+ }));
89
+ if (platformPath !== undefined) {
90
+ if (plugins !== undefined) {
91
+ let pluginManifests = plugins.map(param => {
92
+ let pluginName = param[0];
93
+ let pluginDir = Nodepath.resolve(manifestDir, param[1]);
94
+ let capabilitiesPath = manifestPathFor(pluginDir);
95
+ if (capabilitiesPath !== undefined) {
96
+ let manifest;
97
+ try {
98
+ manifest = S.parseOrThrow(JSON.parse(Nodefs.readFileSync(capabilitiesPath, "utf8")), CapabilityManifest$Reventless.schema);
99
+ } catch (exn) {
100
+ fail(`could not parse ` + capabilitiesPath + ` as a capability manifest`);
101
+ manifest = {
102
+ capabilities: []
103
+ };
104
+ }
105
+ return {
106
+ pluginName: pluginName,
107
+ manifest: manifest
108
+ };
109
+ }
110
+ fail(`no capabilities.json found for plugin "` + pluginName + `" (` + pluginDir + `) — build the plugin first; its build emits src/capabilities.json beside Plugin.res`);
111
+ return {
112
+ pluginName: pluginName,
113
+ manifest: {
114
+ capabilities: []
115
+ }
116
+ };
117
+ });
118
+ let message = PlatformCodegen$Reventless.render(pluginManifests);
119
+ if (message.TAG === "Ok") {
120
+ let outputPath = Nodepath.resolve(manifestDir, platformPath, "src", "PlatformCapabilities.res");
121
+ Nodefs.writeFileSync(outputPath, message._0, "utf8");
122
+ console.log("Generated: " + outputPath);
123
+ } else {
124
+ fail(message._0);
125
+ }
126
+ } else {
127
+ fail(manifestPath + ` needs a \`platform.path\` and a \`plugins\` list with name + path`);
128
+ }
129
+ } else {
130
+ fail(manifestPath + ` needs a \`platform.path\` and a \`plugins\` list with name + path`);
131
+ }
132
+ }
133
+
134
+ export {
135
+ fail,
136
+ asObject,
137
+ stringAt,
138
+ manifestPathFor,
139
+ }
140
+ /* manifestArg Not a pure module */