@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/dist/inspect.js CHANGED
@@ -937,30 +937,41 @@ async function readRouteExports(targetDir, routeFiles, config) {
937
937
  }
938
938
  function parseRouteExports(source, handlerFile, routePath, config) {
939
939
  const exports = [];
940
- const imports = parseNamedImports(source, config);
941
- const exportRegex = /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
942
- const apiRouteExportRegex = /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
943
- for (const match of source.matchAll(apiRouteExportRegex)) {
944
- for (const member of match[1].split(",")) {
945
- const parts = member.split(":");
946
- const method = (parts[1] ?? parts[0])?.trim();
947
- if (!method || !isHttpMethod(method))
940
+ const sourceFile = ts.createSourceFile(handlerFile, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
941
+ const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
942
+ for (const statement of sourceFile.statements) {
943
+ if (!ts.isVariableStatement(statement) ||
944
+ !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
945
+ continue;
946
+ }
947
+ for (const declaration of statement.declarationList.declarations) {
948
+ if (!declaration.initializer ||
949
+ !ts.isObjectBindingPattern(declaration.name) ||
950
+ !isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")) {
948
951
  continue;
949
- exports.push({
950
- method,
951
- handlerFile,
952
- contractRef: routePath,
953
- catchAllPrefix: catchAllRoutePrefix(routePath),
954
- source: "next-route",
955
- });
952
+ }
953
+ for (const element of declaration.name.elements) {
954
+ if (!ts.isIdentifier(element.name))
955
+ continue;
956
+ const method = element.name.text;
957
+ if (!isHttpMethod(method))
958
+ continue;
959
+ exports.push({
960
+ method,
961
+ handlerFile,
962
+ contractRef: routePath,
963
+ catchAllPrefix: catchAllRoutePrefix(routePath),
964
+ source: "next-route",
965
+ });
966
+ }
956
967
  }
957
968
  }
958
- for (const match of source.matchAll(exportRegex)) {
959
- const method = match[1];
960
- const expression = match[2];
961
- const contractMatch = /server\.route\(\s*([A-Za-z_$][\w$]*)\s*\)\.handle/.exec(expression);
962
- if (contractMatch) {
963
- const localName = contractMatch[1];
969
+ for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
970
+ if (!isHttpMethod(exportName) || !declaration.initializer)
971
+ continue;
972
+ const method = exportName;
973
+ const localName = routeLocalContractIdentifier(declaration.initializer);
974
+ if (localName) {
964
975
  const imported = imports.get(localName);
965
976
  exports.push({
966
977
  method,
@@ -971,7 +982,7 @@ function parseRouteExports(source, handlerFile, routePath, config) {
971
982
  });
972
983
  continue;
973
984
  }
974
- if (/server\.api\b/.test(expression)) {
985
+ if (isServerApiExpression(declaration.initializer)) {
975
986
  exports.push({
976
987
  method,
977
988
  handlerFile,
@@ -983,6 +994,69 @@ function parseRouteExports(source, handlerFile, routePath, config) {
983
994
  }
984
995
  return exports;
985
996
  }
997
+ function parseRouteNamedImports(sourceFile, config, handlerFile) {
998
+ const imports = new Map();
999
+ for (const statement of sourceFile.statements) {
1000
+ if (!ts.isImportDeclaration(statement) ||
1001
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1002
+ statement.importClause?.isTypeOnly ||
1003
+ !statement.importClause?.namedBindings ||
1004
+ !ts.isNamedImports(statement.importClause.namedBindings)) {
1005
+ continue;
1006
+ }
1007
+ const contractFile = contractFileFromImport(statement.moduleSpecifier.text, config, handlerFile);
1008
+ for (const element of statement.importClause.namedBindings.elements) {
1009
+ if (element.isTypeOnly)
1010
+ continue;
1011
+ imports.set(element.name.text, {
1012
+ importedName: element.propertyName?.text ?? element.name.text,
1013
+ contractFile,
1014
+ });
1015
+ }
1016
+ }
1017
+ return imports;
1018
+ }
1019
+ function isNamedRouteFactoryCall(expression, name) {
1020
+ const unwrapped = unwrapContractExpression(expression);
1021
+ if (!ts.isCallExpression(unwrapped))
1022
+ return false;
1023
+ const callee = unwrapContractExpression(unwrapped.expression);
1024
+ return ts.isIdentifier(callee) && callee.text === name;
1025
+ }
1026
+ function routeLocalContractIdentifier(expression) {
1027
+ const handleCall = unwrapContractExpression(expression);
1028
+ if (!ts.isCallExpression(handleCall))
1029
+ return undefined;
1030
+ const handleAccess = unwrapContractExpression(handleCall.expression);
1031
+ if (!ts.isPropertyAccessExpression(handleAccess) ||
1032
+ handleAccess.name.text !== "handle") {
1033
+ return undefined;
1034
+ }
1035
+ const routeCall = unwrapContractExpression(handleAccess.expression);
1036
+ if (!ts.isCallExpression(routeCall))
1037
+ return undefined;
1038
+ const routeAccess = unwrapContractExpression(routeCall.expression);
1039
+ if (!ts.isPropertyAccessExpression(routeAccess) ||
1040
+ routeAccess.name.text !== "route" ||
1041
+ !ts.isIdentifier(routeAccess.expression) ||
1042
+ routeAccess.expression.text !== "server") {
1043
+ return undefined;
1044
+ }
1045
+ const contract = routeCall.arguments[0];
1046
+ const unwrappedContract = contract
1047
+ ? unwrapContractExpression(contract)
1048
+ : undefined;
1049
+ return unwrappedContract && ts.isIdentifier(unwrappedContract)
1050
+ ? unwrappedContract.text
1051
+ : undefined;
1052
+ }
1053
+ function isServerApiExpression(expression) {
1054
+ const unwrapped = unwrapContractExpression(expression);
1055
+ return (ts.isPropertyAccessExpression(unwrapped) &&
1056
+ ts.isIdentifier(unwrapped.expression) &&
1057
+ unwrapped.expression.text === "server" &&
1058
+ unwrapped.name.text === "api");
1059
+ }
986
1060
  function catchAllRoutePrefix(routePath) {
987
1061
  const segments = routePath.split("/").filter(Boolean);
988
1062
  const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
@@ -1062,6 +1136,29 @@ function sourceFileFromImport(sourcePath, importerFile = "index.ts", files) {
1062
1136
  return undefined;
1063
1137
  }
1064
1138
  function contractFileFromImport(sourcePath, config, importerFile) {
1139
+ const resolveCandidate = (candidate) => {
1140
+ const extension = path.extname(candidate).toLowerCase();
1141
+ if (!extension)
1142
+ return `${candidate}.ts`;
1143
+ let sourceExtension;
1144
+ switch (extension) {
1145
+ case ".js":
1146
+ sourceExtension = ".ts";
1147
+ break;
1148
+ case ".jsx":
1149
+ sourceExtension = ".tsx";
1150
+ break;
1151
+ case ".mjs":
1152
+ sourceExtension = ".mts";
1153
+ break;
1154
+ case ".cjs":
1155
+ sourceExtension = ".cts";
1156
+ break;
1157
+ }
1158
+ return sourceExtension
1159
+ ? `${candidate.slice(0, -extension.length)}${sourceExtension}`
1160
+ : candidate;
1161
+ };
1065
1162
  const contractsPath = directoryPath(config.paths.contracts);
1066
1163
  const aliasPrefix = `@/${contractsPath}/`;
1067
1164
  const aliasExact = `@/${contractsPath}`;
@@ -1071,14 +1168,14 @@ function contractFileFromImport(sourcePath, config, importerFile) {
1071
1168
  return `${contractsPath}/index.ts`;
1072
1169
  }
1073
1170
  if (sourcePath.startsWith(aliasPrefix)) {
1074
- return `${sourcePath.slice("@/".length)}.ts`;
1171
+ return resolveCandidate(sourcePath.slice("@/".length));
1075
1172
  }
1076
1173
  if (sourcePath.startsWith(relativePrefix)) {
1077
- return `${sourcePath}.ts`;
1174
+ return resolveCandidate(sourcePath);
1078
1175
  }
1079
1176
  if (importerFile && sourcePath.startsWith(".")) {
1080
1177
  const resolved = normalizePath(path.join(path.dirname(importerFile), sourcePath));
1081
- return `${resolved}.ts`;
1178
+ return resolveCandidate(resolved);
1082
1179
  }
1083
1180
  return undefined;
1084
1181
  }
@@ -2691,6 +2788,14 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
2691
2788
  }
2692
2789
  }
2693
2790
  const infraDir = directoryPath(path.dirname(config.paths.portWiring));
2791
+ for (const file of drift.listeners.unsafeLifecycleFiles) {
2792
+ diagnostics.push({
2793
+ severity: "warning",
2794
+ code: "BEIGNET_LISTENER_LIFECYCLE_UNSAFE",
2795
+ file,
2796
+ 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().`,
2797
+ });
2798
+ }
2694
2799
  const listenerTarget = drift.listeners.wiringFile
2695
2800
  ? `${drift.listeners.wiringFile}, which already calls registerListeners(...)`
2696
2801
  : drift.listeners.eventBusFile
@@ -2804,7 +2909,7 @@ async function workflowRegistrationDrift(targetDir, files, config) {
2804
2909
  events: [],
2805
2910
  jobs: [],
2806
2911
  },
2807
- listeners: { unregistered: [] },
2912
+ listeners: { unregistered: [], unsafeLifecycleFiles: [] },
2808
2913
  };
2809
2914
  if (registries.length === 0)
2810
2915
  return drift;
@@ -2840,11 +2945,12 @@ async function workflowRegistrationDrift(targetDir, files, config) {
2840
2945
  }
2841
2946
  const listenerRegistries = byKind("listeners");
2842
2947
  if (listenerRegistries.length > 0) {
2843
- const wiring = await listenerWiringReferences(targetDir, files, config);
2948
+ const wiring = await listenerWiringReferences(targetDir, files, config, listenerRegistries);
2844
2949
  drift.listeners = {
2845
2950
  unregistered: unregisteredWorkflowRegistries(listenerRegistries, wiring.identifiers),
2846
2951
  wiringFile: wiring.wiringFile,
2847
2952
  eventBusFile: wiring.eventBusFile,
2953
+ unsafeLifecycleFiles: wiring.unsafeLifecycleFiles,
2848
2954
  };
2849
2955
  }
2850
2956
  return drift;
@@ -3083,8 +3189,9 @@ async function listenedEventNames(targetDir, files, config) {
3083
3189
  }
3084
3190
  return listened;
3085
3191
  }
3086
- async function listenerWiringReferences(targetDir, files, config) {
3192
+ async function listenerWiringReferences(targetDir, files, config, listenerRegistries) {
3087
3193
  const identifiers = new Set();
3194
+ const unsafeLifecycleFiles = new Set();
3088
3195
  let wiringFile;
3089
3196
  let eventBusFile;
3090
3197
  let centralListenerRegistryReferenced = false;
@@ -3096,29 +3203,43 @@ async function listenerWiringReferences(targetDir, files, config) {
3096
3203
  continue;
3097
3204
  const source = await readFile(path.join(targetDir, file), "utf8");
3098
3205
  const namedImports = parseNamedImportSources(source);
3099
- let foundCall = false;
3100
- for (const match of source.matchAll(/\bregisterListeners\s*\(/g)) {
3101
- const openParen = (match.index ?? 0) + match[0].length - 1;
3102
- const closeParen = matchingDelimiterIndex(source, openParen, "(", ")");
3103
- const argsText = closeParen === -1
3104
- ? source.slice(openParen)
3105
- : source.slice(openParen + 1, closeParen);
3106
- foundCall = true;
3206
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
3207
+ const registerListenersBindings = importedRegisterListenersBindings(source, namedImports);
3208
+ const calls = registerListenersCalls(sourceFile, registerListenersBindings);
3209
+ let foundRelevantCall = false;
3210
+ for (const call of calls) {
3211
+ const argsText = call.arguments
3212
+ .map((argument) => argument.getText(sourceFile))
3213
+ .join(",");
3107
3214
  const callIdentifiers = identifiersFromArrayExpression(argsText);
3108
- for (const identifier of callIdentifiers) {
3109
- identifiers.add(identifier);
3110
- }
3111
- if (callReferencesCentralListenerRegistry({
3215
+ const referencesCentral = callReferencesCentralListenerRegistry({
3112
3216
  callIdentifiers,
3113
3217
  namedImports,
3114
3218
  importerFile: file,
3115
3219
  listenerRegistryFile,
3116
3220
  files,
3117
- })) {
3221
+ });
3222
+ const referencesFeature = callReferencesFeatureListenerRegistry({
3223
+ callIdentifiers,
3224
+ namedImports,
3225
+ importerFile: file,
3226
+ files,
3227
+ listenerRegistries,
3228
+ });
3229
+ if (!referencesCentral && !referencesFeature)
3230
+ continue;
3231
+ foundRelevantCall = true;
3232
+ for (const identifier of callIdentifiers) {
3233
+ identifiers.add(identifier);
3234
+ }
3235
+ if (referencesCentral) {
3118
3236
  centralListenerRegistryReferenced = true;
3119
3237
  }
3238
+ if (!hasSafeListenerRegistrationLifecycle(call, sourceFile)) {
3239
+ unsafeLifecycleFiles.add(file);
3240
+ }
3120
3241
  }
3121
- if (foundCall) {
3242
+ if (foundRelevantCall) {
3122
3243
  wiringFile ??= file;
3123
3244
  }
3124
3245
  else if (!eventBusFile && /\bcreate\w*EventBus\s*\(/.test(source)) {
@@ -3135,7 +3256,256 @@ async function listenerWiringReferences(targetDir, files, config) {
3135
3256
  }
3136
3257
  }
3137
3258
  }
3138
- return { identifiers, wiringFile, eventBusFile };
3259
+ return {
3260
+ identifiers,
3261
+ wiringFile,
3262
+ eventBusFile,
3263
+ unsafeLifecycleFiles: [...unsafeLifecycleFiles].sort(),
3264
+ };
3265
+ }
3266
+ function registerListenersCalls(sourceFile, bindings) {
3267
+ const calls = [];
3268
+ const visit = (node) => {
3269
+ if (ts.isCallExpression(node) &&
3270
+ ((ts.isIdentifier(node.expression) &&
3271
+ bindings.has(node.expression.text)) ||
3272
+ (ts.isPropertyAccessExpression(node.expression) &&
3273
+ node.expression.name.text === "registerListeners" &&
3274
+ ts.isIdentifier(node.expression.expression) &&
3275
+ bindings.has(`${node.expression.expression.text}.*`)))) {
3276
+ calls.push(node);
3277
+ }
3278
+ ts.forEachChild(node, visit);
3279
+ };
3280
+ visit(sourceFile);
3281
+ return calls;
3282
+ }
3283
+ function importedRegisterListenersBindings(source, namedImports) {
3284
+ const bindings = new Set();
3285
+ for (const [localName, imported] of namedImports) {
3286
+ if (imported.importedName === "registerListeners" &&
3287
+ imported.sourcePath === "@beignet/core/events") {
3288
+ bindings.add(localName);
3289
+ }
3290
+ }
3291
+ const namespaceImport = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/core\/events["']/g;
3292
+ for (const match of source.matchAll(namespaceImport)) {
3293
+ bindings.add(`${match[1]}.*`);
3294
+ }
3295
+ return bindings;
3296
+ }
3297
+ function hasSafeListenerRegistrationLifecycle(call, sourceFile) {
3298
+ const startHook = enclosingListenerLifecycleHook(call, "start");
3299
+ const target = listenerRegistrationTarget(call, sourceFile);
3300
+ if (!startHook || !target)
3301
+ return false;
3302
+ const lifecycleOwner = listenerLifecycleOwner(startHook);
3303
+ if (!lifecycleOwner)
3304
+ return false;
3305
+ let observesReadiness = false;
3306
+ let observesCleanup = false;
3307
+ const visit = (node) => {
3308
+ if (!observesReadiness &&
3309
+ ts.isPropertyAccessExpression(node) &&
3310
+ node.name.text === "ready" &&
3311
+ listenerLifecycleTargetsMatch(listenerLifecycleTarget(node.expression, sourceFile), target) &&
3312
+ enclosingListenerLifecycleHook(node, "start") === startHook &&
3313
+ listenerLifecyclePromiseIsObserved(node, startHook)) {
3314
+ observesReadiness = true;
3315
+ }
3316
+ if (!observesCleanup &&
3317
+ ts.isCallExpression(node) &&
3318
+ ts.isPropertyAccessExpression(node.expression) &&
3319
+ node.expression.name.text === "unsubscribe" &&
3320
+ listenerLifecycleTargetsMatch(listenerLifecycleTarget(node.expression.expression, sourceFile), target)) {
3321
+ const stopHook = enclosingListenerLifecycleHook(node, "stop");
3322
+ if (stopHook &&
3323
+ listenerLifecycleOwner(stopHook) === lifecycleOwner &&
3324
+ listenerLifecyclePromiseIsObserved(node, stopHook)) {
3325
+ observesCleanup = true;
3326
+ }
3327
+ }
3328
+ if (!observesReadiness || !observesCleanup) {
3329
+ ts.forEachChild(node, visit);
3330
+ }
3331
+ };
3332
+ visit(lifecycleOwner);
3333
+ return observesReadiness && observesCleanup;
3334
+ }
3335
+ function listenerRegistrationTarget(call, sourceFile) {
3336
+ let expression = call;
3337
+ while (ts.isParenthesizedExpression(expression.parent) ||
3338
+ ts.isAsExpression(expression.parent) ||
3339
+ ts.isSatisfiesExpression(expression.parent) ||
3340
+ ts.isNonNullExpression(expression.parent)) {
3341
+ expression = expression.parent;
3342
+ }
3343
+ const parent = expression.parent;
3344
+ if (ts.isVariableDeclaration(parent) &&
3345
+ parent.initializer === expression &&
3346
+ ts.isIdentifier(parent.name)) {
3347
+ return { text: parent.name.text, binding: parent };
3348
+ }
3349
+ if (ts.isBinaryExpression(parent) &&
3350
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
3351
+ parent.right === expression) {
3352
+ return listenerLifecycleTarget(parent.left, sourceFile);
3353
+ }
3354
+ return undefined;
3355
+ }
3356
+ function listenerLifecycleTarget(expression, sourceFile) {
3357
+ let current = expression;
3358
+ while (ts.isParenthesizedExpression(current) ||
3359
+ ts.isAsExpression(current) ||
3360
+ ts.isSatisfiesExpression(current) ||
3361
+ ts.isNonNullExpression(current)) {
3362
+ current = current.expression;
3363
+ }
3364
+ if (!ts.isIdentifier(current) && !ts.isPropertyAccessExpression(current)) {
3365
+ return undefined;
3366
+ }
3367
+ return {
3368
+ text: current.getText(sourceFile),
3369
+ binding: listenerLifecycleRootIdentifier(current)
3370
+ ? resolveListenerLifecycleBinding(listenerLifecycleRootIdentifier(current))
3371
+ : undefined,
3372
+ };
3373
+ }
3374
+ function listenerLifecycleRootIdentifier(expression) {
3375
+ let current = expression;
3376
+ while (ts.isPropertyAccessExpression(current)) {
3377
+ current = current.expression;
3378
+ }
3379
+ return ts.isIdentifier(current) ? current : undefined;
3380
+ }
3381
+ function resolveListenerLifecycleBinding(identifier) {
3382
+ let current = identifier;
3383
+ while (current) {
3384
+ if (ts.isBlock(current) || ts.isSourceFile(current)) {
3385
+ for (const statement of current.statements) {
3386
+ if (!ts.isVariableStatement(statement))
3387
+ continue;
3388
+ for (const declaration of statement.declarationList.declarations) {
3389
+ if (ts.isIdentifier(declaration.name) &&
3390
+ declaration.name.text === identifier.text) {
3391
+ return declaration;
3392
+ }
3393
+ }
3394
+ }
3395
+ }
3396
+ if (ts.isFunctionLike(current)) {
3397
+ for (const parameter of current.parameters) {
3398
+ if (ts.isIdentifier(parameter.name) &&
3399
+ parameter.name.text === identifier.text) {
3400
+ return parameter;
3401
+ }
3402
+ }
3403
+ }
3404
+ if (ts.isCatchClause(current) &&
3405
+ current.variableDeclaration &&
3406
+ ts.isIdentifier(current.variableDeclaration.name) &&
3407
+ current.variableDeclaration.name.text === identifier.text) {
3408
+ return current.variableDeclaration;
3409
+ }
3410
+ current = current.parent;
3411
+ }
3412
+ return undefined;
3413
+ }
3414
+ function listenerLifecycleTargetsMatch(left, right) {
3415
+ return (left?.text === right.text &&
3416
+ (left.binding !== undefined || right.binding !== undefined
3417
+ ? left.binding === right.binding
3418
+ : true));
3419
+ }
3420
+ function enclosingListenerLifecycleHook(node, hookName) {
3421
+ let current = node.parent;
3422
+ while (current) {
3423
+ if (ts.isMethodDeclaration(current)) {
3424
+ return staticPropertyName(current.name) === hookName
3425
+ ? current
3426
+ : undefined;
3427
+ }
3428
+ if (ts.isFunctionExpression(current) || ts.isArrowFunction(current)) {
3429
+ const parent = current.parent;
3430
+ return ts.isPropertyAssignment(parent) &&
3431
+ staticPropertyName(parent.name) === hookName
3432
+ ? current
3433
+ : undefined;
3434
+ }
3435
+ if (ts.isFunctionDeclaration(current))
3436
+ return undefined;
3437
+ current = current.parent;
3438
+ }
3439
+ return undefined;
3440
+ }
3441
+ function listenerLifecycleOwner(hook) {
3442
+ if (ts.isMethodDeclaration(hook)) {
3443
+ return ts.isObjectLiteralExpression(hook.parent) ? hook.parent : undefined;
3444
+ }
3445
+ const property = hook.parent;
3446
+ return ts.isPropertyAssignment(property) &&
3447
+ ts.isObjectLiteralExpression(property.parent)
3448
+ ? property.parent
3449
+ : undefined;
3450
+ }
3451
+ function listenerLifecyclePromiseIsObserved(node, hook) {
3452
+ let current = node;
3453
+ while (current && current !== hook) {
3454
+ const parent = current.parent;
3455
+ if ((ts.isAwaitExpression(parent) && parent.expression === current) ||
3456
+ (ts.isReturnStatement(parent) && parent.expression === current)) {
3457
+ return true;
3458
+ }
3459
+ if (ts.isParenthesizedExpression(parent) ||
3460
+ ts.isAsExpression(parent) ||
3461
+ ts.isSatisfiesExpression(parent) ||
3462
+ ts.isNonNullExpression(parent)) {
3463
+ current = parent;
3464
+ continue;
3465
+ }
3466
+ if (ts.isPropertyAccessExpression(parent) &&
3467
+ parent.expression === current) {
3468
+ const method = parent.name.text;
3469
+ const invocation = parent.parent;
3470
+ if (!["then", "catch", "finally"].includes(method) ||
3471
+ !ts.isCallExpression(invocation) ||
3472
+ invocation.expression !== parent ||
3473
+ method === "catch" ||
3474
+ (method === "then" && invocation.arguments.length > 1)) {
3475
+ return false;
3476
+ }
3477
+ current = invocation;
3478
+ continue;
3479
+ }
3480
+ return false;
3481
+ }
3482
+ return false;
3483
+ }
3484
+ function callReferencesFeatureListenerRegistry(args) {
3485
+ for (const identifier of args.callIdentifiers) {
3486
+ for (const registry of args.listenerRegistries) {
3487
+ if (identifier === registry.registryName ||
3488
+ registry.members.includes(identifier)) {
3489
+ return true;
3490
+ }
3491
+ }
3492
+ const imported = args.namedImports.get(identifier);
3493
+ if (!imported)
3494
+ continue;
3495
+ const importedFile = sourceFileFromImport(imported.sourcePath, args.importerFile, args.files);
3496
+ if (!importedFile)
3497
+ continue;
3498
+ for (const registry of args.listenerRegistries) {
3499
+ if ((imported.importedName === registry.registryName &&
3500
+ importedFile === registry.indexFile) ||
3501
+ (registry.members.includes(imported.importedName) &&
3502
+ (importedFile === registry.indexFile ||
3503
+ registry.memberFiles.get(imported.importedName) === importedFile))) {
3504
+ return true;
3505
+ }
3506
+ }
3507
+ }
3508
+ return false;
3139
3509
  }
3140
3510
  function callReferencesCentralListenerRegistry(args) {
3141
3511
  if (args.importerFile === args.listenerRegistryFile &&