@beignet/cli 0.0.48 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +76 -21
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +10 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +66 -6
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map.d.ts.map +1 -1
  24. package/dist/app-map.js +311 -92
  25. package/dist/app-map.js.map +1 -1
  26. package/dist/check.js +37 -3
  27. package/dist/check.js.map +1 -1
  28. package/dist/explain.js +4 -4
  29. package/dist/explain.js.map +1 -1
  30. package/dist/inspect.d.ts.map +1 -1
  31. package/dist/inspect.js +415 -168
  32. package/dist/inspect.js.map +1 -1
  33. package/dist/lint.d.ts.map +1 -1
  34. package/dist/lint.js +309 -487
  35. package/dist/lint.js.map +1 -1
  36. package/dist/make/inbox.d.ts.map +1 -1
  37. package/dist/make/inbox.js +11 -4
  38. package/dist/make/inbox.js.map +1 -1
  39. package/dist/make/payments.js +2 -2
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +144 -8
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts +1 -1
  44. package/dist/mcp.d.ts.map +1 -1
  45. package/dist/mcp.js +19 -19
  46. package/dist/mcp.js.map +1 -1
  47. package/dist/provider-add.d.ts.map +1 -1
  48. package/dist/provider-add.js +142 -7
  49. package/dist/provider-add.js.map +1 -1
  50. package/dist/registry-edits.d.ts +7 -0
  51. package/dist/registry-edits.d.ts.map +1 -1
  52. package/dist/registry-edits.js +237 -53
  53. package/dist/registry-edits.js.map +1 -1
  54. package/dist/templates/agents.d.ts.map +1 -1
  55. package/dist/templates/agents.js +16 -0
  56. package/dist/templates/agents.js.map +1 -1
  57. package/dist/templates/base.d.ts.map +1 -1
  58. package/dist/templates/base.js +9 -1
  59. package/dist/templates/base.js.map +1 -1
  60. package/dist/templates/server.d.ts.map +1 -1
  61. package/dist/templates/server.js +2 -0
  62. package/dist/templates/server.js.map +1 -1
  63. package/dist/templates/shared.d.ts +1 -0
  64. package/dist/templates/shared.d.ts.map +1 -1
  65. package/dist/templates/shared.js +3 -2
  66. package/dist/templates/shared.js.map +1 -1
  67. package/package.json +7 -6
  68. package/skills/app-structure/SKILL.md +46 -5
  69. package/src/analysis/contract-factories.ts +256 -0
  70. package/src/analysis/layers.ts +266 -0
  71. package/src/analysis/port-wiring.ts +506 -0
  72. package/src/analysis/source-index.ts +225 -57
  73. package/src/analysis/workspace.ts +115 -6
  74. package/src/app-map.ts +418 -109
  75. package/src/check.ts +51 -3
  76. package/src/explain.ts +4 -4
  77. package/src/inspect.ts +583 -215
  78. package/src/lint.ts +393 -643
  79. package/src/make/inbox.ts +11 -4
  80. package/src/make/payments.ts +2 -2
  81. package/src/make.ts +225 -8
  82. package/src/mcp.ts +19 -22
  83. package/src/provider-add.ts +186 -7
  84. package/src/registry-edits.ts +313 -63
  85. package/src/templates/agents.ts +16 -0
  86. package/src/templates/base.ts +9 -1
  87. package/src/templates/server.ts +2 -0
  88. package/src/templates/shared.ts +3 -2
package/dist/app-map.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import path from "node:path";
2
2
  import ts from "typescript";
3
+ import { contractBindingNames, contractFactoryBindings, isImportedContractDeclaration, } from "./analysis/contract-factories.js";
4
+ import { analyzePortWiring } from "./analysis/port-wiring.js";
3
5
  import { createSourceIndex, exportedVariableDeclarations, literalText, localImportReferences, propertyAssignment, propertyName, visitCalls, } from "./analysis/source-index.js";
4
6
  import { createAnalysisWorkspace, } from "./analysis/workspace.js";
5
7
  import { directoryPath } from "./config.js";
