@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/cli.js CHANGED
@@ -129,7 +129,9 @@ function buildContractManifest(graph, artifacts) {
129
129
  ...route.responses === undefined ? {} : {
130
130
  responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
131
131
  },
132
- ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
132
+ ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence },
133
+ ...route.parse === undefined ? {} : { parse: route.parse },
134
+ ...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody }
133
135
  });
134
136
  }
135
137
  }
@@ -218,7 +220,7 @@ function renderApplication(graph, options) {
218
220
  providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
219
221
  }));
220
222
  }
221
- const imports = new ImportManager;
223
+ const imports = new ImportManager(new Set(["CompiledApplicationModule"]));
222
224
  const factorySections = [];
223
225
  const descriptorEntries = [];
224
226
  for (const module of modules) {
@@ -235,7 +237,13 @@ function renderApplication(graph, options) {
235
237
  "",
236
238
  TYPE_GUARDS,
237
239
  "",
238
- "export function createCompiledModules(): CompiledModule[] {",
240
+ ...modules.length > 0 ? [
241
+ 'export type CompiledApplicationModule = Omit<CompiledModule, "name" | "createServices"> & (',
242
+ ...modules.map((module) => ` | { name: ${JSON.stringify(module.name)}; createServices: typeof create${pascalName(module.name)}Services }`),
243
+ ");"
244
+ ] : ["export type CompiledApplicationModule = CompiledModule;"],
245
+ "",
246
+ "export function createCompiledModules(): CompiledApplicationModule[] {",
239
247
  " return [",
240
248
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
241
249
  " ];",
@@ -316,7 +324,9 @@ function renderApplication(graph, options) {
316
324
  };
317
325
  }
318
326
  async function generateApplication(graph, options) {
319
- const rendered = renderApplication(graph, options);
327
+ return writeRenderedApplication(renderApplication(graph, options), options);
328
+ }
329
+ async function writeRenderedApplication(rendered, options) {
320
330
  await mkdir(options.outDir, { recursive: true });
321
331
  const applicationPath = join2(options.outDir, "application.ts");
322
332
  const manifestPath = join2(options.outDir, "app.manifest.json");
@@ -403,8 +413,12 @@ function indent(text, spaces) {
403
413
  }
404
414
 
405
415
  class ImportManager {
416
+ reserved;
406
417
  entries = new Map;
407
418
  lookup = new Map;
419
+ constructor(reserved = new Set) {
420
+ this.reserved = reserved;
421
+ }
408
422
  get size() {
409
423
  return this.entries.size;
410
424
  }
@@ -419,7 +433,7 @@ class ImportManager {
419
433
  return existing;
420
434
  let local = exported;
421
435
  let counter = 2;
422
- while (this.entries.has(local)) {
436
+ while (this.entries.has(local) || this.reserved.has(local)) {
423
437
  local = `${exported}${counter}`;
424
438
  counter += 1;
425
439
  }
@@ -476,7 +490,7 @@ class ModuleGenerator {
476
490
  renderDescriptor() {
477
491
  const lines = [
478
492
  `{`,
479
- ` name: ${JSON.stringify(this.module.name)},`,
493
+ ` name: ${JSON.stringify(this.module.name)} as const,`,
480
494
  ` createServices: create${this.pascal}Services,`
481
495
  ];
482
496
  if (this.hasFactoryContent("request")) {
@@ -615,6 +629,10 @@ class ModuleGenerator {
615
629
  if (route.contract !== undefined) {
616
630
  fields.push(`contract: ${JSON.stringify(route.contract)}`);
617
631
  }
632
+ if (route.parse === "none")
633
+ fields.push('parse: "none"');
634
+ if (route.allowDeleteBody === true)
635
+ fields.push("allowDeleteBody: true");
618
636
  if (route.schemaKinds !== undefined && Object.keys(route.schemaKinds).length > 0) {
619
637
  fields.push(`schemaKinds: ${JSON.stringify(route.schemaKinds)}`);
620
638
  }
@@ -714,7 +732,7 @@ ${indent(item, 2)}`).join(",")}
714
732
  `function create${this.pascal}Services(`,
715
733
  ` deps: Record<string, unknown>,`,
716
734
  ` imported: Record<string, Record<string, unknown>>,`,
717
- `): Record<string, unknown> {`,
735
+ `) {`,
718
736
  indent(this.renderFactoryBody("services"), 2),
719
737
  `}`
720
738
  ].join(`
@@ -811,7 +829,7 @@ ${indent(item, 2)}`).join(",")}
811
829
  `);
812
830
  }
813
831
  const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
814
- lines.push(`return { ${entries.join(", ")} };`);
832
+ lines.push(`return retainPlatformDependencies({ ${entries.join(", ")} }, deps);`);
815
833
  return lines.join(`
816
834
  `);
817
835
  }
@@ -912,6 +930,9 @@ ${indent(item, 2)}`).join(",")}
912
930
  return this.depExpr(own.useExisting ?? token, kind, options);
913
931
  }
914
932
  }
933
+ if (own && factoryOfScope(own.scope) === "services" && kind !== "services" && !isSkipSelf && !isSelf) {
934
+ return `services.${camelName(token)}`;
935
+ }
915
936
  if (isSelf) {
916
937
  return isOptional ? "undefined" : `services.${camelName(token)}`;
917
938
  }
@@ -929,13 +950,14 @@ ${indent(item, 2)}`).join(",")}
929
950
  return `imported.${mod.name}.${camelName(token)}`;
930
951
  }
931
952
  }
932
- if (isOptional && !this.graph.externalTokens.includes(token)) {
933
- return "undefined";
934
- }
935
953
  if (isSelf)
936
954
  return isOptional ? "undefined" : `services.${camelName(token)}`;
937
955
  if (kind === "services")
938
956
  return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
957
+ if (!own || isSkipSelf) {
958
+ const external = `platformDependencies.get(services)?.${camelName(token)}`;
959
+ return isOptional ? `(${external} ?? undefined)` : external;
960
+ }
939
961
  return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
940
962
  }
941
963
  }
