@supacloud/compiler 0.25.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
@@ -128,7 +128,9 @@ function buildContractManifest(graph, artifacts) {
128
128
  ...route.responses === undefined ? {} : {
129
129
  responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
130
130
  },
131
- ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
131
+ ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence },
132
+ ...route.parse === undefined ? {} : { parse: route.parse },
133
+ ...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody }
132
134
  });
133
135
  }
134
136
  }
@@ -217,7 +219,7 @@ function renderApplication(graph, options) {
217
219
  providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
218
220
  }));
219
221
  }
220
- const imports = new ImportManager;
222
+ const imports = new ImportManager(new Set(["CompiledApplicationModule"]));
221
223
  const factorySections = [];
222
224
  const descriptorEntries = [];
223
225
  for (const module of modules) {
@@ -234,7 +236,13 @@ function renderApplication(graph, options) {
234
236
  "",
235
237
  TYPE_GUARDS,
236
238
  "",
237
- "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[] {",
238
246
  " return [",
239
247
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
240
248
  " ];",
@@ -315,7 +323,9 @@ function renderApplication(graph, options) {
315
323
  };
316
324
  }
317
325
  async function generateApplication(graph, options) {
318
- const rendered = renderApplication(graph, options);
326
+ return writeRenderedApplication(renderApplication(graph, options), options);
327
+ }
328
+ async function writeRenderedApplication(rendered, options) {
319
329
  await mkdir(options.outDir, { recursive: true });
320
330
  const applicationPath = join2(options.outDir, "application.ts");
321
331
  const manifestPath = join2(options.outDir, "app.manifest.json");
@@ -402,8 +412,12 @@ function indent(text, spaces) {
402
412
  }
403
413
 
404
414
  class ImportManager {
415
+ reserved;
405
416
  entries = new Map;
406
417
  lookup = new Map;
418
+ constructor(reserved = new Set) {
419
+ this.reserved = reserved;
420
+ }
407
421
  get size() {
408
422
  return this.entries.size;
409
423
  }
@@ -418,7 +432,7 @@ class ImportManager {
418
432
  return existing;
419
433
  let local = exported;
420
434
  let counter = 2;
421
- while (this.entries.has(local)) {
435
+ while (this.entries.has(local) || this.reserved.has(local)) {
422
436
  local = `${exported}${counter}`;
423
437
  counter += 1;
424
438
  }
@@ -475,7 +489,7 @@ class ModuleGenerator {
475
489
  renderDescriptor() {
476
490
  const lines = [
477
491
  `{`,
478
- ` name: ${JSON.stringify(this.module.name)},`,
492
+ ` name: ${JSON.stringify(this.module.name)} as const,`,
479
493
  ` createServices: create${this.pascal}Services,`
480
494
  ];
481
495
  if (this.hasFactoryContent("request")) {
@@ -614,6 +628,10 @@ class ModuleGenerator {
614
628
  if (route.contract !== undefined) {
615
629
  fields.push(`contract: ${JSON.stringify(route.contract)}`);
616
630
  }
631
+ if (route.parse === "none")
632
+ fields.push('parse: "none"');
633
+ if (route.allowDeleteBody === true)
634
+ fields.push("allowDeleteBody: true");
617
635
  if (route.schemaKinds !== undefined && Object.keys(route.schemaKinds).length > 0) {
618
636
  fields.push(`schemaKinds: ${JSON.stringify(route.schemaKinds)}`);
619
637
  }
@@ -713,7 +731,7 @@ ${indent(item, 2)}`).join(",")}
713
731
  `function create${this.pascal}Services(`,
714
732
  ` deps: Record<string, unknown>,`,
715
733
  ` imported: Record<string, Record<string, unknown>>,`,
716
- `): Record<string, unknown> {`,
734
+ `) {`,
717
735
  indent(this.renderFactoryBody("services"), 2),
718
736
  `}`
719
737
  ].join(`
@@ -810,7 +828,7 @@ ${indent(item, 2)}`).join(",")}
810
828
  `);
811
829
  }
812
830
  const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
813
- lines.push(`return { ${entries.join(", ")} };`);
831
+ lines.push(`return retainPlatformDependencies({ ${entries.join(", ")} }, deps);`);
814
832
  return lines.join(`
815
833
  `);
816
834
  }
@@ -911,6 +929,9 @@ ${indent(item, 2)}`).join(",")}
911
929
  return this.depExpr(own.useExisting ?? token, kind, options);
912
930
  }
913
931
  }
932
+ if (own && factoryOfScope(own.scope) === "services" && kind !== "services" && !isSkipSelf && !isSelf) {
933
+ return `services.${camelName(token)}`;
934
+ }
914
935
  if (isSelf) {
915
936
  return isOptional ? "undefined" : `services.${camelName(token)}`;
916
937
  }
@@ -928,13 +949,14 @@ ${indent(item, 2)}`).join(",")}
928
949
  return `imported.${mod.name}.${camelName(token)}`;
929
950
  }
930
951
  }
931
- if (isOptional && !this.graph.externalTokens.includes(token)) {
932
- return "undefined";
933
- }
934
952
  if (isSelf)
935
953
  return isOptional ? "undefined" : `services.${camelName(token)}`;
936
954
  if (kind === "services")
937
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
+ }
938
960
  return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
939
961
  }
