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