@@ -963,7 +985,7 @@ function orderProviders(providers) {
963
985
  function renderClient(graph, options) {
964
986
  const rootDir = options?.rootDir ?? process.cwd();
965
987
  const outDir = options?.outDir ?? process.cwd();
966
- const imports = new ImportManager;
988
+ const imports = new ImportManager(new Set(["ApiClientError", "ApiClientErrorCode", "ApiClientErrorDetails"]));
967
989
  const controllerEntries = [];
968
990
  const routeTypes = [];
969
991
  const schemaEntries = [];
@@ -1021,11 +1043,14 @@ function renderClient(graph, options) {
1021
1043
  }).filter((entry) => entry.length === 2));
1022
1044
  if (Object.keys(responseSchemas).length > 0)
1023
1045
  usesValue = true;
1024
- const paramNames = [...new Set((fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1)))];
1046
+ const paramNames = [...new Set([
1047
+ ...(fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1)),
1048
+ ...fullPath.endsWith("/*") ? ["*"] : []
1049
+ ])];
1025
1050
  const pathParamsType = paramNames.length > 0 ? `{ ${paramNames.map((p) => `${/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(p) ? p : JSON.stringify(p)}: string | number`).join("; ")} }` : "Record<string, string | number>";
1026
1051
  const paramsType = paramsSchema ? `Static<typeof ${paramsSchema.local}>${paramNames.length > 0 ? ` & ${pathParamsType}` : ""}` : pathParamsType;
1027
1052
  const queryType = querySchema ? `Static<typeof ${querySchema.local}>` : "Record<string, unknown>";
1028
- const bodyType = bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
1053
+ const bodyType = route.parse === "none" ? "BodyInit" : bodySchema ? `Static<typeof ${bodySchema.local}>` : "unknown";
1029
1054
  const headersType = headersSchema ? `Static<typeof ${headersSchema.local}>` : "Record<string, string>";
1030
1055
  const cookieType = cookieSchema ? `Static<typeof ${cookieSchema.local}>` : "Record<string, string | number | boolean>";
