@reventlessdev/reventless-spec 3.0.0-alpha.129 → 3.0.0-alpha.131

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.
@@ -8,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
9
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
10
10
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
11
+ import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
11
12
  import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
12
13
  import * as DcbScopeInference$Reventless from "./DcbScopeInference.res.mjs";
13
14
 
@@ -86,6 +87,62 @@ function isTaggedArray(fieldSchema) {
86
87
  }
87
88
  }
88
89
 
90
+ function nestedRecordProperties(fieldSchema) {
91
+ let unwrapped = Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(fieldSchema), fieldSchema);
92
+ let match;
93
+ if (unwrapped.type === "array") {
94
+ let item = unwrapped.additionalItems;
95
+ match = item === "strip" || item === "strict" ? [
96
+ unwrapped,
97
+ false
98
+ ] : [
99
+ Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(item), item),
100
+ true
101
+ ];
102
+ } else {
103
+ match = [
104
+ unwrapped,
105
+ false
106
+ ];
107
+ }
108
+ let candidate = match[0];
109
+ if (candidate.type === "object") {
110
+ return [
111
+ candidate.properties,
112
+ match[1]
113
+ ];
114
+ }
115
+ }
116
+
117
+ function propertiesCarryTags(properties) {
118
+ return Object.entries(properties).some(param => {
119
+ let s = param[1];
120
+ if (Stdlib_Option.isSome(Sury.$Metadata_get(s, dcbTagId))) {
121
+ return true;
122
+ } else {
123
+ return isTaggedArray(s);
124
+ }
125
+ });
126
+ }
127
+
128
+ function isTaggedRecordArray(fieldSchema) {
129
+ let match = nestedRecordProperties(fieldSchema);
130
+ if (match !== undefined && match[1]) {
131
+ return propertiesCarryTags(match[0]);
132
+ } else {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ function hasNestedTags(fieldSchema) {
138
+ let match = nestedRecordProperties(fieldSchema);
139
+ if (match !== undefined) {
140
+ return propertiesCarryTags(match[0]);
141
+ } else {
142
+ return false;
143
+ }
144
+ }
145
+
89
146
  function isPartitionTag(fieldSchema) {
90
147
  return Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId));
91
148
  }
@@ -139,17 +196,102 @@ function jsonValueToString(json) {
139
196
  }
140
197
  }
141
198
 