@@ -287,6 +289,20 @@ export async function mapApp(options = {}) {
287
289
  if (registry) {
288
290
  addNode(registry.node);
289
291
  registries.push(registry);
292
+ if (registry.outbox?.eventsIndeterminate) {
293
+ unresolved.push({
294
+ code: "outbox_events_unresolved",
295
+ message: `Could not resolve the events entries for outbox registry ${exportName}.`,
296
+ source: registry.node.source,
297
+ });
298
+ }
299
+ if (registry.outbox?.jobsIndeterminate) {
300
+ unresolved.push({
301
+ code: "outbox_jobs_unresolved",
302
+ message: `Could not resolve the jobs entries for outbox registry ${exportName}.`,
303
+ source: registry.node.source,
304
+ });
305
+ }
290
306
  }
291
307
  }
292
308
  }
@@ -402,6 +418,17 @@ export async function mapApp(options = {}) {
402
418
  sourceIndex,
403
419
  addNode,
404
420
  addEdge,
421
+ unresolved,
422
+ });
423
+ await addOutboxDeliveryGraph({
424
+ workspace,
425
+ sourceIndex,
426
+ registries,
427
+ nodes,
428
+ edges,
429
+ addNode,
430
+ addEdge,
431
+ unresolved,
405
432
  });
406
433
  await addTables({ workspace, sourceIndex, addNode });
407
434
  await addUnresolvedLocalImports({ workspace, unresolved });
