@reventlessdev/reventless-spec 3.0.0-alpha.100
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 +931 -0
- package/LICENSE +202 -0
- package/README.md +109 -0
- package/package.json +49 -0
- package/rescript.json +32 -0
- package/run-generator.mjs +2 -0
- package/run-platform-generator.mjs +2 -0
- package/scripts/generate-currency.mjs +215 -0
- package/scripts/iso-4217-list-one.xml +1956 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +192 -0
- package/src/LogPrefix.res.mjs +159 -0
- package/src/PackageVersion.res +67 -0
- package/src/PackageVersion.res.mjs +81 -0
- package/src/components/Aggregate.res +64 -0
- package/src/components/Aggregate.res.mjs +2 -0
- package/src/components/AutomationSlice.res +279 -0
- package/src/components/AutomationSlice.res.mjs +30 -0
- package/src/components/CapabilityManifest.res +74 -0
- package/src/components/CapabilityManifest.res.mjs +61 -0
- package/src/components/ComponentKind.res +99 -0
- package/src/components/ComponentKind.res.mjs +125 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +118 -0
- package/src/components/DcbDecode.res.mjs +100 -0
- package/src/components/DcbScopeInference.res +244 -0
- package/src/components/DcbScopeInference.res.mjs +177 -0
- package/src/components/DcbTag.res +1335 -0
- package/src/components/DcbTag.res.mjs +898 -0
- package/src/components/DcbValidation.res +427 -0
- package/src/components/DcbValidation.res.mjs +423 -0
- package/src/components/DisplayName.res +40 -0
- package/src/components/DisplayName.res.mjs +26 -0
- package/src/components/ExtensionPoint.res +27 -0
- package/src/components/ExtensionPoint.res.mjs +2 -0
- package/src/components/InboundTranslationSlice.res +85 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +153 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +538 -0
- package/src/components/Plugin.res.mjs +264 -0
- package/src/components/PluginName.res +39 -0
- package/src/components/PluginName.res.mjs +45 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +55 -0
- package/src/components/Reference.res.mjs +50 -0
- package/src/components/Snapshot.res +26 -0
- package/src/components/Snapshot.res.mjs +2 -0
- package/src/components/StateAnnotations.res +97 -0
- package/src/components/StateAnnotations.res.mjs +15 -0
- package/src/components/StateChangeSlice.res +131 -0
- package/src/components/StateChangeSlice.res.mjs +2 -0
- package/src/components/StateViewSlice.res +123 -0
- package/src/components/StateViewSlice.res.mjs +2 -0
- package/src/components/Task.res +62 -0
- package/src/components/Task.res.mjs +2 -0
- package/src/generator/Codegen.res +842 -0
- package/src/generator/Codegen.res.mjs +565 -0
- package/src/generator/Config.res +106 -0
- package/src/generator/Config.res.mjs +69 -0
- package/src/generator/Discovery.res +230 -0
- package/src/generator/Discovery.res.mjs +198 -0
- package/src/generator/Generator_Node.res +14 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +460 -0
- package/src/generator/Pairing.res.mjs +415 -0
- package/src/generator/PlatformCodegen.res +207 -0
- package/src/generator/PlatformCodegen.res.mjs +154 -0
- package/src/generator/PlatformGenerator.res +126 -0
- package/src/generator/PlatformGenerator.res.mjs +114 -0
- package/src/generator/PlatformManifests.res +203 -0
- package/src/generator/PlatformManifests.res.mjs +212 -0
- package/src/generator/PluginGenerator.res +57 -0
- package/src/generator/PluginGenerator.res.mjs +73 -0
- package/src/semantic/Bytes.res +54 -0
- package/src/semantic/Bytes.res.mjs +38 -0
- package/src/semantic/Capabilities.res +43 -0
- package/src/semantic/Capabilities.res.mjs +17 -0
- package/src/semantic/Color.res +51 -0
- package/src/semantic/Color.res.mjs +29 -0
- package/src/semantic/Currency.res +598 -0
- package/src/semantic/Currency.res.mjs +743 -0
- package/src/semantic/DateRange.res +148 -0
- package/src/semantic/DateRange.res.mjs +74 -0
- package/src/semantic/Duration.res +53 -0
- package/src/semantic/Duration.res.mjs +26 -0
- package/src/semantic/Email.res +51 -0
- package/src/semantic/Email.res.mjs +31 -0
- package/src/semantic/GeoPoint.res +226 -0
- package/src/semantic/GeoPoint.res.mjs +190 -0
- package/src/semantic/Geocoding.res +127 -0
- package/src/semantic/Geocoding.res.mjs +36 -0
- package/src/semantic/Money.res +196 -0
- package/src/semantic/Money.res.mjs +138 -0
- package/src/semantic/Offload.res +294 -0
- package/src/semantic/Offload.res.mjs +191 -0
- package/src/semantic/Percent.res +53 -0
- package/src/semantic/Percent.res.mjs +33 -0
- package/src/semantic/Phone.res +55 -0
- package/src/semantic/Phone.res.mjs +29 -0
- package/src/semantic/Semantic.res +162 -0
- package/src/semantic/Semantic.res.mjs +95 -0
- package/src/semantic/StorageRef.res +164 -0
- package/src/semantic/StorageRef.res.mjs +111 -0
- package/src/semantic/Url.res +66 -0
- package/src/semantic/Url.res.mjs +48 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +86 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/DateTime.res +29 -0
- package/src/types/DateTime.res.mjs +16 -0
- package/src/types/EventMapping.res +100 -0
- package/src/types/EventMapping.res.mjs +15 -0
- package/src/types/Handler.res +30 -0
- package/src/types/Handler.res.mjs +2 -0
- package/src/types/Id.res +75 -0
- package/src/types/Id.res.mjs +37 -0
- package/src/types/Identity.res +46 -0
- package/src/types/Identity.res.mjs +51 -0
- package/src/types/Message.res +326 -0
- package/src/types/Message.res.mjs +186 -0
- package/src/types/Projection.res +220 -0
- package/src/types/Projection.res.mjs +44 -0
- package/src/types/QueryEngine.res +123 -0
- package/src/types/QueryEngine.res.mjs +12 -0
- package/src/types/ReadConsistency.res +38 -0
- package/src/types/ReadConsistency.res.mjs +29 -0
- package/src/types/Schedule.res +65 -0
- package/src/types/Schedule.res.mjs +68 -0
- package/src/types/SideEffect.res +45 -0
- package/src/types/SideEffect.res.mjs +2 -0
- package/src/types/StoredEvent.res +46 -0
- package/src/types/StoredEvent.res.mjs +32 -0
- package/src/types/Visibility.res +24 -0
- package/src/types/Visibility.res.mjs +25 -0
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
annotation: site.annotation
|
|
19
|
+
}));
|
|
20
|
+
let existing = byKey[entry.key];
|
|
21
|
+
if (existing !== undefined) {
|
|
22
|
+
byKey[entry.key] = {
|
|
23
|
+
kind: existing.kind,
|
|
24
|
+
key: existing.key,
|
|
25
|
+
declaredBy: existing.declaredBy.concat(sites)
|
|
26
|
+
};
|
|
27
|
+
} else {
|
|
28
|
+
byKey[entry.key] = {
|
|
29
|
+
kind: entry.kind,
|
|
30
|
+
key: entry.key,
|
|
31
|
+
declaredBy: sites
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
return Object.values(byKey).toSorted((a, b) => Primitive_string.compare(a.key, b.key));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function quote(s) {
|
|
40
|
+
return JSON.stringify(s);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitKey(key) {
|
|
44
|
+
return Stdlib_Option.map(Stdlib_String.indexOfOpt(key, "."), i => [
|
|
45
|
+
key.slice(0, i),
|
|
46
|
+
key.slice(i + 1 | 0)
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderEntry(entry) {
|
|
51
|
+
let match = splitKey(entry.key);
|
|
52
|
+
if (match === undefined) {
|
|
53
|
+
return {
|
|
54
|
+
TAG: "Error",
|
|
55
|
+
_0: `malformed capability key ` + JSON.stringify(entry.key) + ` — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let comments = entry.declaredBy.map(site => {
|
|
59
|
+
let site_ = ` // ` + site.pluginName + `: ` + site.component + `.` + site.field;
|
|
60
|
+
let annotation = site.annotation;
|
|
61
|
+
if (annotation !== undefined) {
|
|
62
|
+
return site_ + ` @storageRef(` + JSON.stringify(annotation) + `)`;
|
|
63
|
+
} else {
|
|
64
|
+
return site_;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
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
|
+
function duplicatePluginOwners(entries) {
|
|
75
|
+
let owners = {};
|
|
76
|
+
entries.forEach(entry => {
|
|
77
|
+
let match = splitKey(entry.key);
|
|
78
|
+
if (match === undefined) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let plugin = match[0];
|
|
82
|
+
entry.declaredBy.forEach(site => {
|
|
83
|
+
let annotation = site.annotation;
|
|
84
|
+
if (annotation === undefined) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (annotation.includes(".")) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let claimants = Stdlib_Option.getOr(owners[plugin], []);
|
|
91
|
+
if (!claimants.includes(site.pluginName)) {
|
|
92
|
+
owners[plugin] = claimants.concat([site.pluginName]);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
return Object.entries(owners).filter(param => param[1].length > 1).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function duplicatePluginMessage(param) {
|
|
101
|
+
return `Plugin name "` + param[0] + `" is registered by more than one deployable: ` + param[1].join(", ") + `.\n A platform keys its plugin registry by name, so the second registration is read as a new VERSION of the first and supersedes it.\n Give each deployable's plugin a distinct name (plugin.json), or — if these were meant to be one plugin — deploy only one of them.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let header = [
|
|
105
|
+
"// AUTO-GENERATED — do not edit. Run `pnpm run generate:platform` to update.",
|
|
106
|
+
"//",
|
|
107
|
+
"// The platform's capability list, unioned from the committed",
|
|
108
|
+
"// `capabilities.json` manifests of the plugins deploy-manifest.yaml names.",
|
|
109
|
+
"// Each entry keeps its declaring fields as provenance, so when a capability",
|
|
110
|
+
"// disappears from this list, the diff says which field's change removed it.",
|
|
111
|
+
""
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
function render(manifests) {
|
|
115
|
+
let entries = union(manifests);
|
|
116
|
+
let rendered = entries.map(renderEntry);
|
|
117
|
+
let match = duplicatePluginOwners(entries);
|
|
118
|
+
let match$1 = Stdlib_Array.findMap(rendered, r => {
|
|
119
|
+
if (r.TAG === "Ok") {
|
|
120
|
+
return;
|
|
121
|
+
} else {
|
|
122
|
+
return r._0;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (match.length !== 0) {
|
|
126
|
+
return {
|
|
127
|
+
TAG: "Error",
|
|
128
|
+
_0: match.map(duplicatePluginMessage).join("\n\n")
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (match$1 !== undefined) {
|
|
132
|
+
return {
|
|
133
|
+
TAG: "Error",
|
|
134
|
+
_0: match$1
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
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(["]"]);
|
|
138
|
+
return {
|
|
139
|
+
TAG: "Ok",
|
|
140
|
+
_0: header.concat(body).join("\n") + "\n"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
union,
|
|
146
|
+
quote,
|
|
147
|
+
splitKey,
|
|
148
|
+
renderEntry,
|
|
149
|
+
duplicatePluginOwners,
|
|
150
|
+
duplicatePluginMessage,
|
|
151
|
+
header,
|
|
152
|
+
render,
|
|
153
|
+
}
|
|
154
|
+
/* No side effect */
|
|
@@ -0,0 +1,126 @@
|
|
|
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 deploy manifest enumerates *deployables*, not plugins, and a plugin's
|
|
10
|
+
// manifest can sit in any of three places. `PlatformManifests` owns both facts —
|
|
11
|
+
// its head comment states the resolution order and why each arm exists; add a
|
|
12
|
+
// fourth arm there, not here.
|
|
13
|
+
|
|
14
|
+
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
15
|
+
|
|
16
|
+
let fail = (message: string) => {
|
|
17
|
+
Console.error("generate-platform: " ++ message)
|
|
18
|
+
NodeProcess.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let asObject = (json: JSON.t): option<dict<JSON.t>> => json->JSON.Decode.object
|
|
22
|
+
let stringAt = (obj: dict<JSON.t>, field: string): option<string> =>
|
|
23
|
+
obj->Dict.get(field)->Option.flatMap(JSON.Decode.string)
|
|
24
|
+
|
|
25
|
+
let () = {
|
|
26
|
+
let manifestArg = NodeProcess.argv->Array.get(2)->Option.getOr("")
|
|
27
|
+
if manifestArg == "" {
|
|
28
|
+
Console.error("Usage: generate-platform <deploy-manifest.yaml>")
|
|
29
|
+
NodeProcess.exit(1)
|
|
30
|
+
} else {
|
|
31
|
+
let manifestPath = NodePath.resolve([manifestArg])
|
|
32
|
+
if !NodeFs.existsSync(manifestPath) {
|
|
33
|
+
fail(`${manifestPath} not found`)
|
|
34
|
+
}
|
|
35
|
+
let manifestDir = NodePath.dirname(manifestPath)
|
|
36
|
+
let deployManifest = try NodeFs.readFileSync(manifestPath)->parseYaml catch {
|
|
37
|
+
| _ => {
|
|
38
|
+
fail(`could not parse ${manifestPath} as YAML`)
|
|
39
|
+
JSON.Null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let platformPath =
|
|
44
|
+
deployManifest
|
|
45
|
+
->asObject
|
|
46
|
+
->Option.flatMap(m => m->Dict.get("platform"))
|
|
47
|
+
->Option.flatMap(asObject)
|
|
48
|
+
->Option.flatMap(p => p->stringAt("path"))
|
|
49
|
+
let plugins =
|
|
50
|
+
deployManifest
|
|
51
|
+
->asObject
|
|
52
|
+
->Option.flatMap(m => m->Dict.get("plugins"))
|
|
53
|
+
->Option.flatMap(JSON.Decode.array)
|
|
54
|
+
->Option.map(entries =>
|
|
55
|
+
entries->Array.filterMap(entry => {
|
|
56
|
+
let obj = entry->asObject
|
|
57
|
+
switch (
|
|
58
|
+
obj->Option.flatMap(o => o->stringAt("name")),
|
|
59
|
+
obj->Option.flatMap(o => o->stringAt("path")),
|
|
60
|
+
) {
|
|
61
|
+
| (Some(name), Some(path)) => Some((name, path))
|
|
62
|
+
| _ => None
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
switch (platformPath, plugins) {
|
|
68
|
+
| (Some(platformPath), Some(plugins)) => {
|
|
69
|
+
let pluginManifests = plugins->Array.flatMap(((pluginName, pluginPath)) => {
|
|
70
|
+
let pluginDir = NodePath.resolve([manifestDir, pluginPath])
|
|
71
|
+
if !NodeFs.existsSync(pluginDir) {
|
|
72
|
+
fail(`plugin "${pluginName}" points at ${pluginDir}, which does not exist`)
|
|
73
|
+
}
|
|
74
|
+
switch PlatformManifests.resolve(~pluginDir) {
|
|
75
|
+
// A stack whose package graph holds no plugin composition — an
|
|
76
|
+
// SdkService, an SPA, a standalone Lambda — has no capabilities to
|
|
77
|
+
// contribute, which is not an error. Said out loud so a plugin
|
|
78
|
+
// skipped by mistake is visible in the generator's output.
|
|
79
|
+
| NotAPlugin => {
|
|
80
|
+
Console.log(`Skipped: ${pluginName} (${pluginDir}) — no plugin composition`)
|
|
81
|
+
[]
|
|
82
|
+
}
|
|
83
|
+
| Unbuilt({evidence, expected}) => {
|
|
84
|
+
fail(
|
|
85
|
+
`no capabilities.json found for plugin "${pluginName}" (${pluginDir}) — ` ++
|
|
86
|
+
`build the plugin first; its build emits src/capabilities.json beside Plugin.res ` ++
|
|
87
|
+
`(${evidence}; expected ${expected})`,
|
|
88
|
+
)
|
|
89
|
+
[]
|
|
90
|
+
}
|
|
91
|
+
| Manifests(manifests) =>
|
|
92
|
+
manifests->Array.map(({path, via}) => {
|
|
93
|
+
Console.log(
|
|
94
|
+
`Read: ${pluginName} — ${path} (via ${PlatformManifests.describeVia(via)})`,
|
|
95
|
+
)
|
|
96
|
+
let manifest = try NodeFs.readFileSync(path)
|
|
97
|
+
->JSON.parseOrThrow
|
|
98
|
+
->S.parseOrThrow(CapabilityManifest.schema) catch {
|
|
99
|
+
| _ => {
|
|
100
|
+
fail(`could not parse ${path} as a capability manifest`)
|
|
101
|
+
({capabilities: []}: CapabilityManifest.t)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
({pluginName, manifest}: PlatformCodegen.pluginManifest)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
switch PlatformCodegen.render(pluginManifests) {
|
|
110
|
+
| Error(message) => fail(message)
|
|
111
|
+
| Ok(source) => {
|
|
112
|
+
let outputPath = NodePath.resolve([
|
|
113
|
+
manifestDir,
|
|
114
|
+
platformPath,
|
|
115
|
+
"src",
|
|
116
|
+
"PlatformCapabilities.res",
|
|
117
|
+
])
|
|
118
|
+
NodeFs.writeFileSync(outputPath, source)
|
|
119
|
+
Console.log("Generated: " ++ outputPath)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
| _ => fail(`${manifestPath} needs a \`platform.path\` and a \`plugins\` list with name + path`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 PlatformManifests$Reventless from "./PlatformManifests.res.mjs";
|
|
12
|
+
import * as CapabilityManifest$Reventless from "../components/CapabilityManifest.res.mjs";
|
|
13
|
+
|
|
14
|
+
function fail(message) {
|
|
15
|
+
console.error("generate-platform: " + message);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function asObject(json) {
|
|
20
|
+
return Stdlib_JSON.Decode.object(json);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stringAt(obj, field) {
|
|
24
|
+
return Stdlib_Option.flatMap(obj[field], Stdlib_JSON.Decode.string);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let manifestArg = Stdlib_Option.getOr(process.argv[2], "");
|
|
28
|
+
|
|
29
|
+
if (manifestArg === "") {
|
|
30
|
+
console.error("Usage: generate-platform <deploy-manifest.yaml>");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
} else {
|
|
33
|
+
let manifestPath = Nodepath.resolve(manifestArg);
|
|
34
|
+
if (!Nodefs.existsSync(manifestPath)) {
|
|
35
|
+
fail(manifestPath + ` not found`);
|
|
36
|
+
}
|
|
37
|
+
let manifestDir = Nodepath.dirname(manifestPath);
|
|
38
|
+
let deployManifest;
|
|
39
|
+
try {
|
|
40
|
+
deployManifest = Yaml.parse(Nodefs.readFileSync(manifestPath, "utf8"));
|
|
41
|
+
} catch (exn) {
|
|
42
|
+
fail(`could not parse ` + manifestPath + ` as YAML`);
|
|
43
|
+
deployManifest = null;
|
|
44
|
+
}
|
|
45
|
+
let platformPath = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(deployManifest), m => m["platform"]), asObject), p => stringAt(p, "path"));
|
|
46
|
+
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 => {
|
|
47
|
+
let obj = Stdlib_JSON.Decode.object(entry);
|
|
48
|
+
let match = Stdlib_Option.flatMap(obj, o => stringAt(o, "name"));
|
|
49
|
+
let match$1 = Stdlib_Option.flatMap(obj, o => stringAt(o, "path"));
|
|
50
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
51
|
+
return [
|
|
52
|
+
match,
|
|
53
|
+
match$1
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
if (platformPath !== undefined) {
|
|
58
|
+
if (plugins !== undefined) {
|
|
59
|
+
let pluginManifests = plugins.flatMap(param => {
|
|
60
|
+
let pluginName = param[0];
|
|
61
|
+
let pluginDir = Nodepath.resolve(manifestDir, param[1]);
|
|
62
|
+
if (!Nodefs.existsSync(pluginDir)) {
|
|
63
|
+
fail(`plugin "` + pluginName + `" points at ` + pluginDir + `, which does not exist`);
|
|
64
|
+
}
|
|
65
|
+
let manifests = PlatformManifests$Reventless.resolve(undefined, pluginDir);
|
|
66
|
+
if (typeof manifests !== "object") {
|
|
67
|
+
console.log(`Skipped: ` + pluginName + ` (` + pluginDir + `) — no plugin composition`);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
if (manifests.TAG === "Manifests") {
|
|
71
|
+
return manifests._0.map(param => {
|
|
72
|
+
let path = param.path;
|
|
73
|
+
console.log(`Read: ` + pluginName + ` — ` + path + ` (via ` + PlatformManifests$Reventless.describeVia(param.via) + `)`);
|
|
74
|
+
let manifest;
|
|
75
|
+
try {
|
|
76
|
+
manifest = S.parseOrThrow(JSON.parse(Nodefs.readFileSync(path, "utf8")), CapabilityManifest$Reventless.schema);
|
|
77
|
+
} catch (exn) {
|
|
78
|
+
fail(`could not parse ` + path + ` as a capability manifest`);
|
|
79
|
+
manifest = {
|
|
80
|
+
capabilities: []
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
pluginName: pluginName,
|
|
85
|
+
manifest: manifest
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
let match = manifests._0;
|
|
90
|
+
fail(`no capabilities.json found for plugin "` + pluginName + `" (` + pluginDir + `) — build the plugin first; its build emits src/capabilities.json beside Plugin.res ` + (`(` + match.evidence + `; expected ` + match.expected + `)`));
|
|
91
|
+
return [];
|
|
92
|
+
});
|
|
93
|
+
let message = PlatformCodegen$Reventless.render(pluginManifests);
|
|
94
|
+
if (message.TAG === "Ok") {
|
|
95
|
+
let outputPath = Nodepath.resolve(manifestDir, platformPath, "src", "PlatformCapabilities.res");
|
|
96
|
+
Nodefs.writeFileSync(outputPath, message._0, "utf8");
|
|
97
|
+
console.log("Generated: " + outputPath);
|
|
98
|
+
} else {
|
|
99
|
+
fail(message._0);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
fail(manifestPath + ` needs a \`platform.path\` and a \`plugins\` list with name + path`);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
fail(manifestPath + ` needs a \`platform.path\` and a \`plugins\` list with name + path`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
fail,
|
|
111
|
+
asObject,
|
|
112
|
+
stringAt,
|
|
113
|
+
}
|
|
114
|
+
/* manifestArg Not a pure module */
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Where a deploy-manifest entry's capability manifests live — and, when it
|
|
2
|
+
// contributes none, whether that is because the entry is not a plugin at all.
|
|
3
|
+
//
|
|
4
|
+
// A deploy manifest enumerates *deployables*, not plugins. An SdkService stack,
|
|
5
|
+
// a static SPA, a standalone ingester Lambda are all legitimate entries with no
|
|
6
|
+
// `pluginStructure` to derive capabilities from; contributing nothing is the
|
|
7
|
+
// right answer for those. A plugin whose manifest is merely unbuilt is the
|
|
8
|
+
// opposite case, and silently skipping it would deprovision the stores it
|
|
9
|
+
// declared. The two are told apart by evidence — something in the package graph
|
|
10
|
+
// that emits a capability manifest — never by falling through the arms below.
|
|
11
|
+
//
|
|
12
|
+
// Resolution order, first arm that yields manifests wins:
|
|
13
|
+
//
|
|
14
|
+
// 1. `<dir>/src/capabilities.json` — the entry is itself a composition
|
|
15
|
+
// package, so its manifest sits beside its composition root.
|
|
16
|
+
// 2. `<dir>`'s own `generate: generate-plugin --aws <Namespace> <srcDir>`
|
|
17
|
+
// script — an `-aws` root generated from a composition `src/` elsewhere in
|
|
18
|
+
// the same repo. The script is the single existing record of that
|
|
19
|
+
// delegation, so no second hand-typed spelling of the path is introduced.
|
|
20
|
+
// 3. `<dir>`'s dependencies — an `-aws` root that *installs* a published
|
|
21
|
+
// composition package, which is the shape the framework recommends and the
|
|
22
|
+
// one arms 1 and 2 both miss. Every plugin dependency contributes: a root
|
|
23
|
+
// deploying two plugins legitimately needs both manifests.
|
|
24
|
+
// 4. Nothing in the package graph emits a manifest — not a plugin stack.
|
|
25
|
+
//
|
|
26
|
+
// I/O goes through `fileSystem` so the arms are testable without a fixture
|
|
27
|
+
// tree; `nodeFileSystem` is what the generator passes.
|
|
28
|
+
|
|
29
|
+
/** How a manifest was found — reported so the resolving arm never has to be
|
|
30
|
+
reconstructed from the output. */
|
|
31
|
+
type via =
|
|
32
|
+
| Composition
|
|
33
|
+
| Generated(string)
|
|
34
|
+
| Dependency(string)
|
|
35
|
+
|
|
36
|
+
type manifest = {path: string, via: via}
|
|
37
|
+
|
|
38
|
+
/** Why the entry counted as a plugin, and where its manifest was looked for. */
|
|
39
|
+
type unbuilt = {evidence: string, expected: string}
|
|
40
|
+
|
|
41
|
+
type t =
|
|
42
|
+
| Manifests(array<manifest>)
|
|
43
|
+
/** Something here emits a capability manifest, but the manifest is not on
|
|
44
|
+
disk: a plugin that has not been built. */
|
|
45
|
+
| Unbuilt(unbuilt)
|
|
46
|
+
/** No plugin composition anywhere in the entry's package graph. */
|
|
47
|
+
| NotAPlugin
|
|
48
|
+
|
|
49
|
+
type fileSystem = {
|
|
50
|
+
exists: string => bool,
|
|
51
|
+
readJson: string => option<JSON.t>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let nodeFileSystem: fileSystem = {
|
|
55
|
+
exists: NodeFs.existsSync,
|
|
56
|
+
readJson: path =>
|
|
57
|
+
try Some(NodeFs.readFileSync(path)->JSON.parseOrThrow) catch {
|
|
58
|
+
| _ => None
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let describeVia = (via: via): string =>
|
|
63
|
+
switch via {
|
|
64
|
+
| Composition => "its own src/"
|
|
65
|
+
| Generated(srcDir) => `its \`generate\` script's ${srcDir}`
|
|
66
|
+
| Dependency(name) => `dependency ${name}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let asObject = (json: JSON.t): option<dict<JSON.t>> => json->JSON.Decode.object
|
|
70
|
+
|
|
71
|
+
let packageJsonAt = (~fs: fileSystem, ~dir: string): option<dict<JSON.t>> =>
|
|
72
|
+
fs.readJson(NodePath.join([dir, "package.json"]))->Option.flatMap(asObject)
|
|
73
|
+
|
|
74
|
+
let scriptsOf = (pkg: dict<JSON.t>): array<string> =>
|
|
75
|
+
pkg
|
|
76
|
+
->Dict.get("scripts")
|
|
77
|
+
->Option.flatMap(asObject)
|
|
78
|
+
->Option.mapOr([], scripts =>
|
|
79
|
+
scripts->Dict.valuesToArray->Array.filterMap(JSON.Decode.string)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// The composition `src/` an `-aws` root is generated from, read off the script
|
|
83
|
+
// that performs the generation.
|
|
84
|
+
let generatedSrcDir = (pkg: dict<JSON.t>): option<string> =>
|
|
85
|
+
pkg
|
|
86
|
+
->Dict.get("scripts")
|
|
87
|
+
->Option.flatMap(asObject)
|
|
88
|
+
->Option.flatMap(scripts => scripts->Dict.get("generate"))
|
|
89
|
+
->Option.flatMap(JSON.Decode.string)
|
|
90
|
+
->Option.flatMap(script => {
|
|
91
|
+
let tokens = script->String.split(" ")->Array.filter(t => t != "")
|
|
92
|
+
switch tokens {
|
|
93
|
+
| ["generate-plugin", "--aws", _namespace, srcDir] => Some(srcDir)
|
|
94
|
+
| _ => None
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// A package that runs `emit-capabilities` as part of its build is a plugin
|
|
99
|
+
// composition package, whether or not it has been built yet. That is the whole
|
|
100
|
+
// signal G1 needs: it is stated once, by the package that owns the fact, and it
|
|
101
|
+
// is already there in every plugin package — unlike a `kind:` on the deploy
|
|
102
|
+
// manifest, which would restate it a second time.
|
|
103
|
+
//
|
|
104
|
+
// `reventless-local` *provides* the bin under `bin`, and so is correctly not
|
|
105
|
+
// matched here.
|
|
106
|
+
let emitsCapabilities = (pkg: dict<JSON.t>): bool =>
|
|
107
|
+
pkg->scriptsOf->Array.some(script => script->String.includes("emit-capabilities"))
|
|
108
|
+
|
|
109
|
+
let dependencyNames = (pkg: dict<JSON.t>): array<string> => {
|
|
110
|
+
let seen = Dict.make()
|
|
111
|
+
["dependencies", "devDependencies"]
|
|
112
|
+
->Array.flatMap(field =>
|
|
113
|
+
pkg->Dict.get(field)->Option.flatMap(asObject)->Option.mapOr([], Dict.keysToArray)
|
|
114
|
+
)
|
|
115
|
+
->Array.filter(name =>
|
|
116
|
+
switch seen->Dict.get(name) {
|
|
117
|
+
| Some(_) => false
|
|
118
|
+
| None => {
|
|
119
|
+
seen->Dict.set(name, true)
|
|
120
|
+
true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Node's directory resolution: `<dir>/node_modules/<name>`, then each parent's,
|
|
127
|
+
up to the filesystem root. Walking it rather than binding `require.resolve`
|
|
128
|
+
keeps the lookup on the package directory itself — a package's entry point
|
|
129
|
+
may be anywhere, and it is `src/capabilities.json` that is wanted. */
|
|
130
|
+
let resolvePackageDir = (~fs: fileSystem, ~fromDir: string, ~name: string): option<string> => {
|
|
131
|
+
let rec search = dir => {
|
|
132
|
+
let candidate = NodePath.join([dir, "node_modules", name])
|
|
133
|
+
if fs.exists(NodePath.join([candidate, "package.json"])) {
|
|
134
|
+
Some(candidate)
|
|
135
|
+
} else {
|
|
136
|
+
let parent = NodePath.dirname(dir)
|
|
137
|
+
parent == dir ? None : search(parent)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
search(fromDir)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Arm 3. Unbuilt wins over what was found: a root deploying two plugins needs
|
|
144
|
+
// both manifests, so one of them missing is a failure even though the other
|
|
145
|
+
// resolved.
|
|
146
|
+
let fromDependencies = (~fs: fileSystem, ~pluginDir: string, ~pkg: dict<JSON.t>): t => {
|
|
147
|
+
let found = []
|
|
148
|
+
let unbuilt = ref(None)
|
|
149
|
+
pkg
|
|
150
|
+
->dependencyNames
|
|
151
|
+
->Array.forEach(name =>
|
|
152
|
+
switch resolvePackageDir(~fs, ~fromDir=pluginDir, ~name) {
|
|
153
|
+
| None => ()
|
|
154
|
+
| Some(packageDir) => {
|
|
155
|
+
let candidate = NodePath.join([packageDir, "src", "capabilities.json"])
|
|
156
|
+
if fs.exists(candidate) {
|
|
157
|
+
found->Array.push({path: candidate, via: Dependency(name)})
|
|
158
|
+
} else if packageJsonAt(~fs, ~dir=packageDir)->Option.mapOr(false, emitsCapabilities) {
|
|
159
|
+
switch unbuilt.contents {
|
|
160
|
+
| Some(_) => ()
|
|
161
|
+
| None =>
|
|
162
|
+
unbuilt :=
|
|
163
|
+
Some({
|
|
164
|
+
evidence: `dependency ${name} emits a capability manifest`,
|
|
165
|
+
expected: candidate,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
switch (unbuilt.contents, found) {
|
|
173
|
+
| (Some({evidence, expected}), _) => Unbuilt({evidence, expected})
|
|
174
|
+
| (None, []) => NotAPlugin
|
|
175
|
+
| (None, manifests) => Manifests(manifests)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let resolve = (~fs: fileSystem=nodeFileSystem, ~pluginDir: string): t => {
|
|
180
|
+
let direct = NodePath.join([pluginDir, "src", "capabilities.json"])
|
|
181
|
+
if fs.exists(direct) {
|
|
182
|
+
Manifests([{path: direct, via: Composition}])
|
|
183
|
+
} else {
|
|
184
|
+
switch packageJsonAt(~fs, ~dir=pluginDir) {
|
|
185
|
+
| None => NotAPlugin
|
|
186
|
+
| Some(pkg) =>
|
|
187
|
+
switch generatedSrcDir(pkg) {
|
|
188
|
+
| Some(srcDir) => {
|
|
189
|
+
let expected = NodePath.resolve([pluginDir, srcDir, "capabilities.json"])
|
|
190
|
+
if fs.exists(expected) {
|
|
191
|
+
Manifests([{path: expected, via: Generated(srcDir)}])
|
|
192
|
+
} else {
|
|
193
|
+
Unbuilt({
|
|
194
|
+
evidence: `its \`generate\` script composes ${srcDir}`,
|
|
195
|
+
expected,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
| None => fromDependencies(~fs, ~pluginDir, ~pkg)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|