@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.
- package/CHANGELOG.md +32 -0
- package/README.md +54 -5
- package/dist/analysis/workspace.d.ts +1 -0
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +20 -10
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts +103 -0
- package/dist/app-map-changes.d.ts.map +1 -0
- package/dist/app-map-changes.js +949 -0
- package/dist/app-map-changes.js.map +1 -0
- package/dist/git-changes.d.ts +30 -0
- package/dist/git-changes.d.ts.map +1 -0
- package/dist/git-changes.js +367 -0
- package/dist/git-changes.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -4
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +413 -43
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +31 -27
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +0 -2
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +46 -6
- package/dist/mcp.js.map +1 -1
- package/dist/operational-process.d.ts +1 -0
- package/dist/operational-process.d.ts.map +1 -1
- package/dist/operational-process.js.map +1 -1
- package/dist/operational-runner.js +1 -0
- package/dist/operational-runner.js.map +1 -1
- package/dist/outbox.d.ts +1 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +58 -12
- package/dist/outbox.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +28 -3
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +4 -1
- package/dist/templates/base.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +30 -5
- package/src/analysis/workspace.ts +25 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/git-changes.ts +511 -0
- package/src/index.ts +84 -4
- package/src/inspect.ts +586 -51
- package/src/lib.ts +21 -0
- package/src/make/shared.ts +31 -27
- package/src/make.ts +0 -2
- package/src/mcp.ts +65 -12
- package/src/operational-process.ts +1 -0
- package/src/operational-runner.ts +1 -0
- package/src/outbox.ts +63 -12
- package/src/templates/agents.ts +28 -3
- 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
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
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 (
|
|
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
|
|
1171
|
+
return resolveCandidate(sourcePath.slice("@/".length));
|
|
1075
1172
|
}
|
|
1076
1173
|
if (sourcePath.startsWith(relativePrefix)) {
|
|
1077
|
-
return
|
|
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
|
|
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
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
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
|
-
|
|
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 (
|
|
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 {
|
|
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 &&
|