@@ -657,45 +684,51 @@ async function addPorts(args) {
657
684
  if (!args.workspace.fileSet.has(portsFile))
658
685
  return;
659
686
  const sourceFile = await args.workspace.readSourceFile(portsFile);
660
- const aliases = new Map();
661
- for (const statement of sourceFile.statements) {
662
- if (ts.isTypeAliasDeclaration(statement)) {
663
- aliases.set(statement.name.text, statement.type);
664
- }
687
+ const analysis = await analyzePortWiring(args.workspace);
688
+ if (analysis.declarationIndeterminate) {
689
+ args.unresolved.push({
690
+ code: "app_ports_declaration_unresolved",
691
+ message: `Could not fully resolve AppPorts in ${portsFile}.`,
692
+ source: { file: portsFile },
693
+ });
665
694
  }
666
- const appPorts = aliases.get("AppPorts");
667
- if (!appPorts)
668
- return;
669
- const bindings = await readPortBindings(args.workspace);
670
- const properties = collectTypeProperties(appPorts, aliases);
671
- for (const property of properties) {
672
- const name = propertyName(property.name);
673
- if (!name)
674
- continue;
675
- const feature = await portFeature(property.type, portsFile, args.workspace.config.paths.features, args.sourceIndex);
695
+ if (analysis.indeterminate) {
696
+ args.unresolved.push({
697
+ code: "port_wiring_unresolved",
698
+ message: `Could not fully resolve port bindings in ${args.workspace.config.paths.portWiring}.`,
699
+ source: { file: args.workspace.config.paths.portWiring },
700
+ });
701
+ }
702
+ for (const name of analysis.declared) {
703
+ const property = analysis.properties.get(name);
704
+ const feature = property
705
+ ? await portFeature(property.type, portsFile, args.workspace.config.paths.features, args.sourceIndex)
706
+ : undefined;
676
707
  const id = portId(name);
677
708
  args.addNode({
678
709
  id,
679
710
  kind: "port",
680
711
  name,
681
712
  ...(feature ? { feature } : {}),
682
- source: await args.sourceIndex.sourceLocation(portsFile, property),
683
- status: bindings.bound.has(name) || bindings.deferred.has(name)
713
+ source: property
714
+ ? await args.sourceIndex.sourceLocation(portsFile, property)
715
+ : { file: args.workspace.config.paths.portWiring },
716
+ status: analysis.bound.has(name) || analysis.deferred.has(name)
684
717
  ? "registered"
685
718
  : "missing",
686
719
  details: {
687
- binding: bindings.bound.has(name)
720
+ binding: analysis.bound.has(name)
688
721
  ? "bound"
689
- : bindings.deferred.has(name)
722
+ : analysis.deferred.has(name)
690
723
  ? "deferred"
691
724
  : "unresolved",
692
- type: property.type?.getText(sourceFile),
725
+ ...(property?.type ? { type: property.type.getText(sourceFile) } : {}),
693
726
  },
694
727
  });
695
728
  if (feature) {
696
729
  args.addEdge(exactEdge("owns", featureId(feature), id, portsFile));
697
730
  }
698
- if (bindings.bound.has(name)) {
731
+ if (analysis.bound.has(name)) {
699
732
  const bindingId = `provider:app:${name}`;
700
733
  args.addNode({
701
734
  id: bindingId,
@@ -709,6 +742,128 @@ async function addPorts(args) {
709
742
  }
710
743
  }
711
744
  }
745
+ async function addOutboxDeliveryGraph(args) {
746
+ const outboxRegistries = args.registries.filter((registry) => registry.outbox !== undefined);
747
+ if (outboxRegistries.length === 0)
748
+ return;
749
+ const jobRegistries = outboxRegistries.filter((registry) => registry.outbox && registry.outbox.jobs.length > 0);
750
+ const jobsPortId = portId("jobs");
751
+ if (jobRegistries.length > 0 && !args.nodes.has(jobsPortId)) {
752
+ args.addNode({
753
+ id: jobsPortId,
754
+ kind: "port",
755
+ name: "jobs",
756
+ source: { file: args.workspace.config.paths.ports },
757
+ status: "missing",
758
+ details: {
759
+ binding: "unresolved",
760
+ requiredBy: "outbox-jobs",
761
+ },
762
+ });
763
+ }
764
+ for (const registry of jobRegistries) {
765
+ args.addEdge(exactEdge("depends-on", registry.node.id, jobsPortId, registry.file));
766
+ for (const jobId of registeredJobIds(registry.node.id, args.nodes, args.edges)) {
767
+ args.addEdge(exactEdge("depends-on", jobId, jobsPortId, registry.file));
768
+ }
769
+ }
770
+ const runtimeDirectories = new Set([
771
+ directoryPath(args.workspace.config.paths.routes),
772
+ directoryPath(args.workspace.config.paths.server),
773
+ directoryPath(args.workspace.config.paths.outbox),
774
+ directoryPath(args.workspace.config.paths.portWiring),
775
+ "server",
776
+ "infra",
777
+ ]);
778
+ for (const file of args.workspace.files) {
779
+ if (!isTypeScriptSource(file) ||
780
+ isTestFile(file) ||
781
+ ![...runtimeDirectories].some((directory) => directory === "." ||
782
+ file === directory ||
783
+ file.startsWith(`${directory}/`))) {
784
+ continue;
785
+ }
786
+ const sourceFile = await args.workspace.readSourceFile(file);
787
+ const providerLifecycle = isProviderLifecycleSource(file, sourceFile);
788
+ const calls = [];
789
+ visitCalls(sourceFile, (call) => {
790
+ const name = callMemberName(call);
791
+ if (name === "createOutboxDrainRoute" ||
792
+ (name === "drainOutbox" && !providerLifecycle)) {
793
+ calls.push(call);
794
+ }
795
+ });
796
+ for (const [index, call] of calls.entries()) {
797
+ const id = `entrypoint:${file}#outbox-drain${index === 0 ? "" : `-${index + 1}`}`;
798
+ args.addNode({
799
+ id,
800
+ kind: "entrypoint",
801
+ name: `${file} outbox drain`,
802
+ source: await args.sourceIndex.sourceLocation(file, call),
803
+ status: "registered",
804
+ details: { type: "outbox-drain" },
805
+ });
806
+ const options = call.arguments[0];
807
+ const registryExpression = options && ts.isObjectLiteralExpression(unwrapExpression(options))
808
+ ? objectPropertyExpressionAnalysis(unwrapExpression(options), "registry").value
809
+ : undefined;
810
+ const reference = registryExpression
811
+ ? await args.sourceIndex.resolveExpression(file, registryExpression)
812
+ : undefined;
813
+ const resolvedRegistry = reference
814
+ ? outboxRegistries.find((registry) => registry.file === reference.file &&
815
+ registry.node.source.exportName === reference.exportName)
816
+ : outboxRegistries.length === 1
817
+ ? outboxRegistries[0]
818
+ : undefined;
819
+ if (resolvedRegistry) {
820
+ args.addEdge(sourceNodeEdge("depends-on", id, resolvedRegistry.node.id, file, registryExpression ?? call));
821
+ if (resolvedRegistry.outbox?.jobs.length) {
822
+ args.addEdge(sourceNodeEdge("depends-on", id, jobsPortId, file, call));
823
+ }
824
+ }
825
+ else {
826
+ args.unresolved.push({
827
+ code: "outbox_drain_registry_unresolved",
828
+ message: `Could not resolve the outbox registry used by a drain in ${file}.`,
829
+ source: await args.sourceIndex.sourceLocation(file, call),
830
+ });
831
+ }
832
+ }
833
+ }
834
+ }
835
+ function isProviderLifecycleSource(file, sourceFile) {
836
+ if (/(^|\/)provider(s)?\.ts$/.test(file) ||
837
+ /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
838
+ /(^|\/)providers?\//.test(file)) {
839
+ return true;
840
+ }
841
+ let providerCall = false;
842
+ visitCalls(sourceFile, (call) => {
843
+ if (callMemberName(call) === "createProvider")
844
+ providerCall = true;
845
+ });
846
+ return providerCall;
847
+ }
848
+ function registeredJobIds(registryId, nodes, edges) {
849
+ const visited = new Set([registryId]);
850
+ let frontier = [registryId];
851
+ while (frontier.length > 0) {
852
+ const next = [];
853
+ for (const from of frontier) {
854
+ for (const edge of edges) {
855
+ if (edge.kind !== "registers" || edge.from !== from)
856
+ continue;
857
+ if (visited.has(edge.to))
858
+ continue;
859
+ visited.add(edge.to);
860
+ next.push(edge.to);
861
+ }
862
+ }
863
+ frontier = next;
864
+ }
865
+ return [...visited].filter((id) => nodes.get(id)?.kind === "job").sort();
866
+ }
712
867
  async function addOpenApiSurface(args) {
713
868
  const file = args.workspace.config.paths.openapiRoute;
714
869
  if (!args.workspace.fileSet.has(file))
@@ -847,14 +1002,15 @@ async function addUnresolvedLocalImports(args) {
847
1002
  async function addUnresolvedContractDeclarations(args) {
848
1003
  for (const file of appMapContractSourceFiles(args.workspace)) {
849
1004
  const sourceFile = await args.workspace.readSourceFile(file);
1005
+ const factoryBindings = contractFactoryBindings(sourceFile);
1006
+ const bindingNames = contractBindingNames(sourceFile, factoryBindings);
850
1007
  for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
851
1008
  if (args.contractIds.has(symbolKey(file, exportName)))
852
1009
  continue;
853
- const call = declaration.initializer
854
- ? findCall(declaration.initializer, new Set(["defineContract"]))
855
- : undefined;
856
- if (!call)
1010
+ if (!declaration.initializer ||
1011
+ !isImportedContractDeclaration(declaration.initializer, factoryBindings, bindingNames)) {
857
1012
  continue;
1013
+ }
858
1014
  args.unresolved.push({
859
1015
  code: "contract_declaration_unresolved",
860
1016
  message: `Could not resolve the HTTP method and path for contract ${exportName}.`,
@@ -883,14 +1039,15 @@ async function registryRecord(args) {
883
1039
  const initializer = args.declaration.initializer;
884
1040
  if (!initializer)
885
1041
  return undefined;
886
- const values = registryValues(initializer, args.exportName);
887
- if (!values)
1042
+ const registry = registryValues(initializer, args.exportName);
1043
+ if (!registry)
888
1044
  return undefined;
889
1045
  const feature = featureForFile(args.file, directoryPath(args.workspace.config.paths.features));
890
1046
  return {
891
1047
  file: args.file,
892
1048
  declaration: args.declaration,
893
- values,
1049
+ values: registry.values,
1050
+ ...(registry.outbox ? { outbox: registry.outbox } : {}),
894
1051
  node: {
895
1052
  id: registryId(args.file, args.exportName),
896
1053
  kind: "registry",
@@ -898,13 +1055,20 @@ async function registryRecord(args) {
898
1055
  ...(feature ? { feature } : {}),
899
1056
  source: await args.sourceIndex.sourceLocation(args.file, args.declaration, args.exportName),
900
1057
  status: "registered",
1058
+ ...(registry.outbox
1059
+ ? {
1060
+ details: {
1061
+ registryType: "outbox",
1062
+ },
1063
+ }
1064
+ : {}),
901
1065
  },
902
1066
  };
903
1067
  }
904
1068
  function registryValues(initializer, exportName) {
905
1069
  const expression = unwrapExpression(initializer);
906
1070
  if (ts.isArrayLiteralExpression(expression) && isRegistryName(exportName)) {
907
- return arrayExpressions(expression);
1071
+ return { values: arrayExpressions(expression) };
908
1072
  }
909
1073
  if (!ts.isCallExpression(expression))
910
1074
  return undefined;
@@ -913,82 +1077,137 @@ function registryValues(initializer, exportName) {
913
1077
  name === "defineNotificationRegistry" ||
914
1078
  name === "defineAgentCapabilityRegistry") {
915
1079
  const first = expression.arguments[0];
916
- return first && ts.isArrayLiteralExpression(unwrapExpression(first))
917
- ? arrayExpressions(unwrapExpression(first))
918
- : [];
1080
+ return {
1081
+ values: first && ts.isArrayLiteralExpression(unwrapExpression(first))
1082
+ ? arrayExpressions(unwrapExpression(first))
1083
+ : [],
1084
+ };
919
1085
  }
920
- if (name === "defineOutboxRegistry" &&
921
- ts.isObjectLiteralExpression(expression.arguments[0])) {
922
- const object = expression.arguments[0];
923
- return ["events", "jobs"].flatMap((property) => {
924
- const value = propertyAssignment(object, property)?.initializer;
925
- return value && ts.isArrayLiteralExpression(unwrapExpression(value))
926
- ? arrayExpressions(unwrapExpression(value))
927
- : [];
928
- });
1086
+ if (name === "defineOutboxRegistry") {
1087
+ const initializers = localVariableInitializers(expression.getSourceFile());
1088
+ const argument = expression.arguments[0];
1089
+ const object = argument
1090
+ ? resolveRegistryExpression(argument, initializers)
1091
+ : undefined;
1092
+ if (!object || !ts.isObjectLiteralExpression(object)) {
1093
+ return {
1094
+ values: [],
1095
+ outbox: {
1096
+ events: [],
1097
+ jobs: [],
1098
+ eventsIndeterminate: true,
1099
+ jobsIndeterminate: true,
1100
+ },
1101
+ };
1102
+ }
1103
+ const valuesFor = (property) => {
1104
+ const match = objectPropertyExpressionAnalysis(object, property);
1105
+ return {
1106
+ values: match.value
1107
+ ? registryValueExpressions(match.value, initializers)
1108
+ : [],
1109
+ indeterminate: match.indeterminate,
1110
+ };
1111
+ };
1112
+ const events = valuesFor("events");
1113
+ const jobs = valuesFor("jobs");
1114
+ const outbox = {
1115
+ events: events.values,
1116
+ jobs: jobs.values,
1117
+ eventsIndeterminate: events.indeterminate,
1118
+ jobsIndeterminate: jobs.indeterminate,
1119
+ };
1120
+ return { values: [...outbox.events, ...outbox.jobs], outbox };
929
1121
  }
930
1122
  if (name === "defineUploads" &&
931
1123
  ts.isObjectLiteralExpression(expression.arguments[0])) {
932
- return expression.arguments[0].properties.flatMap((property) => ts.isPropertyAssignment(property) ? [property.initializer] : []);
1124
+ return {
1125
+ values: expression.arguments[0].properties.flatMap((property) => ts.isPropertyAssignment(property) ? [property.initializer] : []),
1126
+ };
933
1127
  }
934
1128
  return undefined;
935
1129
  }
936
- function arrayExpressions(array) {
937
- return array.elements.flatMap((element) => {
938
- if (ts.isSpreadElement(element))
939
- return [element.expression];
940
- return ts.isExpression(element) ? [element] : [];
941
- });
942
- }
943
- function collectTypeProperties(type, aliases, visited = new Set()) {
944
- if (ts.isTypeLiteralNode(type)) {
945
- return type.members.filter(ts.isPropertySignature);
946
- }
947
- if (ts.isIntersectionTypeNode(type)) {
948
- return type.types.flatMap((member) => collectTypeProperties(member, aliases, visited));
949
- }
950
- if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
951
- const name = type.typeName.text;
952
- if (visited.has(name))
953
- return [];
954
- const target = aliases.get(name);
955
- if (!target)
956
- return [];
957
- visited.add(name);
958
- return collectTypeProperties(target, aliases, visited);
1130
+ function objectPropertyExpressionAnalysis(object, name) {
1131
+ let value;
1132
+ let indeterminate = false;
1133
+ for (const property of object.properties) {
1134
+ if (ts.isSpreadAssignment(property) || !property.name) {
1135
+ value = undefined;
1136
+ indeterminate = true;
1137
+ continue;
1138
+ }
1139
+ const propertyKey = propertyName(property.name);
1140
+ if (!propertyKey) {
1141
+ value = undefined;
1142
+ indeterminate = true;
1143
+ continue;
1144
+ }
1145
+ if (propertyKey !== name)
1146
+ continue;
1147
+ if (ts.isPropertyAssignment(property)) {
1148
+ value = property.initializer;
1149
+ indeterminate = false;
1150
+ }
1151
+ else if (ts.isShorthandPropertyAssignment(property)) {
1152
+ value = property.name;
1153
+ indeterminate = false;
1154
+ }
1155
+ else {
1156
+ value = undefined;
1157
+ indeterminate = true;
1158
+ }
959
1159
  }
960
- return [];
1160
+ return { ...(value ? { value } : {}), indeterminate };
961
1161
  }
962
- async function readPortBindings(workspace) {
963
- const result = { bound: new Set(), deferred: new Set() };
964
- const file = workspace.config.paths.portWiring;
965
- if (!workspace.fileSet.has(file))
966
- return result;
967
- const sourceFile = await workspace.readSourceFile(file);
968
- visitCalls(sourceFile, (call) => {
969
- const object = call.arguments.find(ts.isObjectLiteralExpression);
970
- if (!object?.properties.some((property) => property.name !== undefined &&
971
- propertyName(property.name) === "bound")) {
972
- return;
973
- }
974
- const bound = propertyAssignment(object, "bound")?.initializer;
975
- if (bound && ts.isObjectLiteralExpression(bound)) {
976
- for (const property of bound.properties) {
977
- const name = property.name ? propertyName(property.name) : undefined;
978
- if (name)
979
- result.bound.add(name);
1162
+ function localVariableInitializers(sourceFile) {
1163
+ const initializers = new Map();
1164
+ for (const statement of sourceFile.statements) {
1165
+ if (!ts.isVariableStatement(statement))
1166
+ continue;
1167
+ for (const declaration of statement.declarationList.declarations) {
1168
+ if (ts.isIdentifier(declaration.name) && declaration.initializer) {
1169
+ initializers.set(declaration.name.text, declaration.initializer);
980
1170
  }
981
1171
  }
982
- const deferred = propertyAssignment(object, "deferred")?.initializer;
983
- if (deferred && ts.isArrayLiteralExpression(deferred)) {
984
- for (const element of deferred.elements) {
985
- const name = literalText(element);
986
- if (name)
987
- result.deferred.add(name);
988
- }
1172
+ }
1173
+ return initializers;
1174
+ }
1175
+ function resolveRegistryExpression(expression, initializers, visited = new Set()) {
1176
+ const current = unwrapExpression(expression);
1177
+ if (!ts.isIdentifier(current) || visited.has(current.text))
1178
+ return current;
1179
+ const initializer = initializers.get(current.text);
1180
+ if (!initializer)
1181
+ return current;
1182
+ const nextVisited = new Set(visited);
1183
+ nextVisited.add(current.text);
1184
+ return resolveRegistryExpression(initializer, initializers, nextVisited);
1185
+ }
1186
+ function registryValueExpressions(expression, initializers, visited = new Set()) {
1187
+ const current = unwrapExpression(expression);
1188
+ if (ts.isArrayLiteralExpression(current)) {
1189
+ return current.elements.flatMap((element) => ts.isSpreadElement(element)
1190
+ ? registryValueExpressions(element.expression, initializers, new Set(visited))
1191
+ : ts.isExpression(element)
1192
+ ? [element]
1193
+ : []);
1194
+ }
1195
+ if (ts.isIdentifier(current) && !visited.has(current.text)) {
1196
+ const initializer = initializers.get(current.text);
1197
+ if (initializer) {
1198
+ const nextVisited = new Set(visited);
1199
+ nextVisited.add(current.text);
1200
+ return registryValueExpressions(initializer, initializers, nextVisited);
989
1201
  }
1202
+ }
1203
+ return [current];
1204
+ }
1205
+ function arrayExpressions(array) {
1206
+ return array.elements.flatMap((element) => {
1207
+ if (ts.isSpreadElement(element))
1208
+ return [element.expression];
1209
+ return ts.isExpression(element) ? [element] : [];
990
1210
  });
991
- return result;
992
1211
  }
993
1212
  function discoverFeatureNames(workspace, featuresPath) {
994
1213
  const names = new Set();