940
962
  }
@@ -962,7 +984,7 @@ function orderProviders(providers) {
962
984
  function renderClient(graph, options) {
963
985
  const rootDir = options?.rootDir ?? process.cwd();
964
986
  const outDir = options?.outDir ?? process.cwd();
965
- const imports = new ImportManager;
987
+ const imports = new ImportManager(new Set(["ApiClientError", "ApiClientErrorCode", "ApiClientErrorDetails"]));
966
988
  const controllerEntries = [];
967
989
  const routeTypes = [];
968
990
  const schemaEntries = [];
@@ -1020,11 +1042,14 @@ function renderClient(graph, options) {
1020
1042
  }).filter((entry) => entry.length === 2));
1021
1043
  if (Object.keys(responseSchemas).length > 0)
1022
1044
  usesValue = true;
1023
- 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
+ ])];
1024
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>";
1025
1050
  const paramsType = paramsSchema ? `Static<typeof ${paramsSchema.local}>${paramNames.length > 0 ? ` & ${pathParamsType}` : ""}` : pathParamsType;
1026
1051
  const queryType = querySchema ? `Static<typeof ${querySchema.local}>` : "Record<string, unknown>";
1027
- const bodyType = bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
1052
+ const bodyType = route.parse === "none" ? "BodyInit" : bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
1028
1053
  const headersType = headersSchema ? `Static<typeof ${headersSchema.local}>` : "Record<string, string>";
1029
1054
  const cookieType = cookieSchema ? `Static<typeof ${cookieSchema.local}>` : "Record<string, string | number | boolean>";