1031
1056
  const responseTypes = [
@@ -1062,7 +1087,9 @@ function renderClient(graph, options) {
1062
1087
  ["queryTransforms", route.queryTransforms],
1063
1088
  ["queryDefaults", route.queryDefaults],
1064
1089
  ["title", route.title],
1065
- ["data", route.data]
1090
+ ["data", route.data],
1091
+ ["parse", route.parse],
1092
+ ["allowDeleteBody", route.allowDeleteBody]
1066
1093
  ]) {
1067
1094
  if (value !== undefined)
1068
1095
  routeFields.push(`${key}: ${JSON.stringify(value)}`);
@@ -1096,7 +1123,7 @@ function renderClient(graph, options) {
1096
1123
  const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
1097
1124
  const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
1098
1125
  routeMethods.push(`
1099
- ${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}),`);
1126
+ ${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}, ${route.parse === "none"}),`);
1100
1127
  }
1101
1128
  controllerEntries.push(`
1102
1129
  ${controllerKey}: {${routeMethods.join("")}
@@ -1127,6 +1154,40 @@ function renderClient(graph, options) {
1127
1154
  "",
1128
1155
  "export type ResponseDecoder<T> = (value: unknown) => T;",
1129
1156
  "",
1157
+ 'export type ApiClientErrorCode = "API_HTTP_ERROR" | "API_RESPONSE_INVALID" | "API_RESPONSE_UNDECLARED";',
1158
+ "",
1159
+ "export interface ApiClientErrorDetails {",
1160
+ " code: ApiClientErrorCode;",
1161
+ " method: string;",
1162
+ " path: string;",
1163
+ " status: number;",
1164
+ " requestId?: string | null;",
1165
+ " response?: Response;",
1166
+ "}",
1167
+ "",
1168
+ "/** Transport/contract metadata, not proof that a failed write rolled back. */",
1169
+ "export class ApiClientError extends Error {",
1170
+ " readonly code: ApiClientErrorCode;",
1171
+ " readonly method: string;",
1172
+ " readonly path: string;",
1173
+ " readonly status: number;",
1174
+ " readonly requestId: string | undefined;",
1175
+ " /** Undeclared HTTP response for explicit inspection; excluded from JSON serialization. */",
1176
+ " readonly response: Response | undefined;",
1177
+ " constructor(message: string, details: ApiClientErrorDetails) {",
1178
+ " super(message);",
1179
+ ' this.name = "ApiClientError";',
1180
+ " const { code, method, path, status, requestId, response } = details;",
1181
+ " this.code = code;",
1182
+ " this.method = method;",
1183
+ " this.path = path;",
1184
+ " this.status = status;",
1185
+ " this.requestId = requestId && /^[A-Za-z0-9._:-]{1,256}$/.test(requestId) ? requestId : undefined;",
1186
+ " this.response = response;",
1187
+ ' Object.defineProperty(this, "response", { enumerable: false });',
1188
+ " }",
1189
+ "}",
1190
+ "",
1130
1191
  "export type RouteMethod<",
1131
1192
  " Options extends ClientRequestOptions = ClientRequestOptions,",
1132
1193
  " Result = never,",
@@ -1180,6 +1241,13 @@ function renderClient(graph, options) {
1180
1241
  ' if (typeof value !== "string" && typeof value !== "number") throw new TypeError(`Missing route parameter: ${key}`);',
1181
1242
  " return encodeURIComponent(String(value));",
1182
1243
  " });",
1244
+ ' if (path.endsWith("/*")) {',
1245
+ ' const wildcard = params && Object.hasOwn(params, "*") ? params["*"] : undefined;',
1246
+ ' if (typeof wildcard !== "string" && typeof wildcard !== "number") throw new TypeError("Missing route parameter: *");',
1247
+ ' const segments = String(wildcard).split("/");',
1248
+ ' if (segments.some(segment => segment === "." || segment === "..")) throw new TypeError("Wildcard dot segments are not supported by Fetch URLs");',
1249
+ ' url = url.slice(0, -1) + segments.map(encodeURIComponent).join("/");',
1250
+ " }",
1183
1251
  " if (query) {",
1184
1252
  " const searchParams = new URLSearchParams();",
1185
1253
  " for (const [k, v] of Object.entries(query)) {",
@@ -1366,6 +1434,10 @@ function renderClient(graph, options) {
1366
1434
  "export function createApiClient(config: ApiClientConfig = {}) {",
1367
1435
  " const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
1368
1436
  ' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
1437
+ " function rawBody(value: unknown): BodyInit {",
1438
+ ' if (typeof value === "string" || value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams || value instanceof ArrayBuffer || ArrayBuffer.isView(value) || value instanceof ReadableStream) return value as BodyInit;',
1439
+ ' throw new TypeError("Raw routes require a BodyInit value; JSON encoding is not implicit");',
1440
+ " }",
1369
1441
  "",
1370
1442
  " async function request<T>(",
1371
1443
  " method: string,",
@@ -1374,6 +1446,7 @@ function renderClient(graph, options) {
1374
1446
  " decode: ResponseDecoder<T>,",
1375
1447
  ` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
1376
1448
  " responseKind?: string,",
1449
+ " raw?: boolean,",
1377
1450
  " ): Promise<T>;",
1378
1451
  " async function request<T>(",
1379
1452
  " method: string,",
@@ -1382,6 +1455,7 @@ function renderClient(graph, options) {
1382
1455
  " decode?: ResponseDecoder<T>,",
1383
1456
  ` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
1384
1457
  " responseKind?: string,",
1458
+ " raw?: boolean,",
1385
1459
  " ): Promise<T | unknown>;",
1386
1460
  " async function request(",
1387
1461
  " method: string,",
@@ -1395,11 +1469,12 @@ function renderClient(graph, options) {
1395
1469
  " decode?: ResponseDecoder<T>,",
1396
1470
  ` responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>,`,
1397
1471
  " responseKind?: string,",
1472
+ " raw = false,",
1398
1473
  " ): Promise<T | unknown> {",
1399
1474
  " const url = `${baseUrl}${buildRouteUrl(path, options.params, options.query)}`;",
1400
1475
  ' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
1401
1476
  " const headers: Record<string, string> = {",
1402
- ' "content-type": "application/json",',
1477
+ ' ...(raw ? {} : { "content-type": "application/json" }),',
1403
1478
  " ...customHeaders,",
1404
1479
  " ...options.headers,",
1405
1480
  " };",
@@ -1418,21 +1493,27 @@ function renderClient(graph, options) {
1418
1493
  " return fetcher(reqPayload.url, {",
1419
1494
  " method: reqPayload.method,",
1420
1495
  " headers: reqPayload.headers,",
1421
- " ...(reqPayload.body === undefined ? {} : { body: JSON.stringify(reqPayload.body) }),",
1496
+ " ...(reqPayload.body === undefined ? {} : { body: raw ? rawBody(reqPayload.body) : JSON.stringify(reqPayload.body) }),",
1497
+ ' ...(raw && reqPayload.body instanceof ReadableStream ? { duplex: "half" } : {}),',
1422
1498
  " });",
1423
1499
  " };",
1424
1500
  " const response = await executeChain(0, { method, url, headers, body: options.body });",
1425
1501
  " const declaredSchema = responseSchemas ? selectResponseSchema(response.status, responseSchemas) : undefined;",
1502
+ " const failure = (code: ApiClientErrorCode, message: string) => new ApiClientError(message, {",
1503
+ ' code, method, path, status: response.status, requestId: response.headers.get("x-request-id"),',
1504
+ ' ...(code === "API_HTTP_ERROR" ? { response } : {}),',
1505
+ " });",
1426
1506
  " if (!response.ok && declaredSchema === undefined) {",
1427
- " const errBody = await response.text();",
1428
- " throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
1507
+ ' throw failure("API_HTTP_ERROR", `API request failed: ${method} ${path} -> ${response.status}`);',
1429
1508
  " }",
1430
1509
  ' const contentType = response.headers?.get("content-type") ?? "";',
1431
1510
  " let value: unknown;",
1432
1511
  " if ([204, 205, 304].includes(response.status)) {",
1433
1512
  " value = undefined;",
1434
1513
  ' } else if (contentType.includes("application/json") || contentType.includes("+json")) {',
1435
- " value = await response.json();",
1514
+ " const responseText = await response.text();",
1515
+ " try { value = JSON.parse(responseText); }",
1516
+ ' catch { throw failure("API_RESPONSE_INVALID", "Response is not valid JSON"); }',
1436
1517
  ' } else if (responseKind === "binary" || contentType.includes("application/octet-stream")) {',
1437
1518
  " value = await response.arrayBuffer();",
1438
1519
  ' } else if (responseKind === "stream") {',
@@ -1443,20 +1524,24 @@ function renderClient(graph, options) {
1443
1524
  " if (!responseSchemas) return decode ? decode(value) : value;",
1444
1525
  " if (declaredSchema === undefined) {",
1445
1526
  ' const transportSuccess = response.ok && (responseKind === "binary" || responseKind === "stream");',
1446
- " if (!transportSuccess) throw new Error(`No response schema declared for HTTP ${response.status}`);",
1527
+ ' if (!transportSuccess) throw failure("API_RESPONSE_UNDECLARED", `No response schema declared for HTTP ${response.status}`);',
1447
1528
  " return decode ? decode(value) : value;",
1448
1529
  " }",
1449
- ...usesValue ? [" const checked = decodeResponseSchema(value, response.status, responseSchemas, { normalize: config.normalize ?? true });"] : [" const checked = value;"],
1530
+ ...usesValue ? [
1531
+ " let checked: unknown;",
1532
+ " try { checked = decodeResponseSchema(value, response.status, responseSchemas, { normalize: config.normalize ?? true }); }",
1533
+ ' catch { throw failure("API_RESPONSE_INVALID", "Response does not match schema"); }'
1534
+ ] : [" const checked = value;"],
1450
1535
  " return decode ? decode(checked) : checked;",
1451
1536
  " }",
1452
1537
  "",
1453
- ` function makeRoute<Options extends ClientRequestOptions, Result = never>(method: string, path: string, responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>, responseKind?: string): RouteMethod<Options, Result> {`,
1538
+ ` function makeRoute<Options extends ClientRequestOptions, Result = never>(method: string, path: string, responseSchemas?: Readonly<Record<string, ${responseSchemaType}>>, responseKind?: string, raw = false): RouteMethod<Options, Result> {`,
1454
1539
  " function route<T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
1455
1540
  " function route(options: Options): Promise<Result>;",
1456
1541
  " function route(options?: Options): Promise<unknown>;",
1457
1542
  " function route<T>(options?: Options, decode?: ResponseDecoder<T>): Promise<T | unknown> {",
1458
1543
  " const requestOptions = options ?? {};",
1459
- " return request(method, path, requestOptions, decode, responseSchemas, responseKind);",
1544
+ " return request(method, path, requestOptions, decode, responseSchemas, responseKind, raw);",
1460
1545
  " }",
1461
1546
  " return route as RouteMethod<Options, Result>;",
1462
1547
  " }",
@@ -1513,11 +1598,15 @@ function renderOpenApi(graph, options) {
1513
1598
  for (const module of graph.modules) {
1514
1599
  for (const controller of module.controllers) {
1515
1600
  for (const route of controller.routes) {
1601
+ const documentation = route.data?.openapi;
1602
+ if (documentation !== null && typeof documentation === "object" && "hide" in documentation && documentation.hide === true)
1603
+ continue;
1516
1604
  const fullPath = joinRoutePaths(controller.path, route.path);
1517
1605
  const responseEntries = normalizedResponseEntries(route.responses);
1518
1606
  const hasResponseMap = responseEntries.length > 0;
1519
1607
  const pathParams = [...new Set([
1520
1608
  ...(fullPath.match(/:([A-Za-z0-9_]+)/g) ?? []).map((item) => item.slice(1)),
1609
+ ...fullPath.endsWith("/*") ? ["*"] : [],
1521
1610
  ...route.pathParams ?? []
1522
1611
  ].filter((item) => item.length > 0))];
1523
1612
  const queryParams = [...new Set([
@@ -1550,6 +1639,8 @@ function renderOpenApi(graph, options) {
1550
1639
  }).filter((entry) => entry.length === 2)) } : {},
1551
1640
  responseContentType,
1552
1641
  responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
1642
+ ...route.parse === undefined ? {} : { parse: route.parse },
1643
+ ...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
1553
1644
  module: module.name,
1554
1645
  controller: controller.className,
1555
1646
  handler: route.handler,
@@ -1633,6 +1724,8 @@ function renderOpenApi(graph, options) {
1633
1724
  " canMatch?: readonly string[];",
1634
1725
  " canDeactivate?: readonly string[];",
1635
1726
  " contract?: Record<string, unknown>;",
1727
+ ' parse?: "none";',
1728
+ " allowDeleteBody?: true;",
1636
1729
  " data?: Record<string, unknown>;",
1637
1730
  " requiresAuth?: boolean;",
1638
1731
  "};",
@@ -1830,8 +1923,15 @@ function renderOpenApi(graph, options) {
1830
1923
  ' return { $ref: `#/components/schemas/${encodeURIComponent(name).replaceAll("%2F", "/")}` };',
1831
1924
  "}",
1832
1925
  "",
1926
+ "function wildcardParameterName(path: string): string {",
1927
+ " const names = new Set([...path.matchAll(/:([A-Za-z0-9_]+)/g)].map(match => match[1]));",
1928
+ ' let name = "wildcard";',
1929
+ ' while (names.has(name)) name += "_";',
1930
+ " return name;",
1931
+ "}",
1932
+ "",
1833
1933
  "function openApiPath(path: string): string {",
1834
- ' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");',
1934
+ ' return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}").replace(/\\/\\*$/, `/{${wildcardParameterName(path)}}`);',
1835
1935
  "}",
1836
1936
  "",
1837
1937
  "function responseHasBody(status: string, kind: string | undefined): boolean {",
@@ -1861,7 +1961,7 @@ function renderOpenApi(graph, options) {
1861
1961
  " const parameters: Record<string, unknown>[] = [];",
1862
1962
  " const paramsSchema = definition.paramsSchema ? OPENAPI_SCHEMA_REGISTRY[definition.paramsSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
1863
1963
  " for (const name of definition.pathParams) parameters.push({",
1864
- ' name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
1964
+ ' name: name === "*" ? wildcardParameterName(definition.path) : name, in: "path", required: true, schema: schemaProperty(paramsSchema, name),',
1865
1965
  " });",
1866
1966
  " const querySchema = definition.querySchema ? OPENAPI_SCHEMA_REGISTRY[definition.querySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
1867
1967
  " const queryNames = new Set([...(querySchema ? schemaPropertyNames(querySchema) : []), ...definition.queryParams]);",
@@ -1898,6 +1998,7 @@ function renderOpenApi(graph, options) {
1898
1998
  " operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
1899
1999
  " ...(parameters.length === 0 ? {} : { parameters }),",
1900
2000
  ' ...(definition.bodySchema ? { requestBody: { required: !schemaAllowsUndefined(OPENAPI_SCHEMA_REGISTRY[definition.bodySchema as keyof typeof OPENAPI_SCHEMA_REGISTRY]), content: { "application/json": { schema: schemaReference(definition.bodySchema) } } } } : {}),',
2001
+ ' ...(definition.parse === "none" ? { requestBody: { required: false, description: "Raw request bytes; validation and media types are owned by the domain handler.", content: { "*/*": { schema: { type: "string", format: "binary" } } } } } : {}),',
1901
2002
  " ...(definition.requiresAuth ? { security: [{ bearerAuth: [] }] } : {}),",
1902
2003
  ' "x-supacloud": {',
1903
2004
  " module: definition.module, controller: definition.controller, handler: definition.handler,",
@@ -1907,6 +2008,8 @@ function renderOpenApi(graph, options) {
1907
2008
  " ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
1908
2009
  " ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
1909
2010
  " ...(definition.contract ? { contract: definition.contract } : {}),",
2011
+ " ...(definition.parse ? { parse: definition.parse } : {}),",
2012
+ " ...(definition.allowDeleteBody ? { allowDeleteBody: true } : {}),",
1910
2013
  " ...(definition.data ? { data: definition.data } : {}),",
1911
2014
  " },",
1912
2015
  " };",
@@ -2002,6 +2105,8 @@ function renderPermissions(graph) {
2002
2105
  `);
2003
2106
  }
2004
2107
  var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES = `export interface CompiledRoute {
2108
+ parse?: "none";
2109
+ allowDeleteBody?: true;
2005
2110
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
2006
2111
  path: string;
2007
2112
  handler: string;
@@ -2154,6 +2259,12 @@ function resolveFactoryValue(value: unknown): unknown {
2154
2259
  }
2155
2260
 
2156
2261
  const scopeDestructions = new WeakMap<object, Promise<void>>();
2262
+ const platformDependencies = new WeakMap<object, Record<string, unknown>>();
2263
+
2264
+ function retainPlatformDependencies<T extends Record<string, unknown>>(services: T, deps: Record<string, unknown>): T {
2265
+ platformDependencies.set(services, deps);
2266
+ return services;
2267
+ }
2157
2268
 
2158
2269
  function destroyScopeInstances(
2159
2270
  scope: Record<string, unknown>,
@@ -3616,6 +3727,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
3616
3727
  };
3617
3728
  function validateGraph(graph, options = false) {
3618
3729
  const strict = typeof options === "boolean" ? options : options.strict ?? false;
3730
+ const generateOpenApi = typeof options === "object" && options.generateOpenApi === true;
3619
3731
  const diagnostics = [];
3620
3732
  for (const module of graph.modules) {
3621
3733
  for (const owner of [...module.providers, ...module.controllers]) {
@@ -3751,7 +3863,7 @@ function validateGraph(graph, options = false) {
3751
3863
  }
3752
3864
  const fullPath = joinRoutePaths2(controller.path, route.path);
3753
3865
  const rawFullPath = joinRawRoutePaths(controller.path, route.path);
3754
- const key = `${route.method} ${fullPath}`;
3866
+ const key = `${route.method} ${generateOpenApi ? fullPath.replace(/:[A-Za-z0-9_]+|\*$/g, "{}") : fullPath}`;
3755
3867
  const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
3756
3868
  if (openApiMatch) {
3757
3869
  error("missing-param-colon", `Route path '${route.path}' in '${controller.className}.${route.handler}' uses OpenAPI-style '{${openApiMatch[1]}}'. SupaCloud routes require Express/Angular-style ':${openApiMatch[1]}'.`, controller.file, undefined, `Replace '{${openApiMatch[1]}}' with ':${openApiMatch[1]}'.`);
@@ -3851,7 +3963,13 @@ function validateGraph(graph, options = false) {
3851
3963
  }
3852
3964
  }
3853
3965
  }
3854
- if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
3966
+ if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
3967
+ error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
3968
+ }
3969
+ if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
3970
+ error("invalid-raw-body-contract", `Route ${controller.className}.${route.handler} parse: "none" requires domain body ownership and evidence, without a parsed body schema or @Body binding.`, controller.file);
3971
+ }
3972
+ if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE" && !route.allowDeleteBody) && (route.hasBodyBinding || route.body)) {
3855
3973
  error("disallowed-body-on-get-delete", `Route handler ${controller.className}.${route.handler} binds @Body() or declares body schema on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param().`, {
3856
3974
  type: "remove_route_body_binding",
3857
3975
  targetFile: controller.file,
@@ -4563,6 +4681,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4563
4681
  tokensByName: new Map,
4564
4682
  classesByName: new Map,
4565
4683
  variablesByName: new Map,
4684
+ moduleHandlerFiles: new Map,
4566
4685
  diagnostics: []
4567
4686
  };
4568
4687
  const nativeTraitFiles = new Map;
@@ -4653,10 +4772,11 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4653
4772
  const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
4654
4773
  const moduleFileExists = currentFileHashes.has(entry.module.file);
4655
4774
  const isAffectedByDep = affectedModuleNames.has(modName);
4656
- if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
4775
+ if (!hasChangedFile && !isAffectedByDep && moduleFileExists && entry.handlerFiles !== undefined) {
4657
4776
  modulesToKeep.set(modName, entry);
4658
4777
  reusedModules.push(modName);
4659
4778
  finalModules.push(entry.module);
4779
+ ctx.moduleHandlerFiles.set(modName, entry.handlerFiles);
4660
4780
  if (entry.diagnostics)
4661
4781
  finalDiagnostics.push(...entry.diagnostics);
4662
4782
  }
@@ -4676,6 +4796,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4676
4796
  }
4677
4797
  cache.modules.set(parsed.name, {
4678
4798
  module: parsed,
4799
+ handlerFiles: ctx.moduleHandlerFiles.get(parsed.name) ?? [],
4679
4800
  ownedFiles: [...ownedFiles],
4680
4801
  fileHashes,
4681
4802
  diagnostics: moduleDiagnostics
@@ -4844,6 +4965,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
4844
4965
  externalTokens,
4845
4966
  diagnostics: ctx.diagnostics.map(withDiagnosticMetadata),
4846
4967
  tokenNames,
4968
+ moduleHandlerFiles: Object.fromEntries(ctx.moduleHandlerFiles),
4847
4969
  ...cache ? { cacheStats: { reusedModules, reanalyzedModules } } : {}
4848
4970
  };
4849
4971
  }
@@ -5056,6 +5178,7 @@ function parseModule(candidate, nameByNode, ctx) {
5056
5178
  arrayProp(options, "commands").forEach(collectHandler);
5057
5179
  arrayProp(options, "jobs").forEach(collectHandler);
5058
5180
  arrayProp(options, "queries").forEach(collectHandler);
5181
+ ctx.moduleHandlerFiles.set(name, [...new Set([...handlerClasses].map((cls) => sourcePath(ctx.rootDir, cls.getSourceFile().fileName)))].sort());
5059
5182
  const commands = [];
5060
5183
  const jobs = [];
5061
5184
  const queries = [];
@@ -5234,6 +5357,25 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
5234
5357
  }
5235
5358
  return ts4.isObjectLiteralExpression(input) ? input : undefined;
5236
5359
  }
5360
+ function routePolicyProperty(object, name, ctx, seen = new Set) {
5361
+ if (seen.has(object))
5362
+ return { unresolved: true };
5363
+ const ancestry = new Set(seen).add(object);
5364
+ let result = { unresolved: false };
5365
+ for (const property of object.properties) {
5366
+ if (ts4.isSpreadAssignment(property)) {
5367
+ const spread = resolveStaticObjectLiteral(property.expression, ctx);
5368
+ const nested = spread ? routePolicyProperty(spread, name, ctx, ancestry) : { unresolved: true };
5369
+ if (nested.unresolved || nested.value !== undefined)
5370
+ result = nested;
5371
+ } else if (ts4.isComputedPropertyName(property.name)) {
5372
+ result = { unresolved: true };
5373
+ } else if (propertyName(property.name) === name) {
5374
+ result = ts4.isPropertyAssignment(property) ? { value: property.initializer, unresolved: false } : { unresolved: true };
5375
+ }
5376
+ }
5377
+ return result;
5378
+ }
5237
5379
  function commandModeProp(object, name) {
5238
5380
  const value = stringLiteralProp(object, name);
5239
5381
  return value === "required" || value === "none" ? value : undefined;
@@ -5741,10 +5883,13 @@ function parseController(input, ctx) {
5741
5883
  const pathParams = [];
5742
5884
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
5743
5885
  let match;
5744
- while ((match = paramRegex.exec(routePath)) !== null) {
5886
+ const fullPath = joinRoutePaths(path, routePath);
5887
+ while ((match = paramRegex.exec(fullPath)) !== null) {
5745
5888
  if (match[1] !== undefined)
5746
5889
  pathParams.push(match[1]);
5747
5890
  }
5891
+ if (joinRoutePaths(path, routePath).endsWith("/*"))
5892
+ pathParams.push("*");
5748
5893
  if (pathParams.length > 0)
5749
5894
  route.pathParams = pathParams;
5750
5895
  const paramBindings = [];
@@ -5895,6 +6040,40 @@ function parseController(input, ctx) {
5895
6040
  const optionsArg = args[1];
5896
6041
  const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
5897
6042
  if (optionsObject) {
6043
+ const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
6044
+ const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
6045
+ if (parsePolicy.unresolved || deletePolicy.unresolved) {
6046
+ ctx.diagnostics.push({
6047
+ severity: "error",
6048
+ code: "unresolved-route-body-policy",
6049
+ file,
6050
+ message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
6051
+ });
6052
+ }
6053
+ const parse = parsePolicy.value;
6054
+ if (parse) {
6055
+ if (ts4.isStringLiteral(parse) && parse.text === "none")
6056
+ route.parse = "none";
6057
+ else
6058
+ ctx.diagnostics.push({
6059
+ severity: "error",
6060
+ code: "invalid-route-parser",
6061
+ file,
6062
+ message: `Route ${route.handler} parse must be the literal "none".`
6063
+ });
6064
+ }
6065
+ const allowDeleteBody = deletePolicy.value;
6066
+ if (allowDeleteBody) {
6067
+ if (allowDeleteBody.kind === ts4.SyntaxKind.TrueKeyword)
6068
+ route.allowDeleteBody = true;
6069
+ else
6070
+ ctx.diagnostics.push({
6071
+ severity: "error",
6072
+ code: "invalid-delete-body-opt-in",
6073
+ file,
6074
+ message: `Route ${route.handler} allowDeleteBody must be the literal true.`
6075
+ });
6076
+ }
5898
6077
  const legacyResponseExpr = getProp(optionsObject, "response");
5899
6078
  const responsesExpr = getProp(optionsObject, "responses");
5900
6079
  if (legacyResponseExpr !== undefined && responsesExpr !== undefined) {
@@ -7086,7 +7265,7 @@ async function compileProject(options) {
7086
7265
  ...options,
7087
7266
  ...options.cache ? { artifactHashes: options.cache.generatedHashes } : {}
7088
7267
  };
7089
- const written = !hasErrors || options.writeOnError === true ? await generateApplication(graph, generatedOptions) : [];
7268
+ const written = !hasErrors || options.writeOnError === true ? await writeRenderedApplication(rendered, generatedOptions) : [];
7090
7269
  if (!hasErrors) {
7091
7270
  for (const [filename, content] of Object.entries(graphql.files)) {
7092
7271
  const path = join4(options.outDir, filename);
@@ -7227,10 +7406,7 @@ function explainGraph(graph, subject) {
7227
7406
  throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
7228
7407
  }
7229
7408
  function createContextPack(graph, subject) {
7230
- const subjectModule = graph.modules.find((module) => module.name === subject);
7231
- if (!subjectModule) {
7232
- throw new Error(`No module named "${subject}". Context packs require a module name.`);
7233
- }
7409
+ const subjectModule = resolveContextModule(graph, subject);
7234
7410
  const byName = new Map(graph.modules.map((module) => [module.name, module]));
7235
7411
  const selected = new Set([subjectModule.name]);
7236
7412
  const reverseDependents = new Map;
@@ -7278,6 +7454,7 @@ function createContextPack(graph, subject) {
7278
7454
  module.file,
7279
7455
  ...module.providers.map((provider) => provider.file),
7280
7456
  ...module.controllers.map((controller) => controller.file),
7457
+ ...graph.moduleHandlerFiles?.[module.name] ?? [],
7281
7458
  ...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
7282
7459
  ]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
7283
7460
  const referencedTokens = new Set;
@@ -7307,6 +7484,18 @@ function createContextPack(graph, subject) {
7307
7484
  }
7308
7485
  };
7309
7486
  }
7487
+ function resolveContextModule(graph, subject) {
7488
+ const exact = graph.modules.find((module) => module.name === subject);
7489
+ if (exact)
7490
+ return exact;
7491
+ const matches = graph.modules.filter((module) => module.className === subject || module.providers.some((provider) => provider.token === subject || provider.useClass === subject) || module.controllers.some((controller) => controller.className === subject) || module.commands.some((command) => command.name === subject || command.className === subject) || (module.jobs ?? []).some((job) => job.name === subject || job.className === subject || job.serviceKey === subject) || module.queries.some((query) => query.name === subject || query.className === subject));
7492
+ if (matches.length === 1)
7493
+ return matches[0];
7494
+ if (matches.length > 1) {
7495
+ throw new Error(`Ambiguous context target "${subject}". Select a module name: ${matches.map((module) => module.name).sort().join(", ")}`);
7496
+ }
7497
+ throw new Error(`No context target named "${subject}". Select a module name: ${graph.modules.map((module) => module.name).sort().join(", ") || "(none)"}`);
7498
+ }
7310
7499
  function allAspects(module) {
7311
7500
  return [
7312
7501
  ...module.aspects ?? [],
@@ -12310,9 +12499,9 @@ function compilerVersion() {
12310
12499
  }
12311
12500
  function migrationDependencies() {
12312
12501
  return {
12313
- "@supacloud/app": "0.16.0",
12502
+ "@supacloud/app": "0.17.0",
12314
12503
  "@supacloud/compiler": compilerVersion(),
12315
- "@supacloud/elysia": "0.18.0",
12504
+ "@supacloud/elysia": "0.19.0",
12316
12505
  elysia: "1.4.30",
12317
12506
  typescript: "7.0.2"
12318
12507
  };
@@ -12393,6 +12582,8 @@ function isDefineRouteContractCall(node, checker) {
12393
12582
  return resolveSymbol(checker.getSymbolAtLocation(node.expression), checker)?.name === "defineRouteContract";
12394
12583
  }
12395
12584
  function isRouteDecoratorCall(node, checker) {
12585
+ if (node.arguments.length < 2)
12586
+ return false;
12396
12587
  const name = node.expression.getText(node.getSourceFile());
12397
12588
  if (ts12.isIdentifier(node.expression) && ROUTE_DECORATORS2.has(node.expression.text))
12398
12589
  return true;
@@ -12677,7 +12868,7 @@ async function migrateProject(options) {
12677
12868
  const before = sourceByPath.get(absoluteFile);
12678
12869
  if (before === undefined)
12679
12870
  continue;
12680
- const result = projectResults?.results.get(absoluteFile) ?? migration.apply(before, file);
12871
+ const result = projectResults ? projectResults.results.get(absoluteFile) ?? { changed: false, content: before, replacements: 0, issues: [] } : migration.apply(before, file);
12681
12872
  sourceByPath.set(absoluteFile, result.content);
12682
12873
  if (result.changed && result.issues.length === 0) {
12683
12874
  pendingWrites.set(absoluteFile, result.content);
@@ -13444,7 +13635,7 @@ Usage:
13444
13635
  supacloud-compiler dev [rootDir] [options]
13445
13636
  supacloud-compiler graph [rootDir] [options]
13446
13637
  supacloud-compiler explain <name> [rootDir] [options]
13447
- supacloud-compiler context <module> [rootDir] [options]
13638
+ supacloud-compiler context <name> [rootDir] [options]
13448
13639
  supacloud-compiler doctor [rootDir] [options]
13449
13640
  supacloud-compiler migrate [rootDir] [options]
13450
13641
  supacloud-compiler migration-assess [rootDir] [options]
@@ -13959,7 +14150,7 @@ Source change detected; compiling...`);
13959
14150
  }
13960
14151
  } else if (command === "context") {
13961
14152
  if (!query) {
13962
- console.error("Error: context requires a module name");
14153
+ console.error("Error: context requires a module or owned symbol name");
13963
14154
  process.exit(1);
13964
14155
  }
13965
14156
  try {