@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/cli.js
CHANGED
|
@@ -126,7 +126,12 @@ function buildContractManifest(graph, artifacts) {
|
|
|
126
126
|
...command?.permission === undefined ? {} : { permission: command.permission },
|
|
127
127
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
128
128
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
129
|
-
...route.
|
|
129
|
+
...route.responses === undefined ? {} : {
|
|
130
|
+
responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
|
|
131
|
+
},
|
|
132
|
+
...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence },
|
|
133
|
+
...route.parse === undefined ? {} : { parse: route.parse },
|
|
134
|
+
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody }
|
|
130
135
|
});
|
|
131
136
|
}
|
|
132
137
|
}
|
|
@@ -215,7 +220,7 @@ function renderApplication(graph, options) {
|
|
|
215
220
|
providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
|
|
216
221
|
}));
|
|
217
222
|
}
|
|
218
|
-
const imports = new ImportManager;
|
|
223
|
+
const imports = new ImportManager(new Set(["CompiledApplicationModule"]));
|
|
219
224
|
const factorySections = [];
|
|
220
225
|
const descriptorEntries = [];
|
|
221
226
|
for (const module of modules) {
|
|
@@ -232,7 +237,13 @@ function renderApplication(graph, options) {
|
|
|
232
237
|
"",
|
|
233
238
|
TYPE_GUARDS,
|
|
234
239
|
"",
|
|
235
|
-
|
|
240
|
+
...modules.length > 0 ? [
|
|
241
|
+
'export type CompiledApplicationModule = Omit<CompiledModule, "name" | "createServices"> & (',
|
|
242
|
+
...modules.map((module) => ` | { name: ${JSON.stringify(module.name)}; createServices: typeof create${pascalName(module.name)}Services }`),
|
|
243
|
+
");"
|
|
244
|
+
] : ["export type CompiledApplicationModule = CompiledModule;"],
|
|
245
|
+
"",
|
|
246
|
+
"export function createCompiledModules(): CompiledApplicationModule[] {",
|
|
236
247
|
" return [",
|
|
237
248
|
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
238
249
|
" ];",
|
|
@@ -313,7 +324,9 @@ function renderApplication(graph, options) {
|
|
|
313
324
|
};
|
|
314
325
|
}
|
|
315
326
|
async function generateApplication(graph, options) {
|
|
316
|
-
|
|
327
|
+
return writeRenderedApplication(renderApplication(graph, options), options);
|
|
328
|
+
}
|
|
329
|
+
async function writeRenderedApplication(rendered, options) {
|
|
317
330
|
await mkdir(options.outDir, { recursive: true });
|
|
318
331
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
319
332
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
@@ -400,8 +413,12 @@ function indent(text, spaces) {
|
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
class ImportManager {
|
|
416
|
+
reserved;
|
|
403
417
|
entries = new Map;
|
|
404
418
|
lookup = new Map;
|
|
419
|
+
constructor(reserved = new Set) {
|
|
420
|
+
this.reserved = reserved;
|
|
421
|
+
}
|
|
405
422
|
get size() {
|
|
406
423
|
return this.entries.size;
|
|
407
424
|
}
|
|
@@ -416,7 +433,7 @@ class ImportManager {
|
|
|
416
433
|
return existing;
|
|
417
434
|
let local = exported;
|
|
418
435
|
let counter = 2;
|
|
419
|
-
while (this.entries.has(local)) {
|
|
436
|
+
while (this.entries.has(local) || this.reserved.has(local)) {
|
|
420
437
|
local = `${exported}${counter}`;
|
|
421
438
|
counter += 1;
|
|
422
439
|
}
|
|
@@ -473,7 +490,7 @@ class ModuleGenerator {
|
|
|
473
490
|
renderDescriptor() {
|
|
474
491
|
const lines = [
|
|
475
492
|
`{`,
|
|
476
|
-
` name: ${JSON.stringify(this.module.name)},`,
|
|
493
|
+
` name: ${JSON.stringify(this.module.name)} as const,`,
|
|
477
494
|
` createServices: create${this.pascal}Services,`
|
|
478
495
|
];
|
|
479
496
|
if (this.hasFactoryContent("request")) {
|
|
@@ -612,6 +629,10 @@ class ModuleGenerator {
|
|
|
612
629
|
if (route.contract !== undefined) {
|
|
613
630
|
fields.push(`contract: ${JSON.stringify(route.contract)}`);
|
|
614
631
|
}
|
|
632
|
+
if (route.parse === "none")
|
|
633
|
+
fields.push('parse: "none"');
|
|
634
|
+
if (route.allowDeleteBody === true)
|
|
635
|
+
fields.push("allowDeleteBody: true");
|
|
615
636
|
if (route.schemaKinds !== undefined && Object.keys(route.schemaKinds).length > 0) {
|
|
616
637
|
fields.push(`schemaKinds: ${JSON.stringify(route.schemaKinds)}`);
|
|
617
638
|
}
|
|
@@ -711,7 +732,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
711
732
|
`function create${this.pascal}Services(`,
|
|
712
733
|
` deps: Record<string, unknown>,`,
|
|
713
734
|
` imported: Record<string, Record<string, unknown>>,`,
|
|
714
|
-
`)
|
|
735
|
+
`) {`,
|
|
715
736
|
indent(this.renderFactoryBody("services"), 2),
|
|
716
737
|
`}`
|
|
717
738
|
].join(`
|
|
@@ -808,7 +829,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
808
829
|
`);
|
|
809
830
|
}
|
|
810
831
|
const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
|
|
811
|
-
lines.push(`return { ${entries.join(", ")} };`);
|
|
832
|
+
lines.push(`return retainPlatformDependencies({ ${entries.join(", ")} }, deps);`);
|
|
812
833
|
return lines.join(`
|
|
813
834
|
`);
|
|
814
835
|
}
|
|
@@ -909,6 +930,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
909
930
|
return this.depExpr(own.useExisting ?? token, kind, options);
|
|
910
931
|
}
|
|
911
932
|
}
|
|
933
|
+
if (own && factoryOfScope(own.scope) === "services" && kind !== "services" && !isSkipSelf && !isSelf) {
|
|
934
|
+
return `services.${camelName(token)}`;
|
|
935
|
+
}
|
|
912
936
|
if (isSelf) {
|
|
913
937
|
return isOptional ? "undefined" : `services.${camelName(token)}`;
|
|
914
938
|
}
|
|
@@ -926,13 +950,14 @@ ${indent(item, 2)}`).join(",")}
|
|
|
926
950
|
return `imported.${mod.name}.${camelName(token)}`;
|
|
927
951
|
}
|
|
928
952
|
}
|
|
929
|
-
if (isOptional && !this.graph.externalTokens.includes(token)) {
|
|
930
|
-
return "undefined";
|
|
931
|
-
}
|
|
932
953
|
if (isSelf)
|
|
933
954
|
return isOptional ? "undefined" : `services.${camelName(token)}`;
|
|
934
955
|
if (kind === "services")
|
|
935
956
|
return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
|
|
957
|
+
if (!own || isSkipSelf) {
|
|
958
|
+
const external = `platformDependencies.get(services)?.${camelName(token)}`;
|
|
959
|
+
return isOptional ? `(${external} ?? undefined)` : external;
|
|
960
|
+
}
|
|
936
961
|
return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
|
|
937
962
|
}
|
|
938
963
|
}
|
|
@@ -960,7 +985,7 @@ function orderProviders(providers) {
|
|
|
960
985
|
function renderClient(graph, options) {
|
|
961
986
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
962
987
|
const outDir = options?.outDir ?? process.cwd();
|
|
963
|
-
const imports = new ImportManager;
|
|
988
|
+
const imports = new ImportManager(new Set(["ApiClientError", "ApiClientErrorCode", "ApiClientErrorDetails"]));
|
|
964
989
|
const controllerEntries = [];
|
|
965
990
|
const routeTypes = [];
|
|
966
991
|
const schemaEntries = [];
|
|
@@ -1018,11 +1043,14 @@ function renderClient(graph, options) {
|
|
|
1018
1043
|
}).filter((entry) => entry.length === 2));
|
|
1019
1044
|
if (Object.keys(responseSchemas).length > 0)
|
|
1020
1045
|
usesValue = true;
|
|
1021
|
-
const paramNames = [...new Set(
|
|
1046
|
+
const paramNames = [...new Set([
|
|
1047
|
+
...(fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1)),
|
|
1048
|
+
...fullPath.endsWith("/*") ? ["*"] : []
|
|
1049
|
+
])];
|
|
1022
1050
|
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>";
|
|
1023
1051
|
const paramsType = paramsSchema ? `Static<typeof ${paramsSchema.local}>${paramNames.length > 0 ? ` & ${pathParamsType}` : ""}` : pathParamsType;
|
|
1024
1052
|
const queryType = querySchema ? `Static<typeof ${querySchema.local}>` : "Record<string, unknown>";
|
|
1025
|
-
const bodyType = bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
|
|
1053
|
+
const bodyType = route.parse === "none" ? "BodyInit" : bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
|
|
1026
1054
|
const headersType = headersSchema ? `Static<typeof ${headersSchema.local}>` : "Record<string, string>";
|
|
1027
1055
|
const cookieType = cookieSchema ? `Static<typeof ${cookieSchema.local}>` : "Record<string, string | number | boolean>";
|
|
1028
1056
|
const responseTypes = [
|
|
@@ -1059,7 +1087,9 @@ function renderClient(graph, options) {
|
|
|
1059
1087
|
["queryTransforms", route.queryTransforms],
|
|
1060
1088
|
["queryDefaults", route.queryDefaults],
|
|
1061
1089
|
["title", route.title],
|
|
1062
|
-
["data", route.data]
|
|
1090
|
+
["data", route.data],
|
|
1091
|
+
["parse", route.parse],
|
|
1092
|
+
["allowDeleteBody", route.allowDeleteBody]
|
|
1063
1093
|
]) {
|
|
1064
1094
|
if (value !== undefined)
|
|
1065
1095
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -1093,7 +1123,7 @@ function renderClient(graph, options) {
|
|
|
1093
1123
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
1094
1124
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
1095
1125
|
routeMethods.push(`
|
|
1096
|
-
${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}),`);
|
|
1126
|
+
${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}, ${route.parse === "none"}),`);
|
|
1097
1127
|
}
|
|
1098
1128
|
controllerEntries.push(`
|
|
1099
1129
|
${controllerKey}: {${routeMethods.join("")}
|
|
@@ -1124,6 +1154,40 @@ function renderClient(graph, options) {
|
|
|
1124
1154
|
"",
|
|
1125
1155
|
"export type ResponseDecoder<T> = (value: unknown) => T;",
|
|
1126
1156
|
"",
|
|
1157
|
+
'export type ApiClientErrorCode = "API_HTTP_ERROR" | "API_RESPONSE_INVALID" | "API_RESPONSE_UNDECLARED";',
|
|
1158
|
+
"",
|
|
1159
|
+
"export interface ApiClientErrorDetails {",
|
|
1160
|
+
" code: ApiClientErrorCode;",
|
|
1161
|
+
" method: string;",
|
|
1162
|
+
" path: string;",
|
|
1163
|
+
" status: number;",
|
|
1164
|
+
" requestId?: string | null;",
|
|
1165
|
+
" response?: Response;",
|
|
1166
|
+
"}",
|
|
1167
|
+
"",
|
|
1168
|
+
"/** Transport/contract metadata, not proof that a failed write rolled back. */",
|
|
1169
|
+
"export class ApiClientError extends Error {",
|
|
1170
|
+
" readonly code: ApiClientErrorCode;",
|
|
1171
|
+
" readonly method: string;",
|
|
1172
|
+
" readonly path: string;",
|
|
1173
|
+
" readonly status: number;",
|
|
1174
|
+
" readonly requestId: string | undefined;",
|
|
1175
|
+
" /** Undeclared HTTP response for explicit inspection; excluded from JSON serialization. */",
|
|
1176
|
+
" readonly response: Response | undefined;",
|
|
1177
|
+
" constructor(message: string, details: ApiClientErrorDetails) {",
|
|
1178
|
+
" super(message);",
|
|
1179
|
+
' this.name = "ApiClientError";',
|
|
1180
|
+
" const { code, method, path, status, requestId, response } = details;",
|
|
1181
|
+
" this.code = code;",
|
|
1182
|
+
" this.method = method;",
|
|
1183
|
+
" this.path = path;",
|
|
1184
|
+
" this.status = status;",
|
|
1185
|
+
" this.requestId = requestId && /^[A-Za-z0-9._:-]{1,256}$/.test(requestId) ? requestId : undefined;",
|
|
1186
|
+
" this.response = response;",
|
|
1187
|
+
' Object.defineProperty(this, "response", { enumerable: false });',
|
|
1188
|
+
" }",
|
|
1189
|
+
"}",
|
|
1190
|
+
"",
|
|
1127
1191
|
"export type RouteMethod<",
|
|
1128
1192
|
" Options extends ClientRequestOptions = ClientRequestOptions,",
|
|
1129
1193
|
" Result = never,",
|
|
@@ -1177,6 +1241,13 @@ function renderClient(graph, options) {
|
|
|
1177
1241
|
' if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`Missing route parameter: ${key}`);',
|
|
1178
1242
|
" return encodeURIComponent(String(value));",
|
|
1179
1243
|
" });",
|
|
1244
|
+
' if (path.endsWith("/*")) {',
|
|
1245
|
+
' const wildcard = params && Object.hasOwn(params, "*") ? params["*"] : undefined;',
|
|
1246
|
+
' if (typeof wildcard !== "string" && typeof wildcard !== "number") throw new TypeError("Missing route parameter: *");',
|
|
1247
|
+
' const segments = String(wildcard).split("/");',
|
|
1248
|
+
' if (segments.some(segment => segment === "." || segment === "..")) throw new TypeError("Wildcard dot segments are not supported by Fetch URLs");',
|
|
1249
|
+
' url = url.slice(0, -1) + segments.map(encodeURIComponent).join("/");',
|
|
1250
|
+
" }",
|
|
1180
1251
|
" if (query) {",
|
|
1181
1252
|
" const searchParams = new URLSearchParams();",
|
|
1182
1253
|
" for (const [k, v] of Object.entries(query)) {",
|
|
@@ -1363,6 +1434,10 @@ function renderClient(graph, options) {
|
|
|
1363
1434
|
"export function createApiClient(config: ApiClientConfig = {}) {",
|
|
1364
1435
|
" const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
|
|
1365
1436
|
' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
|
|
1437
|
+
" function rawBody(value: unknown): BodyInit {",
|
|
1438
|
+
' 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;',
|
|
1439
|
+
' throw new TypeError("Raw routes require a BodyInit value; JSON encoding is not implicit");',
|
|
1440
|
+
" }",
|
|
1366
1441
|
"",
|
|
1367
1442
|
" async function request<T>(",
|
|
1368
1443
|
" method: string,",
|
|
@@ -1371,6 +1446,7 @@ function renderClient(graph, options) {
|
|
|
1371
1446
|
" decode: ResponseDecoder<T>,",
|
|
1372
1447
|
` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
|
|
1373
1448
|
" responseKind?: string,",
|
|
1449
|
+
" raw?: boolean,",
|
|
1374
1450
|
" ): Promise<T>;",
|
|
1375
1451
|
" async function request<T>(",
|
|
1376
1452
|
" method: string,",
|
|
@@ -1379,6 +1455,7 @@ function renderClient(graph, options) {
|
|
|
1379
1455
|
" decode?: ResponseDecoder<T>,",
|
|
1380
1456
|
` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
|
|
1381
1457
|
" responseKind?: string,",
|
|
1458
|
+
" raw?: boolean,",
|
|
1382
1459
|
" ): Promise<T | unknown>;",
|
|
1383
1460
|
" async function request(",
|
|
1384
1461
|
" method: string,",
|
|
@@ -1392,11 +1469,12 @@ function renderClient(graph, options) {
|
|
|
1392
1469
|
" decode?: ResponseDecoder<T>,",
|
|
1393
1470
|
` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
|
|
1394
1471
|
" responseKind?: string,",
|
|
1472
|
+
" raw = false,",
|
|
1395
1473
|
" ): Promise<T | unknown> {",
|
|
1396
1474
|
" const url = `${baseUrl}${buildRouteUrl(path, options.params, options.query)}`;",
|
|
1397
1475
|
' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
|
|
1398
1476
|
" const headers: Record<string, string> = {",
|
|
1399
|
-
' "content-type": "application/json",',
|
|
1477
|
+
' ...(raw ? {} : { "content-type": "application/json" }),',
|
|
1400
1478
|
" ...customHeaders,",
|
|
1401
1479
|
" ...options.headers,",
|
|
1402
1480
|
" };",
|
|
@@ -1415,21 +1493,27 @@ function renderClient(graph, options) {
|
|
|
1415
1493
|
" return fetcher(reqPayload.url, {",
|
|
1416
1494
|
" method: reqPayload.method,",
|
|
1417
1495
|
" headers: reqPayload.headers,",
|
|
1418
|
-
" ...(reqPayload.body === undefined ? {} : { body: JSON.stringify(reqPayload.body) }),",
|
|
1496
|
+
" ...(reqPayload.body === undefined ? {} : { body: raw ? rawBody(reqPayload.body) : JSON.stringify(reqPayload.body) }),",
|
|
1497
|
+
' ...(raw && reqPayload.body instanceof ReadableStream ? { duplex: "half" } : {}),',
|
|
1419
1498
|
" });",
|
|
1420
1499
|
" };",
|
|
1421
1500
|
" const response = await executeChain(0, { method, url, headers, body: options.body });",
|
|
1422
1501
|
" const declaredSchema = responseSchemas ? selectResponseSchema(response.status, responseSchemas) : undefined;",
|
|
1502
|
+
" const failure = (code: ApiClientErrorCode, message: string) => new ApiClientError(message, {",
|
|
1503
|
+
' code, method, path, status: response.status, requestId: response.headers.get("x-request-id"),',
|
|
1504
|
+
' ...(code === "API_HTTP_ERROR" ? { response } : {}),',
|
|
1505
|
+
" });",
|
|
1423
1506
|
" if (!response.ok && declaredSchema === undefined) {",
|
|
1424
|
-
|
|
1425
|
-
" throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
|
|
1507
|
+
' throw failure("API_HTTP_ERROR", `API request failed: ${method} ${path} -> ${response.status}`);',
|
|
1426
1508
|
" }",
|
|
1427
1509
|
' const contentType = response.headers?.get("content-type") ?? "";',
|
|
1428
1510
|
" let value: unknown;",
|
|
1429
1511
|
" if ([204, 205, 304].includes(response.status)) {",
|
|
1430
1512
|
" value = undefined;",
|
|
1431
1513
|
' } else if (contentType.includes("application/json") || contentType.includes("+json")) {',
|
|
1432
|
-
"
|
|
1514
|
+
" const responseText = await response.text();",
|
|
1515
|
+
" try { value = JSON.parse(responseText); }",
|
|
1516
|
+
' catch { throw failure("API_RESPONSE_INVALID", "Response is not valid JSON"); }',
|
|
1433
1517
|
' } else if (responseKind === "binary" || contentType.includes("application/octet-stream")) {',
|
|
1434
1518
|
" value = await response.arrayBuffer();",
|
|
1435
1519
|
' } else if (responseKind === "stream") {',
|
|
@@ -1440,20 +1524,24 @@ function renderClient(graph, options) {
|
|
|
1440
1524
|
" if (!responseSchemas) return decode ? decode(value) : value;",
|
|
1441
1525
|
" if (declaredSchema === undefined) {",
|
|
1442
1526
|
' const transportSuccess = response.ok && (responseKind === "binary" || responseKind === "stream");',
|
|
1443
|
-
|
|
1527
|
+
' if (!transportSuccess) throw failure("API_RESPONSE_UNDECLARED", `No response schema declared for HTTP ${response.status}`);',
|
|
1444
1528
|
" return decode ? decode(value) : value;",
|
|
1445
1529
|
" }",
|
|
1446
|
-
...usesValue ? [
|
|
1530
|
+
...usesValue ? [
|
|
1531
|
+
" let checked: unknown;",
|
|
1532
|
+
" try { checked = decodeResponseSchema(value, response.status, responseSchemas, { normalize: config.normalize ?? true }); }",
|
|
1533
|
+
' catch { throw failure("API_RESPONSE_INVALID", "Response does not match schema"); }'
|
|
1534
|
+
] : [" const checked = value;"],
|
|
1447
1535
|
" return decode ? decode(checked) : checked;",
|
|
1448
1536
|
" }",
|
|
1449
1537
|
"",
|
|
1450
|
-
` function makeRoute<Options extends ClientRequestOptions, Result = never>(method: string, path: string, responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>, responseKind?: string): RouteMethod<Options, Result> {`,
|
|
1538
|
+
` function makeRoute<Options extends ClientRequestOptions, Result = never>(method: string, path: string, responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>, responseKind?: string, raw = false): RouteMethod<Options, Result> {`,
|
|
1451
1539
|
" function route<T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
|
|
1452
1540
|
" function route(options: Options): Promise<Result>;",
|
|
1453
1541
|
" function route(options?: Options): Promise<unknown>;",
|
|
1454
1542
|
" function route<T>(options?: Options, decode?: ResponseDecoder<T>): Promise<T | unknown> {",
|
|
1455
1543
|
" const requestOptions = options ?? {};",
|
|
1456
|
-
" return request(method, path, requestOptions, decode, responseSchemas, responseKind);",
|
|
1544
|
+
" return request(method, path, requestOptions, decode, responseSchemas, responseKind, raw);",
|
|
1457
1545
|
" }",
|
|
1458
1546
|
" return route as RouteMethod<Options, Result>;",
|
|
1459
1547
|
" }",
|
|
@@ -1510,11 +1598,15 @@ function renderOpenApi(graph, options) {
|
|
|
1510
1598
|
for (const module of graph.modules) {
|
|
1511
1599
|
for (const controller of module.controllers) {
|
|
1512
1600
|
for (const route of controller.routes) {
|
|
1601
|
+
const documentation = route.data?.openapi;
|
|
1602
|
+
if (documentation !== null && typeof documentation === "object" && "hide" in documentation && documentation.hide === true)
|
|
1603
|
+
continue;
|
|
1513
1604
|
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1514
1605
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1515
1606
|
const hasResponseMap = responseEntries.length > 0;
|
|
1516
1607
|
const pathParams = [...new Set([
|
|
1517
1608
|
...(fullPath.match(/:([A-Za-z0-9_]+)/g) ?? []).map((item) => item.slice(1)),
|
|
1609
|
+
...fullPath.endsWith("/*") ? ["*"] : [],
|
|
1518
1610
|
...route.pathParams ?? []
|
|
1519
1611
|
].filter((item) => item.length > 0))];
|
|
1520
1612
|
const queryParams = [...new Set([
|
|
@@ -1547,6 +1639,8 @@ function renderOpenApi(graph, options) {
|
|
|
1547
1639
|
}).filter((entry) => entry.length === 2)) } : {},
|
|
1548
1640
|
responseContentType,
|
|
1549
1641
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
1642
|
+
...route.parse === undefined ? {} : { parse: route.parse },
|
|
1643
|
+
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
1550
1644
|
module: module.name,
|
|
1551
1645
|
controller: controller.className,
|
|
1552
1646
|
handler: route.handler,
|
|
@@ -1630,6 +1724,8 @@ function renderOpenApi(graph, options) {
|
|
|
1630
1724
|
" canMatch?: readonly string[];",
|
|
1631
1725
|
" canDeactivate?: readonly string[];",
|
|
1632
1726
|
" contract?: Record<string, unknown>;",
|
|
1727
|
+
' parse?: "none";',
|
|
1728
|
+
" allowDeleteBody?: true;",
|
|
1633
1729
|
" data?: Record<string, unknown>;",
|
|
1634
1730
|
" requiresAuth?: boolean;",
|
|
1635
1731
|
"};",
|
|
@@ -1827,8 +1923,15 @@ function renderOpenApi(graph, options) {
|
|
|
1827
1923
|
' return { $ref: `#/components/schemas/${encodeURIComponent(name).replaceAll("%2F", "/")}` };',
|
|
1828
1924
|
"}",
|
|
1829
1925
|
"",
|
|
1926
|
+
"function wildcardParameterName(path: string): string {",
|
|
1927
|
+
" const names = new Set([...path.matchAll(/:([A-Za-z0-9_]+)/g)].map(match => match[1]));",
|
|
1928
|
+
' let name = "wildcard";',
|
|
1929
|
+
' while (names.has(name)) name += "_";',
|
|
1930
|
+
" return name;",
|
|
1931
|
+
"}",
|
|
1932
|
+
"",
|
|
1830
1933
|
"function openApiPath(path: string): string {",
|
|
1831
|
-
' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");',
|
|
1934
|
+
' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}").replace(/\\/\\*$/, `/{${wildcardParameterName(path)}}`);',
|
|
1832
1935
|
"}",
|
|
1833
1936
|
"",
|
|
1834
1937
|
"function responseHasBody(status: string, kind: string | undefined): boolean {",
|
|
@@ -1858,7 +1961,7 @@ function renderOpenApi(graph, options) {
|
|
|
1858
1961
|
" const parameters: Record<string, unknown>[] = [];",
|
|
1859
1962
|
" const paramsSchema = definition.paramsSchema ? OPENAPI_SCHEMA_REGISTRY[definition.paramsSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
1860
1963
|
" for (const name of definition.pathParams) parameters.push({",
|
|
1861
|
-
' name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
|
|
1964
|
+
' name: name === "*" ? wildcardParameterName(definition.path) : name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
|
|
1862
1965
|
" });",
|
|
1863
1966
|
" const querySchema = definition.querySchema ? OPENAPI_SCHEMA_REGISTRY[definition.querySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
1864
1967
|
" const queryNames = new Set([...(querySchema ? schemaPropertyNames(querySchema) : []), ...definition.queryParams]);",
|
|
@@ -1895,6 +1998,7 @@ function renderOpenApi(graph, options) {
|
|
|
1895
1998
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
1896
1999
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
1897
2000
|
' ...(definition.bodySchema ? { requestBody: { required: !schemaAllowsUndefined(OPENAPI_SCHEMA_REGISTRY[definition.bodySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY]), content: { "application/json": { schema: schemaReference(definition.bodySchema) } } } } : {}),',
|
|
2001
|
+
' ...(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" } } } } } : {}),',
|
|
1898
2002
|
" ...(definition.requiresAuth ? { security: [{ bearerAuth: [] }] } : {}),",
|
|
1899
2003
|
' "x-supacloud": {',
|
|
1900
2004
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
@@ -1904,6 +2008,8 @@ function renderOpenApi(graph, options) {
|
|
|
1904
2008
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
1905
2009
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
1906
2010
|
" ...(definition.contract ? { contract: definition.contract } : {}),",
|
|
2011
|
+
" ...(definition.parse ? { parse: definition.parse } : {}),",
|
|
2012
|
+
" ...(definition.allowDeleteBody ? { allowDeleteBody: true } : {}),",
|
|
1907
2013
|
" ...(definition.data ? { data: definition.data } : {}),",
|
|
1908
2014
|
" },",
|
|
1909
2015
|
" };",
|
|
@@ -1999,6 +2105,8 @@ function renderPermissions(graph) {
|
|
|
1999
2105
|
`);
|
|
2000
2106
|
}
|
|
2001
2107
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES = `export interface CompiledRoute {
|
|
2108
|
+
parse?: "none";
|
|
2109
|
+
allowDeleteBody?: true;
|
|
2002
2110
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
2003
2111
|
path: string;
|
|
2004
2112
|
handler: string;
|
|
@@ -2151,6 +2259,12 @@ function resolveFactoryValue(value: unknown): unknown {
|
|
|
2151
2259
|
}
|
|
2152
2260
|
|
|
2153
2261
|
const scopeDestructions = new WeakMap<object, Promise<void>>();
|
|
2262
|
+
const platformDependencies = new WeakMap<object, Record<string, unknown>>();
|
|
2263
|
+
|
|
2264
|
+
function retainPlatformDependencies<T extends Record<string, unknown>>(services: T, deps: Record<string, unknown>): T {
|
|
2265
|
+
platformDependencies.set(services, deps);
|
|
2266
|
+
return services;
|
|
2267
|
+
}
|
|
2154
2268
|
|
|
2155
2269
|
function destroyScopeInstances(
|
|
2156
2270
|
scope: Record<string, unknown>,
|
|
@@ -2697,12 +2811,12 @@ var init_graphql = __esm(() => {
|
|
|
2697
2811
|
});
|
|
2698
2812
|
|
|
2699
2813
|
// src/database-contracts.ts
|
|
2700
|
-
import { createHash as
|
|
2701
|
-
import { mkdir as mkdir5, readFile as
|
|
2702
|
-
import { dirname as dirname10, relative as relative12, resolve as
|
|
2814
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
2815
|
+
import { mkdir as mkdir5, readFile as readFile12 } from "node:fs/promises";
|
|
2816
|
+
import { dirname as dirname10, relative as relative12, resolve as resolve16 } from "node:path";
|
|
2703
2817
|
import * as ts13 from "@typescript/typescript6";
|
|
2704
2818
|
function hash2(value) {
|
|
2705
|
-
return
|
|
2819
|
+
return createHash10("sha256").update(value).digest("hex");
|
|
2706
2820
|
}
|
|
2707
2821
|
function importPath(out, path) {
|
|
2708
2822
|
const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
|
|
@@ -2727,19 +2841,19 @@ function parseDatabaseContractsOptions(value, directory) {
|
|
|
2727
2841
|
throw new TypeError("migrations must be an ordered list of SQL files");
|
|
2728
2842
|
}
|
|
2729
2843
|
return {
|
|
2730
|
-
rootDir:
|
|
2731
|
-
outDir:
|
|
2732
|
-
postgrestTypes:
|
|
2733
|
-
drizzleSchema:
|
|
2844
|
+
rootDir: resolve16(directory, field("rootDir")),
|
|
2845
|
+
outDir: resolve16(directory, field("outDir")),
|
|
2846
|
+
postgrestTypes: resolve16(directory, field("postgrestTypes")),
|
|
2847
|
+
drizzleSchema: resolve16(directory, field("drizzleSchema")),
|
|
2734
2848
|
role: field("role"),
|
|
2735
|
-
graphql: { ...graphql, schema:
|
|
2736
|
-
migrations: migrations.map((file) =>
|
|
2849
|
+
graphql: { ...graphql, schema: resolve16(directory, graphql.schema) },
|
|
2850
|
+
migrations: migrations.map((file) => resolve16(directory, file))
|
|
2737
2851
|
};
|
|
2738
2852
|
}
|
|
2739
2853
|
async function generateDatabaseContracts(options, check = false) {
|
|
2740
|
-
const rootDir =
|
|
2741
|
-
const postgrestTypes =
|
|
2742
|
-
const snapshot = await
|
|
2854
|
+
const rootDir = resolve16(options.rootDir), outDir = resolve16(options.outDir);
|
|
2855
|
+
const postgrestTypes = resolve16(rootDir, options.postgrestTypes), drizzleSchema = resolve16(rootDir, options.drizzleSchema);
|
|
2856
|
+
const snapshot = await readFile12(postgrestTypes, "utf8");
|
|
2743
2857
|
const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
|
|
2744
2858
|
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));
|
|
2745
2859
|
if (!database)
|
|
@@ -2766,8 +2880,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2766
2880
|
}
|
|
2767
2881
|
const inputs = {};
|
|
2768
2882
|
const addInput = async (path) => {
|
|
2769
|
-
const absolute =
|
|
2770
|
-
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await
|
|
2883
|
+
const absolute = resolve16(rootDir, path);
|
|
2884
|
+
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile12(absolute, "utf8"));
|
|
2771
2885
|
};
|
|
2772
2886
|
await addInput(postgrestTypes);
|
|
2773
2887
|
await addInput(drizzleSchema);
|
|
@@ -2783,8 +2897,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2783
2897
|
if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
|
|
2784
2898
|
await addInput(source.fileName);
|
|
2785
2899
|
}
|
|
2786
|
-
await addInput(
|
|
2787
|
-
if (new Set(options.migrations.map((path) =>
|
|
2900
|
+
await addInput(resolve16(rootDir, options.graphql.schema));
|
|
2901
|
+
if (new Set(options.migrations.map((path) => resolve16(rootDir, path))).size !== options.migrations.length) {
|
|
2788
2902
|
throw new Error("Duplicate migration in database contracts configuration");
|
|
2789
2903
|
}
|
|
2790
2904
|
for (const path of options.migrations)
|
|
@@ -2805,17 +2919,17 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2805
2919
|
version: 1,
|
|
2806
2920
|
role: options.role,
|
|
2807
2921
|
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
|
|
2808
|
-
migrationOrder: options.migrations.map((path) => relative12(rootDir,
|
|
2922
|
+
migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve16(rootDir, path)).replaceAll("\\", "/")),
|
|
2809
2923
|
outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
|
|
2810
2924
|
};
|
|
2811
2925
|
files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
|
|
2812
2926
|
`;
|
|
2813
2927
|
const mismatches = [];
|
|
2814
2928
|
for (const [file, content] of Object.entries(files)) {
|
|
2815
|
-
const path =
|
|
2929
|
+
const path = resolve16(outDir, file);
|
|
2816
2930
|
let current;
|
|
2817
2931
|
try {
|
|
2818
|
-
current = await
|
|
2932
|
+
current = await readFile12(path, "utf8");
|
|
2819
2933
|
} catch (error) {
|
|
2820
2934
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2821
2935
|
throw error;
|
|
@@ -2826,13 +2940,13 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2826
2940
|
if (!check) {
|
|
2827
2941
|
await mkdir5(outDir, { recursive: true });
|
|
2828
2942
|
for (const [file, content] of Object.entries(files))
|
|
2829
|
-
await writeFileIfChanged(
|
|
2943
|
+
await writeFileIfChanged(resolve16(outDir, file), content);
|
|
2830
2944
|
}
|
|
2831
2945
|
return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
|
|
2832
2946
|
}
|
|
2833
2947
|
async function runDatabaseContractsFile(path, check = false) {
|
|
2834
|
-
const absolute =
|
|
2835
|
-
const value = JSON.parse(await
|
|
2948
|
+
const absolute = resolve16(path);
|
|
2949
|
+
const value = JSON.parse(await readFile12(absolute, "utf8"));
|
|
2836
2950
|
return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname10(absolute)), check);
|
|
2837
2951
|
}
|
|
2838
2952
|
var init_database_contracts = __esm(() => {
|
|
@@ -2842,9 +2956,9 @@ var init_database_contracts = __esm(() => {
|
|
|
2842
2956
|
});
|
|
2843
2957
|
|
|
2844
2958
|
// src/graphql-schema.ts
|
|
2845
|
-
import { mkdir as mkdir6, readFile as
|
|
2846
|
-
import { createHash as
|
|
2847
|
-
import { dirname as dirname11, resolve as
|
|
2959
|
+
import { mkdir as mkdir6, readFile as readFile13 } from "node:fs/promises";
|
|
2960
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
2961
|
+
import { dirname as dirname11, resolve as resolve17 } from "node:path";
|
|
2848
2962
|
async function pullGraphqlSchema(options) {
|
|
2849
2963
|
assertGraphqlOptions({ schema: options.output });
|
|
2850
2964
|
const endpoint = new URL(options.url);
|
|
@@ -2884,7 +2998,7 @@ async function pullGraphqlSchema(options) {
|
|
|
2884
2998
|
throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
|
|
2885
2999
|
}
|
|
2886
3000
|
const schema = lexicographicSortSchema(buildClientSchema(data));
|
|
2887
|
-
const path =
|
|
3001
|
+
const path = resolve17(options.output);
|
|
2888
3002
|
const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
|
|
2889
3003
|
` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
|
|
2890
3004
|
# Database First: change database declarations, apply migrations, then re-export for the intended role.
|
|
@@ -2892,13 +3006,13 @@ async function pullGraphqlSchema(options) {
|
|
|
2892
3006
|
`;
|
|
2893
3007
|
let previous;
|
|
2894
3008
|
try {
|
|
2895
|
-
previous = await
|
|
3009
|
+
previous = await readFile13(path, "utf8");
|
|
2896
3010
|
} catch (error) {
|
|
2897
3011
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2898
3012
|
throw error;
|
|
2899
3013
|
}
|
|
2900
3014
|
const upToDate = previous === content;
|
|
2901
|
-
const schemaHash =
|
|
3015
|
+
const schemaHash = createHash11("sha256").update(content).digest("hex");
|
|
2902
3016
|
if (options.check)
|
|
2903
3017
|
return { path, schemaHash, upToDate, written: false };
|
|
2904
3018
|
await mkdir6(dirname11(path), { recursive: true });
|
|
@@ -2911,8 +3025,8 @@ var init_graphql_schema = __esm(() => {
|
|
|
2911
3025
|
});
|
|
2912
3026
|
|
|
2913
3027
|
// src/cli.ts
|
|
2914
|
-
import { resolve as
|
|
2915
|
-
import { readFile as
|
|
3028
|
+
import { resolve as resolve18 } from "node:path";
|
|
3029
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
2916
3030
|
|
|
2917
3031
|
// src/analyze.ts
|
|
2918
3032
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -3613,6 +3727,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3613
3727
|
};
|
|
3614
3728
|
function validateGraph(graph, options = false) {
|
|
3615
3729
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
3730
|
+
const generateOpenApi = typeof options === "object" && options.generateOpenApi === true;
|
|
3616
3731
|
const diagnostics = [];
|
|
3617
3732
|
for (const module of graph.modules) {
|
|
3618
3733
|
for (const owner of [...module.providers, ...module.controllers]) {
|
|
@@ -3748,7 +3863,7 @@ function validateGraph(graph, options = false) {
|
|
|
3748
3863
|
}
|
|
3749
3864
|
const fullPath = joinRoutePaths2(controller.path, route.path);
|
|
3750
3865
|
const rawFullPath = joinRawRoutePaths(controller.path, route.path);
|
|
3751
|
-
const key = `${route.method} ${fullPath}`;
|
|
3866
|
+
const key = `${route.method} ${generateOpenApi ? fullPath.replace(/:[A-Za-z0-9_]+|\*$/g, "{}") : fullPath}`;
|
|
3752
3867
|
const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
|
|
3753
3868
|
if (openApiMatch) {
|
|
3754
3869
|
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]}'.`);
|
|
@@ -3848,7 +3963,13 @@ function validateGraph(graph, options = false) {
|
|
|
3848
3963
|
}
|
|
3849
3964
|
}
|
|
3850
3965
|
}
|
|
3851
|
-
if (
|
|
3966
|
+
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
3967
|
+
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
3968
|
+
}
|
|
3969
|
+
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
3970
|
+
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);
|
|
3971
|
+
}
|
|
3972
|
+
if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE" && !route.allowDeleteBody) && (route.hasBodyBinding || route.body)) {
|
|
3852
3973
|
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().`, {
|
|
3853
3974
|
type: "remove_route_body_binding",
|
|
3854
3975
|
targetFile: controller.file,
|
|
@@ -4560,6 +4681,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4560
4681
|
tokensByName: new Map,
|
|
4561
4682
|
classesByName: new Map,
|
|
4562
4683
|
variablesByName: new Map,
|
|
4684
|
+
moduleHandlerFiles: new Map,
|
|
4563
4685
|
diagnostics: []
|
|
4564
4686
|
};
|
|
4565
4687
|
const nativeTraitFiles = new Map;
|
|
@@ -4650,10 +4772,11 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4650
4772
|
const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
|
|
4651
4773
|
const moduleFileExists = currentFileHashes.has(entry.module.file);
|
|
4652
4774
|
const isAffectedByDep = affectedModuleNames.has(modName);
|
|
4653
|
-
if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
|
|
4775
|
+
if (!hasChangedFile && !isAffectedByDep && moduleFileExists && entry.handlerFiles !== undefined) {
|
|
4654
4776
|
modulesToKeep.set(modName, entry);
|
|
4655
4777
|
reusedModules.push(modName);
|
|
4656
4778
|
finalModules.push(entry.module);
|
|
4779
|
+
ctx.moduleHandlerFiles.set(modName, entry.handlerFiles);
|
|
4657
4780
|
if (entry.diagnostics)
|
|
4658
4781
|
finalDiagnostics.push(...entry.diagnostics);
|
|
4659
4782
|
}
|
|
@@ -4673,6 +4796,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4673
4796
|
}
|
|
4674
4797
|
cache.modules.set(parsed.name, {
|
|
4675
4798
|
module: parsed,
|
|
4799
|
+
handlerFiles: ctx.moduleHandlerFiles.get(parsed.name) ?? [],
|
|
4676
4800
|
ownedFiles: [...ownedFiles],
|
|
4677
4801
|
fileHashes,
|
|
4678
4802
|
diagnostics: moduleDiagnostics
|
|
@@ -4841,6 +4965,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
4841
4965
|
externalTokens,
|
|
4842
4966
|
diagnostics: ctx.diagnostics.map(withDiagnosticMetadata),
|
|
4843
4967
|
tokenNames,
|
|
4968
|
+
moduleHandlerFiles: Object.fromEntries(ctx.moduleHandlerFiles),
|
|
4844
4969
|
...cache ? { cacheStats: { reusedModules, reanalyzedModules } } : {}
|
|
4845
4970
|
};
|
|
4846
4971
|
}
|
|
@@ -5053,6 +5178,7 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
5053
5178
|
arrayProp(options, "commands").forEach(collectHandler);
|
|
5054
5179
|
arrayProp(options, "jobs").forEach(collectHandler);
|
|
5055
5180
|
arrayProp(options, "queries").forEach(collectHandler);
|
|
5181
|
+
ctx.moduleHandlerFiles.set(name, [...new Set([...handlerClasses].map((cls) => sourcePath(ctx.rootDir, cls.getSourceFile().fileName)))].sort());
|
|
5056
5182
|
const commands = [];
|
|
5057
5183
|
const jobs = [];
|
|
5058
5184
|
const queries = [];
|
|
@@ -5231,6 +5357,25 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
|
5231
5357
|
}
|
|
5232
5358
|
return ts4.isObjectLiteralExpression(input) ? input : undefined;
|
|
5233
5359
|
}
|
|
5360
|
+
function routePolicyProperty(object, name, ctx, seen = new Set) {
|
|
5361
|
+
if (seen.has(object))
|
|
5362
|
+
return { unresolved: true };
|
|
5363
|
+
const ancestry = new Set(seen).add(object);
|
|
5364
|
+
let result = { unresolved: false };
|
|
5365
|
+
for (const property of object.properties) {
|
|
5366
|
+
if (ts4.isSpreadAssignment(property)) {
|
|
5367
|
+
const spread = resolveStaticObjectLiteral(property.expression, ctx);
|
|
5368
|
+
const nested = spread ? routePolicyProperty(spread, name, ctx, ancestry) : { unresolved: true };
|
|
5369
|
+
if (nested.unresolved || nested.value !== undefined)
|
|
5370
|
+
result = nested;
|
|
5371
|
+
} else if (ts4.isComputedPropertyName(property.name)) {
|
|
5372
|
+
result = { unresolved: true };
|
|
5373
|
+
} else if (propertyName(property.name) === name) {
|
|
5374
|
+
result = ts4.isPropertyAssignment(property) ? { value: property.initializer, unresolved: false } : { unresolved: true };
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
return result;
|
|
5378
|
+
}
|
|
5234
5379
|
function commandModeProp(object, name) {
|
|
5235
5380
|
const value = stringLiteralProp(object, name);
|
|
5236
5381
|
return value === "required" || value === "none" ? value : undefined;
|
|
@@ -5738,10 +5883,13 @@ function parseController(input, ctx) {
|
|
|
5738
5883
|
const pathParams = [];
|
|
5739
5884
|
const paramRegex = /:([a-zA-Z0-9_]+)/g;
|
|
5740
5885
|
let match;
|
|
5741
|
-
|
|
5886
|
+
const fullPath = joinRoutePaths(path, routePath);
|
|
5887
|
+
while ((match = paramRegex.exec(fullPath)) !== null) {
|
|
5742
5888
|
if (match[1] !== undefined)
|
|
5743
5889
|
pathParams.push(match[1]);
|
|
5744
5890
|
}
|
|
5891
|
+
if (joinRoutePaths(path, routePath).endsWith("/*"))
|
|
5892
|
+
pathParams.push("*");
|
|
5745
5893
|
if (pathParams.length > 0)
|
|
5746
5894
|
route.pathParams = pathParams;
|
|
5747
5895
|
const paramBindings = [];
|
|
@@ -5892,6 +6040,40 @@ function parseController(input, ctx) {
|
|
|
5892
6040
|
const optionsArg = args[1];
|
|
5893
6041
|
const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
|
|
5894
6042
|
if (optionsObject) {
|
|
6043
|
+
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
6044
|
+
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
6045
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved) {
|
|
6046
|
+
ctx.diagnostics.push({
|
|
6047
|
+
severity: "error",
|
|
6048
|
+
code: "unresolved-route-body-policy",
|
|
6049
|
+
file,
|
|
6050
|
+
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6053
|
+
const parse = parsePolicy.value;
|
|
6054
|
+
if (parse) {
|
|
6055
|
+
if (ts4.isStringLiteral(parse) && parse.text === "none")
|
|
6056
|
+
route.parse = "none";
|
|
6057
|
+
else
|
|
6058
|
+
ctx.diagnostics.push({
|
|
6059
|
+
severity: "error",
|
|
6060
|
+
code: "invalid-route-parser",
|
|
6061
|
+
file,
|
|
6062
|
+
message: `Route ${route.handler} parse must be the literal "none".`
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6065
|
+
const allowDeleteBody = deletePolicy.value;
|
|
6066
|
+
if (allowDeleteBody) {
|
|
6067
|
+
if (allowDeleteBody.kind === ts4.SyntaxKind.TrueKeyword)
|
|
6068
|
+
route.allowDeleteBody = true;
|
|
6069
|
+
else
|
|
6070
|
+
ctx.diagnostics.push({
|
|
6071
|
+
severity: "error",
|
|
6072
|
+
code: "invalid-delete-body-opt-in",
|
|
6073
|
+
file,
|
|
6074
|
+
message: `Route ${route.handler} allowDeleteBody must be the literal true.`
|
|
6075
|
+
});
|
|
6076
|
+
}
|
|
5895
6077
|
const legacyResponseExpr = getProp(optionsObject, "response");
|
|
5896
6078
|
const responsesExpr = getProp(optionsObject, "responses");
|
|
5897
6079
|
if (legacyResponseExpr !== undefined && responsesExpr !== undefined) {
|
|
@@ -7083,7 +7265,7 @@ async function compileProject(options) {
|
|
|
7083
7265
|
...options,
|
|
7084
7266
|
...options.cache ? { artifactHashes: options.cache.generatedHashes } : {}
|
|
7085
7267
|
};
|
|
7086
|
-
const written = !hasErrors || options.writeOnError === true ? await
|
|
7268
|
+
const written = !hasErrors || options.writeOnError === true ? await writeRenderedApplication(rendered, generatedOptions) : [];
|
|
7087
7269
|
if (!hasErrors) {
|
|
7088
7270
|
for (const [filename, content] of Object.entries(graphql.files)) {
|
|
7089
7271
|
const path = join4(options.outDir, filename);
|
|
@@ -7224,10 +7406,7 @@ function explainGraph(graph, subject) {
|
|
|
7224
7406
|
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
7225
7407
|
}
|
|
7226
7408
|
function createContextPack(graph, subject) {
|
|
7227
|
-
const subjectModule = graph
|
|
7228
|
-
if (!subjectModule) {
|
|
7229
|
-
throw new Error(`No module named "${subject}". Context packs require a module name.`);
|
|
7230
|
-
}
|
|
7409
|
+
const subjectModule = resolveContextModule(graph, subject);
|
|
7231
7410
|
const byName = new Map(graph.modules.map((module) => [module.name, module]));
|
|
7232
7411
|
const selected = new Set([subjectModule.name]);
|
|
7233
7412
|
const reverseDependents = new Map;
|
|
@@ -7275,6 +7454,7 @@ function createContextPack(graph, subject) {
|
|
|
7275
7454
|
module.file,
|
|
7276
7455
|
...module.providers.map((provider) => provider.file),
|
|
7277
7456
|
...module.controllers.map((controller) => controller.file),
|
|
7457
|
+
...graph.moduleHandlerFiles?.[module.name] ?? [],
|
|
7278
7458
|
...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
|
|
7279
7459
|
]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
|
|
7280
7460
|
const referencedTokens = new Set;
|
|
@@ -7304,6 +7484,18 @@ function createContextPack(graph, subject) {
|
|
|
7304
7484
|
}
|
|
7305
7485
|
};
|
|
7306
7486
|
}
|
|
7487
|
+
function resolveContextModule(graph, subject) {
|
|
7488
|
+
const exact = graph.modules.find((module) => module.name === subject);
|
|
7489
|
+
if (exact)
|
|
7490
|
+
return exact;
|
|
7491
|
+
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));
|
|
7492
|
+
if (matches.length === 1)
|
|
7493
|
+
return matches[0];
|
|
7494
|
+
if (matches.length > 1) {
|
|
7495
|
+
throw new Error(`Ambiguous context target "${subject}". Select a module name: ${matches.map((module) => module.name).sort().join(", ")}`);
|
|
7496
|
+
}
|
|
7497
|
+
throw new Error(`No context target named "${subject}". Select a module name: ${graph.modules.map((module) => module.name).sort().join(", ") || "(none)"}`);
|
|
7498
|
+
}
|
|
7307
7499
|
function allAspects(module) {
|
|
7308
7500
|
return [
|
|
7309
7501
|
...module.aspects ?? [],
|
|
@@ -7611,6 +7803,8 @@ function optionsKeyOf(options) {
|
|
|
7611
7803
|
requireRouteContracts: options.requireRouteContracts,
|
|
7612
7804
|
detectOrphanModules: options.detectOrphanModules,
|
|
7613
7805
|
generateClient: options.generateClient,
|
|
7806
|
+
generateOpenApi: options.generateOpenApi,
|
|
7807
|
+
openApi: options.openApi,
|
|
7614
7808
|
generatePermissions: options.generatePermissions,
|
|
7615
7809
|
typeSafety: options.typeSafety,
|
|
7616
7810
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
@@ -7745,6 +7939,16 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
7745
7939
|
|
|
7746
7940
|
// src/watch.ts
|
|
7747
7941
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
7942
|
+
function isCompilerConfigurationPath(rootDir, changedPath) {
|
|
7943
|
+
const relativePath = relative7(rootDir, changedPath).split(sep7).join("/");
|
|
7944
|
+
return [
|
|
7945
|
+
"supacloud.config.ts",
|
|
7946
|
+
"supacloud.config.mts",
|
|
7947
|
+
"supacloud.config.js",
|
|
7948
|
+
"supacloud.config.mjs",
|
|
7949
|
+
"tsconfig.json"
|
|
7950
|
+
].includes(relativePath) || /^tsconfig\.[^/]+\.json$/.test(relativePath);
|
|
7951
|
+
}
|
|
7748
7952
|
function watchProject(options) {
|
|
7749
7953
|
const rootDir = resolve6(options.rootDir);
|
|
7750
7954
|
const outDir = resolve6(options.outDir);
|
|
@@ -7837,7 +8041,7 @@ function watchProject(options) {
|
|
|
7837
8041
|
const relativePath = relative7(outDir, changedPath);
|
|
7838
8042
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
7839
8043
|
return;
|
|
7840
|
-
if (/\.(tsx?|mts|cts)$/.test(changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
8044
|
+
if (/\.(tsx?|mts|cts)$/.test(changedPath) || isCompilerConfigurationPath(rootDir, changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
7841
8045
|
schedule(relative7(rootDir, changedPath));
|
|
7842
8046
|
}
|
|
7843
8047
|
});
|
|
@@ -12295,9 +12499,9 @@ function compilerVersion() {
|
|
|
12295
12499
|
}
|
|
12296
12500
|
function migrationDependencies() {
|
|
12297
12501
|
return {
|
|
12298
|
-
"@supacloud/app": "0.
|
|
12502
|
+
"@supacloud/app": "0.17.0",
|
|
12299
12503
|
"@supacloud/compiler": compilerVersion(),
|
|
12300
|
-
"@supacloud/elysia": "0.
|
|
12504
|
+
"@supacloud/elysia": "0.19.0",
|
|
12301
12505
|
elysia: "1.4.30",
|
|
12302
12506
|
typescript: "7.0.2"
|
|
12303
12507
|
};
|
|
@@ -12378,6 +12582,8 @@ function isDefineRouteContractCall(node, checker) {
|
|
|
12378
12582
|
return resolveSymbol(checker.getSymbolAtLocation(node.expression), checker)?.name === "defineRouteContract";
|
|
12379
12583
|
}
|
|
12380
12584
|
function isRouteDecoratorCall(node, checker) {
|
|
12585
|
+
if (node.arguments.length < 2)
|
|
12586
|
+
return false;
|
|
12381
12587
|
const name = node.expression.getText(node.getSourceFile());
|
|
12382
12588
|
if (ts12.isIdentifier(node.expression) && ROUTE_DECORATORS2.has(node.expression.text))
|
|
12383
12589
|
return true;
|
|
@@ -12662,7 +12868,7 @@ async function migrateProject(options) {
|
|
|
12662
12868
|
const before = sourceByPath.get(absoluteFile);
|
|
12663
12869
|
if (before === undefined)
|
|
12664
12870
|
continue;
|
|
12665
|
-
const result = projectResults
|
|
12871
|
+
const result = projectResults ? projectResults.results.get(absoluteFile) ?? { changed: false, content: before, replacements: 0, issues: [] } : migration.apply(before, file);
|
|
12666
12872
|
sourceByPath.set(absoluteFile, result.content);
|
|
12667
12873
|
if (result.changed && result.issues.length === 0) {
|
|
12668
12874
|
pendingWrites.set(absoluteFile, result.content);
|
|
@@ -12716,6 +12922,11 @@ async function migrateProject(options) {
|
|
|
12716
12922
|
};
|
|
12717
12923
|
}
|
|
12718
12924
|
|
|
12925
|
+
// src/migration-assess.ts
|
|
12926
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
12927
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
12928
|
+
import { join as join9, resolve as resolve15 } from "node:path";
|
|
12929
|
+
|
|
12719
12930
|
// src/openapi-tools.ts
|
|
12720
12931
|
import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
12721
12932
|
import { dirname as dirname9, resolve as resolve14 } from "node:path";
|
|
@@ -13221,10 +13432,199 @@ function formatOpenApiDiff(result) {
|
|
|
13221
13432
|
`);
|
|
13222
13433
|
}
|
|
13223
13434
|
|
|
13435
|
+
// src/migration-assess.ts
|
|
13436
|
+
var STATUS_PRIORITY = {
|
|
13437
|
+
compatible: 0,
|
|
13438
|
+
"needs-review": 1,
|
|
13439
|
+
"not-proven": 2,
|
|
13440
|
+
unsupported: 3,
|
|
13441
|
+
breaking: 4
|
|
13442
|
+
};
|
|
13443
|
+
function isRecord2(value) {
|
|
13444
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13445
|
+
}
|
|
13446
|
+
function arrayLength(value) {
|
|
13447
|
+
return Array.isArray(value) ? value.length : undefined;
|
|
13448
|
+
}
|
|
13449
|
+
async function sha256(path) {
|
|
13450
|
+
return createHash9("sha256").update(await readFile11(path)).digest("hex");
|
|
13451
|
+
}
|
|
13452
|
+
function overallStatus(findings) {
|
|
13453
|
+
return findings.reduce((current, finding) => STATUS_PRIORITY[finding.status] > STATUS_PRIORITY[current] ? finding.status : current, "compatible");
|
|
13454
|
+
}
|
|
13455
|
+
async function assessMigration(options) {
|
|
13456
|
+
const projectDir = resolve15(options.projectDir);
|
|
13457
|
+
const compile = {
|
|
13458
|
+
...options.compile,
|
|
13459
|
+
rootDir: resolve15(options.compile.rootDir),
|
|
13460
|
+
outDir: resolve15(options.compile.outDir)
|
|
13461
|
+
};
|
|
13462
|
+
const findings = [];
|
|
13463
|
+
const dependencyProblems = await checkMigrationDependencies(projectDir);
|
|
13464
|
+
if (dependencyProblems.length > 0) {
|
|
13465
|
+
findings.push({
|
|
13466
|
+
code: "migration-dependency-tuple-not-proven",
|
|
13467
|
+
status: "not-proven",
|
|
13468
|
+
message: "The installed compiler migration dependency tuple is not the exact tested tuple.",
|
|
13469
|
+
evidence: dependencyProblems.join("; "),
|
|
13470
|
+
remediation: "Install the exact tested package versions before applying a source migration."
|
|
13471
|
+
});
|
|
13472
|
+
}
|
|
13473
|
+
let compiler = {
|
|
13474
|
+
upToDate: false,
|
|
13475
|
+
diagnostics: [],
|
|
13476
|
+
mismatches: []
|
|
13477
|
+
};
|
|
13478
|
+
try {
|
|
13479
|
+
const checked = await checkProject(compile);
|
|
13480
|
+
compiler = {
|
|
13481
|
+
upToDate: checked.upToDate,
|
|
13482
|
+
diagnostics: checked.diagnostics,
|
|
13483
|
+
mismatches: checked.mismatches
|
|
13484
|
+
};
|
|
13485
|
+
const errors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
13486
|
+
if (errors.length > 0) {
|
|
13487
|
+
findings.push({
|
|
13488
|
+
code: "compiler-governance-not-proven",
|
|
13489
|
+
status: "not-proven",
|
|
13490
|
+
message: `Compiler governance has ${errors.length} error(s).`,
|
|
13491
|
+
remediation: "Resolve compiler diagnostics without weakening governance, then reassess."
|
|
13492
|
+
});
|
|
13493
|
+
}
|
|
13494
|
+
if (!checked.upToDate) {
|
|
13495
|
+
findings.push({
|
|
13496
|
+
code: "generated-artifact-drift",
|
|
13497
|
+
status: "needs-review",
|
|
13498
|
+
message: "Generated artifacts do not match the current source and compiler configuration.",
|
|
13499
|
+
evidence: checked.mismatches.join("; "),
|
|
13500
|
+
remediation: "Regenerate candidate artifacts in isolation and review the diff before adoption."
|
|
13501
|
+
});
|
|
13502
|
+
}
|
|
13503
|
+
} catch {
|
|
13504
|
+
findings.push({
|
|
13505
|
+
code: "compiler-check-unavailable",
|
|
13506
|
+
status: "not-proven",
|
|
13507
|
+
message: "The local compiler check could not complete.",
|
|
13508
|
+
remediation: "Verify the project source, configuration and installed dependencies, then reassess."
|
|
13509
|
+
});
|
|
13510
|
+
}
|
|
13511
|
+
const contractsManifestPath = join9(compile.outDir, "contracts.manifest.json");
|
|
13512
|
+
let contractsManifest = {
|
|
13513
|
+
path: contractsManifestPath,
|
|
13514
|
+
present: false
|
|
13515
|
+
};
|
|
13516
|
+
try {
|
|
13517
|
+
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
13518
|
+
if (!isRecord2(parsed) || parsed.version !== 1)
|
|
13519
|
+
throw new Error("invalid manifest");
|
|
13520
|
+
contractsManifest = {
|
|
13521
|
+
path: contractsManifestPath,
|
|
13522
|
+
present: true,
|
|
13523
|
+
sha256: await sha256(contractsManifestPath),
|
|
13524
|
+
version: 1,
|
|
13525
|
+
...arrayLength(parsed.commands) === undefined ? {} : { commands: arrayLength(parsed.commands) },
|
|
13526
|
+
...arrayLength(parsed.routes) === undefined ? {} : { routes: arrayLength(parsed.routes) },
|
|
13527
|
+
...arrayLength(parsed.permissions) === undefined ? {} : { permissions: arrayLength(parsed.permissions) }
|
|
13528
|
+
};
|
|
13529
|
+
} catch {
|
|
13530
|
+
findings.push({
|
|
13531
|
+
code: "contract-manifest-not-proven",
|
|
13532
|
+
status: "not-proven",
|
|
13533
|
+
message: "contracts.manifest.json is missing or invalid.",
|
|
13534
|
+
evidence: contractsManifestPath,
|
|
13535
|
+
remediation: "Generate and review the contract manifest before migration."
|
|
13536
|
+
});
|
|
13537
|
+
}
|
|
13538
|
+
let openApi;
|
|
13539
|
+
if (options.baselineOpenApiPath && options.currentOpenApiPath) {
|
|
13540
|
+
const baselinePath = resolve15(projectDir, options.baselineOpenApiPath);
|
|
13541
|
+
const currentPath = resolve15(projectDir, options.currentOpenApiPath);
|
|
13542
|
+
try {
|
|
13543
|
+
const diff = diffOpenApiDocuments(await readOpenApiJson(baselinePath), await readOpenApiJson(currentPath));
|
|
13544
|
+
openApi = {
|
|
13545
|
+
baselinePath,
|
|
13546
|
+
baselineSha256: await sha256(baselinePath),
|
|
13547
|
+
currentPath,
|
|
13548
|
+
currentSha256: await sha256(currentPath),
|
|
13549
|
+
diff
|
|
13550
|
+
};
|
|
13551
|
+
if (!diff.ok) {
|
|
13552
|
+
findings.push({
|
|
13553
|
+
code: "openapi-breaking-change",
|
|
13554
|
+
status: "breaking",
|
|
13555
|
+
message: `OpenAPI contains ${diff.breaking.length} breaking change(s).`,
|
|
13556
|
+
remediation: "Review and explicitly approve or redesign each breaking contract change."
|
|
13557
|
+
});
|
|
13558
|
+
} else if (diff.changes.length > 0) {
|
|
13559
|
+
findings.push({
|
|
13560
|
+
code: "openapi-compatible-change-review",
|
|
13561
|
+
status: "needs-review",
|
|
13562
|
+
message: `OpenAPI contains ${diff.changes.length} non-breaking change(s) requiring release review.`
|
|
13563
|
+
});
|
|
13564
|
+
}
|
|
13565
|
+
} catch {
|
|
13566
|
+
findings.push({
|
|
13567
|
+
code: "openapi-diff-not-proven",
|
|
13568
|
+
status: "not-proven",
|
|
13569
|
+
message: "The OpenAPI baseline or current document could not be loaded.",
|
|
13570
|
+
remediation: "Export both OpenAPI documents as JSON and rerun the assessment."
|
|
13571
|
+
});
|
|
13572
|
+
}
|
|
13573
|
+
} else {
|
|
13574
|
+
findings.push({
|
|
13575
|
+
code: "openapi-baseline-not-proven",
|
|
13576
|
+
status: "not-proven",
|
|
13577
|
+
message: "No complete OpenAPI baseline/current pair was supplied.",
|
|
13578
|
+
remediation: "Provide both --baseline-openapi and --current-openapi for compatibility evidence."
|
|
13579
|
+
});
|
|
13580
|
+
}
|
|
13581
|
+
findings.push({
|
|
13582
|
+
code: "rendering-mode-preserved",
|
|
13583
|
+
status: "compatible",
|
|
13584
|
+
message: "SupaCloud migration does not require SSR and does not change the existing rendering topology.",
|
|
13585
|
+
evidence: `selected=${options.renderMode ?? "unspecified"}`
|
|
13586
|
+
});
|
|
13587
|
+
const status = overallStatus(findings);
|
|
13588
|
+
return {
|
|
13589
|
+
version: 1,
|
|
13590
|
+
ok: status === "compatible" || status === "needs-review",
|
|
13591
|
+
status,
|
|
13592
|
+
readOnly: true,
|
|
13593
|
+
writesPerformed: false,
|
|
13594
|
+
rendering: {
|
|
13595
|
+
selected: options.renderMode ?? "unspecified",
|
|
13596
|
+
ssrRequired: false,
|
|
13597
|
+
supportedModes: ["browser", "ssr", "edge", "trusted-server"],
|
|
13598
|
+
guidance: "Keep the application's existing CSR, SPA, SSR or edge topology; use trusted adapters only for governed server operations."
|
|
13599
|
+
},
|
|
13600
|
+
dependencies: {
|
|
13601
|
+
expected: migrationDependencies(),
|
|
13602
|
+
problems: dependencyProblems
|
|
13603
|
+
},
|
|
13604
|
+
compiler,
|
|
13605
|
+
artifacts: {
|
|
13606
|
+
contractsManifest,
|
|
13607
|
+
...openApi === undefined ? {} : { openApi }
|
|
13608
|
+
},
|
|
13609
|
+
findings
|
|
13610
|
+
};
|
|
13611
|
+
}
|
|
13612
|
+
function formatMigrationAssessment(result) {
|
|
13613
|
+
return [
|
|
13614
|
+
`Migration assessment: ${result.status}. Read-only; no files, databases or remote environments were changed.`,
|
|
13615
|
+
`Rendering: ${result.rendering.selected}; SSR required: no.`,
|
|
13616
|
+
...result.findings.map((finding) => `${finding.status.toUpperCase()} ${finding.code}: ${finding.message}`)
|
|
13617
|
+
].join(`
|
|
13618
|
+
`);
|
|
13619
|
+
}
|
|
13620
|
+
|
|
13224
13621
|
// src/cli.ts
|
|
13225
13622
|
function isModuleBoundaryPresetName(value) {
|
|
13226
13623
|
return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
|
|
13227
13624
|
}
|
|
13625
|
+
function isMigrationRenderMode(value) {
|
|
13626
|
+
return value === "unspecified" || value === "browser" || value === "ssr" || value === "edge" || value === "trusted-server";
|
|
13627
|
+
}
|
|
13228
13628
|
function printUsage() {
|
|
13229
13629
|
console.log(`
|
|
13230
13630
|
@supacloud/compiler CLI
|
|
@@ -13235,9 +13635,10 @@ Usage:
|
|
|
13235
13635
|
supacloud-compiler dev [rootDir] [options]
|
|
13236
13636
|
supacloud-compiler graph [rootDir] [options]
|
|
13237
13637
|
supacloud-compiler explain <name> [rootDir] [options]
|
|
13238
|
-
supacloud-compiler context <
|
|
13638
|
+
supacloud-compiler context <name> [rootDir] [options]
|
|
13239
13639
|
supacloud-compiler doctor [rootDir] [options]
|
|
13240
13640
|
supacloud-compiler migrate [rootDir] [options]
|
|
13641
|
+
supacloud-compiler migration-assess [rootDir] [options]
|
|
13241
13642
|
supacloud-compiler plan [rootDir] [options]
|
|
13242
13643
|
supacloud-compiler build-delivery [rootDir] [options]
|
|
13243
13644
|
supacloud-compiler openapi-export <openapi-module> <output.json> [options]
|
|
@@ -13255,6 +13656,7 @@ Commands:
|
|
|
13255
13656
|
context Extract an AI-sized module context pack
|
|
13256
13657
|
doctor Run project and generated-artifact health checks
|
|
13257
13658
|
migrate Preview or apply versioned source migrations
|
|
13659
|
+
migration-assess Produce a read-only migration compatibility report
|
|
13258
13660
|
plan Preview deterministic workload targets without writing or deploying
|
|
13259
13661
|
build-delivery Build independent local factories and an atomic delivery manifest (Bun)
|
|
13260
13662
|
openapi-export Export a generated OpenAPI module to a standalone JSON document
|
|
@@ -13278,13 +13680,16 @@ Options:
|
|
|
13278
13680
|
--token-env <name> Environment variable holding the intended user's access token
|
|
13279
13681
|
--check graphql-schema: compare the remote schema without changing the snapshot
|
|
13280
13682
|
--debounce <ms> Debounce source changes in dev mode (default: 100)
|
|
13281
|
-
--json Print machine-readable output for compile/check/graph/explain/context/doctor/plan/build-delivery/openapi-export/openapi-diff
|
|
13683
|
+
--json Print machine-readable output for compile/check/graph/explain/context/doctor/migration-assess/plan/build-delivery/openapi-export/openapi-diff
|
|
13282
13684
|
--space <n> openapi-export: JSON indentation (0-10, default: 2)
|
|
13283
13685
|
--delivery <file> plan/build-delivery: validated JSON configuration (overrides config.delivery)
|
|
13284
13686
|
--dry-run Preview a fix without writing the target file
|
|
13285
13687
|
--write Apply a fix or migration to disk (preview-only by default)
|
|
13286
13688
|
--from-version Migration source-format checkpoint (requires --to-version)
|
|
13287
13689
|
--to-version Migration target checkpoint; verifies installed dependencies
|
|
13690
|
+
--baseline-openapi OpenAPI baseline JSON for migration-assess
|
|
13691
|
+
--current-openapi Current OpenAPI JSON for migration-assess
|
|
13692
|
+
--render-mode Optional browser | ssr | edge | trusted-server label; SSR is never required
|
|
13288
13693
|
--preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
|
|
13289
13694
|
--help, -h Show this help
|
|
13290
13695
|
`);
|
|
@@ -13308,7 +13713,7 @@ async function run() {
|
|
|
13308
13713
|
process.exitCode = 1;
|
|
13309
13714
|
return;
|
|
13310
13715
|
}
|
|
13311
|
-
if (!command || !["compile", "check", "dev", "graph", "explain", "context", "doctor", "migrate", "fix", "graphql-schema", "plan", "build-delivery", "openapi-export", "openapi-diff"].includes(command)) {
|
|
13716
|
+
if (!command || !["compile", "check", "dev", "graph", "explain", "context", "doctor", "migrate", "migration-assess", "fix", "graphql-schema", "plan", "build-delivery", "openapi-export", "openapi-diff"].includes(command)) {
|
|
13312
13717
|
console.error(`Error: unknown command "${command}"`);
|
|
13313
13718
|
printUsage();
|
|
13314
13719
|
process.exit(1);
|
|
@@ -13332,6 +13737,9 @@ async function run() {
|
|
|
13332
13737
|
let tokenEnv;
|
|
13333
13738
|
let checkSchema = false;
|
|
13334
13739
|
let deliveryPath;
|
|
13740
|
+
let baselineOpenApi;
|
|
13741
|
+
let currentOpenApi;
|
|
13742
|
+
let renderMode = "unspecified";
|
|
13335
13743
|
const openApiDiffPaths = [];
|
|
13336
13744
|
const openApiExportPaths = [];
|
|
13337
13745
|
let openApiExportSpace;
|
|
@@ -13436,9 +13844,27 @@ async function run() {
|
|
|
13436
13844
|
} else if (arg === "--dry-run") {
|
|
13437
13845
|
dryRun = true;
|
|
13438
13846
|
} else if (arg === "--write") {
|
|
13439
|
-
if (command === "plan")
|
|
13440
|
-
throw new Error(
|
|
13847
|
+
if (command === "plan" || command === "migration-assess")
|
|
13848
|
+
throw new Error(`${command} is read-only; --write is not supported`);
|
|
13441
13849
|
dryRun = false;
|
|
13850
|
+
} else if (arg === "--baseline-openapi" || arg === "--current-openapi") {
|
|
13851
|
+
if (command !== "migration-assess")
|
|
13852
|
+
throw new Error(`${arg} is only supported by migration-assess`);
|
|
13853
|
+
const value = args[++i];
|
|
13854
|
+
if (!value || value.startsWith("-"))
|
|
13855
|
+
throw new Error(`${arg} requires a JSON file path`);
|
|
13856
|
+
if (arg === "--baseline-openapi")
|
|
13857
|
+
baselineOpenApi = value;
|
|
13858
|
+
else
|
|
13859
|
+
currentOpenApi = value;
|
|
13860
|
+
} else if (arg === "--render-mode") {
|
|
13861
|
+
if (command !== "migration-assess")
|
|
13862
|
+
throw new Error("--render-mode is only supported by migration-assess");
|
|
13863
|
+
const value = args[++i];
|
|
13864
|
+
if (!isMigrationRenderMode(value)) {
|
|
13865
|
+
throw new Error("--render-mode requires browser, ssr, edge or trusted-server");
|
|
13866
|
+
}
|
|
13867
|
+
renderMode = value;
|
|
13442
13868
|
} else if (arg === "--from-version" || arg === "--to-version") {
|
|
13443
13869
|
if (command !== "migrate")
|
|
13444
13870
|
throw new Error(`${arg} is only supported by migrate`);
|
|
@@ -13479,7 +13905,7 @@ async function run() {
|
|
|
13479
13905
|
const currentPath = openApiDiffPaths[1];
|
|
13480
13906
|
if (!basePath || !currentPath)
|
|
13481
13907
|
throw new Error("openapi-diff requires two JSON file paths");
|
|
13482
|
-
const result = diffOpenApiDocuments(await readOpenApiJson(
|
|
13908
|
+
const result = diffOpenApiDocuments(await readOpenApiJson(resolve18(process.cwd(), basePath)), await readOpenApiJson(resolve18(process.cwd(), currentPath)));
|
|
13483
13909
|
console.log(json ? JSON.stringify(result, null, 2) : formatOpenApiDiff(result));
|
|
13484
13910
|
if (!result.ok)
|
|
13485
13911
|
process.exitCode = 1;
|
|
@@ -13494,8 +13920,8 @@ async function run() {
|
|
|
13494
13920
|
if (!modulePath || !outputPath)
|
|
13495
13921
|
throw new Error("openapi-export requires an OpenAPI module and output path");
|
|
13496
13922
|
const result = await exportGeneratedOpenApiJson({
|
|
13497
|
-
modulePath:
|
|
13498
|
-
outputPath:
|
|
13923
|
+
modulePath: resolve18(process.cwd(), modulePath),
|
|
13924
|
+
outputPath: resolve18(process.cwd(), outputPath),
|
|
13499
13925
|
...openApiExportSpace === undefined ? {} : { space: openApiExportSpace }
|
|
13500
13926
|
});
|
|
13501
13927
|
console.log(json ? JSON.stringify({ ok: true, ...result }, null, 2) : result.written ? `OpenAPI JSON written: ${result.path}` : `OpenAPI JSON matches: ${result.path}`);
|
|
@@ -13503,7 +13929,7 @@ async function run() {
|
|
|
13503
13929
|
}
|
|
13504
13930
|
if (command === "migrate") {
|
|
13505
13931
|
const result = await migrateProject({
|
|
13506
|
-
rootDir: rootDir ?
|
|
13932
|
+
rootDir: rootDir ? resolve18(process.cwd(), rootDir) : process.cwd(),
|
|
13507
13933
|
write: !dryRun,
|
|
13508
13934
|
...fromVersion === undefined ? {} : { fromVersion },
|
|
13509
13935
|
...toVersion === undefined ? {} : { toVersion }
|
|
@@ -13531,8 +13957,8 @@ async function run() {
|
|
|
13531
13957
|
if (checkSchema && command !== "graphql-schema")
|
|
13532
13958
|
throw new Error("--check is only supported by graphql-schema");
|
|
13533
13959
|
const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
|
|
13534
|
-
const resolvedRoot = rootDir ?
|
|
13535
|
-
const resolvedOut = outDir ?
|
|
13960
|
+
const resolvedRoot = rootDir ? resolve18(process.cwd(), rootDir) : defaults.rootDir;
|
|
13961
|
+
const resolvedOut = outDir ? resolve18(process.cwd(), outDir) : defaults.outDir;
|
|
13536
13962
|
const configured = compileOptionsFromConfig({
|
|
13537
13963
|
...loadedConfig,
|
|
13538
13964
|
root: resolvedRoot,
|
|
@@ -13551,7 +13977,7 @@ async function run() {
|
|
|
13551
13977
|
let delivery = loadedConfig.delivery;
|
|
13552
13978
|
if (deliveryPath !== undefined) {
|
|
13553
13979
|
try {
|
|
13554
|
-
delivery = JSON.parse(await
|
|
13980
|
+
delivery = JSON.parse(await readFile14(resolve18(process.cwd(), deliveryPath), "utf8"));
|
|
13555
13981
|
} catch {
|
|
13556
13982
|
throw new DeliveryConfigurationError;
|
|
13557
13983
|
}
|
|
@@ -13595,10 +14021,21 @@ ${item.suggestion ?? ""}`).join(`
|
|
|
13595
14021
|
console.log(json ? JSON.stringify({ ok: result.upToDate, ...result }, null, 2) : result.written ? `GraphQL schema written: ${result.path}` : result.upToDate ? `GraphQL schema matches: ${result.path}` : `GraphQL schema drift: ${result.path}. Export the role-scoped snapshot and compile before promotion.`);
|
|
13596
14022
|
if (!result.upToDate)
|
|
13597
14023
|
process.exit(1);
|
|
14024
|
+
} else if (command === "migration-assess") {
|
|
14025
|
+
const result = await assessMigration({
|
|
14026
|
+
projectDir: process.cwd(),
|
|
14027
|
+
compile: compileDefaults,
|
|
14028
|
+
...baselineOpenApi === undefined ? {} : { baselineOpenApiPath: baselineOpenApi },
|
|
14029
|
+
...currentOpenApi === undefined ? {} : { currentOpenApiPath: currentOpenApi },
|
|
14030
|
+
renderMode
|
|
14031
|
+
});
|
|
14032
|
+
console.log(json ? JSON.stringify(result, null, 2) : formatMigrationAssessment(result));
|
|
14033
|
+
if (result.status === "breaking" || result.status === "unsupported")
|
|
14034
|
+
process.exitCode = 1;
|
|
13598
14035
|
} else if (command === "fix") {
|
|
13599
14036
|
if (!query)
|
|
13600
14037
|
throw new Error("fix requires a JSON file containing one DiagnosticFix");
|
|
13601
|
-
const fix = JSON.parse(await
|
|
14038
|
+
const fix = JSON.parse(await readFile14(resolve18(process.cwd(), query), "utf8"));
|
|
13602
14039
|
const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
|
|
13603
14040
|
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
13604
14041
|
} else if (command === "compile") {
|
|
@@ -13713,7 +14150,7 @@ Source change detected; compiling...`);
|
|
|
13713
14150
|
}
|
|
13714
14151
|
} else if (command === "context") {
|
|
13715
14152
|
if (!query) {
|
|
13716
|
-
console.error("Error: context requires a module name");
|
|
14153
|
+
console.error("Error: context requires a module or owned symbol name");
|
|
13717
14154
|
process.exit(1);
|
|
13718
14155
|
}
|
|
13719
14156
|
try {
|