@supacloud/compiler 0.24.0 → 0.26.0
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/README.md +84 -4
- package/dist/benchmark.d.ts +6 -0
- package/dist/cli.js +522 -85
- package/dist/contract-manifest.d.ts +4 -0
- package/dist/generate.d.ts +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1213 -771
- package/dist/migration-assess.d.ts +62 -0
- package/dist/repair-plan.d.ts +17 -0
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -125,7 +125,12 @@ function buildContractManifest(graph, artifacts) {
|
|
|
125
125
|
...command?.permission === undefined ? {} : { permission: command.permission },
|
|
126
126
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
127
127
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
128
|
-
...route.
|
|
128
|
+
...route.responses === undefined ? {} : {
|
|
129
|
+
responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
|
|
130
|
+
},
|
|
131
|
+
...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence },
|
|
132
|
+
...route.parse === undefined ? {} : { parse: route.parse },
|
|
133
|
+
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody }
|
|
129
134
|
});
|
|
130
135
|
}
|
|
131
136
|
}
|
|
@@ -214,7 +219,7 @@ function renderApplication(graph, options) {
|
|
|
214
219
|
providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
|
|
215
220
|
}));
|
|
216
221
|
}
|
|
217
|
-
const imports = new ImportManager;
|
|
222
|
+
const imports = new ImportManager(new Set(["CompiledApplicationModule"]));
|
|
218
223
|
const factorySections = [];
|
|
219
224
|
const descriptorEntries = [];
|
|
220
225
|
for (const module of modules) {
|
|
@@ -231,7 +236,13 @@ function renderApplication(graph, options) {
|
|
|
231
236
|
"",
|
|
232
237
|
TYPE_GUARDS,
|
|
233
238
|
"",
|
|
234
|
-
|
|
239
|
+
...modules.length > 0 ? [
|
|
240
|
+
'export type CompiledApplicationModule = Omit<CompiledModule, "name" | "createServices"> & (',
|
|
241
|
+
...modules.map((module) => ` | { name: ${JSON.stringify(module.name)}; createServices: typeof create${pascalName(module.name)}Services }`),
|
|
242
|
+
");"
|
|
243
|
+
] : ["export type CompiledApplicationModule = CompiledModule;"],
|
|
244
|
+
"",
|
|
245
|
+
"export function createCompiledModules(): CompiledApplicationModule[] {",
|
|
235
246
|
" return [",
|
|
236
247
|
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
237
248
|
" ];",
|
|
@@ -312,7 +323,9 @@ function renderApplication(graph, options) {
|
|
|
312
323
|
};
|
|
313
324
|
}
|
|
314
325
|
async function generateApplication(graph, options) {
|
|
315
|
-
|
|
326
|
+
return writeRenderedApplication(renderApplication(graph, options), options);
|
|
327
|
+
}
|
|
328
|
+
async function writeRenderedApplication(rendered, options) {
|
|
316
329
|
await mkdir(options.outDir, { recursive: true });
|
|
317
330
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
318
331
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
@@ -399,8 +412,12 @@ function indent(text, spaces) {
|
|
|
399
412
|
}
|
|
400
413
|
|
|
401
414
|
class ImportManager {
|
|
415
|
+
reserved;
|
|
402
416
|
entries = new Map;
|
|
403
417
|
lookup = new Map;
|
|
418
|
+
constructor(reserved = new Set) {
|
|
419
|
+
this.reserved = reserved;
|
|
420
|
+
}
|
|
404
421
|
get size() {
|
|
405
422
|
return this.entries.size;
|
|
406
423
|
}
|
|
@@ -415,7 +432,7 @@ class ImportManager {
|
|
|
415
432
|
return existing;
|
|
416
433
|
let local = exported;
|
|
417
434
|
let counter = 2;
|
|
418
|
-
while (this.entries.has(local)) {
|
|
435
|
+
while (this.entries.has(local) || this.reserved.has(local)) {
|
|
419
436
|
local = `${exported}${counter}`;
|
|
420
437
|
counter += 1;
|
|
421
438
|
}
|
|
@@ -472,7 +489,7 @@ class ModuleGenerator {
|
|
|
472
489
|
renderDescriptor() {
|
|
473
490
|
const lines = [
|
|
474
491
|
`{`,
|
|
475
|
-
` name: ${JSON.stringify(this.module.name)},`,
|
|
492
|
+
` name: ${JSON.stringify(this.module.name)} as const,`,
|
|
476
493
|
` createServices: create${this.pascal}Services,`
|
|
477
494
|
];
|
|
478
495
|
if (this.hasFactoryContent("request")) {
|
|
@@ -611,6 +628,10 @@ class ModuleGenerator {
|
|
|
611
628
|
if (route.contract !== undefined) {
|
|
612
629
|
fields.push(`contract: ${JSON.stringify(route.contract)}`);
|
|
613
630
|
}
|
|
631
|
+
if (route.parse === "none")
|
|
632
|
+
fields.push('parse: "none"');
|
|
633
|
+
if (route.allowDeleteBody === true)
|
|
634
|
+
fields.push("allowDeleteBody: true");
|
|
614
635
|
if (route.schemaKinds !== undefined && Object.keys(route.schemaKinds).length > 0) {
|
|
615
636
|
fields.push(`schemaKinds: ${JSON.stringify(route.schemaKinds)}`);
|
|
616
637
|
}
|
|
@@ -710,7 +731,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
710
731
|
`function create${this.pascal}Services(`,
|
|
711
732
|
` deps: Record<string, unknown>,`,
|
|
712
733
|
` imported: Record<string, Record<string, unknown>>,`,
|
|
713
|
-
`)
|
|
734
|
+
`) {`,
|
|
714
735
|
indent(this.renderFactoryBody("services"), 2),
|
|
715
736
|
`}`
|
|
716
737
|
].join(`
|
|
@@ -807,7 +828,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
807
828
|
`);
|
|
808
829
|
}
|
|
809
830
|
const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
|
|
810
|
-
lines.push(`return { ${entries.join(", ")} };`);
|
|
831
|
+
lines.push(`return retainPlatformDependencies({ ${entries.join(", ")} }, deps);`);
|
|
811
832
|
return lines.join(`
|
|
812
833
|
`);
|
|
813
834
|
}
|
|
@@ -908,6 +929,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
908
929
|
return this.depExpr(own.useExisting ?? token, kind, options);
|
|
909
930
|
}
|
|
910
931
|
}
|
|
932
|
+
if (own && factoryOfScope(own.scope) === "services" && kind !== "services" && !isSkipSelf && !isSelf) {
|
|
933
|
+
return `services.${camelName(token)}`;
|
|
934
|
+
}
|
|
911
935
|
if (isSelf) {
|
|
912
936
|
return isOptional ? "undefined" : `services.${camelName(token)}`;
|
|
913
937
|
}
|
|
@@ -925,13 +949,14 @@ ${indent(item, 2)}`).join(",")}
|
|
|
925
949
|
return `imported.${mod.name}.${camelName(token)}`;
|
|
926
950
|
}
|
|
927
951
|
}
|
|
928
|
-
if (isOptional && !this.graph.externalTokens.includes(token)) {
|
|
929
|
-
return "undefined";
|
|
930
|
-
}
|
|
931
952
|
if (isSelf)
|
|
932
953
|
return isOptional ? "undefined" : `services.${camelName(token)}`;
|
|
933
954
|
if (kind === "services")
|
|
934
955
|
return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
|
|
956
|
+
if (!own || isSkipSelf) {
|
|
957
|
+
const external = `platformDependencies.get(services)?.${camelName(token)}`;
|
|
958
|
+
return isOptional ? `(${external} ?? undefined)` : external;
|
|
959
|
+
}
|
|
935
960
|
return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
|
|
936
961
|
}
|
|
937
962
|
}
|
|
@@ -959,7 +984,7 @@ function orderProviders(providers) {
|
|
|
959
984
|
function renderClient(graph, options) {
|
|
960
985
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
961
986
|
const outDir = options?.outDir ?? process.cwd();
|
|
962
|
-
const imports = new ImportManager;
|
|
987
|
+
const imports = new ImportManager(new Set(["ApiClientError", "ApiClientErrorCode", "ApiClientErrorDetails"]));
|
|
963
988
|
const controllerEntries = [];
|
|
964
989
|
const routeTypes = [];
|
|
965
990
|
const schemaEntries = [];
|
|
@@ -1017,11 +1042,14 @@ function renderClient(graph, options) {
|
|
|
1017
1042
|
}).filter((entry) => entry.length === 2));
|
|
1018
1043
|
if (Object.keys(responseSchemas).length > 0)
|
|
1019
1044
|
usesValue = true;
|
|
1020
|
-
const paramNames = [...new Set(
|
|
1045
|
+
const paramNames = [...new Set([
|
|
1046
|
+
...(fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1)),
|
|
1047
|
+
...fullPath.endsWith("/*") ? ["*"] : []
|
|
1048
|
+
])];
|
|
1021
1049
|
const pathParamsType = paramNames.length > 0 ? `{ ${paramNames.map((p) => `${/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(p) ? p : JSON.stringify(p)}: string | number`).join("; ")} }` : "Record<string, string | number>";
|
|
1022
1050
|
const paramsType = paramsSchema ? `Static<typeof ${paramsSchema.local}>${paramNames.length > 0 ? ` & ${pathParamsType}` : ""}` : pathParamsType;
|
|
1023
1051
|
const queryType = querySchema ? `Static<typeof ${querySchema.local}>` : "Record<string, unknown>";
|
|
1024
|
-
const bodyType = bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
|
|
1052
|
+
const bodyType = route.parse === "none" ? "BodyInit" : bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
|
|
1025
1053
|
const headersType = headersSchema ? `Static<typeof ${headersSchema.local}>` : "Record<string, string>";
|
|
1026
1054
|
const cookieType = cookieSchema ? `Static<typeof ${cookieSchema.local}>` : "Record<string, string | number | boolean>";
|
|
1027
1055
|
const responseTypes = [
|
|
@@ -1058,7 +1086,9 @@ function renderClient(graph, options) {
|
|
|
1058
1086
|
["queryTransforms", route.queryTransforms],
|
|
1059
1087
|
["queryDefaults", route.queryDefaults],
|
|
1060
1088
|
["title", route.title],
|
|
1061
|
-
["data", route.data]
|
|
1089
|
+
["data", route.data],
|
|
1090
|
+
["parse", route.parse],
|
|
1091
|
+
["allowDeleteBody", route.allowDeleteBody]
|
|
1062
1092
|
]) {
|
|
1063
1093
|
if (value !== undefined)
|
|
1064
1094
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -1092,7 +1122,7 @@ function renderClient(graph, options) {
|
|
|
1092
1122
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
1093
1123
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
1094
1124
|
routeMethods.push(`
|
|
1095
|
-
${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}),`);
|
|
1125
|
+
${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}, ${route.parse === "none"}),`);
|
|
1096
1126
|
}
|
|
1097
1127
|
controllerEntries.push(`
|
|
1098
1128
|
${controllerKey}: {${routeMethods.join("")}
|
|
@@ -1123,6 +1153,40 @@ function renderClient(graph, options) {
|
|
|
1123
1153
|
"",
|
|
1124
1154
|
"export type ResponseDecoder<T> = (value: unknown) => T;",
|
|
1125
1155
|
"",
|
|
1156
|
+
'export type ApiClientErrorCode = "API_HTTP_ERROR" | "API_RESPONSE_INVALID" | "API_RESPONSE_UNDECLARED";',
|
|
1157
|
+
"",
|
|
1158
|
+
"export interface ApiClientErrorDetails {",
|
|
1159
|
+
" code: ApiClientErrorCode;",
|
|
1160
|
+
" method: string;",
|
|
1161
|
+
" path: string;",
|
|
1162
|
+
" status: number;",
|
|
1163
|
+
" requestId?: string | null;",
|
|
1164
|
+
" response?: Response;",
|
|
1165
|
+
"}",
|
|
1166
|
+
"",
|
|
1167
|
+
"/** Transport/contract metadata, not proof that a failed write rolled back. */",
|
|
1168
|
+
"export class ApiClientError extends Error {",
|
|
1169
|
+
" readonly code: ApiClientErrorCode;",
|
|
1170
|
+
" readonly method: string;",
|
|
1171
|
+
" readonly path: string;",
|
|
1172
|
+
" readonly status: number;",
|
|
1173
|
+
" readonly requestId: string | undefined;",
|
|
1174
|
+
" /** Undeclared HTTP response for explicit inspection; excluded from JSON serialization. */",
|
|
1175
|
+
" readonly response: Response | undefined;",
|
|
1176
|
+
" constructor(message: string, details: ApiClientErrorDetails) {",
|
|
1177
|
+
" super(message);",
|
|
1178
|
+
' this.name = "ApiClientError";',
|
|
1179
|
+
" const { code, method, path, status, requestId, response } = details;",
|
|
1180
|
+
" this.code = code;",
|
|
1181
|
+
" this.method = method;",
|
|
1182
|
+
" this.path = path;",
|
|
1183
|
+
" this.status = status;",
|
|
1184
|
+
" this.requestId = requestId && /^[A-Za-z0-9._:-]{1,256}$/.test(requestId) ? requestId : undefined;",
|
|
1185
|
+
" this.response = response;",
|
|
1186
|
+
' Object.defineProperty(this, "response", { enumerable: false });',
|
|
1187
|
+
" }",
|
|
1188
|
+
"}",
|
|
1189
|
+
"",
|
|
1126
1190
|
"export type RouteMethod<",
|
|
1127
1191
|
" Options extends ClientRequestOptions = ClientRequestOptions,",
|
|
1128
1192
|
" Result = never,",
|
|
@@ -1176,6 +1240,13 @@ function renderClient(graph, options) {
|
|
|
1176
1240
|
' if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`Missing route parameter: ${key}`);',
|
|
1177
1241
|
" return encodeURIComponent(String(value));",
|
|
1178
1242
|
" });",
|
|
1243
|
+
' if (path.endsWith("/*")) {',
|
|
1244
|
+
' const wildcard = params && Object.hasOwn(params, "*") ? params["*"] : undefined;',
|
|
1245
|
+
' if (typeof wildcard !== "string" && typeof wildcard !== "number") throw new TypeError("Missing route parameter: *");',
|
|
1246
|
+
' const segments = String(wildcard).split("/");',
|
|
1247
|
+
' if (segments.some(segment => segment === "." || segment === "..")) throw new TypeError("Wildcard dot segments are not supported by Fetch URLs");',
|
|
1248
|
+
' url = url.slice(0, -1) + segments.map(encodeURIComponent).join("/");',
|
|
1249
|
+
" }",
|
|
1179
1250
|
" if (query) {",
|
|
1180
1251
|
" const searchParams = new URLSearchParams();",
|
|
1181
1252
|
" for (const [k, v] of Object.entries(query)) {",
|
|
@@ -1362,6 +1433,10 @@ function renderClient(graph, options) {
|
|
|
1362
1433
|
"export function createApiClient(config: ApiClientConfig = {}) {",
|
|
1363
1434
|
" const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
|
|
1364
1435
|
' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
|
|
1436
|
+
" function rawBody(value: unknown): BodyInit {",
|
|
1437
|
+
' if (typeof value === "string" || value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams || value instanceof ArrayBuffer || ArrayBuffer.isView(value) || value instanceof ReadableStream) return value as BodyInit;',
|
|
1438
|
+
' throw new TypeError("Raw routes require a BodyInit value; JSON encoding is not implicit");',
|
|
1439
|
+
" }",
|
|
1365
1440
|
"",
|
|
1366
1441
|
" async function request<T>(",
|
|
1367
1442
|
" method: string,",
|
|
@@ -1370,6 +1445,7 @@ function renderClient(graph, options) {
|
|
|
1370
1445
|
" decode: ResponseDecoder<T>,",
|
|
1371
1446
|
` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
|
|
1372
1447
|
" responseKind?: string,",
|
|
1448
|
+
" raw?: boolean,",
|
|
1373
1449
|
" ): Promise<T>;",
|
|
1374
1450
|
" async function request<T>(",
|
|
1375
1451
|
" method: string,",
|
|
@@ -1378,6 +1454,7 @@ function renderClient(graph, options) {
|
|
|
1378
1454
|
" decode?: ResponseDecoder<T>,",
|
|
1379
1455
|
` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
|
|
1380
1456
|
" responseKind?: string,",
|
|
1457
|
+
" raw?: boolean,",
|
|
1381
1458
|
" ): Promise<T | unknown>;",
|
|
1382
1459
|
" async function request(",
|
|
1383
1460
|
" method: string,",
|
|
@@ -1391,11 +1468,12 @@ function renderClient(graph, options) {
|
|
|
1391
1468
|
" decode?: ResponseDecoder<T>,",
|
|
1392
1469
|
` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
|
|
1393
1470
|
" responseKind?: string,",
|
|
1471
|
+
" raw = false,",
|
|
1394
1472
|
" ): Promise<T | unknown> {",
|
|
1395
1473
|
" const url = `${baseUrl}${buildRouteUrl(path, options.params, options.query)}`;",
|
|
1396
1474
|
' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
|
|
1397
1475
|
" const headers: Record<string, string> = {",
|
|
1398
|
-
' "content-type": "application/json",',
|
|
1476
|
+
' ...(raw ? {} : { "content-type": "application/json" }),',
|
|
1399
1477
|
" ...customHeaders,",
|
|
1400
1478
|
" ...options.headers,",
|
|
1401
1479
|
" };",
|
|
@@ -1414,21 +1492,27 @@ function renderClient(graph, options) {
|
|
|
1414
1492
|
" return fetcher(reqPayload.url, {",
|
|
1415
1493
|
" method: reqPayload.method,",
|
|
1416
1494
|
" headers: reqPayload.headers,",
|
|
1417
|
-
" ...(reqPayload.body === undefined ? {} : { body: JSON.stringify(reqPayload.body) }),",
|
|
1495
|
+
" ...(reqPayload.body === undefined ? {} : { body: raw ? rawBody(reqPayload.body) : JSON.stringify(reqPayload.body) }),",
|
|
1496
|
+
' ...(raw && reqPayload.body instanceof ReadableStream ? { duplex: "half" } : {}),',
|
|
1418
1497
|
" });",
|
|
1419
1498
|
" };",
|
|
1420
1499
|
" const response = await executeChain(0, { method, url, headers, body: options.body });",
|
|
1421
1500
|
" const declaredSchema = responseSchemas ? selectResponseSchema(response.status, responseSchemas) : undefined;",
|
|
1501
|
+
" const failure = (code: ApiClientErrorCode, message: string) => new ApiClientError(message, {",
|
|
1502
|
+
' code, method, path, status: response.status, requestId: response.headers.get("x-request-id"),',
|
|
1503
|
+
' ...(code === "API_HTTP_ERROR" ? { response } : {}),',
|
|
1504
|
+
" });",
|
|
1422
1505
|
" if (!response.ok && declaredSchema === undefined) {",
|
|
1423
|
-
|
|
1424
|
-
" throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
|
|
1506
|
+
' throw failure("API_HTTP_ERROR", `API request failed: ${method} ${path} -> ${response.status}`);',
|
|
1425
1507
|
" }",
|
|
1426
1508
|
' const contentType = response.headers?.get("content-type") ?? "";',
|
|
1427
1509
|
" let value: unknown;",
|
|
1428
1510
|
" if ([204, 205, 304].includes(response.status)) {",
|
|
1429
1511
|
" value = undefined;",
|
|
1430
1512
|
' } else if (contentType.includes("application/json") || contentType.includes("+json")) {',
|
|
1431
|
-
"
|
|
1513
|
+
" const responseText = await response.text();",
|
|
1514
|
+
" try { value = JSON.parse(responseText); }",
|
|
1515
|
+
' catch { throw failure("API_RESPONSE_INVALID", "Response is not valid JSON"); }',
|
|
1432
1516
|
' } else if (responseKind === "binary" || contentType.includes("application/octet-stream")) {',
|
|
1433
1517
|
" value = await response.arrayBuffer();",
|
|
1434
1518
|
' } else if (responseKind === "stream") {',
|
|
@@ -1439,20 +1523,24 @@ function renderClient(graph, options) {
|
|
|
1439
1523
|
" if (!responseSchemas) return decode ? decode(value) : value;",
|
|
1440
1524
|
" if (declaredSchema === undefined) {",
|
|
1441
1525
|
' const transportSuccess = response.ok && (responseKind === "binary" || responseKind === "stream");',
|
|
1442
|
-
|
|
1526
|
+
' if (!transportSuccess) throw failure("API_RESPONSE_UNDECLARED", `No response schema declared for HTTP ${response.status}`);',
|
|
1443
1527
|
" return decode ? decode(value) : value;",
|
|
1444
1528
|
" }",
|
|
1445
|
-
...usesValue ? [
|
|
1529
|
+
...usesValue ? [
|
|
1530
|
+
" let checked: unknown;",
|
|
1531
|
+
" try { checked = decodeResponseSchema(value, response.status, responseSchemas, { normalize: config.normalize ?? true }); }",
|
|
1532
|
+
' catch { throw failure("API_RESPONSE_INVALID", "Response does not match schema"); }'
|
|
1533
|
+
] : [" const checked = value;"],
|
|
1446
1534
|
" return decode ? decode(checked) : checked;",
|
|
1447
1535
|
" }",
|
|
1448
1536
|
"",
|
|
1449
|
-
` function makeRoute<Options extends ClientRequestOptions, Result = never>(method: string, path: string, responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>, responseKind?: string): RouteMethod<Options, Result> {`,
|
|
1537
|
+
` function makeRoute<Options extends ClientRequestOptions, Result = never>(method: string, path: string, responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>, responseKind?: string, raw = false): RouteMethod<Options, Result> {`,
|
|
1450
1538
|
" function route<T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
|
|
1451
1539
|
" function route(options: Options): Promise<Result>;",
|
|
1452
1540
|
" function route(options?: Options): Promise<unknown>;",
|
|
1453
1541
|
" function route<T>(options?: Options, decode?: ResponseDecoder<T>): Promise<T | unknown> {",
|
|
1454
1542
|
" const requestOptions = options ?? {};",
|
|
1455
|
-
" return request(method, path, requestOptions, decode, responseSchemas, responseKind);",
|
|
1543
|
+
" return request(method, path, requestOptions, decode, responseSchemas, responseKind, raw);",
|
|
1456
1544
|
" }",
|
|
1457
1545
|
" return route as RouteMethod<Options, Result>;",
|
|
1458
1546
|
" }",
|
|
@@ -1509,11 +1597,15 @@ function renderOpenApi(graph, options) {
|
|
|
1509
1597
|
for (const module of graph.modules) {
|
|
1510
1598
|
for (const controller of module.controllers) {
|
|
1511
1599
|
for (const route of controller.routes) {
|
|
1600
|
+
const documentation = route.data?.openapi;
|
|
1601
|
+
if (documentation !== null && typeof documentation === "object" && "hide" in documentation && documentation.hide === true)
|
|
1602
|
+
continue;
|
|
1512
1603
|
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1513
1604
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1514
1605
|
const hasResponseMap = responseEntries.length > 0;
|
|
1515
1606
|
const pathParams = [...new Set([
|
|
1516
1607
|
...(fullPath.match(/:([A-Za-z0-9_]+)/g) ?? []).map((item) => item.slice(1)),
|
|
1608
|
+
...fullPath.endsWith("/*") ? ["*"] : [],
|
|
1517
1609
|
...route.pathParams ?? []
|
|
1518
1610
|
].filter((item) => item.length > 0))];
|
|
1519
1611
|
const queryParams = [...new Set([
|
|
@@ -1546,6 +1638,8 @@ function renderOpenApi(graph, options) {
|
|
|
1546
1638
|
}).filter((entry) => entry.length === 2)) } : {},
|
|
1547
1639
|
responseContentType,
|
|
1548
1640
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
1641
|
+
...route.parse === undefined ? {} : { parse: route.parse },
|
|
1642
|
+
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
1549
1643
|
module: module.name,
|
|
1550
1644
|
controller: controller.className,
|
|
1551
1645
|
handler: route.handler,
|
|
@@ -1629,6 +1723,8 @@ function renderOpenApi(graph, options) {
|
|
|
1629
1723
|
" canMatch?: readonly string[];",
|
|
1630
1724
|
" canDeactivate?: readonly string[];",
|
|
1631
1725
|
" contract?: Record<string, unknown>;",
|
|
1726
|
+
' parse?: "none";',
|
|
1727
|
+
" allowDeleteBody?: true;",
|
|
1632
1728
|
" data?: Record<string, unknown>;",
|
|
1633
1729
|
" requiresAuth?: boolean;",
|
|
1634
1730
|
"};",
|
|
@@ -1826,8 +1922,15 @@ function renderOpenApi(graph, options) {
|
|
|
1826
1922
|
' return { $ref: `#/components/schemas/${encodeURIComponent(name).replaceAll("%2F", "/")}` };',
|
|
1827
1923
|
"}",
|
|
1828
1924
|
"",
|
|
1925
|
+
"function wildcardParameterName(path: string): string {",
|
|
1926
|
+
" const names = new Set([...path.matchAll(/:([A-Za-z0-9_]+)/g)].map(match => match[1]));",
|
|
1927
|
+
' let name = "wildcard";',
|
|
1928
|
+
' while (names.has(name)) name += "_";',
|
|
1929
|
+
" return name;",
|
|
1930
|
+
"}",
|
|
1931
|
+
"",
|
|
1829
1932
|
"function openApiPath(path: string): string {",
|
|
1830
|
-
' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");',
|
|
1933
|
+
' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}").replace(/\\/\\*$/, `/{${wildcardParameterName(path)}}`);',
|
|
1831
1934
|
"}",
|
|
1832
1935
|
"",
|
|
1833
1936
|
"function responseHasBody(status: string, kind: string | undefined): boolean {",
|
|
@@ -1857,7 +1960,7 @@ function renderOpenApi(graph, options) {
|
|
|
1857
1960
|
" const parameters: Record<string, unknown>[] = [];",
|
|
1858
1961
|
" const paramsSchema = definition.paramsSchema ? OPENAPI_SCHEMA_REGISTRY[definition.paramsSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
1859
1962
|
" for (const name of definition.pathParams) parameters.push({",
|
|
1860
|
-
' name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
|
|
1963
|
+
' name: name === "*" ? wildcardParameterName(definition.path) : name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
|
|
1861
1964
|
" });",
|
|
1862
1965
|
" const querySchema = definition.querySchema ? OPENAPI_SCHEMA_REGISTRY[definition.querySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
1863
1966
|
" const queryNames = new Set([...(querySchema ? schemaPropertyNames(querySchema) : []), ...definition.queryParams]);",
|
|
@@ -1894,6 +1997,7 @@ function renderOpenApi(graph, options) {
|
|
|
1894
1997
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
1895
1998
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
1896
1999
|
' ...(definition.bodySchema ? { requestBody: { required: !schemaAllowsUndefined(OPENAPI_SCHEMA_REGISTRY[definition.bodySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY]), content: { "application/json": { schema: schemaReference(definition.bodySchema) } } } } : {}),',
|
|
2000
|
+
' ...(definition.parse === "none" ? { requestBody: { required: false, description: "Raw request bytes; validation and media types are owned by the domain handler.", content: { "*/*": { schema: { type: "string", format: "binary" } } } } } : {}),',
|
|
1897
2001
|
" ...(definition.requiresAuth ? { security: [{ bearerAuth: [] }] } : {}),",
|
|
1898
2002
|
' "x-supacloud": {',
|
|
1899
2003
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
@@ -1903,6 +2007,8 @@ function renderOpenApi(graph, options) {
|
|
|
1903
2007
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
1904
2008
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
1905
2009
|
" ...(definition.contract ? { contract: definition.contract } : {}),",
|
|
2010
|
+
" ...(definition.parse ? { parse: definition.parse } : {}),",
|
|
2011
|
+
" ...(definition.allowDeleteBody ? { allowDeleteBody: true } : {}),",
|
|
1906
2012
|
" ...(definition.data ? { data: definition.data } : {}),",
|
|
1907
2013
|
" },",
|
|
1908
2014
|
" };",
|
|
@@ -1998,6 +2104,8 @@ function renderPermissions(graph) {
|
|
|
1998
2104
|
`);
|
|
1999
2105
|
}
|
|
2000
2106
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES = `export interface CompiledRoute {
|
|
2107
|
+
parse?: "none";
|
|
2108
|
+
allowDeleteBody?: true;
|
|
2001
2109
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
2002
2110
|
path: string;
|
|
2003
2111
|
handler: string;
|
|
@@ -2150,6 +2258,12 @@ function resolveFactoryValue(value: unknown): unknown {
|
|
|
2150
2258
|
}
|
|
2151
2259
|
|
|
2152
2260
|
const scopeDestructions = new WeakMap<object, Promise<void>>();
|
|
2261
|
+
const platformDependencies = new WeakMap<object, Record<string, unknown>>();
|
|
2262
|
+
|
|
2263
|
+
function retainPlatformDependencies<T extends Record<string, unknown>>(services: T, deps: Record<string, unknown>): T {
|
|
2264
|
+
platformDependencies.set(services, deps);
|
|
2265
|
+
return services;
|
|
2266
|
+
}
|
|
2153
2267
|
|
|
2154
2268
|
function destroyScopeInstances(
|
|
2155
2269
|
scope: Record<string, unknown>,
|
|
@@ -2696,9 +2810,9 @@ var init_graphql = __esm(() => {
|
|
|
2696
2810
|
});
|
|
2697
2811
|
|
|
2698
2812
|
// src/graphql-schema.ts
|
|
2699
|
-
import { mkdir as mkdir5, readFile as
|
|
2700
|
-
import { createHash as
|
|
2701
|
-
import { dirname as dirname10, resolve as
|
|
2813
|
+
import { mkdir as mkdir5, readFile as readFile12 } from "node:fs/promises";
|
|
2814
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
2815
|
+
import { dirname as dirname10, resolve as resolve16 } from "node:path";
|
|
2702
2816
|
async function pullGraphqlSchema(options) {
|
|
2703
2817
|
assertGraphqlOptions({ schema: options.output });
|
|
2704
2818
|
const endpoint = new URL(options.url);
|
|
@@ -2738,7 +2852,7 @@ async function pullGraphqlSchema(options) {
|
|
|
2738
2852
|
throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
|
|
2739
2853
|
}
|
|
2740
2854
|
const schema = lexicographicSortSchema(buildClientSchema(data));
|
|
2741
|
-
const path =
|
|
2855
|
+
const path = resolve16(options.output);
|
|
2742
2856
|
const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
|
|
2743
2857
|
` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
|
|
2744
2858
|
# Database First: change database declarations, apply migrations, then re-export for the intended role.
|
|
@@ -2746,13 +2860,13 @@ async function pullGraphqlSchema(options) {
|
|
|
2746
2860
|
`;
|
|
2747
2861
|
let previous;
|
|
2748
2862
|
try {
|
|
2749
|
-
previous = await
|
|
2863
|
+
previous = await readFile12(path, "utf8");
|
|
2750
2864
|
} catch (error) {
|
|
2751
2865
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2752
2866
|
throw error;
|
|
2753
2867
|
}
|
|
2754
2868
|
const upToDate = previous === content;
|
|
2755
|
-
const schemaHash =
|
|
2869
|
+
const schemaHash = createHash10("sha256").update(content).digest("hex");
|
|
2756
2870
|
if (options.check)
|
|
2757
2871
|
return { path, schemaHash, upToDate, written: false };
|
|
2758
2872
|
await mkdir5(dirname10(path), { recursive: true });
|
|
@@ -2765,12 +2879,12 @@ var init_graphql_schema = __esm(() => {
|
|
|
2765
2879
|
});
|
|
2766
2880
|
|
|
2767
2881
|
// src/database-contracts.ts
|
|
2768
|
-
import { createHash as
|
|
2769
|
-
import { mkdir as mkdir6, readFile as
|
|
2770
|
-
import { dirname as dirname11, relative as relative12, resolve as
|
|
2882
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
2883
|
+
import { mkdir as mkdir6, readFile as readFile13 } from "node:fs/promises";
|
|
2884
|
+
import { dirname as dirname11, relative as relative12, resolve as resolve17 } from "node:path";
|
|
2771
2885
|
import * as ts13 from "@typescript/typescript6";
|
|
2772
2886
|
function hash2(value) {
|
|
2773
|
-
return
|
|
2887
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
2774
2888
|
}
|
|
2775
2889
|
function importPath(out, path) {
|
|
2776
2890
|
const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
|
|
@@ -2795,19 +2909,19 @@ function parseDatabaseContractsOptions(value, directory) {
|
|
|
2795
2909
|
throw new TypeError("migrations must be an ordered list of SQL files");
|
|
2796
2910
|
}
|
|
2797
2911
|
return {
|
|
2798
|
-
rootDir:
|
|
2799
|
-
outDir:
|
|
2800
|
-
postgrestTypes:
|
|
2801
|
-
drizzleSchema:
|
|
2912
|
+
rootDir: resolve17(directory, field("rootDir")),
|
|
2913
|
+
outDir: resolve17(directory, field("outDir")),
|
|
2914
|
+
postgrestTypes: resolve17(directory, field("postgrestTypes")),
|
|
2915
|
+
drizzleSchema: resolve17(directory, field("drizzleSchema")),
|
|
2802
2916
|
role: field("role"),
|
|
2803
|
-
graphql: { ...graphql, schema:
|
|
2804
|
-
migrations: migrations.map((file) =>
|
|
2917
|
+
graphql: { ...graphql, schema: resolve17(directory, graphql.schema) },
|
|
2918
|
+
migrations: migrations.map((file) => resolve17(directory, file))
|
|
2805
2919
|
};
|
|
2806
2920
|
}
|
|
2807
2921
|
async function generateDatabaseContracts(options, check = false) {
|
|
2808
|
-
const rootDir =
|
|
2809
|
-
const postgrestTypes =
|
|
2810
|
-
const snapshot = await
|
|
2922
|
+
const rootDir = resolve17(options.rootDir), outDir = resolve17(options.outDir);
|
|
2923
|
+
const postgrestTypes = resolve17(rootDir, options.postgrestTypes), drizzleSchema = resolve17(rootDir, options.drizzleSchema);
|
|
2924
|
+
const snapshot = await readFile13(postgrestTypes, "utf8");
|
|
2811
2925
|
const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
|
|
2812
2926
|
const database = syntax.statements.find((node) => (ts13.isTypeAliasDeclaration(node) || ts13.isInterfaceDeclaration(node)) && node.name.text === "Database" && node.modifiers?.some((modifier) => modifier.kind === ts13.SyntaxKind.ExportKeyword));
|
|
2813
2927
|
if (!database)
|
|
@@ -2834,8 +2948,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2834
2948
|
}
|
|
2835
2949
|
const inputs = {};
|
|
2836
2950
|
const addInput = async (path) => {
|
|
2837
|
-
const absolute =
|
|
2838
|
-
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await
|
|
2951
|
+
const absolute = resolve17(rootDir, path);
|
|
2952
|
+
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile13(absolute, "utf8"));
|
|
2839
2953
|
};
|
|
2840
2954
|
await addInput(postgrestTypes);
|
|
2841
2955
|
await addInput(drizzleSchema);
|
|
@@ -2851,8 +2965,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2851
2965
|
if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
|
|
2852
2966
|
await addInput(source.fileName);
|
|
2853
2967
|
}
|
|
2854
|
-
await addInput(
|
|
2855
|
-
if (new Set(options.migrations.map((path) =>
|
|
2968
|
+
await addInput(resolve17(rootDir, options.graphql.schema));
|
|
2969
|
+
if (new Set(options.migrations.map((path) => resolve17(rootDir, path))).size !== options.migrations.length) {
|
|
2856
2970
|
throw new Error("Duplicate migration in database contracts configuration");
|
|
2857
2971
|
}
|
|
2858
2972
|
for (const path of options.migrations)
|
|
@@ -2873,17 +2987,17 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2873
2987
|
version: 1,
|
|
2874
2988
|
role: options.role,
|
|
2875
2989
|
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
|
|
2876
|
-
migrationOrder: options.migrations.map((path) => relative12(rootDir,
|
|
2990
|
+
migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve17(rootDir, path)).replaceAll("\\", "/")),
|
|
2877
2991
|
outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
|
|
2878
2992
|
};
|
|
2879
2993
|
files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
|
|
2880
2994
|
`;
|
|
2881
2995
|
const mismatches = [];
|
|
2882
2996
|
for (const [file, content] of Object.entries(files)) {
|
|
2883
|
-
const path =
|
|
2997
|
+
const path = resolve17(outDir, file);
|
|
2884
2998
|
let current;
|
|
2885
2999
|
try {
|
|
2886
|
-
current = await
|
|
3000
|
+
current = await readFile13(path, "utf8");
|
|
2887
3001
|
} catch (error) {
|
|
2888
3002
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2889
3003
|
throw error;
|
|
@@ -2894,13 +3008,13 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2894
3008
|
if (!check) {
|
|
2895
3009
|
await mkdir6(outDir, { recursive: true });
|
|
2896
3010
|
for (const [file, content] of Object.entries(files))
|
|
2897
|
-
await writeFileIfChanged(
|
|
3011
|
+
await writeFileIfChanged(resolve17(outDir, file), content);
|
|
2898
3012
|
}
|
|
2899
3013
|
return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
|
|
2900
3014
|
}
|
|
2901
3015
|
async function runDatabaseContractsFile(path, check = false) {
|
|
2902
|
-
const absolute =
|
|
2903
|
-
const value = JSON.parse(await
|
|
3016
|
+
const absolute = resolve17(path);
|
|
3017
|
+
const value = JSON.parse(await readFile13(absolute, "utf8"));
|
|
2904
3018
|
return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname11(absolute)), check);
|
|
2905
3019
|
}
|
|
2906
3020
|
var init_database_contracts = __esm(() => {
|
|
@@ -3608,6 +3722,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3608
3722
|
};
|
|
3609
3723
|
function validateGraph(graph, options = false) {
|
|
3610
3724
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
3725
|
+
const generateOpenApi = typeof options === "object" && options.generateOpenApi === true;
|
|
3611
3726
|
const diagnostics = [];
|
|
3612
3727
|
for (const module of graph.modules) {
|
|
3613
3728
|
for (const owner of [...module.providers, ...module.controllers]) {
|
|
@@ -3743,7 +3858,7 @@ function validateGraph(graph, options = false) {
|
|
|
3743
3858
|
}
|
|
3744
3859
|
const fullPath = joinRoutePaths2(controller.path, route.path);
|
|
3745
3860
|
const rawFullPath = joinRawRoutePaths(controller.path, route.path);
|
|
3746
|
-
const key = `${route.method} ${fullPath}`;
|
|
3861
|
+
const key = `${route.method} ${generateOpenApi ? fullPath.replace(/:[A-Za-z0-9_]+|\*$/g, "{}") : fullPath}`;
|
|
3747
3862
|
const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
|
|
3748
3863
|
if (openApiMatch) {
|
|
3749
3864
|
error("missing-param-colon", `Route path '${route.path}' in '${controller.className}.${route.handler}' uses OpenAPI-style '{${openApiMatch[1]}}'. SupaCloud routes require Express/Angular-style ':${openApiMatch[1]}'.`, controller.file, undefined, `Replace '{${openApiMatch[1]}}' with ':${openApiMatch[1]}'.`);
|
|
@@ -3843,7 +3958,13 @@ function validateGraph(graph, options = false) {
|
|
|
3843
3958
|
}
|
|
3844
3959
|
}
|
|
3845
3960
|
}
|
|
3846
|
-
if (
|
|
3961
|
+
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
3962
|
+
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
3963
|
+
}
|
|
3964
|
+
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
3965
|
+
error("invalid-raw-body-contract", `Route ${controller.className}.${route.handler} parse: "none" requires domain body ownership and evidence, without a parsed body schema or @Body binding.`, controller.file);
|
|
3966
|
+
}
|
|
3967
|
+
if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE" && !route.allowDeleteBody) && (route.hasBodyBinding || route.body)) {
|
|
3847
3968
|
error("disallowed-body-on-get-delete", `Route handler ${controller.className}.${route.handler} binds @Body() or declares body schema on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param().`, {
|
|
3848
3969
|
type: "remove_route_body_binding",
|
|
3849
3970
|
targetFile: controller.file,
|
|
@@ -4555,6 +4676,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4555
4676
|
tokensByName: new Map,
|
|
4556
4677
|
classesByName: new Map,
|
|
4557
4678
|
variablesByName: new Map,
|
|
4679
|
+
moduleHandlerFiles: new Map,
|
|
4558
4680
|
diagnostics: []
|
|
4559
4681
|
};
|
|
4560
4682
|
const nativeTraitFiles = new Map;
|
|
@@ -4645,10 +4767,11 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4645
4767
|
const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
|
|
4646
4768
|
const moduleFileExists = currentFileHashes.has(entry.module.file);
|
|
4647
4769
|
const isAffectedByDep = affectedModuleNames.has(modName);
|
|
4648
|
-
if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
|
|
4770
|
+
if (!hasChangedFile && !isAffectedByDep && moduleFileExists && entry.handlerFiles !== undefined) {
|
|
4649
4771
|
modulesToKeep.set(modName, entry);
|
|
4650
4772
|
reusedModules.push(modName);
|
|
4651
4773
|
finalModules.push(entry.module);
|
|
4774
|
+
ctx.moduleHandlerFiles.set(modName, entry.handlerFiles);
|
|
4652
4775
|
if (entry.diagnostics)
|
|
4653
4776
|
finalDiagnostics.push(...entry.diagnostics);
|
|
4654
4777
|
}
|
|
@@ -4668,6 +4791,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4668
4791
|
}
|
|
4669
4792
|
cache.modules.set(parsed.name, {
|
|
4670
4793
|
module: parsed,
|
|
4794
|
+
handlerFiles: ctx.moduleHandlerFiles.get(parsed.name) ?? [],
|
|
4671
4795
|
ownedFiles: [...ownedFiles],
|
|
4672
4796
|
fileHashes,
|
|
4673
4797
|
diagnostics: moduleDiagnostics
|
|
@@ -4836,6 +4960,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4836
4960
|
externalTokens,
|
|
4837
4961
|
diagnostics: ctx.diagnostics.map(withDiagnosticMetadata),
|
|
4838
4962
|
tokenNames,
|
|
4963
|
+
moduleHandlerFiles: Object.fromEntries(ctx.moduleHandlerFiles),
|
|
4839
4964
|
...cache ? { cacheStats: { reusedModules, reanalyzedModules } } : {}
|
|
4840
4965
|
};
|
|
4841
4966
|
}
|
|
@@ -5048,6 +5173,7 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
5048
5173
|
arrayProp(options, "commands").forEach(collectHandler);
|
|
5049
5174
|
arrayProp(options, "jobs").forEach(collectHandler);
|
|
5050
5175
|
arrayProp(options, "queries").forEach(collectHandler);
|
|
5176
|
+
ctx.moduleHandlerFiles.set(name, [...new Set([...handlerClasses].map((cls) => sourcePath(ctx.rootDir, cls.getSourceFile().fileName)))].sort());
|
|
5051
5177
|
const commands = [];
|
|
5052
5178
|
const jobs = [];
|
|
5053
5179
|
const queries = [];
|
|
@@ -5226,6 +5352,25 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
|
5226
5352
|
}
|
|
5227
5353
|
return ts4.isObjectLiteralExpression(input) ? input : undefined;
|
|
5228
5354
|
}
|
|
5355
|
+
function routePolicyProperty(object, name, ctx, seen = new Set) {
|
|
5356
|
+
if (seen.has(object))
|
|
5357
|
+
return { unresolved: true };
|
|
5358
|
+
const ancestry = new Set(seen).add(object);
|
|
5359
|
+
let result = { unresolved: false };
|
|
5360
|
+
for (const property of object.properties) {
|
|
5361
|
+
if (ts4.isSpreadAssignment(property)) {
|
|
5362
|
+
const spread = resolveStaticObjectLiteral(property.expression, ctx);
|
|
5363
|
+
const nested = spread ? routePolicyProperty(spread, name, ctx, ancestry) : { unresolved: true };
|
|
5364
|
+
if (nested.unresolved || nested.value !== undefined)
|
|
5365
|
+
result = nested;
|
|
5366
|
+
} else if (ts4.isComputedPropertyName(property.name)) {
|
|
5367
|
+
result = { unresolved: true };
|
|
5368
|
+
} else if (propertyName(property.name) === name) {
|
|
5369
|
+
result = ts4.isPropertyAssignment(property) ? { value: property.initializer, unresolved: false } : { unresolved: true };
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
return result;
|
|
5373
|
+
}
|
|
5229
5374
|
function commandModeProp(object, name) {
|
|
5230
5375
|
const value = stringLiteralProp(object, name);
|
|
5231
5376
|
return value === "required" || value === "none" ? value : undefined;
|
|
@@ -5733,10 +5878,13 @@ function parseController(input, ctx) {
|
|
|
5733
5878
|
const pathParams = [];
|
|
5734
5879
|
const paramRegex = /:([a-zA-Z0-9_]+)/g;
|
|
5735
5880
|
let match;
|
|
5736
|
-
|
|
5881
|
+
const fullPath = joinRoutePaths(path, routePath);
|
|
5882
|
+
while ((match = paramRegex.exec(fullPath)) !== null) {
|
|
5737
5883
|
if (match[1] !== undefined)
|
|
5738
5884
|
pathParams.push(match[1]);
|
|
5739
5885
|
}
|
|
5886
|
+
if (joinRoutePaths(path, routePath).endsWith("/*"))
|
|
5887
|
+
pathParams.push("*");
|
|
5740
5888
|
if (pathParams.length > 0)
|
|
5741
5889
|
route.pathParams = pathParams;
|
|
5742
5890
|
const paramBindings = [];
|
|
@@ -5887,6 +6035,40 @@ function parseController(input, ctx) {
|
|
|
5887
6035
|
const optionsArg = args[1];
|
|
5888
6036
|
const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
|
|
5889
6037
|
if (optionsObject) {
|
|
6038
|
+
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
6039
|
+
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
6040
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved) {
|
|
6041
|
+
ctx.diagnostics.push({
|
|
6042
|
+
severity: "error",
|
|
6043
|
+
code: "unresolved-route-body-policy",
|
|
6044
|
+
file,
|
|
6045
|
+
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
6046
|
+
});
|
|
6047
|
+
}
|
|
6048
|
+
const parse = parsePolicy.value;
|
|
6049
|
+
if (parse) {
|
|
6050
|
+
if (ts4.isStringLiteral(parse) && parse.text === "none")
|
|
6051
|
+
route.parse = "none";
|
|
6052
|
+
else
|
|
6053
|
+
ctx.diagnostics.push({
|
|
6054
|
+
severity: "error",
|
|
6055
|
+
code: "invalid-route-parser",
|
|
6056
|
+
file,
|
|
6057
|
+
message: `Route ${route.handler} parse must be the literal "none".`
|
|
6058
|
+
});
|
|
6059
|
+
}
|
|
6060
|
+
const allowDeleteBody = deletePolicy.value;
|
|
6061
|
+
if (allowDeleteBody) {
|
|
6062
|
+
if (allowDeleteBody.kind === ts4.SyntaxKind.TrueKeyword)
|
|
6063
|
+
route.allowDeleteBody = true;
|
|
6064
|
+
else
|
|
6065
|
+
ctx.diagnostics.push({
|
|
6066
|
+
severity: "error",
|
|
6067
|
+
code: "invalid-delete-body-opt-in",
|
|
6068
|
+
file,
|
|
6069
|
+
message: `Route ${route.handler} allowDeleteBody must be the literal true.`
|
|
6070
|
+
});
|
|
6071
|
+
}
|
|
5890
6072
|
const legacyResponseExpr = getProp(optionsObject, "response");
|
|
5891
6073
|
const responsesExpr = getProp(optionsObject, "responses");
|
|
5892
6074
|
if (legacyResponseExpr !== undefined && responsesExpr !== undefined) {
|
|
@@ -7080,7 +7262,7 @@ async function compileProject(options) {
|
|
|
7080
7262
|
...options,
|
|
7081
7263
|
...options.cache ? { artifactHashes: options.cache.generatedHashes } : {}
|
|
7082
7264
|
};
|
|
7083
|
-
const written = !hasErrors || options.writeOnError === true ? await
|
|
7265
|
+
const written = !hasErrors || options.writeOnError === true ? await writeRenderedApplication(rendered, generatedOptions) : [];
|
|
7084
7266
|
if (!hasErrors) {
|
|
7085
7267
|
for (const [filename, content] of Object.entries(graphql.files)) {
|
|
7086
7268
|
const path = join4(options.outDir, filename);
|
|
@@ -11438,6 +11620,48 @@ function importSymbol(source, path, symbol) {
|
|
|
11438
11620
|
return `import { ${symbol} } from ${JSON.stringify(path)};
|
|
11439
11621
|
${source.text}`;
|
|
11440
11622
|
}
|
|
11623
|
+
// src/repair-plan.ts
|
|
11624
|
+
function createDiagnosticRepairPlan(diagnostics) {
|
|
11625
|
+
return diagnostics.flatMap((diagnostic) => {
|
|
11626
|
+
const fix = diagnostic.fix;
|
|
11627
|
+
if (!fix)
|
|
11628
|
+
return [];
|
|
11629
|
+
return [{
|
|
11630
|
+
code: diagnostic.code,
|
|
11631
|
+
...diagnostic.errorCode === undefined ? {} : { errorCode: diagnostic.errorCode },
|
|
11632
|
+
...diagnostic.file === undefined ? {} : { file: diagnostic.file },
|
|
11633
|
+
...diagnostic.line === undefined ? {} : { line: diagnostic.line },
|
|
11634
|
+
type: fix.type,
|
|
11635
|
+
targetFile: fix.targetFile,
|
|
11636
|
+
fix,
|
|
11637
|
+
...classifyFix(fix)
|
|
11638
|
+
}];
|
|
11639
|
+
});
|
|
11640
|
+
}
|
|
11641
|
+
function classifyFix(fix) {
|
|
11642
|
+
switch (fix.type) {
|
|
11643
|
+
case "set_command_mode":
|
|
11644
|
+
if (fix.value !== "required" && fix.value !== "none") {
|
|
11645
|
+
return { readiness: "input-required", reason: "Choose an explicit transaction or idempotency policy; no policy is inferred." };
|
|
11646
|
+
}
|
|
11647
|
+
break;
|
|
11648
|
+
case "add_command_permission":
|
|
11649
|
+
if (!fix.permission?.trim()) {
|
|
11650
|
+
return { readiness: "input-required", reason: "Choose an explicit business permission; privileges are never inferred." };
|
|
11651
|
+
}
|
|
11652
|
+
break;
|
|
11653
|
+
case "add_module_import":
|
|
11654
|
+
if (!fix.importPath || !fix.symbol) {
|
|
11655
|
+
return { readiness: "input-required", reason: "Supply the module import path and exported symbol before previewing." };
|
|
11656
|
+
}
|
|
11657
|
+
break;
|
|
11658
|
+
case "add_route_parameter_binding":
|
|
11659
|
+
break;
|
|
11660
|
+
default:
|
|
11661
|
+
return { readiness: "manual", reason: "The semantic fix executor does not implement this suggestion; edit and verify it manually." };
|
|
11662
|
+
}
|
|
11663
|
+
return { readiness: "preview", reason: "Preview with the semantic fix executor; source preconditions are checked before writing." };
|
|
11664
|
+
}
|
|
11441
11665
|
// src/watch.ts
|
|
11442
11666
|
import { existsSync as existsSync3, watch } from "node:fs";
|
|
11443
11667
|
import { dirname as dirname7, relative as relative9, resolve as resolve10, sep as sep9 } from "node:path";
|
|
@@ -11585,6 +11809,8 @@ function optionsKeyOf(options) {
|
|
|
11585
11809
|
requireRouteContracts: options.requireRouteContracts,
|
|
11586
11810
|
detectOrphanModules: options.detectOrphanModules,
|
|
11587
11811
|
generateClient: options.generateClient,
|
|
11812
|
+
generateOpenApi: options.generateOpenApi,
|
|
11813
|
+
openApi: options.openApi,
|
|
11588
11814
|
generatePermissions: options.generatePermissions,
|
|
11589
11815
|
typeSafety: options.typeSafety,
|
|
11590
11816
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
@@ -11719,6 +11945,16 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
11719
11945
|
|
|
11720
11946
|
// src/watch.ts
|
|
11721
11947
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
11948
|
+
function isCompilerConfigurationPath(rootDir, changedPath) {
|
|
11949
|
+
const relativePath = relative9(rootDir, changedPath).split(sep9).join("/");
|
|
11950
|
+
return [
|
|
11951
|
+
"supacloud.config.ts",
|
|
11952
|
+
"supacloud.config.mts",
|
|
11953
|
+
"supacloud.config.js",
|
|
11954
|
+
"supacloud.config.mjs",
|
|
11955
|
+
"tsconfig.json"
|
|
11956
|
+
].includes(relativePath) || /^tsconfig\.[^/]+\.json$/.test(relativePath);
|
|
11957
|
+
}
|
|
11722
11958
|
function watchProject(options) {
|
|
11723
11959
|
const rootDir = resolve10(options.rootDir);
|
|
11724
11960
|
const outDir = resolve10(options.outDir);
|
|
@@ -11811,7 +12047,7 @@ function watchProject(options) {
|
|
|
11811
12047
|
const relativePath = relative9(outDir, changedPath);
|
|
11812
12048
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
11813
12049
|
return;
|
|
11814
|
-
if (/\.(tsx?|mts|cts)$/.test(changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
12050
|
+
if (/\.(tsx?|mts|cts)$/.test(changedPath) || isCompilerConfigurationPath(rootDir, changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
11815
12051
|
schedule(relative9(rootDir, changedPath));
|
|
11816
12052
|
}
|
|
11817
12053
|
});
|
|
@@ -11865,9 +12101,9 @@ function compilerVersion() {
|
|
|
11865
12101
|
}
|
|
11866
12102
|
function migrationDependencies() {
|
|
11867
12103
|
return {
|
|
11868
|
-
"@supacloud/app": "0.
|
|
12104
|
+
"@supacloud/app": "0.17.0",
|
|
11869
12105
|
"@supacloud/compiler": compilerVersion(),
|
|
11870
|
-
"@supacloud/elysia": "0.
|
|
12106
|
+
"@supacloud/elysia": "0.19.0",
|
|
11871
12107
|
elysia: "1.4.30",
|
|
11872
12108
|
typescript: "7.0.2"
|
|
11873
12109
|
};
|
|
@@ -11948,6 +12184,8 @@ function isDefineRouteContractCall(node, checker) {
|
|
|
11948
12184
|
return resolveSymbol(checker.getSymbolAtLocation(node.expression), checker)?.name === "defineRouteContract";
|
|
11949
12185
|
}
|
|
11950
12186
|
function isRouteDecoratorCall(node, checker) {
|
|
12187
|
+
if (node.arguments.length < 2)
|
|
12188
|
+
return false;
|
|
11951
12189
|
const name = node.expression.getText(node.getSourceFile());
|
|
11952
12190
|
if (ts12.isIdentifier(node.expression) && ROUTE_DECORATORS2.has(node.expression.text))
|
|
11953
12191
|
return true;
|
|
@@ -12232,7 +12470,7 @@ async function migrateProject(options) {
|
|
|
12232
12470
|
const before = sourceByPath.get(absoluteFile);
|
|
12233
12471
|
if (before === undefined)
|
|
12234
12472
|
continue;
|
|
12235
|
-
const result = projectResults
|
|
12473
|
+
const result = projectResults ? projectResults.results.get(absoluteFile) ?? { changed: false, content: before, replacements: 0, issues: [] } : migration.apply(before, file);
|
|
12236
12474
|
sourceByPath.set(absoluteFile, result.content);
|
|
12237
12475
|
if (result.changed && result.issues.length === 0) {
|
|
12238
12476
|
pendingWrites.set(absoluteFile, result.content);
|
|
@@ -12285,798 +12523,999 @@ async function migrateProject(options) {
|
|
|
12285
12523
|
issues
|
|
12286
12524
|
};
|
|
12287
12525
|
}
|
|
12288
|
-
// src/
|
|
12289
|
-
import {
|
|
12290
|
-
import {
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12526
|
+
// src/migration-assess.ts
|
|
12527
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
12528
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
12529
|
+
import { join as join7, resolve as resolve14 } from "node:path";
|
|
12530
|
+
|
|
12531
|
+
// src/openapi-tools.ts
|
|
12532
|
+
import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
12533
|
+
import { dirname as dirname8, resolve as resolve13 } from "node:path";
|
|
12534
|
+
import { pathToFileURL } from "node:url";
|
|
12535
|
+
|
|
12536
|
+
class OpenApiDocumentError extends Error {
|
|
12537
|
+
code = "OPENAPI_DOCUMENT_INVALID";
|
|
12538
|
+
constructor() {
|
|
12539
|
+
super("OpenAPI document is invalid or could not be loaded.");
|
|
12540
|
+
this.name = "OpenApiDocumentError";
|
|
12300
12541
|
}
|
|
12301
|
-
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
12302
|
-
return lines.join(`
|
|
12303
|
-
`);
|
|
12304
12542
|
}
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
12543
|
+
var HTTP_METHODS = [
|
|
12544
|
+
"get",
|
|
12545
|
+
"put",
|
|
12546
|
+
"post",
|
|
12547
|
+
"delete",
|
|
12548
|
+
"options",
|
|
12549
|
+
"head",
|
|
12550
|
+
"patch",
|
|
12551
|
+
"trace"
|
|
12552
|
+
];
|
|
12553
|
+
var COMPONENT_GROUPS = [
|
|
12554
|
+
"schemas",
|
|
12555
|
+
"responses",
|
|
12556
|
+
"parameters",
|
|
12557
|
+
"requestBodies",
|
|
12558
|
+
"headers",
|
|
12559
|
+
"securitySchemes"
|
|
12560
|
+
];
|
|
12561
|
+
function isRecord(value) {
|
|
12562
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12326
12563
|
}
|
|
12327
|
-
function
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12564
|
+
function stringValue(value) {
|
|
12565
|
+
return typeof value === "string" ? value : undefined;
|
|
12566
|
+
}
|
|
12567
|
+
function recordValue(value) {
|
|
12568
|
+
return isRecord(value) ? value : undefined;
|
|
12569
|
+
}
|
|
12570
|
+
function arrayValue(value) {
|
|
12571
|
+
return Array.isArray(value) ? value : [];
|
|
12572
|
+
}
|
|
12573
|
+
function sortedKeys(value) {
|
|
12574
|
+
return value ? Object.keys(value).sort() : [];
|
|
12575
|
+
}
|
|
12576
|
+
function sameJson(left, right) {
|
|
12577
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
12578
|
+
}
|
|
12579
|
+
function decodeJsonPointerPart(value) {
|
|
12580
|
+
return value.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
12581
|
+
}
|
|
12582
|
+
function resolveSchema(value, document, seen = new Set) {
|
|
12583
|
+
const schema = recordValue(value);
|
|
12584
|
+
if (!schema)
|
|
12585
|
+
return;
|
|
12586
|
+
const ref = stringValue(schema.$ref);
|
|
12587
|
+
if (!ref || !ref.startsWith("#/components/schemas/"))
|
|
12588
|
+
return schema;
|
|
12589
|
+
if (seen.has(ref))
|
|
12590
|
+
return schema;
|
|
12591
|
+
const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
|
|
12592
|
+
const components = recordValue(document.components);
|
|
12593
|
+
const schemas = recordValue(components?.schemas);
|
|
12594
|
+
const target = schemas?.[name];
|
|
12595
|
+
if (target === undefined)
|
|
12596
|
+
return schema;
|
|
12597
|
+
return resolveSchema(target, document, new Set([...seen, ref]));
|
|
12598
|
+
}
|
|
12599
|
+
function schemaEnum(value) {
|
|
12600
|
+
return Array.isArray(value?.enum) ? value.enum : undefined;
|
|
12601
|
+
}
|
|
12602
|
+
function schemaProperties(value) {
|
|
12603
|
+
return recordValue(value?.properties);
|
|
12604
|
+
}
|
|
12605
|
+
function schemaRequired(value) {
|
|
12606
|
+
return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
|
|
12607
|
+
}
|
|
12608
|
+
function schemaType(value) {
|
|
12609
|
+
return value?.type;
|
|
12610
|
+
}
|
|
12611
|
+
function addChange(changes, change) {
|
|
12612
|
+
if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
|
|
12613
|
+
return;
|
|
12614
|
+
changes.push(change);
|
|
12615
|
+
}
|
|
12616
|
+
function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
|
|
12617
|
+
const baseSchema = resolveSchema(baseValue, baseDocument);
|
|
12618
|
+
const currentSchema = resolveSchema(currentValue, currentDocument);
|
|
12619
|
+
if (!baseSchema || !currentSchema)
|
|
12620
|
+
return;
|
|
12621
|
+
const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
|
|
12622
|
+
if (seen.has(pair))
|
|
12623
|
+
return;
|
|
12624
|
+
seen.add(pair);
|
|
12625
|
+
if (schemaType(baseSchema) !== undefined && schemaType(currentSchema) !== undefined && !sameJson(schemaType(baseSchema), schemaType(currentSchema))) {
|
|
12626
|
+
addChange(changes, {
|
|
12627
|
+
kind: "breaking",
|
|
12628
|
+
code: "schema-type-changed",
|
|
12629
|
+
path,
|
|
12630
|
+
message: "The schema type changed."
|
|
12631
|
+
});
|
|
12331
12632
|
}
|
|
12332
|
-
const
|
|
12333
|
-
const
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12633
|
+
const baseEnum = schemaEnum(baseSchema);
|
|
12634
|
+
const currentEnum = schemaEnum(currentSchema);
|
|
12635
|
+
if (baseEnum && currentEnum) {
|
|
12636
|
+
for (const value of baseEnum) {
|
|
12637
|
+
if (!currentEnum.some((candidate) => sameJson(candidate, value))) {
|
|
12638
|
+
addChange(changes, {
|
|
12639
|
+
kind: "breaking",
|
|
12640
|
+
code: "schema-enum-value-removed",
|
|
12641
|
+
path,
|
|
12642
|
+
message: "An enum value accepted by the previous contract was removed."
|
|
12643
|
+
});
|
|
12644
|
+
break;
|
|
12645
|
+
}
|
|
12342
12646
|
}
|
|
12343
12647
|
}
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
if (!visited.has(neighbor) && byName.has(neighbor)) {
|
|
12358
|
-
visited.add(neighbor);
|
|
12359
|
-
selected.add(neighbor);
|
|
12360
|
-
queue.push(neighbor);
|
|
12648
|
+
const baseProperties = schemaProperties(baseSchema);
|
|
12649
|
+
const currentProperties = schemaProperties(currentSchema);
|
|
12650
|
+
if (baseProperties && currentProperties) {
|
|
12651
|
+
for (const name of sortedKeys(baseProperties)) {
|
|
12652
|
+
if (currentProperties[name] === undefined) {
|
|
12653
|
+
const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
|
|
12654
|
+
if (direction === "output" || inputRemovalIsBreaking) {
|
|
12655
|
+
addChange(changes, {
|
|
12656
|
+
kind: "breaking",
|
|
12657
|
+
code: direction === "output" ? "response-property-removed" : "request-property-removed",
|
|
12658
|
+
path: `${path}.properties.${name}`,
|
|
12659
|
+
message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
|
|
12660
|
+
});
|
|
12361
12661
|
}
|
|
12662
|
+
continue;
|
|
12362
12663
|
}
|
|
12664
|
+
compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
|
|
12363
12665
|
}
|
|
12364
12666
|
}
|
|
12365
|
-
const
|
|
12366
|
-
const
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
...module.providers.map((provider) => provider.file),
|
|
12378
|
-
...module.controllers.map((controller) => controller.file),
|
|
12379
|
-
...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
|
|
12380
|
-
]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
|
|
12381
|
-
const referencedTokens = new Set;
|
|
12382
|
-
for (const module of modules) {
|
|
12383
|
-
for (const provider of module.providers) {
|
|
12384
|
-
for (const token of provider.deps)
|
|
12385
|
-
referencedTokens.add(token);
|
|
12667
|
+
const baseRequired = schemaRequired(baseSchema);
|
|
12668
|
+
const currentRequired = schemaRequired(currentSchema);
|
|
12669
|
+
if (direction === "input") {
|
|
12670
|
+
for (const name of currentRequired) {
|
|
12671
|
+
if (!baseRequired.has(name)) {
|
|
12672
|
+
addChange(changes, {
|
|
12673
|
+
kind: "breaking",
|
|
12674
|
+
code: "request-property-required",
|
|
12675
|
+
path: `${path}.required`,
|
|
12676
|
+
message: `Request property '${name}' became required.`
|
|
12677
|
+
});
|
|
12678
|
+
}
|
|
12386
12679
|
}
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12680
|
+
if (baseSchema.additionalProperties !== false && currentSchema.additionalProperties === false) {
|
|
12681
|
+
addChange(changes, {
|
|
12682
|
+
kind: "breaking",
|
|
12683
|
+
code: "request-additional-properties-rejected",
|
|
12684
|
+
path,
|
|
12685
|
+
message: "The request schema now rejects additional properties."
|
|
12686
|
+
});
|
|
12390
12687
|
}
|
|
12391
|
-
}
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
|
|
12402
|
-
relatedModules: {
|
|
12403
|
-
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
12404
|
-
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
12688
|
+
} else {
|
|
12689
|
+
for (const name of baseRequired) {
|
|
12690
|
+
if (!currentRequired.has(name)) {
|
|
12691
|
+
addChange(changes, {
|
|
12692
|
+
kind: "breaking",
|
|
12693
|
+
code: "response-property-optional",
|
|
12694
|
+
path: `${path}.required`,
|
|
12695
|
+
message: `Response property '${name}' is no longer guaranteed.`
|
|
12696
|
+
});
|
|
12697
|
+
}
|
|
12405
12698
|
}
|
|
12406
|
-
}
|
|
12407
|
-
}
|
|
12408
|
-
function allAspects(module) {
|
|
12409
|
-
return [
|
|
12410
|
-
...module.aspects ?? [],
|
|
12411
|
-
...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
|
|
12412
|
-
...module.commands.flatMap((command) => command.aspects ?? []),
|
|
12413
|
-
...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
|
|
12414
|
-
];
|
|
12699
|
+
}
|
|
12415
12700
|
}
|
|
12416
|
-
function
|
|
12417
|
-
const
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
12422
|
-
return {
|
|
12423
|
-
module: module.name,
|
|
12424
|
-
kind: "route",
|
|
12425
|
-
name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
|
|
12426
|
-
...command ? { command: command.name } : {},
|
|
12427
|
-
stages: [
|
|
12428
|
-
...aspects(`module:${module.name}`, module.aspects),
|
|
12429
|
-
...aspects("route", route.aspects),
|
|
12430
|
-
...aspects("command", command?.aspects),
|
|
12431
|
-
...command ? [
|
|
12432
|
-
"commandExecutor",
|
|
12433
|
-
"authorize",
|
|
12434
|
-
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
12435
|
-
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
12436
|
-
...command.transaction === "required" ? ["transaction"] : []
|
|
12437
|
-
]
|
|
12438
|
-
] : [],
|
|
12439
|
-
"handler",
|
|
12440
|
-
...command?.audit && !command.rpc ? ["audit"] : []
|
|
12441
|
-
]
|
|
12442
|
-
};
|
|
12443
|
-
})),
|
|
12444
|
-
...module.commands.map((command) => ({
|
|
12445
|
-
module: module.name,
|
|
12446
|
-
kind: "command",
|
|
12447
|
-
name: command.name,
|
|
12448
|
-
command: command.name,
|
|
12449
|
-
stages: [
|
|
12450
|
-
"authorize",
|
|
12451
|
-
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
12452
|
-
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
12453
|
-
...command.transaction === "required" ? ["transaction"] : []
|
|
12454
|
-
],
|
|
12455
|
-
...aspects(`module:${module.name}`, module.aspects),
|
|
12456
|
-
...aspects("command", command.aspects),
|
|
12457
|
-
"handler",
|
|
12458
|
-
...command.audit && !command.rpc ? ["audit"] : []
|
|
12459
|
-
]
|
|
12460
|
-
})),
|
|
12461
|
-
...(module.jobs ?? []).map((job) => ({
|
|
12462
|
-
module: module.name,
|
|
12463
|
-
kind: "job",
|
|
12464
|
-
name: job.name,
|
|
12465
|
-
stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
|
|
12466
|
-
}))
|
|
12467
|
-
]);
|
|
12701
|
+
function parameterKey(value) {
|
|
12702
|
+
const parameter = recordValue(value);
|
|
12703
|
+
const name = stringValue(parameter?.name);
|
|
12704
|
+
const location = stringValue(parameter?.in);
|
|
12705
|
+
return name && location ? `${location}:${name}` : undefined;
|
|
12468
12706
|
}
|
|
12469
|
-
function
|
|
12470
|
-
const
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
name: "tsconfig",
|
|
12478
|
-
ok: existsSync4(join7(rootDir, "tsconfig.json")),
|
|
12479
|
-
detail: existsSync4(join7(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
12480
|
-
},
|
|
12481
|
-
{
|
|
12482
|
-
name: "modules",
|
|
12483
|
-
ok: graph.modules.length > 0,
|
|
12484
|
-
detail: `${graph.modules.length} module(s) discovered`
|
|
12485
|
-
},
|
|
12486
|
-
{
|
|
12487
|
-
name: "generated-artifacts",
|
|
12488
|
-
ok: upToDate,
|
|
12489
|
-
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
12707
|
+
function operationParameters(pathItem, operation) {
|
|
12708
|
+
const result = new Map;
|
|
12709
|
+
for (const source of [pathItem.parameters, operation.parameters]) {
|
|
12710
|
+
for (const item of arrayValue(source)) {
|
|
12711
|
+
const parameter = recordValue(item);
|
|
12712
|
+
const key = parameterKey(parameter);
|
|
12713
|
+
if (parameter && key)
|
|
12714
|
+
result.set(key, parameter);
|
|
12490
12715
|
}
|
|
12491
|
-
];
|
|
12492
|
-
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
12493
|
-
return {
|
|
12494
|
-
checks,
|
|
12495
|
-
diagnostics: allDiagnostics,
|
|
12496
|
-
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
12497
|
-
};
|
|
12498
|
-
}
|
|
12499
|
-
function explainModule(graph, module) {
|
|
12500
|
-
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
12501
|
-
return [
|
|
12502
|
-
`MODULE ${module.name}`,
|
|
12503
|
-
` file: ${module.file}:${module.line}`,
|
|
12504
|
-
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
12505
|
-
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
12506
|
-
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
12507
|
-
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
12508
|
-
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
|
|
12509
|
-
...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
|
|
12510
|
-
].join(`
|
|
12511
|
-
`);
|
|
12512
|
-
}
|
|
12513
|
-
function explainProvider(graph, module, provider) {
|
|
12514
|
-
const dependents = graph.modules.flatMap((candidate) => [
|
|
12515
|
-
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
12516
|
-
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
12517
|
-
]);
|
|
12518
|
-
return [
|
|
12519
|
-
`PROVIDER ${provider.token}`,
|
|
12520
|
-
` module: ${module.name}`,
|
|
12521
|
-
` file: ${provider.file}:${provider.line}`,
|
|
12522
|
-
` kind: ${provider.kind}`,
|
|
12523
|
-
` scope: ${provider.scope}`,
|
|
12524
|
-
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
12525
|
-
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
12526
|
-
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
12527
|
-
].join(`
|
|
12528
|
-
`);
|
|
12529
|
-
}
|
|
12530
|
-
function findProvider(graph, subject) {
|
|
12531
|
-
for (const module of graph.modules) {
|
|
12532
|
-
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
12533
|
-
if (provider)
|
|
12534
|
-
return { module, provider };
|
|
12535
12716
|
}
|
|
12536
|
-
return;
|
|
12717
|
+
return result;
|
|
12537
12718
|
}
|
|
12538
|
-
function
|
|
12539
|
-
|
|
12540
|
-
for (const mod of graph.modules) {
|
|
12541
|
-
const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12542
|
-
lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
|
|
12543
|
-
for (const imp of mod.imports) {
|
|
12544
|
-
const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12545
|
-
lines.push(` ${safeId} --> ${safeImp}`);
|
|
12546
|
-
}
|
|
12547
|
-
}
|
|
12548
|
-
return lines.join(`
|
|
12549
|
-
`);
|
|
12719
|
+
function parameterSchema(value) {
|
|
12720
|
+
return value.schema;
|
|
12550
12721
|
}
|
|
12551
|
-
function
|
|
12552
|
-
const
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12722
|
+
function compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12723
|
+
const baseParameters = operationParameters(basePathItem, baseOperation);
|
|
12724
|
+
const currentParameters = operationParameters(currentPathItem, currentOperation);
|
|
12725
|
+
for (const key of [...baseParameters.keys()].sort()) {
|
|
12726
|
+
const baseParameter = baseParameters.get(key);
|
|
12727
|
+
const currentParameter = currentParameters.get(key);
|
|
12728
|
+
if (!baseParameter || !currentParameter) {
|
|
12729
|
+
addChange(changes, {
|
|
12730
|
+
kind: "breaking",
|
|
12731
|
+
code: "parameter-removed",
|
|
12732
|
+
path: `${path}.parameters.${key}`,
|
|
12733
|
+
message: "A parameter from the previous contract was removed."
|
|
12734
|
+
});
|
|
12735
|
+
continue;
|
|
12736
|
+
}
|
|
12737
|
+
if (currentParameter.required === true && baseParameter.required !== true) {
|
|
12738
|
+
addChange(changes, {
|
|
12739
|
+
kind: "breaking",
|
|
12740
|
+
code: "parameter-required",
|
|
12741
|
+
path: `${path}.parameters.${key}`,
|
|
12742
|
+
message: "An optional parameter became required."
|
|
12743
|
+
});
|
|
12744
|
+
}
|
|
12745
|
+
if (parameterSchema(baseParameter) !== undefined && parameterSchema(currentParameter) !== undefined) {
|
|
12746
|
+
compareSchema(parameterSchema(baseParameter), parameterSchema(currentParameter), baseDocument, currentDocument, `${path}.parameters.${key}.schema`, "input", changes);
|
|
12561
12747
|
}
|
|
12562
12748
|
}
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
import { pathToFileURL } from "node:url";
|
|
12576
|
-
|
|
12577
|
-
class OpenApiDocumentError extends Error {
|
|
12578
|
-
code = "OPENAPI_DOCUMENT_INVALID";
|
|
12579
|
-
constructor() {
|
|
12580
|
-
super("OpenAPI document is invalid or could not be loaded.");
|
|
12581
|
-
this.name = "OpenApiDocumentError";
|
|
12749
|
+
for (const key of [...currentParameters.keys()].sort()) {
|
|
12750
|
+
if (baseParameters.has(key))
|
|
12751
|
+
continue;
|
|
12752
|
+
const parameter = currentParameters.get(key);
|
|
12753
|
+
if (!parameter)
|
|
12754
|
+
continue;
|
|
12755
|
+
addChange(changes, {
|
|
12756
|
+
kind: parameter.required === true ? "breaking" : "non-breaking",
|
|
12757
|
+
code: parameter.required === true ? "parameter-required" : "parameter-added",
|
|
12758
|
+
path: `${path}.parameters.${key}`,
|
|
12759
|
+
message: parameter.required === true ? "A new required parameter was added." : "An optional parameter was added."
|
|
12760
|
+
});
|
|
12582
12761
|
}
|
|
12583
12762
|
}
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
}
|
|
12611
|
-
function arrayValue(value) {
|
|
12612
|
-
return Array.isArray(value) ? value : [];
|
|
12613
|
-
}
|
|
12614
|
-
function sortedKeys(value) {
|
|
12615
|
-
return value ? Object.keys(value).sort() : [];
|
|
12616
|
-
}
|
|
12617
|
-
function sameJson(left, right) {
|
|
12618
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
12619
|
-
}
|
|
12620
|
-
function decodeJsonPointerPart(value) {
|
|
12621
|
-
return value.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
12622
|
-
}
|
|
12623
|
-
function resolveSchema(value, document, seen = new Set) {
|
|
12624
|
-
const schema = recordValue(value);
|
|
12625
|
-
if (!schema)
|
|
12626
|
-
return;
|
|
12627
|
-
const ref = stringValue(schema.$ref);
|
|
12628
|
-
if (!ref || !ref.startsWith("#/components/schemas/"))
|
|
12629
|
-
return schema;
|
|
12630
|
-
if (seen.has(ref))
|
|
12631
|
-
return schema;
|
|
12632
|
-
const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
|
|
12633
|
-
const components = recordValue(document.components);
|
|
12634
|
-
const schemas = recordValue(components?.schemas);
|
|
12635
|
-
const target = schemas?.[name];
|
|
12636
|
-
if (target === undefined)
|
|
12637
|
-
return schema;
|
|
12638
|
-
return resolveSchema(target, document, new Set([...seen, ref]));
|
|
12639
|
-
}
|
|
12640
|
-
function schemaEnum(value) {
|
|
12641
|
-
return Array.isArray(value?.enum) ? value.enum : undefined;
|
|
12642
|
-
}
|
|
12643
|
-
function schemaProperties(value) {
|
|
12644
|
-
return recordValue(value?.properties);
|
|
12645
|
-
}
|
|
12646
|
-
function schemaRequired(value) {
|
|
12647
|
-
return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
|
|
12648
|
-
}
|
|
12649
|
-
function schemaType(value) {
|
|
12650
|
-
return value?.type;
|
|
12651
|
-
}
|
|
12652
|
-
function addChange(changes, change) {
|
|
12653
|
-
if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
|
|
12654
|
-
return;
|
|
12655
|
-
changes.push(change);
|
|
12656
|
-
}
|
|
12657
|
-
function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
|
|
12658
|
-
const baseSchema = resolveSchema(baseValue, baseDocument);
|
|
12659
|
-
const currentSchema = resolveSchema(currentValue, currentDocument);
|
|
12660
|
-
if (!baseSchema || !currentSchema)
|
|
12661
|
-
return;
|
|
12662
|
-
const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
|
|
12663
|
-
if (seen.has(pair))
|
|
12763
|
+
function compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12764
|
+
const baseBody = recordValue(baseOperation.requestBody);
|
|
12765
|
+
const currentBody = recordValue(currentOperation.requestBody);
|
|
12766
|
+
if (!baseBody || !currentBody) {
|
|
12767
|
+
if (baseBody && !currentBody) {
|
|
12768
|
+
addChange(changes, {
|
|
12769
|
+
kind: "breaking",
|
|
12770
|
+
code: "request-body-removed",
|
|
12771
|
+
path: `${path}.requestBody`,
|
|
12772
|
+
message: "A request body from the previous contract was removed."
|
|
12773
|
+
});
|
|
12774
|
+
} else if (!baseBody && currentBody?.required === true) {
|
|
12775
|
+
addChange(changes, {
|
|
12776
|
+
kind: "breaking",
|
|
12777
|
+
code: "request-body-required",
|
|
12778
|
+
path: `${path}.requestBody`,
|
|
12779
|
+
message: "A request body became required."
|
|
12780
|
+
});
|
|
12781
|
+
} else if (!baseBody && currentBody) {
|
|
12782
|
+
addChange(changes, {
|
|
12783
|
+
kind: "non-breaking",
|
|
12784
|
+
code: "request-body-added",
|
|
12785
|
+
path: `${path}.requestBody`,
|
|
12786
|
+
message: "An optional request body was added."
|
|
12787
|
+
});
|
|
12788
|
+
}
|
|
12664
12789
|
return;
|
|
12665
|
-
|
|
12666
|
-
if (
|
|
12790
|
+
}
|
|
12791
|
+
if (currentBody.required === true && baseBody.required !== true) {
|
|
12667
12792
|
addChange(changes, {
|
|
12668
12793
|
kind: "breaking",
|
|
12669
|
-
code: "
|
|
12670
|
-
path
|
|
12671
|
-
message: "
|
|
12794
|
+
code: "request-body-required",
|
|
12795
|
+
path: `${path}.requestBody`,
|
|
12796
|
+
message: "An optional request body became required."
|
|
12672
12797
|
});
|
|
12673
12798
|
}
|
|
12674
|
-
const
|
|
12675
|
-
const
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12799
|
+
const baseContent = recordValue(baseBody.content);
|
|
12800
|
+
const currentContent = recordValue(currentBody.content);
|
|
12801
|
+
for (const mediaType of sortedKeys(baseContent)) {
|
|
12802
|
+
const baseMedia = recordValue(baseContent?.[mediaType]);
|
|
12803
|
+
const currentMedia = recordValue(currentContent?.[mediaType]);
|
|
12804
|
+
if (!baseMedia || !currentMedia) {
|
|
12805
|
+
addChange(changes, {
|
|
12806
|
+
kind: "breaking",
|
|
12807
|
+
code: "request-media-type-removed",
|
|
12808
|
+
path: `${path}.requestBody.content.${mediaType}`,
|
|
12809
|
+
message: "A request media type from the previous contract was removed."
|
|
12810
|
+
});
|
|
12811
|
+
continue;
|
|
12687
12812
|
}
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
const currentProperties = schemaProperties(currentSchema);
|
|
12691
|
-
if (baseProperties && currentProperties) {
|
|
12692
|
-
for (const name of sortedKeys(baseProperties)) {
|
|
12693
|
-
if (currentProperties[name] === undefined) {
|
|
12694
|
-
const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
|
|
12695
|
-
if (direction === "output" || inputRemovalIsBreaking) {
|
|
12696
|
-
addChange(changes, {
|
|
12697
|
-
kind: "breaking",
|
|
12698
|
-
code: direction === "output" ? "response-property-removed" : "request-property-removed",
|
|
12699
|
-
path: `${path}.properties.${name}`,
|
|
12700
|
-
message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
|
|
12701
|
-
});
|
|
12702
|
-
}
|
|
12703
|
-
continue;
|
|
12704
|
-
}
|
|
12705
|
-
compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
|
|
12813
|
+
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12814
|
+
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
|
|
12706
12815
|
}
|
|
12707
12816
|
}
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12817
|
+
}
|
|
12818
|
+
function compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12819
|
+
const baseResponses = recordValue(baseOperation.responses);
|
|
12820
|
+
const currentResponses = recordValue(currentOperation.responses);
|
|
12821
|
+
for (const status of sortedKeys(baseResponses)) {
|
|
12822
|
+
const baseResponse = recordValue(baseResponses?.[status]);
|
|
12823
|
+
const currentResponse = recordValue(currentResponses?.[status]);
|
|
12824
|
+
if (!baseResponse || !currentResponse) {
|
|
12825
|
+
addChange(changes, {
|
|
12826
|
+
kind: "breaking",
|
|
12827
|
+
code: "response-removed",
|
|
12828
|
+
path: `${path}.responses.${status}`,
|
|
12829
|
+
message: "A response status from the previous contract was removed."
|
|
12830
|
+
});
|
|
12831
|
+
continue;
|
|
12832
|
+
}
|
|
12833
|
+
const baseContent = recordValue(baseResponse.content);
|
|
12834
|
+
const currentContent = recordValue(currentResponse.content);
|
|
12835
|
+
for (const mediaType of sortedKeys(baseContent)) {
|
|
12836
|
+
const baseMedia = recordValue(baseContent?.[mediaType]);
|
|
12837
|
+
const currentMedia = recordValue(currentContent?.[mediaType]);
|
|
12838
|
+
if (!baseMedia || !currentMedia) {
|
|
12713
12839
|
addChange(changes, {
|
|
12714
12840
|
kind: "breaking",
|
|
12715
|
-
code: "
|
|
12716
|
-
path: `${path}.
|
|
12717
|
-
message:
|
|
12841
|
+
code: "response-media-type-removed",
|
|
12842
|
+
path: `${path}.responses.${status}.content.${mediaType}`,
|
|
12843
|
+
message: "A response media type from the previous contract was removed."
|
|
12718
12844
|
});
|
|
12845
|
+
continue;
|
|
12846
|
+
}
|
|
12847
|
+
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12848
|
+
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.responses.${status}.content.${mediaType}.schema`, "output", changes);
|
|
12719
12849
|
}
|
|
12720
12850
|
}
|
|
12721
|
-
|
|
12851
|
+
}
|
|
12852
|
+
for (const status of sortedKeys(currentResponses)) {
|
|
12853
|
+
if (baseResponses?.[status] !== undefined)
|
|
12854
|
+
continue;
|
|
12855
|
+
addChange(changes, {
|
|
12856
|
+
kind: "non-breaking",
|
|
12857
|
+
code: "response-added",
|
|
12858
|
+
path: `${path}.responses.${status}`,
|
|
12859
|
+
message: "A response status was added."
|
|
12860
|
+
});
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
function compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12864
|
+
compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
|
|
12865
|
+
compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
|
|
12866
|
+
compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
|
|
12867
|
+
const baseSecurity = baseOperation.security;
|
|
12868
|
+
const currentSecurity = currentOperation.security;
|
|
12869
|
+
if (Array.isArray(currentSecurity) && currentSecurity.length > 0 && (!Array.isArray(baseSecurity) || baseSecurity.length === 0)) {
|
|
12870
|
+
addChange(changes, {
|
|
12871
|
+
kind: "breaking",
|
|
12872
|
+
code: "security-requirement-added",
|
|
12873
|
+
path: `${path}.security`,
|
|
12874
|
+
message: "The operation now requires authentication or an additional security scheme."
|
|
12875
|
+
});
|
|
12876
|
+
}
|
|
12877
|
+
}
|
|
12878
|
+
function compareComponents(baseDocument, currentDocument, changes) {
|
|
12879
|
+
const baseComponents = recordValue(baseDocument.components);
|
|
12880
|
+
const currentComponents = recordValue(currentDocument.components);
|
|
12881
|
+
for (const group of COMPONENT_GROUPS) {
|
|
12882
|
+
const baseGroup = recordValue(baseComponents?.[group]);
|
|
12883
|
+
const currentGroup = recordValue(currentComponents?.[group]);
|
|
12884
|
+
for (const name of sortedKeys(baseGroup)) {
|
|
12885
|
+
if (currentGroup?.[name] !== undefined)
|
|
12886
|
+
continue;
|
|
12722
12887
|
addChange(changes, {
|
|
12723
12888
|
kind: "breaking",
|
|
12724
|
-
code: "
|
|
12725
|
-
path
|
|
12726
|
-
message: "
|
|
12889
|
+
code: "component-removed",
|
|
12890
|
+
path: `components.${group}.${name}`,
|
|
12891
|
+
message: "A reusable component from the previous contract was removed."
|
|
12727
12892
|
});
|
|
12728
12893
|
}
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
}
|
|
12894
|
+
for (const name of sortedKeys(currentGroup)) {
|
|
12895
|
+
if (baseGroup?.[name] !== undefined)
|
|
12896
|
+
continue;
|
|
12897
|
+
addChange(changes, {
|
|
12898
|
+
kind: "non-breaking",
|
|
12899
|
+
code: "component-added",
|
|
12900
|
+
path: `components.${group}.${name}`,
|
|
12901
|
+
message: "A reusable component was added."
|
|
12902
|
+
});
|
|
12739
12903
|
}
|
|
12740
12904
|
}
|
|
12741
12905
|
}
|
|
12742
|
-
function
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
return
|
|
12906
|
+
function parseOpenApiDocument(value) {
|
|
12907
|
+
if (!isRecord(value) || typeof value.openapi !== "string" || !/^3\.[0-9]+(?:\.[0-9]+)?$/.test(value.openapi) || !isRecord(value.info) || !isRecord(value.paths)) {
|
|
12908
|
+
throw new OpenApiDocumentError;
|
|
12909
|
+
}
|
|
12910
|
+
return { ...value, openapi: value.openapi, info: value.info, paths: value.paths };
|
|
12747
12911
|
}
|
|
12748
|
-
function
|
|
12749
|
-
const
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12912
|
+
function serializeOpenApiJson(document, space = 2) {
|
|
12913
|
+
const parsed = parseOpenApiDocument(document);
|
|
12914
|
+
if (!Number.isInteger(space) || space < 0 || space > 10)
|
|
12915
|
+
throw new OpenApiDocumentError;
|
|
12916
|
+
const serialized = JSON.stringify(parsed, null, space);
|
|
12917
|
+
if (serialized === undefined)
|
|
12918
|
+
throw new OpenApiDocumentError;
|
|
12919
|
+
return `${serialized}
|
|
12920
|
+
`;
|
|
12921
|
+
}
|
|
12922
|
+
async function readOpenApiJson(path) {
|
|
12923
|
+
try {
|
|
12924
|
+
const value = JSON.parse(await readFile10(resolve13(path), "utf8"));
|
|
12925
|
+
return parseOpenApiDocument(value);
|
|
12926
|
+
} catch (error) {
|
|
12927
|
+
if (error instanceof OpenApiDocumentError)
|
|
12928
|
+
throw error;
|
|
12929
|
+
throw new OpenApiDocumentError;
|
|
12757
12930
|
}
|
|
12758
|
-
return result;
|
|
12759
12931
|
}
|
|
12760
|
-
function
|
|
12761
|
-
|
|
12932
|
+
async function writeOpenApiJson(document, outputPath, space = 2) {
|
|
12933
|
+
const path = resolve13(outputPath);
|
|
12934
|
+
const content = serializeOpenApiJson(document, space);
|
|
12935
|
+
await mkdir4(dirname8(path), { recursive: true });
|
|
12936
|
+
try {
|
|
12937
|
+
if (await readFile10(path, "utf8") === content)
|
|
12938
|
+
return { path, written: false };
|
|
12939
|
+
} catch {}
|
|
12940
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
12941
|
+
try {
|
|
12942
|
+
await writeFile5(temporaryPath, content, "utf8");
|
|
12943
|
+
await rename6(temporaryPath, path);
|
|
12944
|
+
} catch (error) {
|
|
12945
|
+
await unlink3(temporaryPath).catch(() => {
|
|
12946
|
+
return;
|
|
12947
|
+
});
|
|
12948
|
+
throw error;
|
|
12949
|
+
}
|
|
12950
|
+
return { path, written: true };
|
|
12762
12951
|
}
|
|
12763
|
-
function
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
const
|
|
12768
|
-
|
|
12769
|
-
|
|
12952
|
+
async function loadGeneratedOpenApiDocument(modulePath) {
|
|
12953
|
+
try {
|
|
12954
|
+
const moduleUrl = pathToFileURL(resolve13(modulePath));
|
|
12955
|
+
moduleUrl.searchParams.set("supacloud-openapi-export", "1");
|
|
12956
|
+
const loaded = await import(moduleUrl.href);
|
|
12957
|
+
if (!isRecord(loaded))
|
|
12958
|
+
throw new OpenApiDocumentError;
|
|
12959
|
+
return parseOpenApiDocument(loaded.OPENAPI_DOCUMENT);
|
|
12960
|
+
} catch (error) {
|
|
12961
|
+
if (error instanceof OpenApiDocumentError)
|
|
12962
|
+
throw error;
|
|
12963
|
+
throw new OpenApiDocumentError;
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
async function exportGeneratedOpenApiJson(options) {
|
|
12967
|
+
const document = await loadGeneratedOpenApiDocument(options.modulePath);
|
|
12968
|
+
return writeOpenApiJson(document, options.outputPath, options.space);
|
|
12969
|
+
}
|
|
12970
|
+
function diffOpenApiDocuments(baseValue, currentValue) {
|
|
12971
|
+
const baseDocument = parseOpenApiDocument(baseValue);
|
|
12972
|
+
const currentDocument = parseOpenApiDocument(currentValue);
|
|
12973
|
+
const changes = [];
|
|
12974
|
+
const basePaths = baseDocument.paths;
|
|
12975
|
+
const currentPaths = currentDocument.paths;
|
|
12976
|
+
for (const path of sortedKeys(basePaths)) {
|
|
12977
|
+
const basePathItem = recordValue(basePaths[path]);
|
|
12978
|
+
const currentPathItem = recordValue(currentPaths[path]);
|
|
12979
|
+
if (!basePathItem || !currentPathItem) {
|
|
12770
12980
|
addChange(changes, {
|
|
12771
12981
|
kind: "breaking",
|
|
12772
|
-
code: "
|
|
12773
|
-
path:
|
|
12774
|
-
message: "A
|
|
12982
|
+
code: "path-removed",
|
|
12983
|
+
path: `paths.${path}`,
|
|
12984
|
+
message: "A path from the previous contract was removed."
|
|
12775
12985
|
});
|
|
12776
12986
|
continue;
|
|
12777
12987
|
}
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12988
|
+
for (const method of HTTP_METHODS) {
|
|
12989
|
+
const baseOperation = recordValue(basePathItem[method]);
|
|
12990
|
+
const currentOperation = recordValue(currentPathItem[method]);
|
|
12991
|
+
if (!baseOperation || !currentOperation) {
|
|
12992
|
+
if (baseOperation) {
|
|
12993
|
+
addChange(changes, {
|
|
12994
|
+
kind: "breaking",
|
|
12995
|
+
code: "operation-removed",
|
|
12996
|
+
path: `paths.${path}.${method}`,
|
|
12997
|
+
message: "An operation from the previous contract was removed."
|
|
12998
|
+
});
|
|
12999
|
+
} else if (currentOperation) {
|
|
13000
|
+
addChange(changes, {
|
|
13001
|
+
kind: "non-breaking",
|
|
13002
|
+
code: "operation-added",
|
|
13003
|
+
path: `paths.${path}.${method}`,
|
|
13004
|
+
message: "An operation was added to an existing path."
|
|
13005
|
+
});
|
|
13006
|
+
}
|
|
13007
|
+
continue;
|
|
13008
|
+
}
|
|
13009
|
+
compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, `paths.${path}.${method}`, changes);
|
|
12788
13010
|
}
|
|
12789
13011
|
}
|
|
12790
|
-
for (const
|
|
12791
|
-
if (
|
|
12792
|
-
continue;
|
|
12793
|
-
const parameter = currentParameters.get(key);
|
|
12794
|
-
if (!parameter)
|
|
13012
|
+
for (const path of sortedKeys(currentPaths)) {
|
|
13013
|
+
if (basePaths[path] !== undefined)
|
|
12795
13014
|
continue;
|
|
12796
13015
|
addChange(changes, {
|
|
12797
|
-
kind:
|
|
12798
|
-
code:
|
|
12799
|
-
path:
|
|
12800
|
-
message:
|
|
13016
|
+
kind: "non-breaking",
|
|
13017
|
+
code: "path-added",
|
|
13018
|
+
path: `paths.${path}`,
|
|
13019
|
+
message: "A path was added."
|
|
12801
13020
|
});
|
|
12802
13021
|
}
|
|
13022
|
+
compareComponents(baseDocument, currentDocument, changes);
|
|
13023
|
+
const breaking = changes.filter((change) => change.kind === "breaking");
|
|
13024
|
+
return { ok: breaking.length === 0, breaking, changes };
|
|
12803
13025
|
}
|
|
12804
|
-
function
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
13026
|
+
function formatOpenApiDiff(result) {
|
|
13027
|
+
if (result.changes.length === 0)
|
|
13028
|
+
return "OpenAPI diff passed: no contract changes.";
|
|
13029
|
+
return [
|
|
13030
|
+
`OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
|
|
13031
|
+
...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
|
|
13032
|
+
].join(`
|
|
13033
|
+
`);
|
|
13034
|
+
}
|
|
13035
|
+
|
|
13036
|
+
// src/migration-assess.ts
|
|
13037
|
+
var STATUS_PRIORITY = {
|
|
13038
|
+
compatible: 0,
|
|
13039
|
+
"needs-review": 1,
|
|
13040
|
+
"not-proven": 2,
|
|
13041
|
+
unsupported: 3,
|
|
13042
|
+
breaking: 4
|
|
13043
|
+
};
|
|
13044
|
+
function isRecord2(value) {
|
|
13045
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13046
|
+
}
|
|
13047
|
+
function arrayLength(value) {
|
|
13048
|
+
return Array.isArray(value) ? value.length : undefined;
|
|
13049
|
+
}
|
|
13050
|
+
async function sha256(path) {
|
|
13051
|
+
return createHash9("sha256").update(await readFile11(path)).digest("hex");
|
|
13052
|
+
}
|
|
13053
|
+
function overallStatus(findings) {
|
|
13054
|
+
return findings.reduce((current, finding) => STATUS_PRIORITY[finding.status] > STATUS_PRIORITY[current] ? finding.status : current, "compatible");
|
|
13055
|
+
}
|
|
13056
|
+
async function assessMigration(options) {
|
|
13057
|
+
const projectDir = resolve14(options.projectDir);
|
|
13058
|
+
const compile = {
|
|
13059
|
+
...options.compile,
|
|
13060
|
+
rootDir: resolve14(options.compile.rootDir),
|
|
13061
|
+
outDir: resolve14(options.compile.outDir)
|
|
13062
|
+
};
|
|
13063
|
+
const findings = [];
|
|
13064
|
+
const dependencyProblems = await checkMigrationDependencies(projectDir);
|
|
13065
|
+
if (dependencyProblems.length > 0) {
|
|
13066
|
+
findings.push({
|
|
13067
|
+
code: "migration-dependency-tuple-not-proven",
|
|
13068
|
+
status: "not-proven",
|
|
13069
|
+
message: "The installed compiler migration dependency tuple is not the exact tested tuple.",
|
|
13070
|
+
evidence: dependencyProblems.join("; "),
|
|
13071
|
+
remediation: "Install the exact tested package versions before applying a source migration."
|
|
13072
|
+
});
|
|
13073
|
+
}
|
|
13074
|
+
let compiler = {
|
|
13075
|
+
upToDate: false,
|
|
13076
|
+
diagnostics: [],
|
|
13077
|
+
mismatches: []
|
|
13078
|
+
};
|
|
13079
|
+
try {
|
|
13080
|
+
const checked = await checkProject(compile);
|
|
13081
|
+
compiler = {
|
|
13082
|
+
upToDate: checked.upToDate,
|
|
13083
|
+
diagnostics: checked.diagnostics,
|
|
13084
|
+
mismatches: checked.mismatches
|
|
13085
|
+
};
|
|
13086
|
+
const errors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
13087
|
+
if (errors.length > 0) {
|
|
13088
|
+
findings.push({
|
|
13089
|
+
code: "compiler-governance-not-proven",
|
|
13090
|
+
status: "not-proven",
|
|
13091
|
+
message: `Compiler governance has ${errors.length} error(s).`,
|
|
13092
|
+
remediation: "Resolve compiler diagnostics without weakening governance, then reassess."
|
|
12821
13093
|
});
|
|
12822
|
-
}
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
code: "
|
|
12826
|
-
|
|
12827
|
-
message: "
|
|
13094
|
+
}
|
|
13095
|
+
if (!checked.upToDate) {
|
|
13096
|
+
findings.push({
|
|
13097
|
+
code: "generated-artifact-drift",
|
|
13098
|
+
status: "needs-review",
|
|
13099
|
+
message: "Generated artifacts do not match the current source and compiler configuration.",
|
|
13100
|
+
evidence: checked.mismatches.join("; "),
|
|
13101
|
+
remediation: "Regenerate candidate artifacts in isolation and review the diff before adoption."
|
|
12828
13102
|
});
|
|
12829
13103
|
}
|
|
12830
|
-
|
|
13104
|
+
} catch {
|
|
13105
|
+
findings.push({
|
|
13106
|
+
code: "compiler-check-unavailable",
|
|
13107
|
+
status: "not-proven",
|
|
13108
|
+
message: "The local compiler check could not complete.",
|
|
13109
|
+
remediation: "Verify the project source, configuration and installed dependencies, then reassess."
|
|
13110
|
+
});
|
|
12831
13111
|
}
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
13112
|
+
const contractsManifestPath = join7(compile.outDir, "contracts.manifest.json");
|
|
13113
|
+
let contractsManifest = {
|
|
13114
|
+
path: contractsManifestPath,
|
|
13115
|
+
present: false
|
|
13116
|
+
};
|
|
13117
|
+
try {
|
|
13118
|
+
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
13119
|
+
if (!isRecord2(parsed) || parsed.version !== 1)
|
|
13120
|
+
throw new Error("invalid manifest");
|
|
13121
|
+
contractsManifest = {
|
|
13122
|
+
path: contractsManifestPath,
|
|
13123
|
+
present: true,
|
|
13124
|
+
sha256: await sha256(contractsManifestPath),
|
|
13125
|
+
version: 1,
|
|
13126
|
+
...arrayLength(parsed.commands) === undefined ? {} : { commands: arrayLength(parsed.commands) },
|
|
13127
|
+
...arrayLength(parsed.routes) === undefined ? {} : { routes: arrayLength(parsed.routes) },
|
|
13128
|
+
...arrayLength(parsed.permissions) === undefined ? {} : { permissions: arrayLength(parsed.permissions) }
|
|
13129
|
+
};
|
|
13130
|
+
} catch {
|
|
13131
|
+
findings.push({
|
|
13132
|
+
code: "contract-manifest-not-proven",
|
|
13133
|
+
status: "not-proven",
|
|
13134
|
+
message: "contracts.manifest.json is missing or invalid.",
|
|
13135
|
+
evidence: contractsManifestPath,
|
|
13136
|
+
remediation: "Generate and review the contract manifest before migration."
|
|
12838
13137
|
});
|
|
12839
13138
|
}
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
const
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
13139
|
+
let openApi;
|
|
13140
|
+
if (options.baselineOpenApiPath && options.currentOpenApiPath) {
|
|
13141
|
+
const baselinePath = resolve14(projectDir, options.baselineOpenApiPath);
|
|
13142
|
+
const currentPath = resolve14(projectDir, options.currentOpenApiPath);
|
|
13143
|
+
try {
|
|
13144
|
+
const diff = diffOpenApiDocuments(await readOpenApiJson(baselinePath), await readOpenApiJson(currentPath));
|
|
13145
|
+
openApi = {
|
|
13146
|
+
baselinePath,
|
|
13147
|
+
baselineSha256: await sha256(baselinePath),
|
|
13148
|
+
currentPath,
|
|
13149
|
+
currentSha256: await sha256(currentPath),
|
|
13150
|
+
diff
|
|
13151
|
+
};
|
|
13152
|
+
if (!diff.ok) {
|
|
13153
|
+
findings.push({
|
|
13154
|
+
code: "openapi-breaking-change",
|
|
13155
|
+
status: "breaking",
|
|
13156
|
+
message: `OpenAPI contains ${diff.breaking.length} breaking change(s).`,
|
|
13157
|
+
remediation: "Review and explicitly approve or redesign each breaking contract change."
|
|
13158
|
+
});
|
|
13159
|
+
} else if (diff.changes.length > 0) {
|
|
13160
|
+
findings.push({
|
|
13161
|
+
code: "openapi-compatible-change-review",
|
|
13162
|
+
status: "needs-review",
|
|
13163
|
+
message: `OpenAPI contains ${diff.changes.length} non-breaking change(s) requiring release review.`
|
|
13164
|
+
});
|
|
13165
|
+
}
|
|
13166
|
+
} catch {
|
|
13167
|
+
findings.push({
|
|
13168
|
+
code: "openapi-diff-not-proven",
|
|
13169
|
+
status: "not-proven",
|
|
13170
|
+
message: "The OpenAPI baseline or current document could not be loaded.",
|
|
13171
|
+
remediation: "Export both OpenAPI documents as JSON and rerun the assessment."
|
|
12851
13172
|
});
|
|
12852
|
-
continue;
|
|
12853
|
-
}
|
|
12854
|
-
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12855
|
-
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
|
|
12856
13173
|
}
|
|
13174
|
+
} else {
|
|
13175
|
+
findings.push({
|
|
13176
|
+
code: "openapi-baseline-not-proven",
|
|
13177
|
+
status: "not-proven",
|
|
13178
|
+
message: "No complete OpenAPI baseline/current pair was supplied.",
|
|
13179
|
+
remediation: "Provide both --baseline-openapi and --current-openapi for compatibility evidence."
|
|
13180
|
+
});
|
|
12857
13181
|
}
|
|
13182
|
+
findings.push({
|
|
13183
|
+
code: "rendering-mode-preserved",
|
|
13184
|
+
status: "compatible",
|
|
13185
|
+
message: "SupaCloud migration does not require SSR and does not change the existing rendering topology.",
|
|
13186
|
+
evidence: `selected=${options.renderMode ?? "unspecified"}`
|
|
13187
|
+
});
|
|
13188
|
+
const status = overallStatus(findings);
|
|
13189
|
+
return {
|
|
13190
|
+
version: 1,
|
|
13191
|
+
ok: status === "compatible" || status === "needs-review",
|
|
13192
|
+
status,
|
|
13193
|
+
readOnly: true,
|
|
13194
|
+
writesPerformed: false,
|
|
13195
|
+
rendering: {
|
|
13196
|
+
selected: options.renderMode ?? "unspecified",
|
|
13197
|
+
ssrRequired: false,
|
|
13198
|
+
supportedModes: ["browser", "ssr", "edge", "trusted-server"],
|
|
13199
|
+
guidance: "Keep the application's existing CSR, SPA, SSR or edge topology; use trusted adapters only for governed server operations."
|
|
13200
|
+
},
|
|
13201
|
+
dependencies: {
|
|
13202
|
+
expected: migrationDependencies(),
|
|
13203
|
+
problems: dependencyProblems
|
|
13204
|
+
},
|
|
13205
|
+
compiler,
|
|
13206
|
+
artifacts: {
|
|
13207
|
+
contractsManifest,
|
|
13208
|
+
...openApi === undefined ? {} : { openApi }
|
|
13209
|
+
},
|
|
13210
|
+
findings
|
|
13211
|
+
};
|
|
12858
13212
|
}
|
|
12859
|
-
function
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
13213
|
+
function formatMigrationAssessment(result) {
|
|
13214
|
+
return [
|
|
13215
|
+
`Migration assessment: ${result.status}. Read-only; no files, databases or remote environments were changed.`,
|
|
13216
|
+
`Rendering: ${result.rendering.selected}; SSR required: no.`,
|
|
13217
|
+
...result.findings.map((finding) => `${finding.status.toUpperCase()} ${finding.code}: ${finding.message}`)
|
|
13218
|
+
].join(`
|
|
13219
|
+
`);
|
|
13220
|
+
}
|
|
13221
|
+
// src/inspect.ts
|
|
13222
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
13223
|
+
import { dirname as dirname9, join as join8, relative as relative11, sep as sep10 } from "node:path";
|
|
13224
|
+
function formatGraph(graph) {
|
|
13225
|
+
const lines = [];
|
|
13226
|
+
for (const module of graph.modules) {
|
|
13227
|
+
lines.push(`MODULE ${module.name}`);
|
|
13228
|
+
lines.push(` file: ${module.file}:${module.line}`);
|
|
13229
|
+
lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
|
|
13230
|
+
lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
|
|
13231
|
+
lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
|
|
13232
|
+
lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
|
|
13233
|
+
}
|
|
13234
|
+
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
13235
|
+
return lines.join(`
|
|
13236
|
+
`);
|
|
13237
|
+
}
|
|
13238
|
+
function explainGraph(graph, subject) {
|
|
13239
|
+
const module = graph.modules.find((candidate) => candidate.name === subject);
|
|
13240
|
+
if (module)
|
|
13241
|
+
return explainModule(graph, module);
|
|
13242
|
+
const provider = findProvider(graph, subject);
|
|
13243
|
+
if (provider)
|
|
13244
|
+
return explainProvider(graph, provider.module, provider.provider);
|
|
13245
|
+
if (graph.externalTokens.includes(subject)) {
|
|
13246
|
+
const references = graph.modules.flatMap((candidate) => [
|
|
13247
|
+
...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
|
|
13248
|
+
...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
|
|
13249
|
+
]);
|
|
13250
|
+
return [
|
|
13251
|
+
`EXTERNAL TOKEN ${subject}`,
|
|
13252
|
+
" provided by: platform runtime",
|
|
13253
|
+
` references: ${references.length > 0 ? references.join(", ") : "-"}`
|
|
13254
|
+
].join(`
|
|
13255
|
+
`);
|
|
13256
|
+
}
|
|
13257
|
+
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
13258
|
+
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
13259
|
+
}
|
|
13260
|
+
function createContextPack(graph, subject) {
|
|
13261
|
+
const subjectModule = resolveContextModule(graph, subject);
|
|
13262
|
+
const byName = new Map(graph.modules.map((module) => [module.name, module]));
|
|
13263
|
+
const selected = new Set([subjectModule.name]);
|
|
13264
|
+
const reverseDependents = new Map;
|
|
13265
|
+
for (const mod of graph.modules) {
|
|
13266
|
+
for (const imp of mod.imports) {
|
|
13267
|
+
const list = reverseDependents.get(imp);
|
|
13268
|
+
if (list)
|
|
13269
|
+
list.push(mod.name);
|
|
13270
|
+
else
|
|
13271
|
+
reverseDependents.set(imp, [mod.name]);
|
|
12873
13272
|
}
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
code: "response-media-type-removed",
|
|
12883
|
-
path: `${path}.responses.${status}.content.${mediaType}`,
|
|
12884
|
-
message: "A response media type from the previous contract was removed."
|
|
12885
|
-
});
|
|
13273
|
+
}
|
|
13274
|
+
for (const direction of ["imports", "dependents"]) {
|
|
13275
|
+
const visited = new Set([subjectModule.name]);
|
|
13276
|
+
const queue = [subjectModule.name];
|
|
13277
|
+
let head = 0;
|
|
13278
|
+
while (head < queue.length) {
|
|
13279
|
+
const current = queue[head++];
|
|
13280
|
+
if (!current)
|
|
12886
13281
|
continue;
|
|
12887
|
-
|
|
12888
|
-
if (
|
|
12889
|
-
|
|
13282
|
+
const module = byName.get(current);
|
|
13283
|
+
if (!module)
|
|
13284
|
+
continue;
|
|
13285
|
+
const neighbors = direction === "imports" ? module.imports : reverseDependents.get(module.name) ?? [];
|
|
13286
|
+
for (const neighbor of neighbors) {
|
|
13287
|
+
if (!visited.has(neighbor) && byName.has(neighbor)) {
|
|
13288
|
+
visited.add(neighbor);
|
|
13289
|
+
selected.add(neighbor);
|
|
13290
|
+
queue.push(neighbor);
|
|
13291
|
+
}
|
|
12890
13292
|
}
|
|
12891
13293
|
}
|
|
12892
13294
|
}
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
13295
|
+
const modules = graph.modules.filter((module) => selected.has(module.name));
|
|
13296
|
+
const queryDocuments = graph.graphql?.documents.filter((file) => modules.some((module) => {
|
|
13297
|
+
const path = relative11(dirname9(module.file), file);
|
|
13298
|
+
return path !== ".." && !path.startsWith(`..${sep10}`);
|
|
13299
|
+
})) ?? [];
|
|
13300
|
+
const graphql = graph.graphql ? {
|
|
13301
|
+
...graph.graphql,
|
|
13302
|
+
documents: queryDocuments,
|
|
13303
|
+
operations: graph.graphql.operations.filter((operation) => queryDocuments.includes(operation.file))
|
|
13304
|
+
} : undefined;
|
|
13305
|
+
const files = [...new Set(modules.flatMap((module) => [
|
|
13306
|
+
module.file,
|
|
13307
|
+
...module.providers.map((provider) => provider.file),
|
|
13308
|
+
...module.controllers.map((controller) => controller.file),
|
|
13309
|
+
...graph.moduleHandlerFiles?.[module.name] ?? [],
|
|
13310
|
+
...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
|
|
13311
|
+
]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
|
|
13312
|
+
const referencedTokens = new Set;
|
|
13313
|
+
for (const module of modules) {
|
|
13314
|
+
for (const provider of module.providers) {
|
|
13315
|
+
for (const token of provider.deps)
|
|
13316
|
+
referencedTokens.add(token);
|
|
13317
|
+
}
|
|
13318
|
+
for (const controller of module.controllers) {
|
|
13319
|
+
for (const token of controller.deps)
|
|
13320
|
+
referencedTokens.add(token);
|
|
13321
|
+
}
|
|
12902
13322
|
}
|
|
13323
|
+
return {
|
|
13324
|
+
version: 1,
|
|
13325
|
+
subject: subjectModule.name,
|
|
13326
|
+
modules,
|
|
13327
|
+
files,
|
|
13328
|
+
externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
|
|
13329
|
+
executionPlans: createExecutionPlans({ ...graph, modules }),
|
|
13330
|
+
routeContracts: inspectRouteContracts({ ...graph, modules }),
|
|
13331
|
+
...graphql ? { graphql } : {},
|
|
13332
|
+
diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
|
|
13333
|
+
relatedModules: {
|
|
13334
|
+
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
13335
|
+
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
13336
|
+
}
|
|
13337
|
+
};
|
|
12903
13338
|
}
|
|
12904
|
-
function
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
const
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
code: "security-requirement-added",
|
|
12914
|
-
path: `${path}.security`,
|
|
12915
|
-
message: "The operation now requires authentication or an additional security scheme."
|
|
12916
|
-
});
|
|
13339
|
+
function resolveContextModule(graph, subject) {
|
|
13340
|
+
const exact = graph.modules.find((module) => module.name === subject);
|
|
13341
|
+
if (exact)
|
|
13342
|
+
return exact;
|
|
13343
|
+
const matches = graph.modules.filter((module) => module.className === subject || module.providers.some((provider) => provider.token === subject || provider.useClass === subject) || module.controllers.some((controller) => controller.className === subject) || module.commands.some((command) => command.name === subject || command.className === subject) || (module.jobs ?? []).some((job) => job.name === subject || job.className === subject || job.serviceKey === subject) || module.queries.some((query) => query.name === subject || query.className === subject));
|
|
13344
|
+
if (matches.length === 1)
|
|
13345
|
+
return matches[0];
|
|
13346
|
+
if (matches.length > 1) {
|
|
13347
|
+
throw new Error(`Ambiguous context target "${subject}". Select a module name: ${matches.map((module) => module.name).sort().join(", ")}`);
|
|
12917
13348
|
}
|
|
13349
|
+
throw new Error(`No context target named "${subject}". Select a module name: ${graph.modules.map((module) => module.name).sort().join(", ") || "(none)"}`);
|
|
13350
|
+
}
|
|
13351
|
+
function allAspects(module) {
|
|
13352
|
+
return [
|
|
13353
|
+
...module.aspects ?? [],
|
|
13354
|
+
...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
|
|
13355
|
+
...module.commands.flatMap((command) => command.aspects ?? []),
|
|
13356
|
+
...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
|
|
13357
|
+
];
|
|
13358
|
+
}
|
|
13359
|
+
function createExecutionPlans(graph) {
|
|
13360
|
+
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
13361
|
+
return graph.modules.flatMap((module) => [
|
|
13362
|
+
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
13363
|
+
const command = module.commands.find((item) => item.className === route.command);
|
|
13364
|
+
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
13365
|
+
return {
|
|
13366
|
+
module: module.name,
|
|
13367
|
+
kind: "route",
|
|
13368
|
+
name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
|
|
13369
|
+
...command ? { command: command.name } : {},
|
|
13370
|
+
stages: [
|
|
13371
|
+
...aspects(`module:${module.name}`, module.aspects),
|
|
13372
|
+
...aspects("route", route.aspects),
|
|
13373
|
+
...aspects("command", command?.aspects),
|
|
13374
|
+
...command ? [
|
|
13375
|
+
"commandExecutor",
|
|
13376
|
+
"authorize",
|
|
13377
|
+
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
13378
|
+
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
13379
|
+
...command.transaction === "required" ? ["transaction"] : []
|
|
13380
|
+
]
|
|
13381
|
+
] : [],
|
|
13382
|
+
"handler",
|
|
13383
|
+
...command?.audit && !command.rpc ? ["audit"] : []
|
|
13384
|
+
]
|
|
13385
|
+
};
|
|
13386
|
+
})),
|
|
13387
|
+
...module.commands.map((command) => ({
|
|
13388
|
+
module: module.name,
|
|
13389
|
+
kind: "command",
|
|
13390
|
+
name: command.name,
|
|
13391
|
+
command: command.name,
|
|
13392
|
+
stages: [
|
|
13393
|
+
"authorize",
|
|
13394
|
+
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
13395
|
+
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
13396
|
+
...command.transaction === "required" ? ["transaction"] : []
|
|
13397
|
+
],
|
|
13398
|
+
...aspects(`module:${module.name}`, module.aspects),
|
|
13399
|
+
...aspects("command", command.aspects),
|
|
13400
|
+
"handler",
|
|
13401
|
+
...command.audit && !command.rpc ? ["audit"] : []
|
|
13402
|
+
]
|
|
13403
|
+
})),
|
|
13404
|
+
...(module.jobs ?? []).map((job) => ({
|
|
13405
|
+
module: module.name,
|
|
13406
|
+
kind: "job",
|
|
13407
|
+
name: job.name,
|
|
13408
|
+
stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
|
|
13409
|
+
}))
|
|
13410
|
+
]);
|
|
12918
13411
|
}
|
|
12919
|
-
function
|
|
12920
|
-
const
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
code: "component-added",
|
|
12941
|
-
path: `components.${group}.${name}`,
|
|
12942
|
-
message: "A reusable component was added."
|
|
12943
|
-
});
|
|
13412
|
+
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
13413
|
+
const checks = [
|
|
13414
|
+
{
|
|
13415
|
+
name: "project-root",
|
|
13416
|
+
ok: existsSync4(rootDir),
|
|
13417
|
+
detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
|
|
13418
|
+
},
|
|
13419
|
+
{
|
|
13420
|
+
name: "tsconfig",
|
|
13421
|
+
ok: existsSync4(join8(rootDir, "tsconfig.json")),
|
|
13422
|
+
detail: existsSync4(join8(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
13423
|
+
},
|
|
13424
|
+
{
|
|
13425
|
+
name: "modules",
|
|
13426
|
+
ok: graph.modules.length > 0,
|
|
13427
|
+
detail: `${graph.modules.length} module(s) discovered`
|
|
13428
|
+
},
|
|
13429
|
+
{
|
|
13430
|
+
name: "generated-artifacts",
|
|
13431
|
+
ok: upToDate,
|
|
13432
|
+
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
12944
13433
|
}
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
13434
|
+
];
|
|
13435
|
+
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
13436
|
+
return {
|
|
13437
|
+
checks,
|
|
13438
|
+
diagnostics: allDiagnostics,
|
|
13439
|
+
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
13440
|
+
};
|
|
12952
13441
|
}
|
|
12953
|
-
function
|
|
12954
|
-
const
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
13442
|
+
function explainModule(graph, module) {
|
|
13443
|
+
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
13444
|
+
return [
|
|
13445
|
+
`MODULE ${module.name}`,
|
|
13446
|
+
` file: ${module.file}:${module.line}`,
|
|
13447
|
+
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
13448
|
+
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
13449
|
+
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
13450
|
+
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
13451
|
+
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
|
|
13452
|
+
...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
|
|
13453
|
+
].join(`
|
|
13454
|
+
`);
|
|
12962
13455
|
}
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
}
|
|
13456
|
+
function explainProvider(graph, module, provider) {
|
|
13457
|
+
const dependents = graph.modules.flatMap((candidate) => [
|
|
13458
|
+
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
13459
|
+
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
13460
|
+
]);
|
|
13461
|
+
return [
|
|
13462
|
+
`PROVIDER ${provider.token}`,
|
|
13463
|
+
` module: ${module.name}`,
|
|
13464
|
+
` file: ${provider.file}:${provider.line}`,
|
|
13465
|
+
` kind: ${provider.kind}`,
|
|
13466
|
+
` scope: ${provider.scope}`,
|
|
13467
|
+
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
13468
|
+
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
13469
|
+
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
13470
|
+
].join(`
|
|
13471
|
+
`);
|
|
12972
13472
|
}
|
|
12973
|
-
|
|
12974
|
-
const
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
if (await readFile10(path, "utf8") === content)
|
|
12979
|
-
return { path, written: false };
|
|
12980
|
-
} catch {}
|
|
12981
|
-
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
12982
|
-
try {
|
|
12983
|
-
await writeFile5(temporaryPath, content, "utf8");
|
|
12984
|
-
await rename6(temporaryPath, path);
|
|
12985
|
-
} catch (error) {
|
|
12986
|
-
await unlink3(temporaryPath).catch(() => {
|
|
12987
|
-
return;
|
|
12988
|
-
});
|
|
12989
|
-
throw error;
|
|
13473
|
+
function findProvider(graph, subject) {
|
|
13474
|
+
for (const module of graph.modules) {
|
|
13475
|
+
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
13476
|
+
if (provider)
|
|
13477
|
+
return { module, provider };
|
|
12990
13478
|
}
|
|
12991
|
-
return
|
|
13479
|
+
return;
|
|
12992
13480
|
}
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
if (error instanceof OpenApiDocumentError)
|
|
13003
|
-
throw error;
|
|
13004
|
-
throw new OpenApiDocumentError;
|
|
13481
|
+
function exportGraphMermaid(graph) {
|
|
13482
|
+
const lines = ["graph TD"];
|
|
13483
|
+
for (const mod of graph.modules) {
|
|
13484
|
+
const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
13485
|
+
lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
|
|
13486
|
+
for (const imp of mod.imports) {
|
|
13487
|
+
const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
13488
|
+
lines.push(` ${safeId} --> ${safeImp}`);
|
|
13489
|
+
}
|
|
13005
13490
|
}
|
|
13491
|
+
return lines.join(`
|
|
13492
|
+
`);
|
|
13006
13493
|
}
|
|
13007
|
-
|
|
13008
|
-
const
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
const
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
for (const path of sortedKeys(basePaths)) {
|
|
13018
|
-
const basePathItem = recordValue(basePaths[path]);
|
|
13019
|
-
const currentPathItem = recordValue(currentPaths[path]);
|
|
13020
|
-
if (!basePathItem || !currentPathItem) {
|
|
13021
|
-
addChange(changes, {
|
|
13022
|
-
kind: "breaking",
|
|
13023
|
-
code: "path-removed",
|
|
13024
|
-
path: `paths.${path}`,
|
|
13025
|
-
message: "A path from the previous contract was removed."
|
|
13026
|
-
});
|
|
13027
|
-
continue;
|
|
13028
|
-
}
|
|
13029
|
-
for (const method of HTTP_METHODS) {
|
|
13030
|
-
const baseOperation = recordValue(basePathItem[method]);
|
|
13031
|
-
const currentOperation = recordValue(currentPathItem[method]);
|
|
13032
|
-
if (!baseOperation || !currentOperation) {
|
|
13033
|
-
if (baseOperation) {
|
|
13034
|
-
addChange(changes, {
|
|
13035
|
-
kind: "breaking",
|
|
13036
|
-
code: "operation-removed",
|
|
13037
|
-
path: `paths.${path}.${method}`,
|
|
13038
|
-
message: "An operation from the previous contract was removed."
|
|
13039
|
-
});
|
|
13040
|
-
} else if (currentOperation) {
|
|
13041
|
-
addChange(changes, {
|
|
13042
|
-
kind: "non-breaking",
|
|
13043
|
-
code: "operation-added",
|
|
13044
|
-
path: `paths.${path}.${method}`,
|
|
13045
|
-
message: "An operation was added to an existing path."
|
|
13046
|
-
});
|
|
13047
|
-
}
|
|
13048
|
-
continue;
|
|
13049
|
-
}
|
|
13050
|
-
compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, `paths.${path}.${method}`, changes);
|
|
13494
|
+
function exportGraphDot(graph) {
|
|
13495
|
+
const lines = [
|
|
13496
|
+
"digraph ApplicationGraph {",
|
|
13497
|
+
" rankdir=LR;",
|
|
13498
|
+
' node [shape=box, fontname="Helvetica"];'
|
|
13499
|
+
];
|
|
13500
|
+
for (const mod of graph.modules) {
|
|
13501
|
+
lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
|
|
13502
|
+
for (const imp of mod.imports) {
|
|
13503
|
+
lines.push(` "${mod.name}" -> "${imp}";`);
|
|
13051
13504
|
}
|
|
13052
13505
|
}
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
continue;
|
|
13056
|
-
addChange(changes, {
|
|
13057
|
-
kind: "non-breaking",
|
|
13058
|
-
code: "path-added",
|
|
13059
|
-
path: `paths.${path}`,
|
|
13060
|
-
message: "A path was added."
|
|
13061
|
-
});
|
|
13062
|
-
}
|
|
13063
|
-
compareComponents(baseDocument, currentDocument, changes);
|
|
13064
|
-
const breaking = changes.filter((change) => change.kind === "breaking");
|
|
13065
|
-
return { ok: breaking.length === 0, breaking, changes };
|
|
13066
|
-
}
|
|
13067
|
-
function formatOpenApiDiff(result) {
|
|
13068
|
-
if (result.changes.length === 0)
|
|
13069
|
-
return "OpenAPI diff passed: no contract changes.";
|
|
13070
|
-
return [
|
|
13071
|
-
`OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
|
|
13072
|
-
...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
|
|
13073
|
-
].join(`
|
|
13506
|
+
lines.push("}");
|
|
13507
|
+
return lines.join(`
|
|
13074
13508
|
`);
|
|
13075
13509
|
}
|
|
13510
|
+
|
|
13511
|
+
// src/index.ts
|
|
13512
|
+
init_generate();
|
|
13513
|
+
init_contract_manifest();
|
|
13514
|
+
|
|
13076
13515
|
// src/config.ts
|
|
13077
13516
|
init_graphql_options();
|
|
13078
13517
|
import { existsSync as existsSync5 } from "node:fs";
|
|
13079
|
-
import { join as
|
|
13518
|
+
import { join as join9, resolve as resolve15 } from "node:path";
|
|
13080
13519
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
13081
13520
|
var DEFAULT_SUPACLOUD_CONFIG = {
|
|
13082
13521
|
graphql: false,
|
|
@@ -13169,8 +13608,8 @@ function validateGovernanceConfig(config) {
|
|
|
13169
13608
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
13170
13609
|
const resolved = defineSupacloudConfig(config);
|
|
13171
13610
|
return {
|
|
13172
|
-
rootDir:
|
|
13173
|
-
outDir:
|
|
13611
|
+
rootDir: resolve15(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
|
|
13612
|
+
outDir: resolve15(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
13174
13613
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
13175
13614
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
13176
13615
|
requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
|
|
@@ -13188,16 +13627,16 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
13188
13627
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
13189
13628
|
graphql: resolved.graphql ? {
|
|
13190
13629
|
...resolved.graphql,
|
|
13191
|
-
schema:
|
|
13630
|
+
schema: resolve15(cwd, resolved.graphql.schema)
|
|
13192
13631
|
} : undefined
|
|
13193
13632
|
};
|
|
13194
13633
|
}
|
|
13195
13634
|
async function loadSupacloudConfig(cwd = process.cwd()) {
|
|
13196
13635
|
const candidates = [
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13636
|
+
join9(cwd, "supacloud.config.ts"),
|
|
13637
|
+
join9(cwd, "supacloud.config.mts"),
|
|
13638
|
+
join9(cwd, "supacloud.config.js"),
|
|
13639
|
+
join9(cwd, "supacloud.config.mjs")
|
|
13201
13640
|
];
|
|
13202
13641
|
const configPath = candidates.find((candidate) => existsSync5(candidate));
|
|
13203
13642
|
if (!configPath)
|
|
@@ -13234,6 +13673,7 @@ export {
|
|
|
13234
13673
|
TraitCompiler,
|
|
13235
13674
|
analyzeProject,
|
|
13236
13675
|
applyDiagnosticFix,
|
|
13676
|
+
assessMigration,
|
|
13237
13677
|
buildContractManifest,
|
|
13238
13678
|
buildDeliveryProject,
|
|
13239
13679
|
camelName,
|
|
@@ -13244,6 +13684,7 @@ export {
|
|
|
13244
13684
|
createContextPack,
|
|
13245
13685
|
createDeliveryPlan,
|
|
13246
13686
|
createDependencyGraphCache,
|
|
13687
|
+
createDiagnosticRepairPlan,
|
|
13247
13688
|
createExecutionPlans,
|
|
13248
13689
|
createIncrementalCompiler,
|
|
13249
13690
|
createIncrementalProgramSession,
|
|
@@ -13256,6 +13697,7 @@ export {
|
|
|
13256
13697
|
exportGraphMermaid,
|
|
13257
13698
|
formatDeliveryPlan,
|
|
13258
13699
|
formatGraph,
|
|
13700
|
+
formatMigrationAssessment,
|
|
13259
13701
|
formatOpenApiDiff,
|
|
13260
13702
|
generateApplication,
|
|
13261
13703
|
generateDatabaseContracts,
|