@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/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.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
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
- "export function createCompiledModules(): CompiledModule[] {",
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
- const rendered = renderApplication(graph, options);
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
- `): Record<string, unknown> {`,
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((fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1)))];
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
- " const errBody = await response.text();",
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
- " value = await response.json();",
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
- " if (!transportSuccess) throw new Error(`No response schema declared for HTTP ${response.status}`);",
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 ? [" const checked = decodeResponseSchema(value, response.status, responseSchemas, { normalize: config.normalize ?? true });"] : [" const checked = value;"],
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 createHash9 } from "node:crypto";
2701
- import { mkdir as mkdir5, readFile as readFile11 } from "node:fs/promises";
2702
- import { dirname as dirname10, relative as relative12, resolve as resolve15 } from "node:path";
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 createHash9("sha256").update(value).digest("hex");
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: resolve15(directory, field("rootDir")),
2731
- outDir: resolve15(directory, field("outDir")),
2732
- postgrestTypes: resolve15(directory, field("postgrestTypes")),
2733
- drizzleSchema: resolve15(directory, field("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: resolve15(directory, graphql.schema) },
2736
- migrations: migrations.map((file) => resolve15(directory, 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 = resolve15(options.rootDir), outDir = resolve15(options.outDir);
2741
- const postgrestTypes = resolve15(rootDir, options.postgrestTypes), drizzleSchema = resolve15(rootDir, options.drizzleSchema);
2742
- const snapshot = await readFile11(postgrestTypes, "utf8");
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 = resolve15(rootDir, path);
2770
- inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile11(absolute, "utf8"));
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(resolve15(rootDir, options.graphql.schema));
2787
- if (new Set(options.migrations.map((path) => resolve15(rootDir, path))).size !== options.migrations.length) {
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, resolve15(rootDir, path)).replaceAll("\\", "/")),
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 = resolve15(outDir, file);
2929
+ const path = resolve16(outDir, file);
2816
2930
  let current;
2817
2931
  try {
2818
- current = await readFile11(path, "utf8");
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(resolve15(outDir, file), content);
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 = resolve15(path);
2835
- const value = JSON.parse(await readFile11(absolute, "utf8"));
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 readFile12 } from "node:fs/promises";
2846
- import { createHash as createHash10 } from "node:crypto";
2847
- import { dirname as dirname11, resolve as resolve16 } from "node:path";
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 = resolve16(options.output);
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 readFile12(path, "utf8");
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 = createHash10("sha256").update(content).digest("hex");
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 resolve17 } from "node:path";
2915
- import { readFile as readFile13 } from "node:fs/promises";
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 ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
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
- while ((match = paramRegex.exec(routePath)) !== null) {
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 generateApplication(graph, generatedOptions) : [];
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.modules.find((module) => module.name === subject);
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.16.0",
12502
+ "@supacloud/app": "0.17.0",
12299
12503
  "@supacloud/compiler": compilerVersion(),
12300
- "@supacloud/elysia": "0.18.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?.results.get(absoluteFile) ?? migration.apply(before, file);
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 <module> [rootDir] [options]
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("plan is read-only; --write is not supported");
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(resolve17(process.cwd(), basePath)), await readOpenApiJson(resolve17(process.cwd(), currentPath)));
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: resolve17(process.cwd(), modulePath),
13498
- outputPath: resolve17(process.cwd(), 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 ? resolve17(process.cwd(), rootDir) : process.cwd(),
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 ? resolve17(process.cwd(), rootDir) : defaults.rootDir;
13535
- const resolvedOut = outDir ? resolve17(process.cwd(), outDir) : defaults.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 readFile13(resolve17(process.cwd(), deliveryPath), "utf8"));
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 readFile13(resolve17(process.cwd(), query), "utf8"));
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 {