@beignet/cli 0.0.49 → 0.0.51

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 (64) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +54 -5
  3. package/dist/analysis/workspace.d.ts +1 -0
  4. package/dist/analysis/workspace.d.ts.map +1 -1
  5. package/dist/analysis/workspace.js +20 -10
  6. package/dist/analysis/workspace.js.map +1 -1
  7. package/dist/app-map-changes.d.ts +103 -0
  8. package/dist/app-map-changes.d.ts.map +1 -0
  9. package/dist/app-map-changes.js +949 -0
  10. package/dist/app-map-changes.js.map +1 -0
  11. package/dist/git-changes.d.ts +30 -0
  12. package/dist/git-changes.d.ts.map +1 -0
  13. package/dist/git-changes.js +367 -0
  14. package/dist/git-changes.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +58 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/inspect.js +413 -43
  20. package/dist/inspect.js.map +1 -1
  21. package/dist/lib.d.ts +3 -0
  22. package/dist/lib.d.ts.map +1 -1
  23. package/dist/lib.js +1 -0
  24. package/dist/lib.js.map +1 -1
  25. package/dist/make/shared.d.ts.map +1 -1
  26. package/dist/make/shared.js +31 -27
  27. package/dist/make/shared.js.map +1 -1
  28. package/dist/make.d.ts.map +1 -1
  29. package/dist/make.js +0 -2
  30. package/dist/make.js.map +1 -1
  31. package/dist/mcp.d.ts.map +1 -1
  32. package/dist/mcp.js +46 -6
  33. package/dist/mcp.js.map +1 -1
  34. package/dist/operational-process.d.ts +1 -0
  35. package/dist/operational-process.d.ts.map +1 -1
  36. package/dist/operational-process.js.map +1 -1
  37. package/dist/operational-runner.js +1 -0
  38. package/dist/operational-runner.js.map +1 -1
  39. package/dist/outbox.d.ts +1 -0
  40. package/dist/outbox.d.ts.map +1 -1
  41. package/dist/outbox.js +58 -12
  42. package/dist/outbox.js.map +1 -1
  43. package/dist/templates/agents.d.ts.map +1 -1
  44. package/dist/templates/agents.js +28 -3
  45. package/dist/templates/agents.js.map +1 -1
  46. package/dist/templates/base.d.ts.map +1 -1
  47. package/dist/templates/base.js +4 -1
  48. package/dist/templates/base.js.map +1 -1
  49. package/package.json +2 -2
  50. package/skills/app-structure/SKILL.md +30 -5
  51. package/src/analysis/workspace.ts +25 -9
  52. package/src/app-map-changes.ts +1462 -0
  53. package/src/git-changes.ts +511 -0
  54. package/src/index.ts +84 -4
  55. package/src/inspect.ts +586 -51
  56. package/src/lib.ts +21 -0
  57. package/src/make/shared.ts +31 -27
  58. package/src/make.ts +0 -2
  59. package/src/mcp.ts +65 -12
  60. package/src/operational-process.ts +1 -0
  61. package/src/operational-runner.ts +1 -0
  62. package/src/outbox.ts +63 -12
  63. package/src/templates/agents.ts +28 -3
  64. package/src/templates/base.ts +4 -1
