@beignet/cli 0.0.48 → 0.0.50
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 +54 -0
- package/README.md +117 -22
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +11 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +78 -8
- 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/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.js +37 -3
- package/dist/check.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- 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.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +706 -186
- 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/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +11 -4
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +2 -2
- 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 +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +43 -21
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +142 -7
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +37 -1
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +13 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +3 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +71 -7
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +134 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/git-changes.ts +511 -0
- package/src/index.ts +39 -1
- package/src/inspect.ts +1005 -238
- package/src/lib.ts +21 -0
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make/shared.ts +31 -27
- package/src/make.ts +225 -8
- package/src/mcp.ts +51 -24
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +37 -1
- package/src/templates/base.ts +13 -2
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +3 -2
package/src/inspect.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import {
|
|
6
|
+
type ContractFactoryBindings,
|
|
7
|
+
type ContractFactoryName,
|
|
8
|
+
contractFactoryBindings,
|
|
9
|
+
isImportedContractFactoryCall,
|
|
10
|
+
} from "./analysis/contract-factories.js";
|
|
11
|
+
import { canonicalFeatureLayerPath } from "./analysis/layers.js";
|
|
12
|
+
import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
|
|
13
|
+
import { exportedVariableDeclarations } from "./analysis/source-index.js";
|
|
4
14
|
import { createPainter } from "./ansi.js";
|
|
5
15
|
import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
|
|
6
16
|
import {
|
|
@@ -66,7 +76,8 @@ import {
|
|
|
66
76
|
identifiersFromArrayExpression,
|
|
67
77
|
insertAfterImports,
|
|
68
78
|
matchingDelimiterIndex,
|
|
69
|
-
|
|
79
|
+
outboxRegistryEntryState,
|
|
80
|
+
outboxRegistryIdentifiers,
|
|
70
81
|
} from "./registry-edits.js";
|
|
71
82
|
import {
|
|
72
83
|
currentGeneratedPackageScripts,
|
|
@@ -221,6 +232,16 @@ type FeatureRouteGroup = {
|
|
|
221
232
|
}>;
|
|
222
233
|
};
|
|
223
234
|
|
|
235
|
+
type ReadContractsResult = {
|
|
236
|
+
contracts: InspectedContract[];
|
|
237
|
+
queryTransportDiagnostics: InspectDiagnostic[];
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
type ParsedContract = {
|
|
241
|
+
contract: InspectedContract;
|
|
242
|
+
queryTransportMissing: boolean;
|
|
243
|
+
};
|
|
244
|
+
|
|
224
245
|
export async function inspectApp(
|
|
225
246
|
options: InspectAppOptions = {},
|
|
226
247
|
): Promise<InspectAppResult> {
|
|
@@ -232,7 +253,8 @@ export async function inspectApp(
|
|
|
232
253
|
? resolveConfig(options.config)
|
|
233
254
|
: await loadBeignetConfig(targetDir, files);
|
|
234
255
|
const convention = inspectConvention(files, config);
|
|
235
|
-
const
|
|
256
|
+
const inspectedContracts = await readContracts(targetDir, files, config);
|
|
257
|
+
const { contracts } = inspectedContracts;
|
|
236
258
|
const matchedRoutes = await inspectRouteSurface(
|
|
237
259
|
targetDir,
|
|
238
260
|
files,
|
|
@@ -247,6 +269,7 @@ export async function inspectApp(
|
|
|
247
269
|
contracts,
|
|
248
270
|
matchedRoutes,
|
|
249
271
|
Boolean(options.strict),
|
|
272
|
+
inspectedContracts.queryTransportDiagnostics,
|
|
250
273
|
);
|
|
251
274
|
|
|
252
275
|
return {
|
|
@@ -330,7 +353,7 @@ async function buildDoctorFixPlan(
|
|
|
330
353
|
? resolveConfig(options.config)
|
|
331
354
|
: await loadBeignetConfig(targetDir, files);
|
|
332
355
|
const convention = inspectConvention(files, config);
|
|
333
|
-
const contracts = await readContracts(targetDir, files, config);
|
|
356
|
+
const { contracts } = await readContracts(targetDir, files, config);
|
|
334
357
|
const matchedRoutes = await inspectRouteSurface(
|
|
335
358
|
targetDir,
|
|
336
359
|
files,
|
|
@@ -711,7 +734,6 @@ export const productionHardeningDiagnosticCodes = new Set([
|
|
|
711
734
|
"BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
|
|
712
735
|
"BEIGNET_CORS_CREDENTIALS_WILDCARD",
|
|
713
736
|
"BEIGNET_CRON_SECRET_MISSING",
|
|
714
|
-
"BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
|
|
715
737
|
"BEIGNET_ENTITLEMENTS_PORT_MISSING",
|
|
716
738
|
"BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
|
|
717
739
|
"BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
|
|
@@ -830,16 +852,32 @@ async function readContracts(
|
|
|
830
852
|
targetDir: string,
|
|
831
853
|
files: string[],
|
|
832
854
|
config: ResolvedBeignetConfig,
|
|
833
|
-
): Promise<
|
|
855
|
+
): Promise<ReadContractsResult> {
|
|
834
856
|
const contracts: InspectedContract[] = [];
|
|
857
|
+
const queryTransportDiagnostics: InspectDiagnostic[] = [];
|
|
835
858
|
const contractFiles = contractSourceFiles(files, config);
|
|
836
859
|
|
|
837
860
|
for (const file of contractFiles) {
|
|
838
861
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
839
|
-
|
|
862
|
+
for (const parsed of parseContracts(source, file)) {
|
|
863
|
+
contracts.push(parsed.contract);
|
|
864
|
+
if (!parsed.queryTransportMissing) continue;
|
|
865
|
+
|
|
866
|
+
queryTransportDiagnostics.push({
|
|
867
|
+
severity: "error",
|
|
868
|
+
code: "BEIGNET_QUERY_TRANSPORT_MISSING",
|
|
869
|
+
file,
|
|
870
|
+
contract: parsed.contract.exportName,
|
|
871
|
+
subject: { file, exportName: parsed.contract.exportName },
|
|
872
|
+
message: `${parsed.contract.exportName} declares a query schema without an explicit query transport. Pass defineQueryTransport(...) as the second .query(...) argument so the client, server, and OpenAPI document share one wire format.`,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
840
875
|
}
|
|
841
876
|
|
|
842
|
-
return
|
|
877
|
+
return {
|
|
878
|
+
contracts: contracts.sort(compareContracts),
|
|
879
|
+
queryTransportDiagnostics,
|
|
880
|
+
};
|
|
843
881
|
}
|
|
844
882
|
|
|
845
883
|
function inspectConvention(
|
|
@@ -901,68 +939,126 @@ function hasRequirement(files: string[], requirement: string): boolean {
|
|
|
901
939
|
return files.includes(requirement);
|
|
902
940
|
}
|
|
903
941
|
|
|
904
|
-
function parseContracts(source: string, file: string):
|
|
905
|
-
const contracts:
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
942
|
+
function parseContracts(source: string, file: string): ParsedContract[] {
|
|
943
|
+
const contracts: ParsedContract[] = [];
|
|
944
|
+
const sourceFile = ts.createSourceFile(
|
|
945
|
+
file,
|
|
946
|
+
source,
|
|
947
|
+
ts.ScriptTarget.Latest,
|
|
948
|
+
true,
|
|
949
|
+
ts.ScriptKind.TS,
|
|
950
|
+
);
|
|
951
|
+
const assignments = variableAssignments(sourceFile);
|
|
952
|
+
const assignmentsByName = new Map(
|
|
953
|
+
assignments.map((assignment) => [assignment.name, assignment.expression]),
|
|
954
|
+
);
|
|
955
|
+
const factoryBindings = contractFactoryBindings(sourceFile);
|
|
910
956
|
|
|
911
|
-
for (const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const routePath = joinContractPaths(pathPrefix, methodMatch[3]);
|
|
957
|
+
for (const { exportName, declaration } of exportedVariableDeclarations(
|
|
958
|
+
sourceFile,
|
|
959
|
+
)) {
|
|
960
|
+
if (!declaration.initializer) continue;
|
|
961
|
+
const chain = resolveContractCallChain(
|
|
962
|
+
declaration.initializer,
|
|
963
|
+
assignmentsByName,
|
|
964
|
+
);
|
|
965
|
+
if (!chain) continue;
|
|
921
966
|
|
|
967
|
+
const directContract = parseDirectContract(chain, factoryBindings);
|
|
968
|
+
if (directContract) {
|
|
922
969
|
contracts.push({
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
970
|
+
contract: {
|
|
971
|
+
exportName,
|
|
972
|
+
file,
|
|
973
|
+
metadata: parseContractMetadata(
|
|
974
|
+
applyContractMetadataCalls(
|
|
975
|
+
emptyContractMetadataState(),
|
|
976
|
+
chain.calls,
|
|
977
|
+
),
|
|
978
|
+
),
|
|
979
|
+
method: directContract.method,
|
|
980
|
+
path: normalizeContractPath(directContract.path),
|
|
981
|
+
},
|
|
982
|
+
queryTransportMissing: contractQueryHasTransport(chain) === false,
|
|
931
983
|
});
|
|
932
984
|
continue;
|
|
933
985
|
}
|
|
934
986
|
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
987
|
+
const methodIndex = chain.calls.findIndex((call) =>
|
|
988
|
+
isHttpMethod(call.name.toUpperCase()),
|
|
989
|
+
);
|
|
990
|
+
if (methodIndex === -1) continue;
|
|
991
|
+
const methodCall = chain.calls[methodIndex];
|
|
992
|
+
const routePath = staticString(methodCall.arguments[0]);
|
|
993
|
+
if (!routePath) continue;
|
|
994
|
+
|
|
995
|
+
const hasGroupFactory = contractFactoryCall(
|
|
996
|
+
chain,
|
|
997
|
+
"defineContractGroup",
|
|
998
|
+
factoryBindings,
|
|
999
|
+
);
|
|
1000
|
+
if (!hasGroupFactory) continue;
|
|
1001
|
+
|
|
1002
|
+
const pathPrefix = applyStaticPrefixes(
|
|
1003
|
+
"",
|
|
1004
|
+
chain.calls.slice(0, methodIndex),
|
|
1005
|
+
);
|
|
1006
|
+
if (pathPrefix === undefined) continue;
|
|
1007
|
+
|
|
1008
|
+
contracts.push({
|
|
1009
|
+
contract: {
|
|
1010
|
+
exportName,
|
|
939
1011
|
file,
|
|
940
|
-
metadata: parseContractMetadata(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1012
|
+
metadata: parseContractMetadata(
|
|
1013
|
+
applyContractMetadataCalls(emptyContractMetadataState(), chain.calls),
|
|
1014
|
+
),
|
|
1015
|
+
method: methodCall.name.toUpperCase() as HttpMethod,
|
|
1016
|
+
path: joinContractPaths(pathPrefix, routePath),
|
|
1017
|
+
},
|
|
1018
|
+
queryTransportMissing: contractQueryHasTransport(chain) === false,
|
|
1019
|
+
});
|
|
945
1020
|
}
|
|
946
1021
|
|
|
947
1022
|
return contracts;
|
|
948
1023
|
}
|
|
949
1024
|
|
|
1025
|
+
function unwrapContractExpression(expression: ts.Expression): ts.Expression {
|
|
1026
|
+
let current = expression;
|
|
1027
|
+
while (
|
|
1028
|
+
ts.isParenthesizedExpression(current) ||
|
|
1029
|
+
ts.isAsExpression(current) ||
|
|
1030
|
+
ts.isTypeAssertionExpression(current) ||
|
|
1031
|
+
ts.isNonNullExpression(current) ||
|
|
1032
|
+
ts.isSatisfiesExpression(current)
|
|
1033
|
+
) {
|
|
1034
|
+
current = current.expression;
|
|
1035
|
+
}
|
|
1036
|
+
return current;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
type ContractMetadataState = {
|
|
1040
|
+
authRequired?: boolean;
|
|
1041
|
+
authorizationAbility?: string;
|
|
1042
|
+
authorizationAuditRequired?: boolean;
|
|
1043
|
+
audit?: boolean;
|
|
1044
|
+
auditRequired?: boolean;
|
|
1045
|
+
catalogErrors: InspectedCatalogErrorRef[];
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
function emptyContractMetadataState(): ContractMetadataState {
|
|
1049
|
+
return { catalogErrors: [] };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
950
1052
|
function parseContractMetadata(
|
|
951
|
-
|
|
952
|
-
inheritedCatalogErrors: InspectedCatalogErrorRef[] = [],
|
|
1053
|
+
state: ContractMetadataState,
|
|
953
1054
|
): InspectedContractMetadata | undefined {
|
|
954
|
-
const authRequired =
|
|
955
|
-
const authorizationAbility =
|
|
956
|
-
/\bauthorization\s*:\s*\{[\s\S]*?\bability\s*:\s*["']([^"']+)["']/.exec(
|
|
957
|
-
block,
|
|
958
|
-
)?.[1];
|
|
1055
|
+
const authRequired = state.authRequired === true;
|
|
1056
|
+
const authorizationAbility = state.authorizationAbility;
|
|
959
1057
|
const auditRequired =
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
...parseCatalogErrorRefs(block),
|
|
965
|
-
]);
|
|
1058
|
+
state.audit === true ||
|
|
1059
|
+
state.auditRequired === true ||
|
|
1060
|
+
state.authorizationAuditRequired === true;
|
|
1061
|
+
const catalogErrors = state.catalogErrors;
|
|
966
1062
|
|
|
967
1063
|
if (
|
|
968
1064
|
!authRequired &&
|
|
@@ -981,21 +1077,129 @@ function parseContractMetadata(
|
|
|
981
1077
|
};
|
|
982
1078
|
}
|
|
983
1079
|
|
|
984
|
-
function
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1080
|
+
function applyContractMetadataCalls(
|
|
1081
|
+
initialState: ContractMetadataState,
|
|
1082
|
+
calls: ContractCall[],
|
|
1083
|
+
): ContractMetadataState {
|
|
1084
|
+
const state: ContractMetadataState = {
|
|
1085
|
+
...initialState,
|
|
1086
|
+
catalogErrors: [...initialState.catalogErrors],
|
|
1087
|
+
};
|
|
989
1088
|
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1089
|
+
for (const call of calls) {
|
|
1090
|
+
if (call.name === "meta") {
|
|
1091
|
+
applyContractMetaObject(state, objectLiteral(call.arguments[0]));
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (call.name === "errors") {
|
|
1095
|
+
state.catalogErrors = applyCatalogErrorCall(
|
|
1096
|
+
state.catalogErrors,
|
|
1097
|
+
call.arguments[0],
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
994
1101
|
|
|
995
|
-
|
|
996
|
-
|
|
1102
|
+
return state;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function applyContractMetaObject(
|
|
1106
|
+
state: ContractMetadataState,
|
|
1107
|
+
metadata: ts.ObjectLiteralExpression | undefined,
|
|
1108
|
+
): void {
|
|
1109
|
+
if (!metadata) {
|
|
1110
|
+
state.authRequired = undefined;
|
|
1111
|
+
state.authorizationAbility = undefined;
|
|
1112
|
+
state.authorizationAuditRequired = undefined;
|
|
1113
|
+
state.audit = undefined;
|
|
1114
|
+
state.auditRequired = undefined;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
997
1117
|
|
|
998
|
-
|
|
1118
|
+
const auth = objectPropertyResult(metadata, "auth");
|
|
1119
|
+
if (auth.kind !== "absent") {
|
|
1120
|
+
state.authRequired =
|
|
1121
|
+
auth.kind === "value" ? staticRequiredValue(auth.value) : undefined;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const authorization = objectPropertyResult(metadata, "authorization");
|
|
1125
|
+
if (authorization.kind !== "absent") {
|
|
1126
|
+
const value =
|
|
1127
|
+
authorization.kind === "value"
|
|
1128
|
+
? objectLiteral(authorization.value)
|
|
1129
|
+
: undefined;
|
|
1130
|
+
if (!value) {
|
|
1131
|
+
state.authorizationAbility = undefined;
|
|
1132
|
+
state.authorizationAuditRequired = undefined;
|
|
1133
|
+
} else {
|
|
1134
|
+
const ability = objectPropertyResult(value, "ability");
|
|
1135
|
+
state.authorizationAbility =
|
|
1136
|
+
ability.kind === "value" ? staticString(ability.value) : undefined;
|
|
1137
|
+
state.authorizationAuditRequired = objectRequiresAudit(value);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const audit = objectPropertyResult(metadata, "audit");
|
|
1142
|
+
const auditRequired = objectPropertyResult(metadata, "auditRequired");
|
|
1143
|
+
if (audit.kind !== "absent") {
|
|
1144
|
+
state.audit =
|
|
1145
|
+
audit.kind === "value" ? staticRequiredValue(audit.value) : undefined;
|
|
1146
|
+
}
|
|
1147
|
+
if (auditRequired.kind !== "absent") {
|
|
1148
|
+
state.auditRequired =
|
|
1149
|
+
auditRequired.kind === "value"
|
|
1150
|
+
? staticBoolean(auditRequired.value)
|
|
1151
|
+
: undefined;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function objectRequiresAudit(
|
|
1156
|
+
metadata: ts.ObjectLiteralExpression,
|
|
1157
|
+
): boolean | undefined {
|
|
1158
|
+
const audit = objectPropertyResult(metadata, "audit");
|
|
1159
|
+
const auditRequired = objectPropertyResult(metadata, "auditRequired");
|
|
1160
|
+
if (audit.kind === "unknown" || auditRequired.kind === "unknown") {
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}
|
|
1163
|
+
const auditValue =
|
|
1164
|
+
audit.kind === "value" ? staticRequiredValue(audit.value) : false;
|
|
1165
|
+
const auditRequiredValue =
|
|
1166
|
+
auditRequired.kind === "value" ? staticBoolean(auditRequired.value) : false;
|
|
1167
|
+
if (auditValue === undefined || auditRequiredValue === undefined) {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
return auditValue || auditRequiredValue;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function staticRequiredValue(expression: ts.Expression): boolean | undefined {
|
|
1174
|
+
const value = staticString(expression);
|
|
1175
|
+
return value === undefined ? undefined : value === "required";
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function staticBoolean(expression: ts.Expression): boolean | undefined {
|
|
1179
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
1180
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function parseDirectContract(
|
|
1185
|
+
chain: ContractCallChain,
|
|
1186
|
+
factoryBindings: ContractFactoryBindings,
|
|
1187
|
+
): { method: HttpMethod; path: string } | undefined {
|
|
1188
|
+
const factoryCall = contractFactoryCall(
|
|
1189
|
+
chain,
|
|
1190
|
+
"defineContract",
|
|
1191
|
+
factoryBindings,
|
|
1192
|
+
);
|
|
1193
|
+
if (!factoryCall) return undefined;
|
|
1194
|
+
const options = objectLiteral(factoryCall.arguments[0]);
|
|
1195
|
+
if (!options) return undefined;
|
|
1196
|
+
const methodValue = staticString(objectProperty(options, "method"));
|
|
1197
|
+
const contractPath = staticString(objectProperty(options, "path"));
|
|
1198
|
+
if (!methodValue || !contractPath) return undefined;
|
|
1199
|
+
|
|
1200
|
+
const method = methodValue.toUpperCase();
|
|
1201
|
+
if (!isHttpMethod(method)) return undefined;
|
|
1202
|
+
return { method, path: contractPath };
|
|
999
1203
|
}
|
|
1000
1204
|
|
|
1001
1205
|
function isHttpMethod(method: string): method is HttpMethod {
|
|
@@ -1004,118 +1208,245 @@ function isHttpMethod(method: string): method is HttpMethod {
|
|
|
1004
1208
|
);
|
|
1005
1209
|
}
|
|
1006
1210
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
),
|
|
1012
|
-
].map((match) => ({
|
|
1013
|
-
name: match[1],
|
|
1014
|
-
expression: match[2],
|
|
1015
|
-
}));
|
|
1016
|
-
const prefixes = new Map<string, string>();
|
|
1211
|
+
type VariableAssignment = {
|
|
1212
|
+
name: string;
|
|
1213
|
+
expression: ts.Expression;
|
|
1214
|
+
};
|
|
1017
1215
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1216
|
+
type ContractCall = {
|
|
1217
|
+
name: string;
|
|
1218
|
+
arguments: readonly ts.Expression[];
|
|
1219
|
+
node: ts.CallExpression;
|
|
1220
|
+
};
|
|
1023
1221
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1222
|
+
type ContractCallChain = {
|
|
1223
|
+
baseIdentifier?: string;
|
|
1224
|
+
rootCall?: ContractCall;
|
|
1225
|
+
calls: ContractCall[];
|
|
1226
|
+
};
|
|
1026
1227
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1228
|
+
function variableAssignments(sourceFile: ts.SourceFile): VariableAssignment[] {
|
|
1229
|
+
return sourceFile.statements
|
|
1230
|
+
.filter(ts.isVariableStatement)
|
|
1231
|
+
.flatMap((statement) =>
|
|
1232
|
+
[...statement.declarationList.declarations]
|
|
1233
|
+
.filter(
|
|
1234
|
+
(
|
|
1235
|
+
declaration,
|
|
1236
|
+
): declaration is ts.VariableDeclaration & {
|
|
1237
|
+
name: ts.Identifier;
|
|
1238
|
+
initializer: ts.Expression;
|
|
1239
|
+
} =>
|
|
1240
|
+
ts.isIdentifier(declaration.name) &&
|
|
1241
|
+
declaration.initializer !== undefined,
|
|
1242
|
+
)
|
|
1243
|
+
.map((declaration) => ({
|
|
1244
|
+
name: declaration.name.text,
|
|
1245
|
+
expression: declaration.initializer,
|
|
1246
|
+
})),
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function resolveContractCallChain(
|
|
1251
|
+
expression: ts.Expression,
|
|
1252
|
+
assignments: ReadonlyMap<string, ts.Expression>,
|
|
1253
|
+
resolving = new Set<string>(),
|
|
1254
|
+
): ContractCallChain | undefined {
|
|
1255
|
+
const chain = contractCallChain(expression);
|
|
1256
|
+
if (!chain || chain.rootCall || !chain.baseIdentifier) return chain;
|
|
1257
|
+
|
|
1258
|
+
const baseExpression = assignments.get(chain.baseIdentifier);
|
|
1259
|
+
if (!baseExpression || resolving.has(chain.baseIdentifier)) return chain;
|
|
1260
|
+
|
|
1261
|
+
const nextResolving = new Set(resolving);
|
|
1262
|
+
nextResolving.add(chain.baseIdentifier);
|
|
1263
|
+
const baseChain = resolveContractCallChain(
|
|
1264
|
+
baseExpression,
|
|
1265
|
+
assignments,
|
|
1266
|
+
nextResolving,
|
|
1267
|
+
);
|
|
1268
|
+
if (!baseChain) return chain;
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
...baseChain,
|
|
1272
|
+
calls: [...baseChain.calls, ...chain.calls],
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function contractCallChain(
|
|
1277
|
+
rawExpression: ts.Expression,
|
|
1278
|
+
): ContractCallChain | undefined {
|
|
1279
|
+
const calls: ContractCall[] = [];
|
|
1280
|
+
let expression = rawExpression;
|
|
1281
|
+
|
|
1282
|
+
while (true) {
|
|
1283
|
+
expression = unwrapContractExpression(expression);
|
|
1284
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1285
|
+
expression = expression.expression;
|
|
1286
|
+
continue;
|
|
1033
1287
|
}
|
|
1288
|
+
if (ts.isCallExpression(expression)) {
|
|
1289
|
+
const callee = unwrapContractExpression(expression.expression);
|
|
1290
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
1291
|
+
calls.unshift({
|
|
1292
|
+
name: callee.name.text,
|
|
1293
|
+
arguments: [...expression.arguments],
|
|
1294
|
+
node: expression,
|
|
1295
|
+
});
|
|
1296
|
+
expression = callee.expression;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (ts.isIdentifier(callee)) {
|
|
1300
|
+
return {
|
|
1301
|
+
baseIdentifier: callee.text,
|
|
1302
|
+
rootCall: {
|
|
1303
|
+
name: callee.text,
|
|
1304
|
+
arguments: [...expression.arguments],
|
|
1305
|
+
node: expression,
|
|
1306
|
+
},
|
|
1307
|
+
calls,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
return undefined;
|
|
1311
|
+
}
|
|
1312
|
+
if (ts.isIdentifier(expression)) {
|
|
1313
|
+
return { baseIdentifier: expression.text, calls };
|
|
1314
|
+
}
|
|
1315
|
+
return undefined;
|
|
1034
1316
|
}
|
|
1317
|
+
}
|
|
1035
1318
|
|
|
1036
|
-
|
|
1319
|
+
function contractFactoryCall(
|
|
1320
|
+
chain: ContractCallChain,
|
|
1321
|
+
name: ContractFactoryName,
|
|
1322
|
+
factoryBindings: ContractFactoryBindings,
|
|
1323
|
+
): ContractCall | undefined {
|
|
1324
|
+
const calls = chain.rootCall ? [chain.rootCall] : chain.calls;
|
|
1325
|
+
return calls.find((call) =>
|
|
1326
|
+
isImportedContractFactoryCall(call.node, name, factoryBindings),
|
|
1327
|
+
);
|
|
1037
1328
|
}
|
|
1038
1329
|
|
|
1039
|
-
function
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}));
|
|
1050
|
-
const groupErrors = new Map<string, InspectedCatalogErrorRef[]>();
|
|
1051
|
-
|
|
1052
|
-
let changed = true;
|
|
1053
|
-
while (changed) {
|
|
1054
|
-
changed = false;
|
|
1055
|
-
for (const assignment of assignments) {
|
|
1056
|
-
if (groupErrors.has(assignment.name)) continue;
|
|
1057
|
-
|
|
1058
|
-
const baseErrors = baseGroupErrors(assignment.expression, groupErrors);
|
|
1059
|
-
if (baseErrors === undefined) continue;
|
|
1060
|
-
|
|
1061
|
-
groupErrors.set(
|
|
1062
|
-
assignment.name,
|
|
1063
|
-
dedupeCatalogErrorRefs([
|
|
1064
|
-
...baseErrors,
|
|
1065
|
-
...parseCatalogErrorRefs(assignment.expression),
|
|
1066
|
-
]),
|
|
1067
|
-
);
|
|
1068
|
-
changed = true;
|
|
1069
|
-
}
|
|
1330
|
+
function applyStaticPrefixes(
|
|
1331
|
+
initialPrefix: string,
|
|
1332
|
+
calls: ContractCall[],
|
|
1333
|
+
): string | undefined {
|
|
1334
|
+
let prefix = initialPrefix;
|
|
1335
|
+
for (const call of calls) {
|
|
1336
|
+
if (call.name !== "prefix") continue;
|
|
1337
|
+
const next = staticString(call.arguments[0]);
|
|
1338
|
+
if (next === undefined) return undefined;
|
|
1339
|
+
prefix = joinContractPaths(prefix, next);
|
|
1070
1340
|
}
|
|
1341
|
+
return prefix;
|
|
1342
|
+
}
|
|
1071
1343
|
|
|
1072
|
-
|
|
1344
|
+
function contractQueryHasTransport(
|
|
1345
|
+
chain: ContractCallChain,
|
|
1346
|
+
): boolean | undefined {
|
|
1347
|
+
const queryCall = chain.calls.find((call) => call.name === "query");
|
|
1348
|
+
return queryCall ? queryCall.arguments.length >= 2 : undefined;
|
|
1073
1349
|
}
|
|
1074
1350
|
|
|
1075
|
-
function
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
): InspectedCatalogErrorRef[]
|
|
1079
|
-
|
|
1351
|
+
function applyCatalogErrorCall(
|
|
1352
|
+
current: InspectedCatalogErrorRef[],
|
|
1353
|
+
expression: ts.Expression | undefined,
|
|
1354
|
+
): InspectedCatalogErrorRef[] {
|
|
1355
|
+
const errors = objectLiteral(expression);
|
|
1356
|
+
if (!errors) return [];
|
|
1357
|
+
let merged = [...current];
|
|
1080
1358
|
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1359
|
+
for (const property of errors.properties) {
|
|
1360
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1361
|
+
merged = [];
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const key = staticPropertyName(property.name);
|
|
1365
|
+
if (!key) {
|
|
1366
|
+
merged = [];
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1083
1369
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1370
|
+
merged = merged.filter((ref) => ref.key !== key);
|
|
1371
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
1372
|
+
const value = unwrapContractExpression(property.initializer);
|
|
1373
|
+
if (
|
|
1374
|
+
!ts.isPropertyAccessExpression(value) ||
|
|
1375
|
+
!ts.isIdentifier(value.expression)
|
|
1376
|
+
) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
merged.push({
|
|
1380
|
+
key,
|
|
1381
|
+
catalog: value.expression.text,
|
|
1382
|
+
catalogKey: value.name.text,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1086
1385
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
prefixes: Map<string, string>,
|
|
1090
|
-
): string | undefined {
|
|
1091
|
-
if (expression.includes("defineContractGroup(")) return "";
|
|
1386
|
+
return merged;
|
|
1387
|
+
}
|
|
1092
1388
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1389
|
+
function objectLiteral(
|
|
1390
|
+
expression: ts.Expression | undefined,
|
|
1391
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
1392
|
+
if (!expression) return undefined;
|
|
1393
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1394
|
+
return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : undefined;
|
|
1395
|
+
}
|
|
1095
1396
|
|
|
1096
|
-
|
|
1397
|
+
function objectProperty(
|
|
1398
|
+
object: ts.ObjectLiteralExpression,
|
|
1399
|
+
name: string,
|
|
1400
|
+
): ts.Expression | undefined {
|
|
1401
|
+
const result = objectPropertyResult(object, name);
|
|
1402
|
+
return result.kind === "value" ? result.value : undefined;
|
|
1097
1403
|
}
|
|
1098
1404
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1405
|
+
type ObjectPropertyResult =
|
|
1406
|
+
| { kind: "absent" }
|
|
1407
|
+
| { kind: "unknown" }
|
|
1408
|
+
| { kind: "value"; value: ts.Expression };
|
|
1409
|
+
|
|
1410
|
+
function objectPropertyResult(
|
|
1411
|
+
object: ts.ObjectLiteralExpression,
|
|
1412
|
+
name: string,
|
|
1413
|
+
): ObjectPropertyResult {
|
|
1414
|
+
for (const property of [...object.properties].reverse()) {
|
|
1415
|
+
if (ts.isSpreadAssignment(property)) return { kind: "unknown" };
|
|
1416
|
+
const propertyName = staticPropertyName(property.name);
|
|
1417
|
+
if (propertyName === undefined) return { kind: "unknown" };
|
|
1418
|
+
if (propertyName !== name) continue;
|
|
1419
|
+
if (ts.isPropertyAssignment(property)) {
|
|
1420
|
+
return {
|
|
1421
|
+
kind: "value",
|
|
1422
|
+
value: unwrapContractExpression(property.initializer),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return { kind: "unknown" };
|
|
1426
|
+
}
|
|
1427
|
+
return { kind: "absent" };
|
|
1103
1428
|
}
|
|
1104
1429
|
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
);
|
|
1430
|
+
function staticPropertyName(name: ts.PropertyName): string | undefined {
|
|
1431
|
+
if (
|
|
1432
|
+
ts.isIdentifier(name) ||
|
|
1433
|
+
ts.isStringLiteral(name) ||
|
|
1434
|
+
ts.isNumericLiteral(name)
|
|
1435
|
+
) {
|
|
1436
|
+
return name.text;
|
|
1437
|
+
}
|
|
1438
|
+
return undefined;
|
|
1115
1439
|
}
|
|
1116
1440
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1441
|
+
function staticString(
|
|
1442
|
+
expression: ts.Expression | undefined,
|
|
1443
|
+
): string | undefined {
|
|
1444
|
+
if (!expression) return undefined;
|
|
1445
|
+
const value = unwrapContractExpression(expression);
|
|
1446
|
+
return ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
|
|
1447
|
+
? value.text
|
|
1448
|
+
: undefined;
|
|
1449
|
+
}
|
|
1119
1450
|
|
|
1120
1451
|
function parseErrorCatalogKeys(source: string): Set<string> {
|
|
1121
1452
|
const keys = new Set<string>();
|
|
@@ -1208,23 +1539,6 @@ function topLevelObjectKeys(body: string): string[] {
|
|
|
1208
1539
|
return keys;
|
|
1209
1540
|
}
|
|
1210
1541
|
|
|
1211
|
-
function dedupeCatalogErrorRefs(
|
|
1212
|
-
refs: InspectedCatalogErrorRef[],
|
|
1213
|
-
): InspectedCatalogErrorRef[] {
|
|
1214
|
-
const seen = new Set<string>();
|
|
1215
|
-
const deduped: InspectedCatalogErrorRef[] = [];
|
|
1216
|
-
|
|
1217
|
-
for (const ref of refs) {
|
|
1218
|
-
const key = `${ref.key}:${ref.catalog}:${ref.catalogKey}`;
|
|
1219
|
-
if (seen.has(key)) continue;
|
|
1220
|
-
|
|
1221
|
-
seen.add(key);
|
|
1222
|
-
deduped.push(ref);
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
return deduped;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
1542
|
function joinContractPaths(prefix: string, routePath: string): string {
|
|
1229
1543
|
if (!prefix) return normalizeContractPath(routePath);
|
|
1230
1544
|
const segments = [
|
|
@@ -1667,8 +1981,9 @@ async function inspectDiagnostics(
|
|
|
1667
1981
|
contracts: InspectedContract[],
|
|
1668
1982
|
matchedRoutes: MatchedRoutesResult,
|
|
1669
1983
|
strict: boolean,
|
|
1984
|
+
queryTransportDiagnostics: InspectDiagnostic[],
|
|
1670
1985
|
): Promise<InspectDiagnostic[]> {
|
|
1671
|
-
const diagnostics: InspectDiagnostic[] = [];
|
|
1986
|
+
const diagnostics: InspectDiagnostic[] = [...queryTransportDiagnostics];
|
|
1672
1987
|
const frameworkLayout =
|
|
1673
1988
|
config.framework === "next" ? convention.nextLayout : convention.webLayout;
|
|
1674
1989
|
const missingFrameworkLayout =
|
|
@@ -1775,6 +2090,7 @@ async function inspectDiagnostics(
|
|
|
1775
2090
|
contracts,
|
|
1776
2091
|
)),
|
|
1777
2092
|
...(await inspectServerlessFootguns(targetDir, files, config, convention)),
|
|
2093
|
+
...(await inspectOutboxJobDispatcherDrift(targetDir, files, config)),
|
|
1778
2094
|
...(await inspectProductionReadiness(targetDir, files, config, convention)),
|
|
1779
2095
|
...(await inspectFeatureArtifactDrift(
|
|
1780
2096
|
targetDir,
|
|
@@ -2472,14 +2788,14 @@ async function inspectProductionReadiness(
|
|
|
2472
2788
|
|
|
2473
2789
|
if (
|
|
2474
2790
|
containsCallExpression(source, "createDevtoolsRoute") &&
|
|
2475
|
-
|
|
2476
|
-
!/\
|
|
2791
|
+
!/\bauthorize\s*(?::|\()/.test(source) &&
|
|
2792
|
+
!/\ballowUnauthenticatedDevelopmentAccess\s*:\s*true\b/.test(source)
|
|
2477
2793
|
) {
|
|
2478
2794
|
diagnostics.push({
|
|
2479
2795
|
severity: "warning",
|
|
2480
|
-
code: "
|
|
2796
|
+
code: "BEIGNET_DEVTOOLS_ROUTE_POLICY_MISSING",
|
|
2481
2797
|
file,
|
|
2482
|
-
message: `${file}
|
|
2798
|
+
message: `${file} creates a devtools route without an access policy, so the route remains hidden. Add app-owned authorize(...), or explicitly set allowUnauthenticatedDevelopmentAccess: true only for a development server bound exclusively to loopback.`,
|
|
2483
2799
|
});
|
|
2484
2800
|
}
|
|
2485
2801
|
|
|
@@ -3882,9 +4198,7 @@ async function inspectFeatureArtifactDrift(
|
|
|
3882
4198
|
}
|
|
3883
4199
|
|
|
3884
4200
|
for (const file of files) {
|
|
3885
|
-
const match =
|
|
3886
|
-
`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.test\\.ts$`,
|
|
3887
|
-
).exec(file);
|
|
4201
|
+
const match = directFeatureTestMatch(file, featuresPath);
|
|
3888
4202
|
if (!match) continue;
|
|
3889
4203
|
diagnostics.push({
|
|
3890
4204
|
severity: "warning",
|
|
@@ -3896,23 +4210,27 @@ async function inspectFeatureArtifactDrift(
|
|
|
3896
4210
|
|
|
3897
4211
|
for (const file of files) {
|
|
3898
4212
|
if (
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
file.endsWith(".d.ts")
|
|
4213
|
+
!/\.(?:ts|tsx|mts|cts)$/.test(file) ||
|
|
4214
|
+
/\.d\.(?:ts|mts|cts)$/.test(file)
|
|
3902
4215
|
) {
|
|
3903
4216
|
continue;
|
|
3904
4217
|
}
|
|
3905
4218
|
|
|
3906
|
-
const misplaced =
|
|
4219
|
+
const misplaced = directFeatureTestMatch(file, featuresPath)
|
|
4220
|
+
? undefined
|
|
4221
|
+
: misplacedFeatureLayer(file, config);
|
|
3907
4222
|
if (misplaced) {
|
|
3908
4223
|
diagnostics.push({
|
|
3909
4224
|
severity: "warning",
|
|
3910
4225
|
code: "BEIGNET_FEATURE_ARTIFACT_FOLDER",
|
|
3911
4226
|
file,
|
|
3912
|
-
message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned
|
|
4227
|
+
message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned files in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
|
|
3913
4228
|
});
|
|
3914
4229
|
}
|
|
3915
4230
|
|
|
4231
|
+
if (isTestSourceFile(file)) continue;
|
|
4232
|
+
if (!file.endsWith(".ts")) continue;
|
|
4233
|
+
|
|
3916
4234
|
if (
|
|
3917
4235
|
isNotificationDefinitionFile(file, featuresPath) ||
|
|
3918
4236
|
isInfraOrServerFile(file, config)
|
|
@@ -3933,6 +4251,15 @@ async function inspectFeatureArtifactDrift(
|
|
|
3933
4251
|
return diagnostics;
|
|
3934
4252
|
}
|
|
3935
4253
|
|
|
4254
|
+
function directFeatureTestMatch(
|
|
4255
|
+
file: string,
|
|
4256
|
+
featuresPath: string,
|
|
4257
|
+
): RegExpExecArray | null {
|
|
4258
|
+
return new RegExp(
|
|
4259
|
+
`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`,
|
|
4260
|
+
).exec(file);
|
|
4261
|
+
}
|
|
4262
|
+
|
|
3936
4263
|
function isFeatureArtifactFile(
|
|
3937
4264
|
file: string,
|
|
3938
4265
|
featuresPath: string,
|
|
@@ -3965,34 +4292,30 @@ async function appHasUploadRoute(
|
|
|
3965
4292
|
return false;
|
|
3966
4293
|
}
|
|
3967
4294
|
|
|
3968
|
-
function
|
|
4295
|
+
function misplacedFeatureLayer(
|
|
3969
4296
|
file: string,
|
|
3970
|
-
|
|
4297
|
+
config: ResolvedBeignetConfig,
|
|
3971
4298
|
): { actual: string; expected: string } | undefined {
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
|
|
4299
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
4300
|
+
const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/(.+)$`).exec(
|
|
4301
|
+
file,
|
|
4302
|
+
);
|
|
3975
4303
|
if (!match) return undefined;
|
|
3976
4304
|
|
|
3977
|
-
const
|
|
3978
|
-
|
|
3979
|
-
command: "tasks/",
|
|
3980
|
-
commands: "tasks/",
|
|
3981
|
-
event: "domain/events/",
|
|
3982
|
-
events: "domain/events/",
|
|
3983
|
-
job: "jobs/",
|
|
3984
|
-
listener: "listeners/",
|
|
3985
|
-
notification: "notifications/",
|
|
3986
|
-
schedule: "schedules/",
|
|
3987
|
-
scheduled: "schedules/",
|
|
3988
|
-
task: "tasks/",
|
|
3989
|
-
upload: "uploads/",
|
|
3990
|
-
};
|
|
4305
|
+
const canonicalPath = canonicalFeatureLayerPath(file, config);
|
|
4306
|
+
if (!canonicalPath) return undefined;
|
|
3991
4307
|
|
|
3992
|
-
const
|
|
3993
|
-
|
|
4308
|
+
const featurePrefix = `${featuresPath}/${match[1]}/`;
|
|
4309
|
+
const actualRelative = file.slice(featurePrefix.length);
|
|
4310
|
+
const expectedRelative = canonicalPath.slice(featurePrefix.length);
|
|
4311
|
+
const actual = actualRelative.includes("/")
|
|
4312
|
+
? `${actualRelative.split("/")[0]}/`
|
|
4313
|
+
: actualRelative;
|
|
4314
|
+
const expectedDirectory = path.posix.dirname(expectedRelative);
|
|
4315
|
+
const expected =
|
|
4316
|
+
expectedDirectory === "." ? expectedRelative : `${expectedDirectory}/`;
|
|
3994
4317
|
|
|
3995
|
-
return { actual
|
|
4318
|
+
return { actual, expected };
|
|
3996
4319
|
}
|
|
3997
4320
|
|
|
3998
4321
|
function isNotificationDefinitionFile(
|
|
@@ -4081,6 +4404,7 @@ type WorkflowRegistrationDrift = {
|
|
|
4081
4404
|
unregistered: UnregisteredWorkflowRegistry[];
|
|
4082
4405
|
wiringFile?: string;
|
|
4083
4406
|
eventBusFile?: string;
|
|
4407
|
+
unsafeLifecycleFiles: string[];
|
|
4084
4408
|
};
|
|
4085
4409
|
};
|
|
4086
4410
|
|
|
@@ -4159,6 +4483,15 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
4159
4483
|
}
|
|
4160
4484
|
|
|
4161
4485
|
const infraDir = directoryPath(path.dirname(config.paths.portWiring));
|
|
4486
|
+
for (const file of drift.listeners.unsafeLifecycleFiles) {
|
|
4487
|
+
diagnostics.push({
|
|
4488
|
+
severity: "warning",
|
|
4489
|
+
code: "BEIGNET_LISTENER_LIFECYCLE_UNSAFE",
|
|
4490
|
+
file,
|
|
4491
|
+
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().`,
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4162
4495
|
const listenerTarget = drift.listeners.wiringFile
|
|
4163
4496
|
? `${drift.listeners.wiringFile}, which already calls registerListeners(...)`
|
|
4164
4497
|
: drift.listeners.eventBusFile
|
|
@@ -4329,7 +4662,7 @@ async function workflowRegistrationDrift(
|
|
|
4329
4662
|
events: [],
|
|
4330
4663
|
jobs: [],
|
|
4331
4664
|
},
|
|
4332
|
-
listeners: { unregistered: [] },
|
|
4665
|
+
listeners: { unregistered: [], unsafeLifecycleFiles: [] },
|
|
4333
4666
|
};
|
|
4334
4667
|
if (registries.length === 0) return drift;
|
|
4335
4668
|
|
|
@@ -4371,10 +4704,7 @@ async function workflowRegistrationDrift(
|
|
|
4371
4704
|
// Events without listeners have nothing to deliver on drain, so only
|
|
4372
4705
|
// record-only events stay out of the registry without a warning.
|
|
4373
4706
|
const listened = await listenedEventNames(targetDir, files, config);
|
|
4374
|
-
const
|
|
4375
|
-
const registered = value
|
|
4376
|
-
? identifiersFromArrayExpression(value.text)
|
|
4377
|
-
: new Set<string>();
|
|
4707
|
+
const registered = outboxRegistryIdentifiers(outboxSource, "events");
|
|
4378
4708
|
drift.outbox.events = unregisteredWorkflowRegistries(
|
|
4379
4709
|
eventRegistries,
|
|
4380
4710
|
registered,
|
|
@@ -4383,10 +4713,7 @@ async function workflowRegistrationDrift(
|
|
|
4383
4713
|
}
|
|
4384
4714
|
|
|
4385
4715
|
if (jobRegistries.length > 0) {
|
|
4386
|
-
const
|
|
4387
|
-
const registered = value
|
|
4388
|
-
? identifiersFromArrayExpression(value.text)
|
|
4389
|
-
: new Set<string>();
|
|
4716
|
+
const registered = outboxRegistryIdentifiers(outboxSource, "jobs");
|
|
4390
4717
|
drift.outbox.jobs = unregisteredWorkflowRegistries(
|
|
4391
4718
|
jobRegistries,
|
|
4392
4719
|
registered,
|
|
@@ -4397,7 +4724,12 @@ async function workflowRegistrationDrift(
|
|
|
4397
4724
|
|
|
4398
4725
|
const listenerRegistries = byKind("listeners");
|
|
4399
4726
|
if (listenerRegistries.length > 0) {
|
|
4400
|
-
const wiring = await listenerWiringReferences(
|
|
4727
|
+
const wiring = await listenerWiringReferences(
|
|
4728
|
+
targetDir,
|
|
4729
|
+
files,
|
|
4730
|
+
config,
|
|
4731
|
+
listenerRegistries,
|
|
4732
|
+
);
|
|
4401
4733
|
drift.listeners = {
|
|
4402
4734
|
unregistered: unregisteredWorkflowRegistries(
|
|
4403
4735
|
listenerRegistries,
|
|
@@ -4405,6 +4737,7 @@ async function workflowRegistrationDrift(
|
|
|
4405
4737
|
),
|
|
4406
4738
|
wiringFile: wiring.wiringFile,
|
|
4407
4739
|
eventBusFile: wiring.eventBusFile,
|
|
4740
|
+
unsafeLifecycleFiles: wiring.unsafeLifecycleFiles,
|
|
4408
4741
|
};
|
|
4409
4742
|
}
|
|
4410
4743
|
|
|
@@ -4739,12 +5072,15 @@ async function listenerWiringReferences(
|
|
|
4739
5072
|
targetDir: string,
|
|
4740
5073
|
files: string[],
|
|
4741
5074
|
config: ResolvedBeignetConfig,
|
|
5075
|
+
listenerRegistries: FeatureWorkflowRegistry[],
|
|
4742
5076
|
): Promise<{
|
|
4743
5077
|
identifiers: Set<string>;
|
|
4744
5078
|
wiringFile?: string;
|
|
4745
5079
|
eventBusFile?: string;
|
|
5080
|
+
unsafeLifecycleFiles: string[];
|
|
4746
5081
|
}> {
|
|
4747
5082
|
const identifiers = new Set<string>();
|
|
5083
|
+
const unsafeLifecycleFiles = new Set<string>();
|
|
4748
5084
|
let wiringFile: string | undefined;
|
|
4749
5085
|
let eventBusFile: string | undefined;
|
|
4750
5086
|
let centralListenerRegistryReferenced = false;
|
|
@@ -4756,35 +5092,54 @@ async function listenerWiringReferences(
|
|
|
4756
5092
|
|
|
4757
5093
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4758
5094
|
const namedImports = parseNamedImportSources(source);
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
5095
|
+
const sourceFile = ts.createSourceFile(
|
|
5096
|
+
file,
|
|
5097
|
+
source,
|
|
5098
|
+
ts.ScriptTarget.Latest,
|
|
5099
|
+
true,
|
|
5100
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
5101
|
+
);
|
|
5102
|
+
const registerListenersBindings = importedRegisterListenersBindings(
|
|
5103
|
+
source,
|
|
5104
|
+
namedImports,
|
|
5105
|
+
);
|
|
5106
|
+
const calls = registerListenersCalls(sourceFile, registerListenersBindings);
|
|
5107
|
+
let foundRelevantCall = false;
|
|
4768
5108
|
|
|
4769
|
-
|
|
5109
|
+
for (const call of calls) {
|
|
5110
|
+
const argsText = call.arguments
|
|
5111
|
+
.map((argument) => argument.getText(sourceFile))
|
|
5112
|
+
.join(",");
|
|
4770
5113
|
const callIdentifiers = identifiersFromArrayExpression(argsText);
|
|
5114
|
+
const referencesCentral = callReferencesCentralListenerRegistry({
|
|
5115
|
+
callIdentifiers,
|
|
5116
|
+
namedImports,
|
|
5117
|
+
importerFile: file,
|
|
5118
|
+
listenerRegistryFile,
|
|
5119
|
+
files,
|
|
5120
|
+
});
|
|
5121
|
+
const referencesFeature = callReferencesFeatureListenerRegistry({
|
|
5122
|
+
callIdentifiers,
|
|
5123
|
+
namedImports,
|
|
5124
|
+
importerFile: file,
|
|
5125
|
+
files,
|
|
5126
|
+
listenerRegistries,
|
|
5127
|
+
});
|
|
5128
|
+
if (!referencesCentral && !referencesFeature) continue;
|
|
5129
|
+
|
|
5130
|
+
foundRelevantCall = true;
|
|
4771
5131
|
for (const identifier of callIdentifiers) {
|
|
4772
5132
|
identifiers.add(identifier);
|
|
4773
5133
|
}
|
|
4774
|
-
if (
|
|
4775
|
-
callReferencesCentralListenerRegistry({
|
|
4776
|
-
callIdentifiers,
|
|
4777
|
-
namedImports,
|
|
4778
|
-
importerFile: file,
|
|
4779
|
-
listenerRegistryFile,
|
|
4780
|
-
files,
|
|
4781
|
-
})
|
|
4782
|
-
) {
|
|
5134
|
+
if (referencesCentral) {
|
|
4783
5135
|
centralListenerRegistryReferenced = true;
|
|
4784
5136
|
}
|
|
5137
|
+
if (!hasSafeListenerRegistrationLifecycle(call, sourceFile)) {
|
|
5138
|
+
unsafeLifecycleFiles.add(file);
|
|
5139
|
+
}
|
|
4785
5140
|
}
|
|
4786
5141
|
|
|
4787
|
-
if (
|
|
5142
|
+
if (foundRelevantCall) {
|
|
4788
5143
|
wiringFile ??= file;
|
|
4789
5144
|
} else if (!eventBusFile && /\bcreate\w*EventBus\s*\(/.test(source)) {
|
|
4790
5145
|
eventBusFile = file;
|
|
@@ -4807,7 +5162,368 @@ async function listenerWiringReferences(
|
|
|
4807
5162
|
}
|
|
4808
5163
|
}
|
|
4809
5164
|
|
|
4810
|
-
return {
|
|
5165
|
+
return {
|
|
5166
|
+
identifiers,
|
|
5167
|
+
wiringFile,
|
|
5168
|
+
eventBusFile,
|
|
5169
|
+
unsafeLifecycleFiles: [...unsafeLifecycleFiles].sort(),
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
type ListenerLifecycleHook =
|
|
5174
|
+
| ts.MethodDeclaration
|
|
5175
|
+
| ts.FunctionExpression
|
|
5176
|
+
| ts.ArrowFunction;
|
|
5177
|
+
|
|
5178
|
+
interface ListenerLifecycleTarget {
|
|
5179
|
+
text: string;
|
|
5180
|
+
binding?: ts.Node;
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
function registerListenersCalls(
|
|
5184
|
+
sourceFile: ts.SourceFile,
|
|
5185
|
+
bindings: ReadonlySet<string>,
|
|
5186
|
+
): ts.CallExpression[] {
|
|
5187
|
+
const calls: ts.CallExpression[] = [];
|
|
5188
|
+
const visit = (node: ts.Node): void => {
|
|
5189
|
+
if (
|
|
5190
|
+
ts.isCallExpression(node) &&
|
|
5191
|
+
((ts.isIdentifier(node.expression) &&
|
|
5192
|
+
bindings.has(node.expression.text)) ||
|
|
5193
|
+
(ts.isPropertyAccessExpression(node.expression) &&
|
|
5194
|
+
node.expression.name.text === "registerListeners" &&
|
|
5195
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
5196
|
+
bindings.has(`${node.expression.expression.text}.*`)))
|
|
5197
|
+
) {
|
|
5198
|
+
calls.push(node);
|
|
5199
|
+
}
|
|
5200
|
+
ts.forEachChild(node, visit);
|
|
5201
|
+
};
|
|
5202
|
+
visit(sourceFile);
|
|
5203
|
+
return calls;
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
function importedRegisterListenersBindings(
|
|
5207
|
+
source: string,
|
|
5208
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>,
|
|
5209
|
+
): Set<string> {
|
|
5210
|
+
const bindings = new Set<string>();
|
|
5211
|
+
for (const [localName, imported] of namedImports) {
|
|
5212
|
+
if (
|
|
5213
|
+
imported.importedName === "registerListeners" &&
|
|
5214
|
+
imported.sourcePath === "@beignet/core/events"
|
|
5215
|
+
) {
|
|
5216
|
+
bindings.add(localName);
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
const namespaceImport =
|
|
5220
|
+
/import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/core\/events["']/g;
|
|
5221
|
+
for (const match of source.matchAll(namespaceImport)) {
|
|
5222
|
+
bindings.add(`${match[1]}.*`);
|
|
5223
|
+
}
|
|
5224
|
+
return bindings;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
function hasSafeListenerRegistrationLifecycle(
|
|
5228
|
+
call: ts.CallExpression,
|
|
5229
|
+
sourceFile: ts.SourceFile,
|
|
5230
|
+
): boolean {
|
|
5231
|
+
const startHook = enclosingListenerLifecycleHook(call, "start");
|
|
5232
|
+
const target = listenerRegistrationTarget(call, sourceFile);
|
|
5233
|
+
if (!startHook || !target) return false;
|
|
5234
|
+
const lifecycleOwner = listenerLifecycleOwner(startHook);
|
|
5235
|
+
if (!lifecycleOwner) return false;
|
|
5236
|
+
|
|
5237
|
+
let observesReadiness = false;
|
|
5238
|
+
let observesCleanup = false;
|
|
5239
|
+
const visit = (node: ts.Node): void => {
|
|
5240
|
+
if (
|
|
5241
|
+
!observesReadiness &&
|
|
5242
|
+
ts.isPropertyAccessExpression(node) &&
|
|
5243
|
+
node.name.text === "ready" &&
|
|
5244
|
+
listenerLifecycleTargetsMatch(
|
|
5245
|
+
listenerLifecycleTarget(node.expression, sourceFile),
|
|
5246
|
+
target,
|
|
5247
|
+
) &&
|
|
5248
|
+
enclosingListenerLifecycleHook(node, "start") === startHook &&
|
|
5249
|
+
listenerLifecyclePromiseIsObserved(node, startHook)
|
|
5250
|
+
) {
|
|
5251
|
+
observesReadiness = true;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
if (
|
|
5255
|
+
!observesCleanup &&
|
|
5256
|
+
ts.isCallExpression(node) &&
|
|
5257
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
5258
|
+
node.expression.name.text === "unsubscribe" &&
|
|
5259
|
+
listenerLifecycleTargetsMatch(
|
|
5260
|
+
listenerLifecycleTarget(node.expression.expression, sourceFile),
|
|
5261
|
+
target,
|
|
5262
|
+
)
|
|
5263
|
+
) {
|
|
5264
|
+
const stopHook = enclosingListenerLifecycleHook(node, "stop");
|
|
5265
|
+
if (
|
|
5266
|
+
stopHook &&
|
|
5267
|
+
listenerLifecycleOwner(stopHook) === lifecycleOwner &&
|
|
5268
|
+
listenerLifecyclePromiseIsObserved(node, stopHook)
|
|
5269
|
+
) {
|
|
5270
|
+
observesCleanup = true;
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
if (!observesReadiness || !observesCleanup) {
|
|
5275
|
+
ts.forEachChild(node, visit);
|
|
5276
|
+
}
|
|
5277
|
+
};
|
|
5278
|
+
visit(lifecycleOwner);
|
|
5279
|
+
return observesReadiness && observesCleanup;
|
|
5280
|
+
}
|
|
5281
|
+
|
|
5282
|
+
function listenerRegistrationTarget(
|
|
5283
|
+
call: ts.CallExpression,
|
|
5284
|
+
sourceFile: ts.SourceFile,
|
|
5285
|
+
): ListenerLifecycleTarget | undefined {
|
|
5286
|
+
let expression: ts.Expression = call;
|
|
5287
|
+
while (
|
|
5288
|
+
ts.isParenthesizedExpression(expression.parent) ||
|
|
5289
|
+
ts.isAsExpression(expression.parent) ||
|
|
5290
|
+
ts.isSatisfiesExpression(expression.parent) ||
|
|
5291
|
+
ts.isNonNullExpression(expression.parent)
|
|
5292
|
+
) {
|
|
5293
|
+
expression = expression.parent;
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
const parent = expression.parent;
|
|
5297
|
+
if (
|
|
5298
|
+
ts.isVariableDeclaration(parent) &&
|
|
5299
|
+
parent.initializer === expression &&
|
|
5300
|
+
ts.isIdentifier(parent.name)
|
|
5301
|
+
) {
|
|
5302
|
+
return { text: parent.name.text, binding: parent };
|
|
5303
|
+
}
|
|
5304
|
+
if (
|
|
5305
|
+
ts.isBinaryExpression(parent) &&
|
|
5306
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
5307
|
+
parent.right === expression
|
|
5308
|
+
) {
|
|
5309
|
+
return listenerLifecycleTarget(parent.left, sourceFile);
|
|
5310
|
+
}
|
|
5311
|
+
return undefined;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
function listenerLifecycleTarget(
|
|
5315
|
+
expression: ts.Expression,
|
|
5316
|
+
sourceFile: ts.SourceFile,
|
|
5317
|
+
): ListenerLifecycleTarget | undefined {
|
|
5318
|
+
let current = expression;
|
|
5319
|
+
while (
|
|
5320
|
+
ts.isParenthesizedExpression(current) ||
|
|
5321
|
+
ts.isAsExpression(current) ||
|
|
5322
|
+
ts.isSatisfiesExpression(current) ||
|
|
5323
|
+
ts.isNonNullExpression(current)
|
|
5324
|
+
) {
|
|
5325
|
+
current = current.expression;
|
|
5326
|
+
}
|
|
5327
|
+
if (!ts.isIdentifier(current) && !ts.isPropertyAccessExpression(current)) {
|
|
5328
|
+
return undefined;
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
return {
|
|
5332
|
+
text: current.getText(sourceFile),
|
|
5333
|
+
binding: listenerLifecycleRootIdentifier(current)
|
|
5334
|
+
? resolveListenerLifecycleBinding(
|
|
5335
|
+
listenerLifecycleRootIdentifier(current) as ts.Identifier,
|
|
5336
|
+
)
|
|
5337
|
+
: undefined,
|
|
5338
|
+
};
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
function listenerLifecycleRootIdentifier(
|
|
5342
|
+
expression: ts.Identifier | ts.PropertyAccessExpression,
|
|
5343
|
+
): ts.Identifier | undefined {
|
|
5344
|
+
let current: ts.Expression = expression;
|
|
5345
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
5346
|
+
current = current.expression;
|
|
5347
|
+
}
|
|
5348
|
+
return ts.isIdentifier(current) ? current : undefined;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
function resolveListenerLifecycleBinding(
|
|
5352
|
+
identifier: ts.Identifier,
|
|
5353
|
+
): ts.Node | undefined {
|
|
5354
|
+
let current: ts.Node | undefined = identifier;
|
|
5355
|
+
while (current) {
|
|
5356
|
+
if (ts.isBlock(current) || ts.isSourceFile(current)) {
|
|
5357
|
+
for (const statement of current.statements) {
|
|
5358
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
5359
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
5360
|
+
if (
|
|
5361
|
+
ts.isIdentifier(declaration.name) &&
|
|
5362
|
+
declaration.name.text === identifier.text
|
|
5363
|
+
) {
|
|
5364
|
+
return declaration;
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
if (ts.isFunctionLike(current)) {
|
|
5371
|
+
for (const parameter of current.parameters) {
|
|
5372
|
+
if (
|
|
5373
|
+
ts.isIdentifier(parameter.name) &&
|
|
5374
|
+
parameter.name.text === identifier.text
|
|
5375
|
+
) {
|
|
5376
|
+
return parameter;
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
if (
|
|
5382
|
+
ts.isCatchClause(current) &&
|
|
5383
|
+
current.variableDeclaration &&
|
|
5384
|
+
ts.isIdentifier(current.variableDeclaration.name) &&
|
|
5385
|
+
current.variableDeclaration.name.text === identifier.text
|
|
5386
|
+
) {
|
|
5387
|
+
return current.variableDeclaration;
|
|
5388
|
+
}
|
|
5389
|
+
current = current.parent;
|
|
5390
|
+
}
|
|
5391
|
+
return undefined;
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
function listenerLifecycleTargetsMatch(
|
|
5395
|
+
left: ListenerLifecycleTarget | undefined,
|
|
5396
|
+
right: ListenerLifecycleTarget,
|
|
5397
|
+
): boolean {
|
|
5398
|
+
return (
|
|
5399
|
+
left?.text === right.text &&
|
|
5400
|
+
(left.binding !== undefined || right.binding !== undefined
|
|
5401
|
+
? left.binding === right.binding
|
|
5402
|
+
: true)
|
|
5403
|
+
);
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
function enclosingListenerLifecycleHook(
|
|
5407
|
+
node: ts.Node,
|
|
5408
|
+
hookName: "start" | "stop",
|
|
5409
|
+
): ListenerLifecycleHook | undefined {
|
|
5410
|
+
let current: ts.Node | undefined = node.parent;
|
|
5411
|
+
while (current) {
|
|
5412
|
+
if (ts.isMethodDeclaration(current)) {
|
|
5413
|
+
return staticPropertyName(current.name) === hookName
|
|
5414
|
+
? current
|
|
5415
|
+
: undefined;
|
|
5416
|
+
}
|
|
5417
|
+
if (ts.isFunctionExpression(current) || ts.isArrowFunction(current)) {
|
|
5418
|
+
const parent = current.parent;
|
|
5419
|
+
return ts.isPropertyAssignment(parent) &&
|
|
5420
|
+
staticPropertyName(parent.name) === hookName
|
|
5421
|
+
? current
|
|
5422
|
+
: undefined;
|
|
5423
|
+
}
|
|
5424
|
+
if (ts.isFunctionDeclaration(current)) return undefined;
|
|
5425
|
+
current = current.parent;
|
|
5426
|
+
}
|
|
5427
|
+
return undefined;
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
function listenerLifecycleOwner(
|
|
5431
|
+
hook: ListenerLifecycleHook,
|
|
5432
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
5433
|
+
if (ts.isMethodDeclaration(hook)) {
|
|
5434
|
+
return ts.isObjectLiteralExpression(hook.parent) ? hook.parent : undefined;
|
|
5435
|
+
}
|
|
5436
|
+
const property = hook.parent;
|
|
5437
|
+
return ts.isPropertyAssignment(property) &&
|
|
5438
|
+
ts.isObjectLiteralExpression(property.parent)
|
|
5439
|
+
? property.parent
|
|
5440
|
+
: undefined;
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
function listenerLifecyclePromiseIsObserved(
|
|
5444
|
+
node: ts.Node,
|
|
5445
|
+
hook: ListenerLifecycleHook,
|
|
5446
|
+
): boolean {
|
|
5447
|
+
let current: ts.Node | undefined = node;
|
|
5448
|
+
while (current && current !== hook) {
|
|
5449
|
+
const parent: ts.Node = current.parent;
|
|
5450
|
+
if (
|
|
5451
|
+
(ts.isAwaitExpression(parent) && parent.expression === current) ||
|
|
5452
|
+
(ts.isReturnStatement(parent) && parent.expression === current)
|
|
5453
|
+
) {
|
|
5454
|
+
return true;
|
|
5455
|
+
}
|
|
5456
|
+
if (
|
|
5457
|
+
ts.isParenthesizedExpression(parent) ||
|
|
5458
|
+
ts.isAsExpression(parent) ||
|
|
5459
|
+
ts.isSatisfiesExpression(parent) ||
|
|
5460
|
+
ts.isNonNullExpression(parent)
|
|
5461
|
+
) {
|
|
5462
|
+
current = parent;
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (
|
|
5466
|
+
ts.isPropertyAccessExpression(parent) &&
|
|
5467
|
+
parent.expression === current
|
|
5468
|
+
) {
|
|
5469
|
+
const method = parent.name.text;
|
|
5470
|
+
const invocation = parent.parent;
|
|
5471
|
+
if (
|
|
5472
|
+
!["then", "catch", "finally"].includes(method) ||
|
|
5473
|
+
!ts.isCallExpression(invocation) ||
|
|
5474
|
+
invocation.expression !== parent ||
|
|
5475
|
+
method === "catch" ||
|
|
5476
|
+
(method === "then" && invocation.arguments.length > 1)
|
|
5477
|
+
) {
|
|
5478
|
+
return false;
|
|
5479
|
+
}
|
|
5480
|
+
current = invocation;
|
|
5481
|
+
continue;
|
|
5482
|
+
}
|
|
5483
|
+
return false;
|
|
5484
|
+
}
|
|
5485
|
+
return false;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
function callReferencesFeatureListenerRegistry(args: {
|
|
5489
|
+
callIdentifiers: Set<string>;
|
|
5490
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>;
|
|
5491
|
+
importerFile: string;
|
|
5492
|
+
files: string[];
|
|
5493
|
+
listenerRegistries: FeatureWorkflowRegistry[];
|
|
5494
|
+
}): boolean {
|
|
5495
|
+
for (const identifier of args.callIdentifiers) {
|
|
5496
|
+
for (const registry of args.listenerRegistries) {
|
|
5497
|
+
if (
|
|
5498
|
+
identifier === registry.registryName ||
|
|
5499
|
+
registry.members.includes(identifier)
|
|
5500
|
+
) {
|
|
5501
|
+
return true;
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
const imported = args.namedImports.get(identifier);
|
|
5506
|
+
if (!imported) continue;
|
|
5507
|
+
const importedFile = sourceFileFromImport(
|
|
5508
|
+
imported.sourcePath,
|
|
5509
|
+
args.importerFile,
|
|
5510
|
+
args.files,
|
|
5511
|
+
);
|
|
5512
|
+
if (!importedFile) continue;
|
|
5513
|
+
|
|
5514
|
+
for (const registry of args.listenerRegistries) {
|
|
5515
|
+
if (
|
|
5516
|
+
(imported.importedName === registry.registryName &&
|
|
5517
|
+
importedFile === registry.indexFile) ||
|
|
5518
|
+
(registry.members.includes(imported.importedName) &&
|
|
5519
|
+
(importedFile === registry.indexFile ||
|
|
5520
|
+
registry.memberFiles.get(imported.importedName) === importedFile))
|
|
5521
|
+
) {
|
|
5522
|
+
return true;
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
return false;
|
|
4811
5527
|
}
|
|
4812
5528
|
|
|
4813
5529
|
function callReferencesCentralListenerRegistry(args: {
|
|
@@ -5086,6 +5802,56 @@ async function appHasOutboxDrainEntrypoint(
|
|
|
5086
5802
|
return false;
|
|
5087
5803
|
}
|
|
5088
5804
|
|
|
5805
|
+
async function inspectOutboxJobDispatcherDrift(
|
|
5806
|
+
targetDir: string,
|
|
5807
|
+
files: string[],
|
|
5808
|
+
config: ResolvedBeignetConfig,
|
|
5809
|
+
): Promise<InspectDiagnostic[]> {
|
|
5810
|
+
if (!hasOutboxRegistry(files, config)) return [];
|
|
5811
|
+
|
|
5812
|
+
const source = await readFile(
|
|
5813
|
+
path.join(targetDir, config.paths.outbox),
|
|
5814
|
+
"utf8",
|
|
5815
|
+
);
|
|
5816
|
+
const jobsState = outboxRegistryEntryState(source, "jobs");
|
|
5817
|
+
if (jobsState === "absent" || jobsState === "empty") return [];
|
|
5818
|
+
|
|
5819
|
+
const analysis = await analyzePortWiringFiles({ targetDir, files, config });
|
|
5820
|
+
const declaresJobs = analysis.declared.has("jobs");
|
|
5821
|
+
const wiresJobs = analysis.bound.has("jobs") || analysis.deferred.has("jobs");
|
|
5822
|
+
if (declaresJobs && wiresJobs) return [];
|
|
5823
|
+
|
|
5824
|
+
const file =
|
|
5825
|
+
jobsState === "indeterminate"
|
|
5826
|
+
? config.paths.outbox
|
|
5827
|
+
: !declaresJobs
|
|
5828
|
+
? config.paths.ports
|
|
5829
|
+
: config.paths.portWiring;
|
|
5830
|
+
const reason = !analysis.portsFileExists
|
|
5831
|
+
? `${config.paths.ports} does not exist at the configured path`
|
|
5832
|
+
: !declaresJobs
|
|
5833
|
+
? analysis.declarationIndeterminate
|
|
5834
|
+
? `${config.paths.ports} does not expose a statically verifiable jobs declaration`
|
|
5835
|
+
: `${config.paths.ports} does not declare a jobs port`
|
|
5836
|
+
: !analysis.portWiringFileExists
|
|
5837
|
+
? `${config.paths.portWiring} does not exist at the configured path`
|
|
5838
|
+
: analysis.indeterminate
|
|
5839
|
+
? `${config.paths.portWiring} does not expose statically verifiable jobs wiring`
|
|
5840
|
+
: `${config.paths.portWiring} does not bind or defer the jobs port`;
|
|
5841
|
+
|
|
5842
|
+
return [
|
|
5843
|
+
{
|
|
5844
|
+
severity: "warning",
|
|
5845
|
+
code: "BEIGNET_OUTBOX_JOB_DISPATCHER_MISSING",
|
|
5846
|
+
file,
|
|
5847
|
+
message:
|
|
5848
|
+
jobsState === "indeterminate"
|
|
5849
|
+
? `The jobs entries in the outbox registry at ${config.paths.outbox} could not be statically resolved, and ${reason}. Keep the jobs property explicit so doctor can verify it, or add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`
|
|
5850
|
+
: `The outbox registry in ${config.paths.outbox} contains jobs, but ${reason}. Add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`,
|
|
5851
|
+
},
|
|
5852
|
+
];
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5089
5855
|
async function inspectPortProviderDrift(
|
|
5090
5856
|
targetDir: string,
|
|
5091
5857
|
files: string[],
|
|
@@ -8384,8 +9150,9 @@ async function contractsFromRoutesSource(
|
|
|
8384
9150
|
source,
|
|
8385
9151
|
routeIdentifier,
|
|
8386
9152
|
);
|
|
9153
|
+
const { contracts } = await readContracts(targetDir, files, config);
|
|
8387
9154
|
const registeredContracts = contractsForRegisteredRouteGroups(
|
|
8388
|
-
|
|
9155
|
+
contracts,
|
|
8389
9156
|
routeGroups,
|
|
8390
9157
|
registeredGroups,
|
|
8391
9158
|
);
|