199
+ function dedupeTags(tags) {
200
+ let seen = new Set();
201
+ return tags.filter(param => {
202
+ let identity = JSON.stringify(param.key) + param.value;
203
+ if (seen.has(identity)) {
204
+ return false;
205
+ } else {
206
+ seen.add(identity);
207
+ return true;
208
+ }
209
+ });
210
+ }
211
+
212
+ function flatFieldTagsExpanded(fieldName, fieldSchema, jsonDict) {
213
+ if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
214
+ let jsonValue = jsonDict[fieldName];
215
+ if (jsonValue !== undefined) {
216
+ return [{
217
+ key: resolveTagKey(fieldName, fieldSchema),
218
+ value: jsonValueToString(jsonValue)
219
+ }];
220
+ } else {
221
+ return [];
222
+ }
223
+ }
224
+ if (!isTaggedArray(fieldSchema)) {
225
+ return [];
226
+ }
227
+ let match = jsonDict[fieldName];
228
+ if (match === undefined) {
229
+ return [];
230
+ }
231
+ if (!Array.isArray(match)) {
232
+ return [];
233
+ }
234
+ let tagKey = resolveArrayTagKey(fieldName, fieldSchema);
235
+ return match.map(element => ({
236
+ key: tagKey,
237
+ value: jsonValueToString(element)
238
+ }));
239
+ }
240
+
241
+ function flatTagsExpanded(properties, jsonDict) {
242
+ return Object.entries(properties).flatMap(param => flatFieldTagsExpanded(param[0], param[1], jsonDict));
243
+ }
244
+
245
+ function nestedRecordTags(fieldSchema, jsonValue) {
246
+ let match = nestedRecordProperties(fieldSchema);
247
+ if (match === undefined) {
248
+ return [];
249
+ }
250
+ let properties = match[0];
251
+ let ofObject = json => {
252
+ let elementDict = Stdlib_JSON.Decode.object(json);
253
+ if (elementDict !== undefined) {
254
+ return flatTagsExpanded(properties, elementDict);
255
+ } else {
256
+ return [];
257
+ }
258
+ };
259
+ if (match[1]) {
260
+ if (Array.isArray(jsonValue)) {
261
+ return jsonValue.flatMap(ofObject);
262
+ } else {
263
+ return [];
264
+ }
265
+ } else {
266
+ return ofObject(jsonValue);
267
+ }
268
+ }
269
+
142
270
  function extractTagsFromProperties(properties, jsonDict) {
143
- return Stdlib_Array.filterMap(Object.entries(properties), param => {
271
+ return dedupeTags(Object.entries(properties).flatMap(param => {
144
272
  let fieldSchema = param[1];
145
273
  let fieldName = param[0];
146
274
  if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
147
- return Stdlib_Option.map(jsonDict[fieldName], jsonValue => ({
148
- key: resolveTagKey(fieldName, fieldSchema),
149
- value: jsonValueToString(jsonValue)
150
- }));
275
+ let jsonValue = jsonDict[fieldName];
276
+ if (jsonValue !== undefined) {
277
+ return [{
278
+ key: resolveTagKey(fieldName, fieldSchema),
279
+ value: jsonValueToString(jsonValue)
280
+ }];
281
+ } else {
282
+ return [];
283
+ }
151
284
  }
152
- });
285
+ if (!hasNestedTags(fieldSchema)) {
286
+ return [];
287
+ }
288
+ let jsonValue$1 = jsonDict[fieldName];
289
+ if (jsonValue$1 !== undefined) {
290
+ return nestedRecordTags(fieldSchema, jsonValue$1);
291
+ } else {
292
+ return [];
293
+ }
294
+ }));
153
295
  }
154
296
 
155
297
  function variantTagName(properties) {
@@ -275,36 +417,22 @@ function isVariantPayloadBearing(schema, name) {
275
417
  }
276
418
 
277
419
  function extractTagsFromPropertiesExpanded(properties, jsonDict) {
278
- return Object.entries(properties).flatMap(param => {
420
+ return dedupeTags(Object.entries(properties).flatMap(param => {
279
421
  let fieldSchema = param[1];
280
422
  let fieldName = param[0];
281
- if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
282
- let jsonValue = jsonDict[fieldName];
283
- if (jsonValue !== undefined) {
284
- return [{
285
- key: resolveTagKey(fieldName, fieldSchema),
286
- value: jsonValueToString(jsonValue)
287
- }];
288
- } else {
289
- return [];
290
- }
291
- }
292
- if (!isTaggedArray(fieldSchema)) {
293
- return [];
423
+ if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId)) || isTaggedArray(fieldSchema)) {
424
+ return flatFieldTagsExpanded(fieldName, fieldSchema, jsonDict);
294
425
  }
295
- let match = jsonDict[fieldName];
296
- if (match === undefined) {
426
+ if (!hasNestedTags(fieldSchema)) {
297
427
  return [];
298
428
  }
299
- if (!Array.isArray(match)) {
429
+ let jsonValue = jsonDict[fieldName];
430
+ if (jsonValue !== undefined) {
431
+ return nestedRecordTags(fieldSchema, jsonValue);
432
+ } else {
300
433
  return [];
301
434
  }
302
- let tagKey = resolveArrayTagKey(fieldName, fieldSchema);
303
- return match.map(element => ({
304
- key: tagKey,
305
- value: jsonValueToString(element)
306
- }));
307
- });
435
+ }));
308
436
  }