package/src/inspect.ts CHANGED
@@ -1576,36 +1576,57 @@ function parseRouteExports(
1576
1576
  config: ResolvedBeignetConfig,
1577
1577
  ): RouteExport[] {
1578
1578
  const exports: RouteExport[] = [];
1579
- const imports = parseNamedImports(source, config);
1580
- const exportRegex =
1581
- /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
1582
- const apiRouteExportRegex =
1583
- /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
1579
+ const sourceFile = ts.createSourceFile(
1580
+ handlerFile,
1581
+ source,
1582
+ ts.ScriptTarget.Latest,
1583
+ true,
1584
+ ts.ScriptKind.TS,
1585
+ );
1586
+ const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
1584
1587
 
1585
- for (const match of source.matchAll(apiRouteExportRegex)) {
1586
- for (const member of match[1].split(",")) {
1587
- const parts = member.split(":");
1588
- const method = (parts[1] ?? parts[0])?.trim();
1589
- if (!method || !isHttpMethod(method)) continue;
1588
+ for (const statement of sourceFile.statements) {
1589
+ if (
1590
+ !ts.isVariableStatement(statement) ||
1591
+ !statement.modifiers?.some(
1592
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
1593
+ )
1594
+ ) {
1595
+ continue;
1596
+ }
1590
1597
 
1591
- exports.push({
1592
- method,
1593
- handlerFile,
1594
- contractRef: routePath,
1595
- catchAllPrefix: catchAllRoutePrefix(routePath),
1596
- source: "next-route",
1597
- });
1598
+ for (const declaration of statement.declarationList.declarations) {
1599
+ if (
1600
+ !declaration.initializer ||
1601
+ !ts.isObjectBindingPattern(declaration.name) ||
1602
+ !isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")
1603
+ ) {
1604
+ continue;
1605
+ }
1606
+
1607
+ for (const element of declaration.name.elements) {
1608
+ if (!ts.isIdentifier(element.name)) continue;
1609
+ const method = element.name.text;
1610
+ if (!isHttpMethod(method)) continue;
1611
+ exports.push({
1612
+ method,
1613
+ handlerFile,
1614
+ contractRef: routePath,
1615
+ catchAllPrefix: catchAllRoutePrefix(routePath),
1616
+ source: "next-route",
1617
+ });
1618
+ }
1598
1619
  }
1599
1620
  }
1600
1621
 
1601
- for (const match of source.matchAll(exportRegex)) {
1602
- const method = match[1] as HttpMethod;
1603
- const expression = match[2];
1604
- const contractMatch =
1605
- /server\.route\(\s*([A-Za-z_$][\w$]*)\s*\)\.handle/.exec(expression);
1622
+ for (const { exportName, declaration } of exportedVariableDeclarations(
1623
+ sourceFile,
1624
+ )) {
1625
+ if (!isHttpMethod(exportName) || !declaration.initializer) continue;
1626
+ const method = exportName;
1627
+ const localName = routeLocalContractIdentifier(declaration.initializer);
1606
1628
 
1607
- if (contractMatch) {
1608
- const localName = contractMatch[1];
1629
+ if (localName) {
1609
1630
  const imported = imports.get(localName);
1610
1631
  exports.push({
1611
1632
  method,
@@ -1617,7 +1638,7 @@ function parseRouteExports(
1617
1638
  continue;
1618
1639
  }
1619
1640
 
1620
- if (/server\.api\b/.test(expression)) {
1641
+ if (isServerApiExpression(declaration.initializer)) {
1621
1642
  exports.push({
1622
1643
  method,
1623
1644
  handlerFile,
@@ -1631,6 +1652,98 @@ function parseRouteExports(
1631
1652
  return exports;
1632
1653
  }
1633
1654
 
1655
+ function parseRouteNamedImports(
1656
+ sourceFile: ts.SourceFile,
1657
+ config: ResolvedBeignetConfig,
1658
+ handlerFile: string,
1659
+ ): Map<string, { importedName: string; contractFile?: string }> {
1660
+ const imports = new Map<
1661
+ string,
1662
+ { importedName: string; contractFile?: string }
1663
+ >();
1664
+
1665
+ for (const statement of sourceFile.statements) {
1666
+ if (
1667
+ !ts.isImportDeclaration(statement) ||
1668
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1669
+ statement.importClause?.isTypeOnly ||
1670
+ !statement.importClause?.namedBindings ||
1671
+ !ts.isNamedImports(statement.importClause.namedBindings)
1672
+ ) {
1673
+ continue;
1674
+ }
1675
+
1676
+ const contractFile = contractFileFromImport(
1677
+ statement.moduleSpecifier.text,
1678
+ config,
1679
+ handlerFile,
1680
+ );
1681
+ for (const element of statement.importClause.namedBindings.elements) {
1682
+ if (element.isTypeOnly) continue;
1683
+ imports.set(element.name.text, {
1684
+ importedName: element.propertyName?.text ?? element.name.text,
1685
+ contractFile,
1686
+ });
1687
+ }
1688
+ }
1689
+
1690
+ return imports;
1691
+ }
1692
+
1693
+ function isNamedRouteFactoryCall(
1694
+ expression: ts.Expression,
1695
+ name: string,
1696
+ ): boolean {
1697
+ const unwrapped = unwrapContractExpression(expression);
1698
+ if (!ts.isCallExpression(unwrapped)) return false;
1699
+ const callee = unwrapContractExpression(unwrapped.expression);
1700
+ return ts.isIdentifier(callee) && callee.text === name;
1701
+ }
1702
+
1703
+ function routeLocalContractIdentifier(
1704
+ expression: ts.Expression,
1705
+ ): string | undefined {
1706
+ const handleCall = unwrapContractExpression(expression);
1707
+ if (!ts.isCallExpression(handleCall)) return undefined;
1708
+ const handleAccess = unwrapContractExpression(handleCall.expression);
1709
+ if (
1710
+ !ts.isPropertyAccessExpression(handleAccess) ||
1711
+ handleAccess.name.text !== "handle"
1712
+ ) {
1713
+ return undefined;
1714
+ }
1715
+
1716
+ const routeCall = unwrapContractExpression(handleAccess.expression);
1717
+ if (!ts.isCallExpression(routeCall)) return undefined;
1718
+ const routeAccess = unwrapContractExpression(routeCall.expression);
1719
+ if (
1720
+ !ts.isPropertyAccessExpression(routeAccess) ||
1721
+ routeAccess.name.text !== "route" ||
1722
+ !ts.isIdentifier(routeAccess.expression) ||
1723
+ routeAccess.expression.text !== "server"
1724
+ ) {
1725
+ return undefined;
1726
+ }
1727
+
1728
+ const contract = routeCall.arguments[0];
1729
+ const unwrappedContract = contract
1730
+ ? unwrapContractExpression(contract)
1731
+ : undefined;
1732
+ return unwrappedContract && ts.isIdentifier(unwrappedContract)
1733
+ ? unwrappedContract.text
1734
+ : undefined;
1735
+ }
1736
+
1737
+ function isServerApiExpression(expression: ts.Expression): boolean {
1738
+ const unwrapped = unwrapContractExpression(expression);
1739
+ return (
1740
+ ts.isPropertyAccessExpression(unwrapped) &&
1741
+ ts.isIdentifier(unwrapped.expression) &&
1742
+ unwrapped.expression.text === "server" &&
1743
+ unwrapped.name.text === "api"
1744
+ );
1745
+ }
1746
+
1634
1747
  function catchAllRoutePrefix(routePath: string): string | undefined {
1635
1748
  const segments = routePath.split("/").filter(Boolean);
1636
1749
  const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
@@ -1762,6 +1875,29 @@ function contractFileFromImport(
1762
1875
  config: ResolvedBeignetConfig,
1763
1876
  importerFile?: string,
1764
1877
  ): string | undefined {
1878
+ const resolveCandidate = (candidate: string) => {
1879
+ const extension = path.extname(candidate).toLowerCase();
1880
+ if (!extension) return `${candidate}.ts`;
1881
+
1882
+ let sourceExtension: string | undefined;
1883
+ switch (extension) {
1884
+ case ".js":
1885
+ sourceExtension = ".ts";
1886
+ break;
1887
+ case ".jsx":
1888
+ sourceExtension = ".tsx";
1889
+ break;
1890
+ case ".mjs":
1891
+ sourceExtension = ".mts";
1892
+ break;
1893
+ case ".cjs":
1894
+ sourceExtension = ".cts";
1895
+ break;
1896
+ }
1897
+ return sourceExtension
1898
+ ? `${candidate.slice(0, -extension.length)}${sourceExtension}`
1899
+ : candidate;
1900
+ };
1765
1901
  const contractsPath = directoryPath(config.paths.contracts);
1766
1902
  const aliasPrefix = `@/${contractsPath}/`;
1767
1903
  const aliasExact = `@/${contractsPath}`;
@@ -1772,18 +1908,18 @@ function contractFileFromImport(
1772
1908
  return `${contractsPath}/index.ts`;
1773
1909
  }
1774
1910
  if (sourcePath.startsWith(aliasPrefix)) {
1775
- return `${sourcePath.slice("@/".length)}.ts`;
1911
+ return resolveCandidate(sourcePath.slice("@/".length));
1776
1912
  }
1777
1913
 
1778
1914
  if (sourcePath.startsWith(relativePrefix)) {
1779
- return `${sourcePath}.ts`;
1915
+ return resolveCandidate(sourcePath);
1780
1916
  }
1781
1917
 
1782
1918
  if (importerFile && sourcePath.startsWith(".")) {
1783
1919
  const resolved = normalizePath(
1784
1920
  path.join(path.dirname(importerFile), sourcePath),
1785
1921
  );
1786
- return `${resolved}.ts`;
1922
+ return resolveCandidate(resolved);
1787
1923
  }
1788
1924
 
1789
1925
  return undefined;
@@ -4404,6 +4540,7 @@ type WorkflowRegistrationDrift = {
4404
4540
  unregistered: UnregisteredWorkflowRegistry[];
4405
4541
  wiringFile?: string;
4406
4542
  eventBusFile?: string;
4543
+ unsafeLifecycleFiles: string[];
4407
4544
  };
4408
4545
  };
4409
4546
 
@@ -4482,6 +4619,15 @@ async function inspectWorkflowRegistrationDrift(
4482
4619
  }
4483
4620
 
4484
4621
  const infraDir = directoryPath(path.dirname(config.paths.portWiring));
4622
+ for (const file of drift.listeners.unsafeLifecycleFiles) {
4623
+ diagnostics.push({
4624
+ severity: "warning",
4625
+ code: "BEIGNET_LISTENER_LIFECYCLE_UNSAFE",
4626
+ file,
4627
+ message: `${file} calls registerListeners(...) without the complete provider lifecycle, so server startup can resolve before listeners are ready or shutdown can leak subscriptions. Register listeners in start(), return or await registration.ready, and return or await registration.unsubscribe() in stop().`,
4628
+ });
4629
+ }
4630
+
4485
4631
  const listenerTarget = drift.listeners.wiringFile
4486
4632
  ? `${drift.listeners.wiringFile}, which already calls registerListeners(...)`
4487
4633
  : drift.listeners.eventBusFile
@@ -4652,7 +4798,7 @@ async function workflowRegistrationDrift(
4652
4798
  events: [],
4653
4799
  jobs: [],
4654
4800
  },
4655
- listeners: { unregistered: [] },
4801
+ listeners: { unregistered: [], unsafeLifecycleFiles: [] },
4656
4802
  };
4657
4803
  if (registries.length === 0) return drift;
4658
4804
 
@@ -4714,7 +4860,12 @@ async function workflowRegistrationDrift(
4714
4860
 
4715
4861
  const listenerRegistries = byKind("listeners");
4716
4862
  if (listenerRegistries.length > 0) {
4717
- const wiring = await listenerWiringReferences(targetDir, files, config);
4863
+ const wiring = await listenerWiringReferences(
4864
+ targetDir,
4865
+ files,
4866
+ config,
4867
+ listenerRegistries,
4868
+ );
4718
4869
  drift.listeners = {
4719
4870
  unregistered: unregisteredWorkflowRegistries(
4720
4871
  listenerRegistries,
@@ -4722,6 +4873,7 @@ async function workflowRegistrationDrift(
4722
4873
  ),
4723
4874
  wiringFile: wiring.wiringFile,
4724
4875
  eventBusFile: wiring.eventBusFile,
4876
+ unsafeLifecycleFiles: wiring.unsafeLifecycleFiles,
4725
4877
  };
4726
4878
  }
4727
4879
 
@@ -5056,12 +5208,15 @@ async function listenerWiringReferences(
5056
5208
  targetDir: string,
5057
5209
  files: string[],
5058
5210
  config: ResolvedBeignetConfig,
5211
+ listenerRegistries: FeatureWorkflowRegistry[],
5059
5212
  ): Promise<{
5060
5213
  identifiers: Set<string>;
5061
5214
  wiringFile?: string;
5062
5215
  eventBusFile?: string;
5216
+ unsafeLifecycleFiles: string[];
5063
5217
  }> {
5064
5218
  const identifiers = new Set<string>();
5219
+ const unsafeLifecycleFiles = new Set<string>();
5065
5220
  let wiringFile: string | undefined;
5066
5221
  let eventBusFile: string | undefined;
5067
5222
  let centralListenerRegistryReferenced = false;
@@ -5073,35 +5228,54 @@ async function listenerWiringReferences(
5073
5228
 
5074
5229
  const source = await readFile(path.join(targetDir, file), "utf8");
5075
5230
  const namedImports = parseNamedImportSources(source);
5076
- let foundCall = false;
5077
-
5078
- for (const match of source.matchAll(/\bregisterListeners\s*\(/g)) {
5079
- const openParen = (match.index ?? 0) + match[0].length - 1;
5080
- const closeParen = matchingDelimiterIndex(source, openParen, "(", ")");
5081
- const argsText =
5082
- closeParen === -1
5083
- ? source.slice(openParen)
5084
- : source.slice(openParen + 1, closeParen);
5231
+ const sourceFile = ts.createSourceFile(
5232
+ file,
5233
+ source,
5234
+ ts.ScriptTarget.Latest,
5235
+ true,
5236
+ file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
5237
+ );
5238
+ const registerListenersBindings = importedRegisterListenersBindings(
5239
+ source,
5240
+ namedImports,
5241
+ );
5242
+ const calls = registerListenersCalls(sourceFile, registerListenersBindings);
5243
+ let foundRelevantCall = false;
5085
5244
 
5086
- foundCall = true;
5245
+ for (const call of calls) {
5246
+ const argsText = call.arguments
5247
+ .map((argument) => argument.getText(sourceFile))
5248
+ .join(",");
5087
5249
  const callIdentifiers = identifiersFromArrayExpression(argsText);
5250
+ const referencesCentral = callReferencesCentralListenerRegistry({
5251
+ callIdentifiers,
5252
+ namedImports,
5253
+ importerFile: file,
5254
+ listenerRegistryFile,
5255
+ files,
5256
+ });
5257
+ const referencesFeature = callReferencesFeatureListenerRegistry({
5258
+ callIdentifiers,
5259
+ namedImports,
5260
+ importerFile: file,
5261
+ files,
5262
+ listenerRegistries,
5263
+ });
5264
+ if (!referencesCentral && !referencesFeature) continue;
5265
+
5266
+ foundRelevantCall = true;
5088
5267
  for (const identifier of callIdentifiers) {
5089
5268
  identifiers.add(identifier);
5090
5269
  }
5091
- if (
5092
- callReferencesCentralListenerRegistry({
5093
- callIdentifiers,
5094
- namedImports,
5095
- importerFile: file,
5096
- listenerRegistryFile,
5097
- files,
5098
- })
5099
- ) {
5270
+ if (referencesCentral) {
5100
5271
  centralListenerRegistryReferenced = true;
5101
5272
  }
5273
+ if (!hasSafeListenerRegistrationLifecycle(call, sourceFile)) {
5274
+ unsafeLifecycleFiles.add(file);
5275
+ }
5102
5276
  }
5103
5277
 
5104
- if (foundCall) {
5278
+ if (foundRelevantCall) {
5105
5279
  wiringFile ??= file;
5106
5280
  } else if (!eventBusFile && /\bcreate\w*EventBus\s*\(/.test(source)) {
5107
5281
  eventBusFile = file;
@@ -5124,7 +5298,368 @@ async function listenerWiringReferences(
5124
5298
  }
5125
5299
  }
5126
5300
 
5127
- return { identifiers, wiringFile, eventBusFile };
5301
+ return {
5302
+ identifiers,
5303
+ wiringFile,
5304
+ eventBusFile,
5305
+ unsafeLifecycleFiles: [...unsafeLifecycleFiles].sort(),
5306
+ };
5307
+ }
5308
+
5309
+ type ListenerLifecycleHook =
5310
+ | ts.MethodDeclaration
5311
+ | ts.FunctionExpression
5312
+ | ts.ArrowFunction;
5313
+
5314
+ interface ListenerLifecycleTarget {
5315
+ text: string;
5316
+ binding?: ts.Node;
5317
+ }
5318
+
5319
+ function registerListenersCalls(
5320
+ sourceFile: ts.SourceFile,
5321
+ bindings: ReadonlySet<string>,
5322
+ ): ts.CallExpression[] {
5323
+ const calls: ts.CallExpression[] = [];
5324
+ const visit = (node: ts.Node): void => {
5325
+ if (
5326
+ ts.isCallExpression(node) &&
5327
+ ((ts.isIdentifier(node.expression) &&
5328
+ bindings.has(node.expression.text)) ||
5329
+ (ts.isPropertyAccessExpression(node.expression) &&
5330
+ node.expression.name.text === "registerListeners" &&
5331
+ ts.isIdentifier(node.expression.expression) &&
5332
+ bindings.has(`${node.expression.expression.text}.*`)))
5333
+ ) {
5334
+ calls.push(node);
5335
+ }
5336
+ ts.forEachChild(node, visit);
5337
+ };
5338
+ visit(sourceFile);
5339
+ return calls;
5340
+ }
5341
+
5342
+ function importedRegisterListenersBindings(
5343
+ source: string,
5344
+ namedImports: Map<string, { importedName: string; sourcePath: string }>,
5345
+ ): Set<string> {
5346
+ const bindings = new Set<string>();
5347
+ for (const [localName, imported] of namedImports) {
5348
+ if (
5349
+ imported.importedName === "registerListeners" &&
5350
+ imported.sourcePath === "@beignet/core/events"
5351
+ ) {
5352
+ bindings.add(localName);
5353
+ }
5354
+ }
5355
+ const namespaceImport =
5356
+ /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/core\/events["']/g;
5357
+ for (const match of source.matchAll(namespaceImport)) {
5358
+ bindings.add(`${match[1]}.*`);
5359
+ }
5360
+ return bindings;
5361
+ }
5362
+
5363
+ function hasSafeListenerRegistrationLifecycle(
5364
+ call: ts.CallExpression,
5365
+ sourceFile: ts.SourceFile,
5366
+ ): boolean {
5367
+ const startHook = enclosingListenerLifecycleHook(call, "start");
5368
+ const target = listenerRegistrationTarget(call, sourceFile);
5369
+ if (!startHook || !target) return false;
5370
+ const lifecycleOwner = listenerLifecycleOwner(startHook);
5371
+ if (!lifecycleOwner) return false;
5372
+
5373
+ let observesReadiness = false;
5374
+ let observesCleanup = false;
5375
+ const visit = (node: ts.Node): void => {
5376
+ if (
5377
+ !observesReadiness &&
5378
+ ts.isPropertyAccessExpression(node) &&
5379
+ node.name.text === "ready" &&
5380
+ listenerLifecycleTargetsMatch(
5381
+ listenerLifecycleTarget(node.expression, sourceFile),
5382
+ target,
5383
+ ) &&
5384
+ enclosingListenerLifecycleHook(node, "start") === startHook &&
5385
+ listenerLifecyclePromiseIsObserved(node, startHook)
5386
+ ) {
5387
+ observesReadiness = true;
5388
+ }
5389
+
5390
+ if (
5391
+ !observesCleanup &&
5392
+ ts.isCallExpression(node) &&
5393
+ ts.isPropertyAccessExpression(node.expression) &&
5394
+ node.expression.name.text === "unsubscribe" &&
5395
+ listenerLifecycleTargetsMatch(
5396
+ listenerLifecycleTarget(node.expression.expression, sourceFile),
5397
+ target,
5398
+ )
5399
+ ) {
5400
+ const stopHook = enclosingListenerLifecycleHook(node, "stop");
5401
+ if (
5402
+ stopHook &&
5403
+ listenerLifecycleOwner(stopHook) === lifecycleOwner &&
5404
+ listenerLifecyclePromiseIsObserved(node, stopHook)
5405
+ ) {
5406
+ observesCleanup = true;
5407
+ }
5408
+ }
5409
+
5410
+ if (!observesReadiness || !observesCleanup) {
5411
+ ts.forEachChild(node, visit);
5412
+ }
5413
+ };
5414
+ visit(lifecycleOwner);
5415
+ return observesReadiness && observesCleanup;
5416
+ }
5417
+
5418
+ function listenerRegistrationTarget(
5419
+ call: ts.CallExpression,
5420
+ sourceFile: ts.SourceFile,
5421
+ ): ListenerLifecycleTarget | undefined {
5422
+ let expression: ts.Expression = call;
5423
+ while (
5424
+ ts.isParenthesizedExpression(expression.parent) ||
5425
+ ts.isAsExpression(expression.parent) ||
5426
+ ts.isSatisfiesExpression(expression.parent) ||
5427
+ ts.isNonNullExpression(expression.parent)
5428
+ ) {
5429
+ expression = expression.parent;
5430
+ }
5431
+
5432
+ const parent = expression.parent;
5433
+ if (
5434
+ ts.isVariableDeclaration(parent) &&
5435
+ parent.initializer === expression &&
5436
+ ts.isIdentifier(parent.name)
5437
+ ) {
5438
+ return { text: parent.name.text, binding: parent };
5439
+ }
5440
+ if (
5441
+ ts.isBinaryExpression(parent) &&
5442
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
5443
+ parent.right === expression
5444
+ ) {
5445
+ return listenerLifecycleTarget(parent.left, sourceFile);
5446
+ }
5447
+ return undefined;
5448
+ }
5449
+
5450
+ function listenerLifecycleTarget(
5451
+ expression: ts.Expression,
5452
+ sourceFile: ts.SourceFile,
5453
+ ): ListenerLifecycleTarget | undefined {
5454
+ let current = expression;
5455
+ while (
5456
+ ts.isParenthesizedExpression(current) ||
5457
+ ts.isAsExpression(current) ||
5458
+ ts.isSatisfiesExpression(current) ||
5459
+ ts.isNonNullExpression(current)
5460
+ ) {
5461
+ current = current.expression;
5462
+ }
5463
+ if (!ts.isIdentifier(current) && !ts.isPropertyAccessExpression(current)) {
5464
+ return undefined;
5465
+ }
5466
+
5467
+ return {
5468
+ text: current.getText(sourceFile),
5469
+ binding: listenerLifecycleRootIdentifier(current)
5470
+ ? resolveListenerLifecycleBinding(
5471
+ listenerLifecycleRootIdentifier(current) as ts.Identifier,
5472
+ )
5473
+ : undefined,
5474
+ };
5475
+ }
5476
+
5477
+ function listenerLifecycleRootIdentifier(
5478
+ expression: ts.Identifier | ts.PropertyAccessExpression,
5479
+ ): ts.Identifier | undefined {
5480
+ let current: ts.Expression = expression;
5481
+ while (ts.isPropertyAccessExpression(current)) {
5482
+ current = current.expression;
5483
+ }
5484
+ return ts.isIdentifier(current) ? current : undefined;
5485
+ }
5486
+
5487
+ function resolveListenerLifecycleBinding(
5488
+ identifier: ts.Identifier,
5489
+ ): ts.Node | undefined {
5490
+ let current: ts.Node | undefined = identifier;
5491
+ while (current) {
5492
+ if (ts.isBlock(current) || ts.isSourceFile(current)) {
5493
+ for (const statement of current.statements) {
5494
+ if (!ts.isVariableStatement(statement)) continue;
5495
+ for (const declaration of statement.declarationList.declarations) {
5496
+ if (
5497
+ ts.isIdentifier(declaration.name) &&
5498
+ declaration.name.text === identifier.text
5499
+ ) {
5500
+ return declaration;
5501
+ }
5502
+ }
5503
+ }
5504
+ }
5505
+
5506
+ if (ts.isFunctionLike(current)) {
5507
+ for (const parameter of current.parameters) {
5508
+ if (
5509
+ ts.isIdentifier(parameter.name) &&
5510
+ parameter.name.text === identifier.text
5511
+ ) {
5512
+ return parameter;
5513
+ }
5514
+ }
5515
+ }
5516
+
5517
+ if (
5518
+ ts.isCatchClause(current) &&
5519
+ current.variableDeclaration &&
5520
+ ts.isIdentifier(current.variableDeclaration.name) &&
5521
+ current.variableDeclaration.name.text === identifier.text
5522
+ ) {
5523
+ return current.variableDeclaration;
5524
+ }
5525
+ current = current.parent;
5526
+ }
5527
+ return undefined;
5528
+ }
5529
+
5530
+ function listenerLifecycleTargetsMatch(
5531
+ left: ListenerLifecycleTarget | undefined,
5532
+ right: ListenerLifecycleTarget,
5533
+ ): boolean {
5534
+ return (
5535
+ left?.text === right.text &&
5536
+ (left.binding !== undefined || right.binding !== undefined
5537
+ ? left.binding === right.binding
5538
+ : true)
5539
+ );
5540
+ }
5541
+
5542
+ function enclosingListenerLifecycleHook(
5543
+ node: ts.Node,
5544
+ hookName: "start" | "stop",
5545
+ ): ListenerLifecycleHook | undefined {
5546
+ let current: ts.Node | undefined = node.parent;
5547
+ while (current) {
5548
+ if (ts.isMethodDeclaration(current)) {
5549
+ return staticPropertyName(current.name) === hookName
5550
+ ? current
5551
+ : undefined;
5552
+ }
5553
+ if (ts.isFunctionExpression(current) || ts.isArrowFunction(current)) {
5554
+ const parent = current.parent;
5555
+ return ts.isPropertyAssignment(parent) &&
5556
+ staticPropertyName(parent.name) === hookName
5557
+ ? current
5558
+ : undefined;
5559
+ }
5560
+ if (ts.isFunctionDeclaration(current)) return undefined;
5561
+ current = current.parent;
5562
+ }
5563
+ return undefined;
5564
+ }
5565
+
5566
+ function listenerLifecycleOwner(
5567
+ hook: ListenerLifecycleHook,
5568
+ ): ts.ObjectLiteralExpression | undefined {
5569
+ if (ts.isMethodDeclaration(hook)) {
5570
+ return ts.isObjectLiteralExpression(hook.parent) ? hook.parent : undefined;
5571
+ }
5572
+ const property = hook.parent;
5573
+ return ts.isPropertyAssignment(property) &&
5574
+ ts.isObjectLiteralExpression(property.parent)
5575
+ ? property.parent
5576
+ : undefined;
5577
+ }
5578
+
5579
+ function listenerLifecyclePromiseIsObserved(
5580
+ node: ts.Node,
5581
+ hook: ListenerLifecycleHook,
5582
+ ): boolean {
5583
+ let current: ts.Node | undefined = node;
5584
+ while (current && current !== hook) {
5585
+ const parent: ts.Node = current.parent;
5586
+ if (
5587
+ (ts.isAwaitExpression(parent) && parent.expression === current) ||
5588
+ (ts.isReturnStatement(parent) && parent.expression === current)
5589
+ ) {
5590
+ return true;
5591
+ }
5592
+ if (
5593
+ ts.isParenthesizedExpression(parent) ||
5594
+ ts.isAsExpression(parent) ||
5595
+ ts.isSatisfiesExpression(parent) ||
5596
+ ts.isNonNullExpression(parent)
5597
+ ) {
5598
+ current = parent;
5599
+ continue;
5600
+ }
5601
+ if (
5602
+ ts.isPropertyAccessExpression(parent) &&
5603
+ parent.expression === current
5604
+ ) {
5605
+ const method = parent.name.text;
5606
+ const invocation = parent.parent;
5607
+ if (
5608
+ !["then", "catch", "finally"].includes(method) ||
5609
+ !ts.isCallExpression(invocation) ||
5610
+ invocation.expression !== parent ||
5611
+ method === "catch" ||
5612
+ (method === "then" && invocation.arguments.length > 1)
5613
+ ) {
5614
+ return false;
5615
+ }
5616
+ current = invocation;
5617
+ continue;
5618
+ }
5619
+ return false;
5620
+ }
5621
+ return false;
5622
+ }
5623
+
5624
+ function callReferencesFeatureListenerRegistry(args: {
5625
+ callIdentifiers: Set<string>;
5626
+ namedImports: Map<string, { importedName: string; sourcePath: string }>;
5627
+ importerFile: string;
5628
+ files: string[];
5629
+ listenerRegistries: FeatureWorkflowRegistry[];
5630
+ }): boolean {
5631
+ for (const identifier of args.callIdentifiers) {
5632
+ for (const registry of args.listenerRegistries) {
5633
+ if (
5634
+ identifier === registry.registryName ||
5635
+ registry.members.includes(identifier)
5636
+ ) {
5637
+ return true;
5638
+ }
5639
+ }
5640
+
5641
+ const imported = args.namedImports.get(identifier);
5642
+ if (!imported) continue;
5643
+ const importedFile = sourceFileFromImport(
5644
+ imported.sourcePath,
5645
+ args.importerFile,
5646
+ args.files,
5647
+ );
5648
+ if (!importedFile) continue;
5649
+
5650
+ for (const registry of args.listenerRegistries) {
5651
+ if (
5652
+ (imported.importedName === registry.registryName &&
5653
+ importedFile === registry.indexFile) ||
5654
+ (registry.members.includes(imported.importedName) &&
5655
+ (importedFile === registry.indexFile ||
5656
+ registry.memberFiles.get(imported.importedName) === importedFile))
5657
+ ) {
5658
+ return true;
5659
+ }
5660
+ }
5661
+ }
5662
+ return false;
5128
5663
  }
5129
5664
 
5130
5665
  function callReferencesCentralListenerRegistry(args: {