1030
1055
  const responseTypes = [
@@ -1061,7 +1086,9 @@ function renderClient(graph, options) {
1061
1086
  ["queryTransforms", route.queryTransforms],
1062
1087
  ["queryDefaults", route.queryDefaults],
1063
1088
  ["title", route.title],
1064
- ["data", route.data]
1089
+ ["data", route.data],
1090
+ ["parse", route.parse],
1091
+ ["allowDeleteBody", route.allowDeleteBody]
1065
1092
  ]) {
1066
1093
  if (value !== undefined)
1067
1094
  routeFields.push(`${key}: ${JSON.stringify(value)}`);
@@ -1095,7 +1122,7 @@ function renderClient(graph, options) {
1095
1122
  const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
1096
1123
  const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
1097
1124
  routeMethods.push(`
1098
- ${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"}),`);
1099
1126
  }
1100
1127
  controllerEntries.push(`
1101
1128
  ${controllerKey}: {${routeMethods.join("")}
@@ -1126,6 +1153,40 @@ function renderClient(graph, options) {
1126
1153
  "",
1127
1154
  "export type ResponseDecoder<T> = (value: unknown) => T;",
1128
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
+ "",
1129
1190
  "export type RouteMethod<",
1130
1191
  " Options extends ClientRequestOptions = ClientRequestOptions,",
1131
1192
  " Result = never,",
@@ -1179,6 +1240,13 @@ function renderClient(graph, options) {
1179
1240
  ' if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`Missing route parameter: ${key}`);',
1180
1241
  " return encodeURIComponent(String(value));",
1181
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
+ " }",
1182
1250
  " if (query) {",
1183
1251
  " const searchParams = new URLSearchParams();",
1184
1252
  " for (const [k, v] of Object.entries(query)) {",
@@ -1365,6 +1433,10 @@ function renderClient(graph, options) {
1365
1433
  "export function createApiClient(config: ApiClientConfig = {}) {",
1366
1434
  " const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
1367
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
+ " }",
1368
1440
  "",
1369
1441
  " async function request<T>(",
1370
1442
  " method: string,",
@@ -1373,6 +1445,7 @@ function renderClient(graph, options) {
1373
1445
  " decode: ResponseDecoder<T>,",
1374
1446
  ` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
1375
1447
  " responseKind?: string,",
1448
+ " raw?: boolean,",
1376
1449
  " ): Promise<T>;",
1377
1450
  " async function request<T>(",
1378
1451
  " method: string,",
@@ -1381,6 +1454,7 @@ function renderClient(graph, options) {
1381
1454
  " decode?: ResponseDecoder<T>,",
1382
1455
  ` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
1383
1456
  " responseKind?: string,",
1457
+ " raw?: boolean,",
1384
1458
  " ): Promise<T | unknown>;",
1385
1459
  " async function request(",
1386
1460
  " method: string,",
@@ -1394,11 +1468,12 @@ function renderClient(graph, options) {
1394
1468
  " decode?: ResponseDecoder<T>,",
1395
1469
  ` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
1396
1470
  " responseKind?: string,",
1471
+ " raw = false,",
1397
1472
  " ): Promise<T | unknown> {",
1398
1473
  " const url = `${baseUrl}${buildRouteUrl(path, options.params, options.query)}`;",
1399
1474
  ' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
1400
1475
  " const headers: Record<string, string> = {",
1401
- ' "content-type": "application/json",',
1476
+ ' ...(raw ? {} : { "content-type": "application/json" }),',
1402
1477
  " ...customHeaders,",
1403
1478
  " ...options.headers,",
1404
1479
  " };",
@@ -1417,21 +1492,27 @@ function renderClient(graph, options) {
1417
1492
  " return fetcher(reqPayload.url, {",
1418
1493
  " method: reqPayload.method,",
1419
1494
  " headers: reqPayload.headers,",
1420
- " ...(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" } : {}),',
1421
1497
  " });",
1422
1498
  " };",
1423
1499
  " const response = await executeChain(0, { method, url, headers, body: options.body });",
1424
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
+ " });",
1425
1505
  " if (!response.ok && declaredSchema === undefined) {",
1426
- " const errBody = await response.text();",
1427
- " throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
1506
+ ' throw failure("API_HTTP_ERROR", `API request failed: ${method} ${path} -> ${response.status}`);',
1428
1507
  " }",
1429
1508
  ' const contentType = response.headers?.get("content-type") ?? "";',
1430
1509
  " let value: unknown;",
1431
1510
  " if ([204, 205, 304].includes(response.status)) {",
1432
1511
  " value = undefined;",
1433
1512
  ' } else if (contentType.includes("application/json") || contentType.includes("+json")) {',
1434
- " 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"); }',
1435
1516
  ' } else if (responseKind === "binary" || contentType.includes("application/octet-stream")) {',
1436
1517
  " value = await response.arrayBuffer();",
1437
1518
  ' } else if (responseKind === "stream") {',