309
437
 
310
438
  function extractTagsFromJsonExpanded(schema, json) {
@@ -352,11 +480,25 @@ function extractTagsExpanded(schema, value) {
352
480
  function hasTaggedArrayFields(schema) {
353
481
  switch (schema.type) {
354
482
  case "object" :
355
- return Object.entries(schema.properties).some(param => isTaggedArray(param[1]));
483
+ return Object.entries(schema.properties).some(param => {
484
+ let fieldSchema = param[1];
485
+ if (isTaggedArray(fieldSchema)) {
486
+ return true;
487
+ } else {
488
+ return isTaggedRecordArray(fieldSchema);
489
+ }
490
+ });
356
491
  case "anyOf" :
357
492
  return schema.anyOf.some(variantSchema => {
358
493
  if (variantSchema.type === "object") {
359
- return Object.entries(variantSchema.properties).some(param => isTaggedArray(param[1]));
494
+ return Object.entries(variantSchema.properties).some(param => {
495
+ let fieldSchema = param[1];
496
+ if (isTaggedArray(fieldSchema)) {
497
+ return true;
498
+ } else {
499
+ return isTaggedRecordArray(fieldSchema);
500
+ }
501
+ });
360
502
  } else {
361
503
  return false;
362
504
  }
@@ -366,7 +508,7 @@ function hasTaggedArrayFields(schema) {
366
508
  }
367
509
  }
368
510
 
369
- function tagKeysOfProperties(properties) {
511
+ function flatTagKeysOfProperties(properties) {
370
512
  return Stdlib_Array.filterMap(Object.entries(properties), param => {
371
513
  let fieldSchema = param[1];
372
514
  let fieldName = param[0];
@@ -380,6 +522,25 @@ function tagKeysOfProperties(properties) {
380
522
  });
381
523
  }
382
524
 
525
+ function tagKeysOfProperties(properties) {
526
+ return Object.entries(properties).flatMap(param => {
527
+ let fieldSchema = param[1];
528
+ let fieldName = param[0];
529
+ if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
530
+ return [resolveTagKey(fieldName, fieldSchema)];
531
+ }
532
+ if (isTaggedArray(fieldSchema)) {
533
+ return [resolveArrayTagKey(fieldName, fieldSchema)];
534
+ }
535
+ let match = nestedRecordProperties(fieldSchema);
536
+ if (match !== undefined) {
537
+ return flatTagKeysOfProperties(match[0]);
538
+ } else {
539
+ return [];
540
+ }
541
+ });
542
+ }
543
+
383
544
  function extractTagKeysByEventType(schema) {
384
545
  let result = {};
385
546
  let addVariant = variantSchema => {
@@ -530,17 +691,38 @@ function extractCrossPartitionTagKeys(schema) {
530
691
  }
531
692
 
532
693
  function idFieldsOfProperties(properties) {
533
- return Stdlib_Array.filterMap(Object.entries(properties), param => {
694
+ let isIdName = name => {
695
+ if (name.endsWith("Ids")) {
696
+ return true;
697
+ } else {
698
+ return name.endsWith("Id");
699
+ }
700
+ };
701
+ return Object.entries(properties).flatMap(param => {
702
+ let fieldSchema = param[1];
534
703
  let name = param[0];
535
- if (!(name.endsWith("Ids") || name.endsWith("Id"))) {
536
- return;
704
+ if (isIdName(name)) {
705
+ let isList;
706
+ isList = fieldSchema.type === "array";
707
+ return [{
708
+ name: name,
709
+ isList: isList
710
+ }];
537
711
  }
538
- let isList;
539
- isList = param[1].type === "array";
540
- return {
541
- name: name,
542
- isList: isList
543
- };
712
+ let match = nestedRecordProperties(fieldSchema);
713
+ if (match === undefined) {
714
+ return [];
715
+ }
716
+ let nestedIsList = match[1];
717
+ return Stdlib_Array.filterMap(Object.entries(match[0]), param => {
718
+ let nestedName = param[0];
719
+ if (isIdName(nestedName)) {
720
+ return {
721
+ name: nestedName,
722
+ isList: nestedIsList
723
+ };
724
+ }
725
+ });
544
726
  });
545
727
  }
546
728
 
@@ -862,12 +1044,20 @@ export {
862
1044
  derivedPartitionTagSchema,
863
1045
  isTagged,
864
1046
  isTaggedArray,
1047
+ nestedRecordProperties,
1048
+ propertiesCarryTags,
1049
+ isTaggedRecordArray,
1050
+ hasNestedTags,
865
1051
  isPartitionTag,
866
1052
  isCrossPartitionTag,
867
1053
  isCrossPartitionTaggedArray,
868
1054
  resolveTagKey,
869
1055
  resolveArrayTagKey,
870
1056
  jsonValueToString,
1057
+ dedupeTags,
1058
+ flatFieldTagsExpanded,
1059
+ flatTagsExpanded,
1060
+ nestedRecordTags,
871
1061
  extractTagsFromProperties,
872
1062
  variantTagName,
873
1063
  extractTagsFromJson,
@@ -879,6 +1069,7 @@ export {
879
1069
  extractTagsFromJsonExpanded,
880
1070
  extractTagsExpanded,
881
1071
  hasTaggedArrayFields,
1072
+ flatTagKeysOfProperties,
882
1073
  tagKeysOfProperties,
883
1074
  extractTagKeysByEventType,
884
1075
  mergeTagKeysByEventType,
@@ -122,6 +122,32 @@ let uiFragmentManifestOptionSchema = S.option(uiFragmentManifestSchema)
122
122
  @schema
123
123
  type commandLevel = Collection | Instance
124
124
 
125
+ /** One command's lifecycle edge as the component's own scenarios describe it:
126
+ where a scenario shows the command taking effect, where those scenarios land,
127
+ and whether it brings a row into existence or acts on one.
128
+
129
+ Harvested from the GWT corpus by `check:lifecycle` and committed beside the
130
+ plugin as `src/LifecycleModel.res`, so structure assembly reads a value rather
131
+ than the test tree — which is not published with a plugin package.
132
+
133
+ No `@schema`: nothing serialises this. It is read once, while the structure is
134
+ assembled, and what leaves is the `commandDef` fields it resolved. */
135
+ type derivedEdge = {
136
+ /** The writable's `Spec.name`, which is what the structure knows it by. */
137
+ component: string,
138
+ command: string,
139
+ /** Absent where the corpus could not label a history — the honest answer when
140
+ the linked views declare no lifecycle field, and the reason the name-stem
141
+ guess is still there to fall back to. */
142
+ level?: commandLevel,
143
+ /** States a scenario shows the command taking effect from. Empty means the
144
+ corpus said nothing, NOT that the command belongs nowhere. */
145
+ allowedStates: array<string>,
146
+ /** States those scenarios land in. More than one is an edge a single
147
+ `targetState` cannot carry, so it is left to the declaration. */
148
+ targets: array<string>,
149
+ }
150
+
125
151
  @schema
126
152
  type fieldReference = {
127
153
  fieldName: string,
@@ -137,12 +163,29 @@ type commandDef = {
137
163
  aggregateIdField: option<string>,
138
164
  mutationField: string,
139
165
  references: array<fieldReference>,
140
- /** The declared *from* set — lifecycle states this command is meaningful in.
141
- `None` means always available; `Some([])` means never show. */
166
+ /** The *from* set — lifecycle states this command is meaningful in. `None`
167
+ means always available; `Some([])` means never show. */
142
168
  allowedStates: option<array<string>>,
143
- /** The declared *to* state this command's handler writes. `None` with a
144
- from-set present means the command does not move the row. */
169
+ /** The *to* state this command's handler writes. `None` with a from-set present
170
+ means the command does not move the row. */
145
171
  targetState: option<string>,
172
+ /** Where the from-set came from, ranking an inherited edge against an authored
173
+ one the way `queryableDef.labelFieldSource` does for its field:
174
+
175
+ - `"derived"` — the component's own scenarios.
176
+ - `"declared"` — its `commandTransition` switch.
177
+ - `"unrestricted"` — the switch declares the command legal in *every*
178
+ state, so there is no from-set to publish and `allowedStates` is absent.
179
+
180
+ Absent means the command names no states to come from and nothing said that
181
+ was deliberate — a spec that wrote no switch, or one whose command creates
182
+ the row. So a consumer drawing a state machine can tell an edge left
183
+ unconstrained on purpose from one that simply has no from-set.
184
+
185
+ It speaks for the from-set only. The two halves of an edge are reported
186
+ separately because they fail separately: a corpus routinely shows a command
187
+ taking effect without ever showing where it lands. */
188
+ allowedStatesSource?: string,
146
189
  /** Whether the variant is exposed in the generated API (non-`@noApi`). */
147
190
  apiExposed: option<bool>,
148
191
  /** Access keys — any one of them — a caller needs to be *offered* this command.
@@ -99,6 +99,7 @@ let commandDefSchema = Sury.$schema(s => ({
99
99
  references: s.m(Sury.array(fieldReferenceSchema)),
100
100
  allowedStates: s.m(Sury.$option(Sury.array(Sury.string))),
101
101
  targetState: s.m(Sury.$option(Sury.string)),
102
+ allowedStatesSource: s.m(Sury.$option(Sury.string)),
102
103
  apiExposed: s.m(Sury.$option(Sury.bool)),
103
104
  requiredAccess: s.m(Sury.$option(Sury.array(Sury.string))),
104
105
  ownerField: s.m(Sury.$option(Sury.string))
@@ -62,6 +62,11 @@ Only wrappers around the field's own value are followed — the optional union a
62
62
  the array element, to any depth. Object properties are not: a reference declared
63
63
  on a nested record's field belongs to that field, and attributing it to the
64
64
  enclosing one would name the wrong field.
65
+
66
+ That is still the right answer to the question this asks. A caller collecting
67
+ *every* reference a command or event declares — including the ones on the fields
68
+ of records it holds — wants [`collectFieldTargets`], which names each one by its
69
+ own path.
65
70
  */
66
71
  let rec getFieldTarget = (schema: S.t<unknown>): option<target> =>
67
72
  switch getTarget(schema) {
@@ -75,6 +80,49 @@ let rec getFieldTarget = (schema: S.t<unknown>): option<target> =>
75
80
  }
76
81
  }
77
82
 
83
+ /**
84
+ Every reference a field declares, each paired with the path that names it.
85
+
86
+ A reference on the field's own value keeps the bare field name (`customerId`), so
87
+ nothing that reads today's paths has to learn the new form to go on working. One
88
+ declared on a field of a record the field holds is named by its path:
89
+ `lineItems[].productId` — `[]` for "each element of", `.` for "property of", and
90
+ no index, because a marker lives on the element *schema* and so applies to every
91
+ element or to none.
92
+
93
+ ⚠️ A path is a **widened** contract, not a compatible one, for a consumer that
94
+ uses the name as a dictionary key into a value's properties: such a consumer
95
+ simply misses a nested reference and falls back to whatever it does for an
96
+ unreferenced field. That degradation is the correct one, but it is a degradation.
97
+
98
+ Only one record deep, matching [`DcbTag.nestedRecordProperties`], which is the
99
+ walk shared with the tag extraction so a marker is found in the same places by
100
+ both.
101
+ */
102
+ let collectFieldTargets = (
103
+ fieldName: string,
104
+ schema: S.t<unknown>,
105
+ ): array<(string, target)> =>
106
+ switch getFieldTarget(schema) {
107
+ | Some(target) => [(fieldName, target)]
108
+ | None =>
109
+ switch DcbTag.nestedRecordProperties(schema) {
110
+ | None => []
111
+ | Some((properties, isList)) =>
112
+ let prefix = isList ? fieldName ++ "[]" : fieldName
113
+ properties
114
+ ->Dict.toArray
115
+ ->Array.filterMap(((propName, propSchema)) =>
116
+ propName === "TAG"
117
+ ? None
118
+ : getFieldTarget(propSchema)->Option.map(target => (
119
+ `${prefix}.${propName}`,
120
+ target,
121
+ ))
122
+ )
123
+ }
124
+ }
125
+
78
126
  /**
79
127
  Like `to_` but does not imply DCB tag semantics.
80
128
  Use with `@ref("Entity") @noDcbTag` when the field references another entity
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as DcbTag$Reventless from "./DcbTag.res.mjs";
6
7
  import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
@@ -53,6 +54,32 @@ function getFieldTarget(_schema) {
53
54
  };
54
55
  }
55
56
 
57
+ function collectFieldTargets(fieldName, schema) {
58
+ let target = getFieldTarget(schema);
59
+ if (target !== undefined) {
60
+ return [[
61
+ fieldName,
62
+ target
63
+ ]];
64
+ }
65
+ let match = DcbTag$Reventless.nestedRecordProperties(schema);
66
+ if (match === undefined) {
67
+ return [];
68
+ }
69
+ let prefix = match[1] ? fieldName + "[]" : fieldName;
70
+ return Stdlib_Array.filterMap(Object.entries(match[0]), param => {
71
+ let propName = param[0];
72
+ if (propName === "TAG") {
73
+ return;
74
+ } else {
75
+ return Stdlib_Option.map(getFieldTarget(param[1]), target => [
76
+ prefix + `.` + propName,
77
+ target
78
+ ]);
79
+ }
80
+ });
81
+ }
82
+
56
83
  function toWithoutDcbTag(plugin, entity) {
57
84
  return Semantic$Reventless.mark(Sury.string, Semantic$Reventless.Id.reference, {
58
85
  TAG: "ReferenceTo",
@@ -67,6 +94,7 @@ export {
67
94
  to_,
68
95
  getTarget,
69
96
  getFieldTarget,
97
+ collectFieldTargets,
70
98
  toWithoutDcbTag,
71
99
  }
72
100
  /* sury Not a pure module */
@@ -350,6 +350,7 @@ let renderPluginStructureCall = (
350
350
  ~extensions: array<string>,
351
351
  ~extensionPoints: array<Pairing.extensionPointDef>,
352
352
  ~componentChapters: array<(string, string)>,
353
+ ~hasLifecycleModel: bool,
353
354
  ): option<array<string>> => {
354
355
  // The structure call carries the EP *mapping* files (one per Delegate
355
356
  // connection), not the wrapped ExtensionPoint module — Plugin_Structure reads
@@ -428,6 +429,15 @@ let renderPluginStructureCall = (
428
429
  )
429
430
  ls->Array.push(" ~componentChapters=Dict.fromArray([" ++ entries->Array.join(", ") ++ "]),")
430
431
  }
432
+ // What the plugin's own scenarios say about each command's lifecycle edge.
433
+ // Written by `check:lifecycle:update`, not here: the sidecars it harvests are
434
+ // produced by the build this generator runs *before*, so a generator-written
435
+ // model would always be one build stale and a cold clone would have none at
436
+ // all. Referenced only when the file is on disk, so a plugin that has never
437
+ // been harvested keeps a byte-identical generated Plugin.res.
438
+ if hasLifecycleModel {
439
+ ls->Array.push(" ~lifecycleModel=LifecycleModel.model,")
440
+ }
431
441
  ls->Array.push(" )")
432
442
  Some(ls)
433
443
  }
@@ -595,6 +605,7 @@ let renderComposition = (
595
605
  ~config: Config.config,
596
606
  ~resolved: Pairing.resolved,
597
607
  ~componentChapters: array<(string, string)>,
608
+ ~hasLifecycleModel: bool,
598
609
  ): string => {
599
610
  let lines: array<string> = []
600
611
 
@@ -740,6 +751,7 @@ let renderComposition = (
740
751
  ~extensions=resolved.extensions,
741
752
  ~extensionPoints=resolved.extensionPoints,
742
753
  ~componentChapters,
754
+ ~hasLifecycleModel,
743
755
  )
744
756
  let hasPluginStructure = pluginStructureLines->Option.isSome
745
757
  switch pluginStructureLines {
@@ -833,6 +845,7 @@ let render = (
833
845
  ~config: Config.config,
834
846
  ~resolved: Pairing.resolved,
835
847
  ~discovered: array<Discovery.discoveredFile>,
848
+ ~hasLifecycleModel: bool=false,
836
849
  ): string => {
837
850
  validateUniqueSpecStems(~discovered)
838
851
  validateSliceTargets(~resolved)
@@ -858,6 +871,6 @@ let render = (
858
871
  )
859
872
  switch config.variant {
860
873
  | Aws({compositionNamespace}) => renderAwsWrapper(~compositionNamespace)
861
- | Composition => renderComposition(~config, ~resolved, ~componentChapters)
874
+ | Composition => renderComposition(~config, ~resolved, ~componentChapters, ~hasLifecycleModel)
862
875
  }
863
876
  }
@@ -239,7 +239,7 @@ function renderTaskMakeParam(tasks) {
239
239
  return " ~tasks=[" + entries.join(", ") + "],";
240
240
  }
241
241
 
242
- function renderPluginStructureCall(name, aggregates, readModels, stateViewSlices, stateViewSlicesStream, stateChangeSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, extensions, extensionPoints, componentChapters) {
242
+ function renderPluginStructureCall(name, aggregates, readModels, stateViewSlices, stateViewSlicesStream, stateChangeSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, extensions, extensionPoints, componentChapters, hasLifecycleModel) {
243
243
  let epMappingStems = extensionPoints.flatMap(param => param.mappings);
244
244
  let hasComponents = aggregates.length !== 0 || readModels.length !== 0 || stateViewSlices.length !== 0 || stateViewSlicesStream.length !== 0 || stateChangeSlices.length !== 0 || automationSlices.length !== 0 || outboundTranslationSlices.length !== 0 || inboundTranslationSlices.length !== 0 || extensions.length !== 0 || epMappingStems.length !== 0;
245
245
  if (!hasComponents) {
@@ -295,6 +295,9 @@ function renderPluginStructureCall(name, aggregates, readModels, stateViewSlices
295
295
  let entries$8 = componentChapters.map(param => "(\"" + param[0] + "\", \"" + param[1] + "\")");
296
296
  ls.push(" ~componentChapters=Dict.fromArray([" + entries$8.join(", ") + "]),");
297
297
  }
298
+ if (hasLifecycleModel) {
299
+ ls.push(" ~lifecycleModel=LifecycleModel.model,");
300
+ }
298
301
  ls.push(" )");
299
302
  return ls;
300
303
  }
@@ -394,7 +397,7 @@ function renderAwsWrapper(compositionNamespace) {
394
397
  ].join("\n");
395
398
  }
396
399
 
397
- function renderComposition(config, resolved, componentChapters) {
400
+ function renderComposition(config, resolved, componentChapters, hasLifecycleModel) {
398
401
  let lines = [];
399
402
  lines.push("// AUTO-GENERATED — do not edit. Run `npm run generate` to update.");
400
403
  lines.push("");
@@ -459,7 +462,7 @@ function renderComposition(config, resolved, componentChapters) {
459
462
  lines.push(" // Extensions");
460
463
  push(renderExtensions(resolved.extensions));
461
464
  }
462
- let pluginStructureLines = renderPluginStructureCall(config.name, resolved.aggregates, resolved.readModels, resolved.stateViewSlices, resolved.stateViewSlicesStream, resolved.stateChangeSlices, resolved.automationSlices, resolved.outboundTranslationSlices, resolved.inboundTranslationSlices, resolved.extensions, resolved.extensionPoints, componentChapters);
465
+ let pluginStructureLines = renderPluginStructureCall(config.name, resolved.aggregates, resolved.readModels, resolved.stateViewSlices, resolved.stateViewSlicesStream, resolved.stateChangeSlices, resolved.automationSlices, resolved.outboundTranslationSlices, resolved.inboundTranslationSlices, resolved.extensions, resolved.extensionPoints, componentChapters, hasLifecycleModel);
463
466
  let hasPluginStructure = Stdlib_Option.isSome(pluginStructureLines);
464
467
  if (pluginStructureLines !== undefined) {
465
468
  pluginStructureLines.forEach(l => {
@@ -517,7 +520,8 @@ function renderComposition(config, resolved, componentChapters) {
517
520
  return lines.join("\n");
518
521
  }
519
522
 
520
- function render(config, resolved, discovered) {
523
+ function render(config, resolved, discovered, hasLifecycleModelOpt) {
524
+ let hasLifecycleModel = hasLifecycleModelOpt !== undefined ? hasLifecycleModelOpt : false;
521
525
  validateUniqueSpecStems(discovered);
522
526
  validateSliceTargets(resolved);
523
527
  let componentStems = [
@@ -533,7 +537,7 @@ function render(config, resolved, discovered) {
533
537
  let componentChapters = Discovery$Reventless.chaptersByStem(discovered).filter(param => componentStems.includes(param[0]));
534
538
  let match = config.variant;
535
539
  if (typeof match !== "object") {
536
- return renderComposition(config, resolved, componentChapters);
540
+ return renderComposition(config, resolved, componentChapters, hasLifecycleModel);
537
541
  } else {
538
542
  return renderAwsWrapper(match.compositionNamespace);
539
543
  }
@@ -34,7 +34,10 @@ let () = {
34
34
  let config = {...Config.read(~srcDir), variant}
35
35
  let discovered = Discovery.scan(~srcDir, ~exclude=config.exclude)
36
36
  let resolved = Pairing.resolve(discovered, ~srcDir)
37
- let source = Codegen.render(~config, ~resolved, ~discovered)
37
+ // Sits at the src root rather than in a kind folder, so `Discovery` never
38
+ // sees it and it is not a component. Its presence is the whole question.
39
+ let hasLifecycleModel = NodePath.join([srcDir, "LifecycleModel.res"])->NodeFs.existsSync
40
+ let source = Codegen.render(~config, ~resolved, ~discovered, ~hasLifecycleModel)
38
41
 
39
42
  let outputDir = switch variant {
40
43
  | Config.Composition => srcDir
@@ -56,7 +56,8 @@ if (srcDirArg === "") {
56
56
  };
57
57
  let discovered = Discovery$Reventless.scan(srcDir, config_exclude);
58
58
  let resolved = Pairing$Reventless.resolve(discovered, srcDir);
59
- let source = Codegen$Reventless.render(config, resolved, discovered);
59
+ let hasLifecycleModel = Nodefs.existsSync(Nodepath.join(srcDir, "LifecycleModel.res"));
60
+ let source = Codegen$Reventless.render(config, resolved, discovered, hasLifecycleModel);
60
61
  let outputDir;
61
62
  outputDir = typeof variant !== "object" ? srcDir : Nodepath.join(process.cwd(), "src");
62
63
  let pluginPath = Nodepath.join(outputDir, "Plugin.res");