@reventlessdev/reventless-spec 3.0.0-alpha.77 → 3.0.0-alpha.79
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 +16 -0
- package/package.json +5 -5
- package/src/PackageVersion.res +1 -1
- package/src/PackageVersion.res.mjs +1 -1
- package/src/generator/Codegen.res +86 -0
- package/src/generator/Codegen.res.mjs +60 -1
- package/src/generator/Config.res +38 -0
- package/src/generator/Config.res.mjs +16 -0
- package/src/generator/PluginGenerator.res.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.79 (2026-07-17)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.78 (2026-07-16)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **aws:** consume per-component runtime hints; fold into shared command Lambdas ([7ba2485](https://github.com/ReventlessDev/reventless-core/commit/7ba248579f2472f2fc847b26033927199353f3e6))
|
|
19
|
+
* **core:** thread per-component runtime hints from plugin.json (spec/infra/core/local) ([a94f419](https://github.com/ReventlessDev/reventless-core/commit/a94f4199c4ac7eeeb24dd0a71d58fb73c1e514e6))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.77 (2026-07-15)
|
|
7
23
|
|
|
8
24
|
### Performance Improvements
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.79",
|
|
4
4
|
"description": "Specifications for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"sury-ppx": "11.0.0-alpha.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"rescript": "
|
|
25
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.
|
|
24
|
+
"rescript": "12.3.0",
|
|
25
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"rescript": "
|
|
28
|
+
"rescript": "12.3.0"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"registry": "https://registry.npmjs.org"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
35
|
"url": "git+https://github.com/ReventlessDev/reventless-core.git",
|
|
36
|
-
"directory": "reventless/
|
|
36
|
+
"directory": "reventless/spec"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "rescript build",
|
package/src/PackageVersion.res
CHANGED
|
@@ -40,7 +40,7 @@ let fromModuleUrl = (url: string) => findVersion(url->fileURLToPath->dirname)
|
|
|
40
40
|
let callerFile: unit => option<string> = %raw(`
|
|
41
41
|
function() {
|
|
42
42
|
var frameworkMarkers = [
|
|
43
|
-
'/reventless/
|
|
43
|
+
'/reventless/',
|
|
44
44
|
'/@reventlessdev/reventless-',
|
|
45
45
|
];
|
|
46
46
|
var oldPrepare = Error.prepareStackTrace;
|
|
@@ -249,6 +249,86 @@ let renderExtensionMakeParam = (extensions: array<string>): option<string> =>
|
|
|
249
249
|
Some(" ~extensions=[" ++ entries->Array.join(", ") ++ "],")
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// ── Per-component runtime hints (plugin.json `runtime` block) ─────────────────
|
|
253
|
+
// Emits `~componentRuntime=Dict.fromArray([("Customers", {memorySize: Some(2048),
|
|
254
|
+
// timeout: None}), …])` into `Platform.Plugin.make(...)`. Only emitted when the
|
|
255
|
+
// plugin declares a `runtime` block, so plugins without one keep a byte-identical
|
|
256
|
+
// generated Plugin.res.
|
|
257
|
+
|
|
258
|
+
let renderRuntimeHints = (h: Config.runtimeHints): string => {
|
|
259
|
+
let optInt = (o: option<int>) =>
|
|
260
|
+
switch o {
|
|
261
|
+
| Some(n) => "Some(" ++ n->Int.toString ++ ")"
|
|
262
|
+
| None => "None"
|
|
263
|
+
}
|
|
264
|
+
// Qualify the first field with the full module path so the record type resolves
|
|
265
|
+
// at the `Dict.fromArray([...])` call site (type-directed disambiguation does not
|
|
266
|
+
// propagate through the tuple/dict, so a bare `{memorySize, …}` has no type in scope).
|
|
267
|
+
"{ReventlessInfra.RuntimeHints.memorySize: " ++
|
|
268
|
+
optInt(h.memorySize) ++
|
|
269
|
+
", timeout: " ++
|
|
270
|
+
optInt(h.timeout) ++ "}"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Every component name reachable from the resolved plugin — the valid keys for
|
|
274
|
+
// the plugin.json `runtime` block. Mirrors the module names emitted above.
|
|
275
|
+
let collectComponentNames = (resolved: Pairing.resolved): array<string> =>
|
|
276
|
+
Array.flat([
|
|
277
|
+
resolved.aggregates->Array.map(({spec}) => spec),
|
|
278
|
+
resolved.readModels->Array.map(({readModel}) => readModel),
|
|
279
|
+
resolved.stateChangeSlices,
|
|
280
|
+
resolved.stateViewSlices,
|
|
281
|
+
resolved.stateViewSlicesStream,
|
|
282
|
+
resolved.automationSlices,
|
|
283
|
+
resolved.outboundTranslationSlices,
|
|
284
|
+
resolved.inboundTranslationSlices,
|
|
285
|
+
resolved.tasks,
|
|
286
|
+
resolved.extensions,
|
|
287
|
+
resolved.extensionPoints->Array.map(({group, mappings}) =>
|
|
288
|
+
switch group {
|
|
289
|
+
| Some(g) => g
|
|
290
|
+
| None => epModuleName(mappings->Array.getUnsafe(0))
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
])
|
|
294
|
+
|
|
295
|
+
// Typo protection: a `runtime` key that names no component is a warning, not a
|
|
296
|
+
// silent no-op. The override is still emitted (harmless — the deploy loop simply
|
|
297
|
+
// never looks it up).
|
|
298
|
+
let validateComponentRuntimeKeys = (
|
|
299
|
+
~resolved: Pairing.resolved,
|
|
300
|
+
~componentRuntime: dict<Config.runtimeHints>,
|
|
301
|
+
): unit => {
|
|
302
|
+
let known = collectComponentNames(resolved)
|
|
303
|
+
componentRuntime
|
|
304
|
+
->Dict.keysToArray
|
|
305
|
+
->Array.forEach(name =>
|
|
306
|
+
if !(known->Array.includes(name)) {
|
|
307
|
+
Console.warn(
|
|
308
|
+
"Generator: plugin.json `runtime` override for `" ++
|
|
309
|
+
name ++
|
|
310
|
+
"` matches no component in this plugin. Known components: " ++
|
|
311
|
+
known->Array.join(", "),
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let renderComponentRuntimeParam = (
|
|
318
|
+
componentRuntime: dict<Config.runtimeHints>,
|
|
319
|
+
): option<string> => {
|
|
320
|
+
let entries = componentRuntime->Dict.toArray
|
|
321
|
+
if entries->Array.length === 0 {
|
|
322
|
+
None
|
|
323
|
+
} else {
|
|
324
|
+
let rendered =
|
|
325
|
+
entries
|
|
326
|
+
->Array.map(((name, hints)) => "(\"" ++ name ++ "\", " ++ renderRuntimeHints(hints) ++ ")")
|
|
327
|
+
->Array.join(", ")
|
|
328
|
+
Some(" ~componentRuntime=Dict.fromArray([" ++ rendered ++ "]),")
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
252
332
|
let renderTaskMakeParam = (tasks: array<string>): option<string> =>
|
|
253
333
|
if tasks->Array.length === 0 {
|
|
254
334
|
None
|
|
@@ -705,8 +785,14 @@ let renderComposition = (
|
|
|
705
785
|
->Array.join(", ") ++ "],",
|
|
706
786
|
)
|
|
707
787
|
: None,
|
|
788
|
+
// Per-component runtime hints from plugin.json `runtime` (only emitted when
|
|
789
|
+
// present, so plugins without one keep a byte-identical generated Plugin.res).
|
|
790
|
+
renderComponentRuntimeParam(config.componentRuntime),
|
|
708
791
|
]
|
|
709
792
|
|
|
793
|
+
// Typo protection: warn on `runtime` keys that name no discovered component.
|
|
794
|
+
validateComponentRuntimeKeys(~resolved, ~componentRuntime=config.componentRuntime)
|
|
795
|
+
|
|
710
796
|
makeParams->Array.filterMap(x => x)->Array.forEach(line => lines->Array.push(line))
|
|
711
797
|
switch uiFragmentsParam {
|
|
712
798
|
| Some(uiLines) => uiLines->Array.forEach(line => lines->Array.push(line))
|
|
@@ -178,6 +178,59 @@ function renderExtensionMakeParam(extensions) {
|
|
|
178
178
|
return " ~extensions=[" + entries.join(", ") + "],";
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
function renderRuntimeHints(h) {
|
|
182
|
+
let optInt = o => {
|
|
183
|
+
if (o !== undefined) {
|
|
184
|
+
return "Some(" + o.toString() + ")";
|
|
185
|
+
} else {
|
|
186
|
+
return "None";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return "{ReventlessInfra.RuntimeHints.memorySize: " + optInt(h.memorySize) + ", timeout: " + optInt(h.timeout) + "}";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function collectComponentNames(resolved) {
|
|
193
|
+
return [
|
|
194
|
+
resolved.aggregates.map(param => param.spec),
|
|
195
|
+
resolved.readModels.map(param => param.readModel),
|
|
196
|
+
resolved.stateChangeSlices,
|
|
197
|
+
resolved.stateViewSlices,
|
|
198
|
+
resolved.stateViewSlicesStream,
|
|
199
|
+
resolved.automationSlices,
|
|
200
|
+
resolved.outboundTranslationSlices,
|
|
201
|
+
resolved.inboundTranslationSlices,
|
|
202
|
+
resolved.tasks,
|
|
203
|
+
resolved.extensions,
|
|
204
|
+
resolved.extensionPoints.map(param => {
|
|
205
|
+
let group = param.group;
|
|
206
|
+
if (group !== undefined) {
|
|
207
|
+
return group;
|
|
208
|
+
} else {
|
|
209
|
+
return stripSuffix(param.mappings[0], "Mapping");
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
].flat();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateComponentRuntimeKeys(resolved, componentRuntime) {
|
|
216
|
+
let known = collectComponentNames(resolved);
|
|
217
|
+
Object.keys(componentRuntime).forEach(name => {
|
|
218
|
+
if (!known.includes(name)) {
|
|
219
|
+
console.warn("Generator: plugin.json `runtime` override for `" + name + "` matches no component in this plugin. Known components: " + known.join(", "));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renderComponentRuntimeParam(componentRuntime) {
|
|
226
|
+
let entries = Object.entries(componentRuntime);
|
|
227
|
+
if (entries.length === 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let rendered = entries.map(param => "(\"" + param[0] + "\", " + renderRuntimeHints(param[1]) + ")").join(", ");
|
|
231
|
+
return " ~componentRuntime=Dict.fromArray([" + rendered + "]),";
|
|
232
|
+
}
|
|
233
|
+
|
|
181
234
|
function renderTaskMakeParam(tasks) {
|
|
182
235
|
if (tasks.length === 0) {
|
|
183
236
|
return;
|
|
@@ -438,8 +491,10 @@ function renderComposition(config, resolved, componentChapters) {
|
|
|
438
491
|
renderMakeParam("outboundTranslationSlices", resolved.outboundTranslationSlices, "Slice"),
|
|
439
492
|
renderMakeParam("inboundTranslationSlices", resolved.inboundTranslationSlices, "Slice"),
|
|
440
493
|
hasPluginStructure ? " ~pluginStructure=pluginStructure," : undefined,
|
|
441
|
-
resolved.systemCallableComponents.length !== 0 ? " ~systemCallableComponents=[" + resolved.systemCallableComponents.map(n => "\"" + n + "\"").join(", ") + "]," : undefined
|
|
494
|
+
resolved.systemCallableComponents.length !== 0 ? " ~systemCallableComponents=[" + resolved.systemCallableComponents.map(n => "\"" + n + "\"").join(", ") + "]," : undefined,
|
|
495
|
+
renderComponentRuntimeParam(config.componentRuntime)
|
|
442
496
|
];
|
|
497
|
+
validateComponentRuntimeKeys(resolved, config.componentRuntime);
|
|
443
498
|
Stdlib_Array.filterMap(makeParams, x => x).forEach(line => {
|
|
444
499
|
lines.push(line);
|
|
445
500
|
});
|
|
@@ -493,6 +548,10 @@ export {
|
|
|
493
548
|
renderAggregateMakeParam,
|
|
494
549
|
renderEpMakeParam,
|
|
495
550
|
renderExtensionMakeParam,
|
|
551
|
+
renderRuntimeHints,
|
|
552
|
+
collectComponentNames,
|
|
553
|
+
validateComponentRuntimeKeys,
|
|
554
|
+
renderComponentRuntimeParam,
|
|
496
555
|
renderTaskMakeParam,
|
|
497
556
|
renderPluginStructureCall,
|
|
498
557
|
validateUniqueSpecStems,
|
package/src/generator/Config.res
CHANGED
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
type variant = Composition | Aws({compositionNamespace: string})
|
|
5
5
|
|
|
6
|
+
// Per-component runtime resource hints, parsed from the optional `runtime`
|
|
7
|
+
// block in plugin.json and keyed by component name. Both fields are optional;
|
|
8
|
+
// an omitted field falls through to the per-kind builder default. See
|
|
9
|
+
// docs/plans/configurable-component-runtime-resources.md.
|
|
10
|
+
type runtimeHints = {
|
|
11
|
+
memorySize: option<int>,
|
|
12
|
+
timeout: option<int>,
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
type config = {
|
|
7
16
|
name: string,
|
|
8
17
|
heartbeatInterval: int,
|
|
9
18
|
exclude: array<string>,
|
|
19
|
+
// Keyed by component name (e.g. "Customers", "PlaceOrder"). Empty when the
|
|
20
|
+
// plugin.json has no `runtime` block.
|
|
21
|
+
componentRuntime: dict<runtimeHints>,
|
|
10
22
|
variant: variant,
|
|
11
23
|
}
|
|
12
24
|
|
|
@@ -41,6 +53,31 @@ let getStrArrayField = (json: JSON.t, key: string): option<array<string>> =>
|
|
|
41
53
|
->Option.flatMap(JSON.Decode.array)
|
|
42
54
|
->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
|
|
43
55
|
|
|
56
|
+
// Parse the optional `runtime` object: each key is a component name, each value
|
|
57
|
+
// an object with optional `memorySize` / `timeout` ints. A missing block yields
|
|
58
|
+
// an empty dict; a non-object entry is skipped.
|
|
59
|
+
let getComponentRuntime = (json: JSON.t): dict<runtimeHints> => {
|
|
60
|
+
let result: dict<runtimeHints> = Dict.make()
|
|
61
|
+
json
|
|
62
|
+
->JSON.Decode.object
|
|
63
|
+
->Option.flatMap(d => d->Dict.get("runtime"))
|
|
64
|
+
->Option.flatMap(JSON.Decode.object)
|
|
65
|
+
->Option.forEach(runtimeObj =>
|
|
66
|
+
runtimeObj
|
|
67
|
+
->Dict.toArray
|
|
68
|
+
->Array.forEach(((componentName, entry)) =>
|
|
69
|
+
result->Dict.set(
|
|
70
|
+
componentName,
|
|
71
|
+
{
|
|
72
|
+
memorySize: getIntField(entry, "memorySize"),
|
|
73
|
+
timeout: getIntField(entry, "timeout"),
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
result
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
let read = (~srcDir: string): config => {
|
|
45
82
|
// Read package.json `name` from the parent of srcDir
|
|
46
83
|
let packageJsonPath = Generator_Node.join([Generator_Node.dirname(srcDir), "package.json"])
|
|
@@ -63,6 +100,7 @@ let read = (~srcDir: string): config => {
|
|
|
63
100
|
->Option.flatMap(j => getIntField(j, "heartbeatInterval"))
|
|
64
101
|
->Option.getOr(5),
|
|
65
102
|
exclude: pluginJson->Option.flatMap(j => getStrArrayField(j, "exclude"))->Option.getOr([]),
|
|
103
|
+
componentRuntime: pluginJson->Option.map(getComponentRuntime)->Option.getOr(Dict.make()),
|
|
66
104
|
variant: Composition,
|
|
67
105
|
}
|
|
68
106
|
}
|
|
@@ -27,6 +27,20 @@ function getStrArrayField(json, key) {
|
|
|
27
27
|
return Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.array), arr => Stdlib_Array.filterMap(arr, Stdlib_JSON.Decode.string));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function getComponentRuntime(json) {
|
|
31
|
+
let result = {};
|
|
32
|
+
Stdlib_Option.forEach(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["runtime"]), Stdlib_JSON.Decode.object), runtimeObj => {
|
|
33
|
+
Object.entries(runtimeObj).forEach(param => {
|
|
34
|
+
let entry = param[1];
|
|
35
|
+
result[param[0]] = {
|
|
36
|
+
memorySize: getIntField(entry, "memorySize"),
|
|
37
|
+
timeout: getIntField(entry, "timeout")
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
function read(srcDir) {
|
|
31
45
|
let packageJsonPath = Nodepath.join(Nodepath.dirname(srcDir), "package.json");
|
|
32
46
|
let packageJsonName = Stdlib_Option.flatMap(readJson(packageJsonPath), j => getStrField(j, "name"));
|
|
@@ -36,6 +50,7 @@ function read(srcDir) {
|
|
|
36
50
|
name: PluginName$Reventless.resolve(Stdlib_Option.flatMap(pluginJson, j => getStrField(j, "name")), packageJsonName),
|
|
37
51
|
heartbeatInterval: Stdlib_Option.getOr(Stdlib_Option.flatMap(pluginJson, j => getIntField(j, "heartbeatInterval")), 5),
|
|
38
52
|
exclude: Stdlib_Option.getOr(Stdlib_Option.flatMap(pluginJson, j => getStrArrayField(j, "exclude")), []),
|
|
53
|
+
componentRuntime: Stdlib_Option.getOr(Stdlib_Option.map(pluginJson, getComponentRuntime), {}),
|
|
39
54
|
variant: "Composition"
|
|
40
55
|
};
|
|
41
56
|
}
|
|
@@ -48,6 +63,7 @@ export {
|
|
|
48
63
|
getStrField,
|
|
49
64
|
getIntField,
|
|
50
65
|
getStrArrayField,
|
|
66
|
+
getComponentRuntime,
|
|
51
67
|
read,
|
|
52
68
|
}
|
|
53
69
|
/* node:fs Not a pure module */
|
|
@@ -46,10 +46,12 @@ if (srcDirArg === "") {
|
|
|
46
46
|
let config_name = init.name;
|
|
47
47
|
let config_heartbeatInterval = init.heartbeatInterval;
|
|
48
48
|
let config_exclude = init.exclude;
|
|
49
|
+
let config_componentRuntime = init.componentRuntime;
|
|
49
50
|
let config = {
|
|
50
51
|
name: config_name,
|
|
51
52
|
heartbeatInterval: config_heartbeatInterval,
|
|
52
53
|
exclude: config_exclude,
|
|
54
|
+
componentRuntime: config_componentRuntime,
|
|
53
55
|
variant: variant
|
|
54
56
|
};
|
|
55
57
|
let discovered = Discovery$Reventless.scan(srcDir, config_exclude);
|