@@ -1442,20 +1523,24 @@ function renderClient(graph, options) {
1442
1523
  " if (!responseSchemas) return decode ? decode(value) : value;",
1443
1524
  " if (declaredSchema === undefined) {",
1444
1525
  ' const transportSuccess = response.ok && (responseKind === "binary" || responseKind === "stream");',
1445
- " 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}`);',
1446
1527
  " return decode ? decode(value) : value;",
1447
1528
  " }",
1448
- ...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;"],
1449
1534
  " return decode ? decode(checked) : checked;",
1450
1535
  " }",
1451
1536
  "",
1452
- ` 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> {`,
1453
1538
  " function route<T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
1454
1539
  " function route(options: Options): Promise<Result>;",
1455
1540
  " function route(options?: Options): Promise<unknown>;",
1456
1541
  " function route<T>(options?: Options, decode?: ResponseDecoder<T>): Promise<T | unknown> {",
1457
1542
  " const requestOptions = options ?? {};",
1458
- " return request(method, path, requestOptions, decode, responseSchemas, responseKind);",
1543
+ " return request(method, path, requestOptions, decode, responseSchemas, responseKind, raw);",
1459
1544
  " }",
1460
1545
  " return route as RouteMethod<Options, Result>;",
1461
1546
  " }",
@@ -1512,11 +1597,15 @@ function renderOpenApi(graph, options) {
1512
1597
  for (const module of graph.modules) {
1513
1598
  for (const controller of module.controllers) {
1514
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;
1515
1603
  const fullPath = joinRoutePaths(controller.path, route.path);
1516
1604
  const responseEntries = normalizedResponseEntries(route.responses);
1517
1605
  const hasResponseMap = responseEntries.length > 0;
1518
1606
  const pathParams = [...new Set([
1519
1607
  ...(fullPath.match(/:([A-Za-z0-9_]+)/g) ?? []).map((item) => item.slice(1)),
1608
+ ...fullPath.endsWith("/*") ? ["*"] : [],
1520
1609
  ...route.pathParams ?? []
1521
1610
  ].filter((item) => item.length > 0))];
1522
1611
  const queryParams = [...new Set([
@@ -1549,6 +1638,8 @@ function renderOpenApi(graph, options) {
1549
1638
  }).filter((entry) => entry.length === 2)) } : {},
1550
1639
  responseContentType,
1551
1640
  responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
1641
+ ...route.parse === undefined ? {} : { parse: route.parse },
1642
+ ...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
1552
1643
  module: module.name,
1553
1644
  controller: controller.className,
1554
1645
  handler: route.handler,
@@ -1632,6 +1723,8 @@ function renderOpenApi(graph, options) {
1632
1723
  " canMatch?: readonly string[];",
1633
1724
  " canDeactivate?: readonly string[];",
1634
1725
  " contract?: Record<string, unknown>;",
1726
+ ' parse?: "none";',
1727
+ " allowDeleteBody?: true;",
1635
1728
  " data?: Record<string, unknown>;",
1636
1729
  " requiresAuth?: boolean;",
1637
1730
  "};",
@@ -1829,8 +1922,15 @@ function renderOpenApi(graph, options) {
1829
1922
  ' return { $ref: `#/components/schemas/${encodeURIComponent(name).replaceAll("%2F", "/")}` };',
1830
1923
  "}",
1831
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
+ "",
1832
1932
  "function openApiPath(path: string): string {",
1833
- ' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");',
1933
+ ' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}").replace(/\\/\\*$/, `/{${wildcardParameterName(path)}}`);',
1834
1934
  "}",
1835
1935
  "",
1836
1936
  "function responseHasBody(status: string, kind: string | undefined): boolean {",
@@ -1860,7 +1960,7 @@ function renderOpenApi(graph, options) {
1860
1960
  " const parameters: Record<string, unknown>[] = [];",
1861
1961
  " const paramsSchema = definition.paramsSchema ? OPENAPI_SCHEMA_REGISTRY[definition.paramsSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
1862
1962
  " for (const name of definition.pathParams) parameters.push({",
1863
- ' name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
1963
+ ' name: name === "*" ? wildcardParameterName(definition.path) : name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
1864
1964
  " });",
1865
1965
  " const querySchema = definition.querySchema ? OPENAPI_SCHEMA_REGISTRY[definition.querySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
1866
1966
  " const queryNames = new Set([...(querySchema ? schemaPropertyNames(querySchema) : []), ...definition.queryParams]);",
@@ -1897,6 +1997,7 @@ function renderOpenApi(graph, options) {
1897
1997
  " operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
1898
1998
  " ...(parameters.length === 0 ? {} : { parameters }),",
1899
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" } } } } } : {}),',
1900
2001
  " ...(definition.requiresAuth ? { security: [{ bearerAuth: [] }] } : {}),",
1901
2002
  ' "x-supacloud": {',
1902
2003
  " module: definition.module, controller: definition.controller, handler: definition.handler,",
@@ -1906,6 +2007,8 @@ function renderOpenApi(graph, options) {
1906
2007
  " ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
1907
2008
  " ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
1908
2009
  " ...(definition.contract ? { contract: definition.contract } : {}),",
2010
+ " ...(definition.parse ? { parse: definition.parse } : {}),",
2011
+ " ...(definition.allowDeleteBody ? { allowDeleteBody: true } : {}),",
1909
2012
  " ...(definition.data ? { data: definition.data } : {}),",
1910
2013
  " },",
1911
2014
  " };",
@@ -2001,6 +2104,8 @@ function renderPermissions(graph) {
2001
2104
  `);
2002
2105
  }
2003
2106
  var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES = `export interface CompiledRoute {
2107
+ parse?: "none";
2108
+ allowDeleteBody?: true;
2004
2109
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
2005
2110
  path: string;
2006
2111
  handler: string;
@@ -2153,6 +2258,12 @@ function resolveFactoryValue(value: unknown): unknown {
2153
2258
  }
2154
2259
 
2155
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
+ }
2156
2267
 
2157
2268
  function destroyScopeInstances(
2158
2269
  scope: Record<string, unknown>,
@@ -3611,6 +3722,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
3611
3722
  };
3612
3723
  function validateGraph(graph, options = false) {
3613
3724
  const strict = typeof options === "boolean" ? options : options.strict ?? false;
3725
+ const generateOpenApi = typeof options === "object" && options.generateOpenApi === true;
3614
3726
  const diagnostics = [];
3615
3727
  for (const module of graph.modules) {
3616
3728
  for (const owner of [...module.providers, ...module.controllers]) {
@@ -3746,7 +3858,7 @@ function validateGraph(graph, options = false) {
3746
3858
  }
3747
3859
  const fullPath = joinRoutePaths2(controller.path, route.path);
3748
3860
  const rawFullPath = joinRawRoutePaths(controller.path, route.path);
3749
- const key = `${route.method} ${fullPath}`;
3861
+ const key = `${route.method} ${generateOpenApi ? fullPath.replace(/:[A-Za-z0-9_]+|\*$/g, "{}") : fullPath}`;
3750
3862
  const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
3751
3863
  if (openApiMatch) {
3752
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]}'.`);
@@ -3846,7 +3958,13 @@ function validateGraph(graph, options = false) {
3846
3958
  }
3847
3959
  }
3848
3960
  }
3849
- 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)) {
3850
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().`, {
3851
3969
  type: "remove_route_body_binding",
3852
3970
  targetFile: controller.file,
@@ -4558,6 +4676,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4558
4676
  tokensByName: new Map,
4559
4677
  classesByName: new Map,
4560
4678
  variablesByName: new Map,
4679
+ moduleHandlerFiles: new Map,
4561
4680
  diagnostics: []
4562
4681
  };
4563
4682
  const nativeTraitFiles = new Map;
@@ -4648,10 +4767,11 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4648
4767
  const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
4649
4768
  const moduleFileExists = currentFileHashes.has(entry.module.file);
4650
4769
  const isAffectedByDep = affectedModuleNames.has(modName);
4651
- if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
4770
+ if (!hasChangedFile && !isAffectedByDep && moduleFileExists && entry.handlerFiles !== undefined) {
4652
4771
  modulesToKeep.set(modName, entry);
4653
4772
  reusedModules.push(modName);
4654
4773
  finalModules.push(entry.module);
4774
+ ctx.moduleHandlerFiles.set(modName, entry.handlerFiles);
4655
4775
  if (entry.diagnostics)
4656
4776
  finalDiagnostics.push(...entry.diagnostics);
4657
4777
  }
@@ -4671,6 +4791,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4671
4791
  }
4672
4792
  cache.modules.set(parsed.name, {
4673
4793
  module: parsed,
4794
+ handlerFiles: ctx.moduleHandlerFiles.get(parsed.name) ?? [],
4674
4795
  ownedFiles: [...ownedFiles],
4675
4796
  fileHashes,
4676
4797
  diagnostics: moduleDiagnostics
@@ -4839,6 +4960,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4839
4960
  externalTokens,
4840
4961
  diagnostics: ctx.diagnostics.map(withDiagnosticMetadata),
4841
4962
  tokenNames,
4963
+ moduleHandlerFiles: Object.fromEntries(ctx.moduleHandlerFiles),
4842
4964
  ...cache ? { cacheStats: { reusedModules, reanalyzedModules } } : {}
4843
4965
  };
4844
4966
  }
@@ -5051,6 +5173,7 @@ function parseModule(candidate, nameByNode, ctx) {
5051
5173
  arrayProp(options, "commands").forEach(collectHandler);
5052
5174
  arrayProp(options, "jobs").forEach(collectHandler);
5053
5175
  arrayProp(options, "queries").forEach(collectHandler);
5176
+ ctx.moduleHandlerFiles.set(name, [...new Set([...handlerClasses].map((cls) => sourcePath(ctx.rootDir, cls.getSourceFile().fileName)))].sort());
5054
5177
  const commands = [];
5055
5178
  const jobs = [];
5056
5179
  const queries = [];
@@ -5229,6 +5352,25 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
5229
5352
  }
5230
5353
  return ts4.isObjectLiteralExpression(input) ? input : undefined;
5231
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
+ }
5232
5374
  function commandModeProp(object, name) {
5233
5375
  const value = stringLiteralProp(object, name);
5234
5376
  return value === "required" || value === "none" ? value : undefined;
@@ -5736,10 +5878,13 @@ function parseController(input, ctx) {
5736
5878
  const pathParams = [];
5737
5879
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
5738
5880
  let match;
5739
- while ((match = paramRegex.exec(routePath)) !== null) {
5881
+ const fullPath = joinRoutePaths(path, routePath);
5882
+ while ((match = paramRegex.exec(fullPath)) !== null) {
5740
5883
  if (match[1] !== undefined)
5741
5884
  pathParams.push(match[1]);
5742
5885
  }
5886
+ if (joinRoutePaths(path, routePath).endsWith("/*"))
5887
+ pathParams.push("*");
5743
5888
  if (pathParams.length > 0)
5744
5889
  route.pathParams = pathParams;
5745
5890
  const paramBindings = [];
@@ -5890,6 +6035,40 @@ function parseController(input, ctx) {
5890
6035
  const optionsArg = args[1];
5891
6036
  const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
5892
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
+ }
5893
6072
  const legacyResponseExpr = getProp(optionsObject, "response");
5894
6073
  const responsesExpr = getProp(optionsObject, "responses");
5895
6074
  if (legacyResponseExpr !== undefined && responsesExpr !== undefined) {
@@ -7083,7 +7262,7 @@ async function compileProject(options) {
7083
7262
  ...options,
7084
7263
  ...options.cache ? { artifactHashes: options.cache.generatedHashes } : {}
7085
7264
  };
7086
- const written = !hasErrors || options.writeOnError === true ? await generateApplication(graph, generatedOptions) : [];
7265
+ const written = !hasErrors || options.writeOnError === true ? await writeRenderedApplication(rendered, generatedOptions) : [];
7087
7266
  if (!hasErrors) {
7088
7267
  for (const [filename, content] of Object.entries(graphql.files)) {
7089
7268
  const path = join4(options.outDir, filename);
@@ -11441,6 +11620,48 @@ function importSymbol(source, path, symbol) {
11441
11620
  return `import { ${symbol} } from ${JSON.stringify(path)};
11442
11621
  ${source.text}`;
11443
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
+ }
11444
11665
  // src/watch.ts
11445
11666
  import { existsSync as existsSync3, watch } from "node:fs";
11446
11667
  import { dirname as dirname7, relative as relative9, resolve as resolve10, sep as sep9 } from "node:path";
@@ -11880,9 +12101,9 @@ function compilerVersion() {
11880
12101
  }
11881
12102
  function migrationDependencies() {
11882
12103
  return {
11883
- "@supacloud/app": "0.16.0",
12104
+ "@supacloud/app": "0.17.0",
11884
12105
  "@supacloud/compiler": compilerVersion(),
11885
- "@supacloud/elysia": "0.18.0",
12106
+ "@supacloud/elysia": "0.19.0",
11886
12107
  elysia: "1.4.30",
11887
12108
  typescript: "7.0.2"
11888
12109
  };
@@ -11963,6 +12184,8 @@ function isDefineRouteContractCall(node, checker) {
11963
12184
  return resolveSymbol(checker.getSymbolAtLocation(node.expression), checker)?.name === "defineRouteContract";
11964
12185
  }
11965
12186
  function isRouteDecoratorCall(node, checker) {
12187
+ if (node.arguments.length < 2)
12188
+ return false;
11966
12189
  const name = node.expression.getText(node.getSourceFile());
11967
12190
  if (ts12.isIdentifier(node.expression) && ROUTE_DECORATORS2.has(node.expression.text))
11968
12191
  return true;
@@ -12247,7 +12470,7 @@ async function migrateProject(options) {
12247
12470
  const before = sourceByPath.get(absoluteFile);
12248
12471
  if (before === undefined)
12249
12472
  continue;
12250
- 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);
12251
12474
  sourceByPath.set(absoluteFile, result.content);
12252
12475
  if (result.changed && result.issues.length === 0) {
12253
12476
  pendingWrites.set(absoluteFile, result.content);
@@ -13035,10 +13258,7 @@ function explainGraph(graph, subject) {
13035
13258
  throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
13036
13259
  }
13037
13260
  function createContextPack(graph, subject) {
13038
- const subjectModule = graph.modules.find((module) => module.name === subject);
13039
- if (!subjectModule) {
13040
- throw new Error(`No module named "${subject}". Context packs require a module name.`);
13041
- }
13261
+ const subjectModule = resolveContextModule(graph, subject);
13042
13262
  const byName = new Map(graph.modules.map((module) => [module.name, module]));
13043
13263
  const selected = new Set([subjectModule.name]);
13044
13264
  const reverseDependents = new Map;
@@ -13086,6 +13306,7 @@ function createContextPack(graph, subject) {
13086
13306
  module.file,
13087
13307
  ...module.providers.map((provider) => provider.file),
13088
13308
  ...module.controllers.map((controller) => controller.file),
13309
+ ...graph.moduleHandlerFiles?.[module.name] ?? [],
13089
13310
  ...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
13090
13311
  ]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
13091
13312
  const referencedTokens = new Set;
@@ -13115,6 +13336,18 @@ function createContextPack(graph, subject) {
13115
13336
  }
13116
13337
  };
13117
13338
  }
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(", ")}`);
13348
+ }
13349
+ throw new Error(`No context target named "${subject}". Select a module name: ${graph.modules.map((module) => module.name).sort().join(", ") || "(none)"}`);
13350
+ }
13118
13351
  function allAspects(module) {
13119
13352
  return [
13120
13353
  ...module.aspects ?? [],
@@ -13451,6 +13684,7 @@ export {
13451
13684
  createContextPack,
13452
13685
  createDeliveryPlan,
13453
13686
  createDependencyGraphCache,
13687
+ createDiagnosticRepairPlan,
13454
13688
  createExecutionPlans,
13455
13689
  createIncrementalCompiler,
13456
13690
  createIncrementalProgramSession,