@supacloud/compiler 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -257,6 +257,24 @@ bun test
257
257
  bun run build
258
258
  ```
259
259
 
260
+ ## Route Contract Policy
261
+
262
+ Enable `requireRouteContracts: true` in `defineSupacloudConfig(...)` or
263
+ `CompileOptions` to report `route-contract-required` errors in both compile and
264
+ check (including JSON diagnostics). Changing this option invalidates incremental
265
+ results. Combine it with `writeOnError: false` when programmatic compilation must
266
+ not emit files on errors.
267
+
268
+ `inspectRouteContracts(graph)` lists each route and its missing body, params,
269
+ query, and response declarations. Required inputs are detected from handler
270
+ bindings and controller/route path parameters. Responses always require an
271
+ explicit declaration, including intentional void contracts.
272
+
273
+ This checks declaration coverage only, not schema quality, handler/schema type
274
+ equivalence, or database authorization. It deliberately does not auto-fix missing
275
+ schemas with `unknown` placeholders. Consumers must define the actual contracts
276
+ and test decoding separately. The policy defaults to false for existing projects.
277
+
260
278
  ## License
261
279
 
262
280
  MIT
package/dist/cli.js CHANGED
@@ -4682,6 +4682,39 @@ function isAnyKeyword(node) {
4682
4682
  return node.kind === ts4.SyntaxKind.AnyKeyword;
4683
4683
  }
4684
4684
 
4685
+ // src/route-contracts.ts
4686
+ function inspectRouteContracts(graph) {
4687
+ return graph.modules.flatMap((module) => module.controllers.flatMap((controller) => controller.routes.map((route) => {
4688
+ const missing = [];
4689
+ if ((route.hasBodyBinding || route.handlerParams?.some((param) => param.kind === "body")) && !route.body)
4690
+ missing.push("body");
4691
+ if ((route.pathParams?.length || route.paramBindings?.length || route.handlerParams?.some((param) => param.kind === "param") || /:[^/]+/.test(`${controller.path}/${route.path}`)) && !route.params)
4692
+ missing.push("params");
4693
+ if ((route.queryBindings?.length || route.handlerParams?.some((param) => param.kind === "query")) && !route.query)
4694
+ missing.push("query");
4695
+ if (!route.response)
4696
+ missing.push("response");
4697
+ return {
4698
+ module: module.name,
4699
+ controller: controller.className,
4700
+ handler: route.handler,
4701
+ method: route.method,
4702
+ path: `${controller.path}${route.path}`,
4703
+ file: controller.file,
4704
+ missing
4705
+ };
4706
+ })));
4707
+ }
4708
+ function validateRouteContracts(graph) {
4709
+ return inspectRouteContracts(graph).filter((route) => route.missing.length).map((route) => ({
4710
+ severity: "error",
4711
+ code: "route-contract-required",
4712
+ file: route.file,
4713
+ message: `${route.controller}.${route.handler} (${route.method} ${route.path}) is missing contract declarations: ${route.missing.join(", ")}.`,
4714
+ suggestion: "Declare the missing schemas and test actual request/response decoding. An opaque schema is not proof of validation."
4715
+ }));
4716
+ }
4717
+
4685
4718
  // src/compile.ts
4686
4719
  async function compileProject(options) {
4687
4720
  const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
@@ -4704,6 +4737,8 @@ async function compileProject(options) {
4704
4737
  }
4705
4738
  }
4706
4739
  const typeSafety = resolveTypeSafety(options);
4740
+ if (options.requireRouteContracts)
4741
+ diagnostics.push(...validateRouteContracts(graph));
4707
4742
  const rendered = renderApplication(graph, {
4708
4743
  rootDir: options.rootDir,
4709
4744
  outDir: options.outDir,
@@ -4767,6 +4802,8 @@ async function checkProject(options) {
4767
4802
  }
4768
4803
  }
4769
4804
  const typeSafety = resolveTypeSafety(options);
4805
+ if (options.requireRouteContracts)
4806
+ diagnostics.push(...validateRouteContracts(graph));
4770
4807
  const rendered = renderApplication(graph, {
4771
4808
  rootDir: options.rootDir,
4772
4809
  outDir: options.outDir,
@@ -5144,6 +5181,7 @@ function optionsKeyOf(options) {
5144
5181
  allowRouteCommandBindings: options.allowRouteCommandBindings,
5145
5182
  commandCapabilities: options.commandCapabilities,
5146
5183
  disallowControllerDirectDb: options.disallowControllerDirectDb,
5184
+ requireRouteContracts: options.requireRouteContracts,
5147
5185
  detectOrphanModules: options.detectOrphanModules,
5148
5186
  generateClient: options.generateClient,
5149
5187
  generatePermissions: options.generatePermissions,
@@ -5399,6 +5437,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
5399
5437
  outDir: "generated",
5400
5438
  include: ["**/*.module.ts", "**/*.ts"],
5401
5439
  strict: true,
5440
+ requireRouteContracts: false,
5402
5441
  generateClient: true,
5403
5442
  generatePermissions: true,
5404
5443
  treeShakeUnusedProviders: true,
@@ -5418,6 +5457,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
5418
5457
  outDir: resolve5(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
5419
5458
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
5420
5459
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
5460
+ requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
5421
5461
  generateClient: resolved.generateClient ?? DEFAULT_SUPACLOUD_CONFIG.generateClient,
5422
5462
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
5423
5463
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
@@ -5445,6 +5485,7 @@ function compileOptionsFromConfig(config, cwd = process.cwd()) {
5445
5485
  outDir: resolved.outDir,
5446
5486
  include: resolved.include,
5447
5487
  strict: resolved.strict,
5488
+ requireRouteContracts: resolved.requireRouteContracts,
5448
5489
  generateClient: resolved.generateClient,
5449
5490
  generatePermissions: resolved.generatePermissions,
5450
5491
  moduleBoundaryPreset: resolved.moduleBoundaryPreset,
package/dist/config.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface SupaCloudConfig {
4
4
  outDir?: string;
5
5
  include?: string[];
6
6
  strict?: boolean;
7
+ requireRouteContracts?: boolean;
7
8
  generateClient?: boolean;
8
9
  generatePermissions?: boolean;
9
10
  moduleBoundaryPreset?: ModuleBoundaryPresetName;
@@ -20,6 +21,7 @@ export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: s
20
21
  outDir: string;
21
22
  include: string[];
22
23
  strict: boolean;
24
+ requireRouteContracts: boolean;
23
25
  generateClient: boolean;
24
26
  generatePermissions: boolean;
25
27
  moduleBoundaryPreset: ModuleBoundaryPresetName;
package/dist/index.d.ts CHANGED
@@ -24,3 +24,4 @@ export type { SupaCloudConfig } from "./config";
24
24
  export { camelName } from "./util";
25
25
  export { ANGULAR_ENTERPRISE_RULES, CLEAN_ARCHITECTURE_RULES, MODULAR_MONOLITH_RULES, MODULE_BOUNDARY_PROFILES, getModuleBoundaryPreset, getModuleBoundaryProfile, resolveModuleBoundaries, } from "./profiles";
26
26
  export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, } from "./types";
27
+ export { inspectRouteContracts, validateRouteContracts } from "./route-contracts";
package/dist/index.js CHANGED
@@ -4863,6 +4863,39 @@ function isAnyKeyword(node) {
4863
4863
  return node.kind === ts5.SyntaxKind.AnyKeyword;
4864
4864
  }
4865
4865
 
4866
+ // src/route-contracts.ts
4867
+ function inspectRouteContracts(graph) {
4868
+ return graph.modules.flatMap((module) => module.controllers.flatMap((controller) => controller.routes.map((route) => {
4869
+ const missing = [];
4870
+ if ((route.hasBodyBinding || route.handlerParams?.some((param) => param.kind === "body")) && !route.body)
4871
+ missing.push("body");
4872
+ if ((route.pathParams?.length || route.paramBindings?.length || route.handlerParams?.some((param) => param.kind === "param") || /:[^/]+/.test(`${controller.path}/${route.path}`)) && !route.params)
4873
+ missing.push("params");
4874
+ if ((route.queryBindings?.length || route.handlerParams?.some((param) => param.kind === "query")) && !route.query)
4875
+ missing.push("query");
4876
+ if (!route.response)
4877
+ missing.push("response");
4878
+ return {
4879
+ module: module.name,
4880
+ controller: controller.className,
4881
+ handler: route.handler,
4882
+ method: route.method,
4883
+ path: `${controller.path}${route.path}`,
4884
+ file: controller.file,
4885
+ missing
4886
+ };
4887
+ })));
4888
+ }
4889
+ function validateRouteContracts(graph) {
4890
+ return inspectRouteContracts(graph).filter((route) => route.missing.length).map((route) => ({
4891
+ severity: "error",
4892
+ code: "route-contract-required",
4893
+ file: route.file,
4894
+ message: `${route.controller}.${route.handler} (${route.method} ${route.path}) is missing contract declarations: ${route.missing.join(", ")}.`,
4895
+ suggestion: "Declare the missing schemas and test actual request/response decoding. An opaque schema is not proof of validation."
4896
+ }));
4897
+ }
4898
+
4866
4899
  // src/compile.ts
4867
4900
  async function compileProject(options) {
4868
4901
  const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
@@ -4885,6 +4918,8 @@ async function compileProject(options) {
4885
4918
  }
4886
4919
  }
4887
4920
  const typeSafety = resolveTypeSafety(options);
4921
+ if (options.requireRouteContracts)
4922
+ diagnostics.push(...validateRouteContracts(graph));
4888
4923
  const rendered = renderApplication(graph, {
4889
4924
  rootDir: options.rootDir,
4890
4925
  outDir: options.outDir,
@@ -4948,6 +4983,8 @@ async function checkProject(options) {
4948
4983
  }
4949
4984
  }
4950
4985
  const typeSafety = resolveTypeSafety(options);
4986
+ if (options.requireRouteContracts)
4987
+ diagnostics.push(...validateRouteContracts(graph));
4951
4988
  const rendered = renderApplication(graph, {
4952
4989
  rootDir: options.rootDir,
4953
4990
  outDir: options.outDir,
@@ -5132,6 +5169,7 @@ function optionsKeyOf(options) {
5132
5169
  allowRouteCommandBindings: options.allowRouteCommandBindings,
5133
5170
  commandCapabilities: options.commandCapabilities,
5134
5171
  disallowControllerDirectDb: options.disallowControllerDirectDb,
5172
+ requireRouteContracts: options.requireRouteContracts,
5135
5173
  detectOrphanModules: options.detectOrphanModules,
5136
5174
  generateClient: options.generateClient,
5137
5175
  generatePermissions: options.generatePermissions,
@@ -5577,6 +5615,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
5577
5615
  outDir: "generated",
5578
5616
  include: ["**/*.module.ts", "**/*.ts"],
5579
5617
  strict: true,
5618
+ requireRouteContracts: false,
5580
5619
  generateClient: true,
5581
5620
  generatePermissions: true,
5582
5621
  treeShakeUnusedProviders: true,
@@ -5596,6 +5635,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
5596
5635
  outDir: resolve6(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
5597
5636
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
5598
5637
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
5638
+ requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
5599
5639
  generateClient: resolved.generateClient ?? DEFAULT_SUPACLOUD_CONFIG.generateClient,
5600
5640
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
5601
5641
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
@@ -5623,6 +5663,7 @@ function compileOptionsFromConfig(config, cwd = process.cwd()) {
5623
5663
  outDir: resolved.outDir,
5624
5664
  include: resolved.include,
5625
5665
  strict: resolved.strict,
5666
+ requireRouteContracts: resolved.requireRouteContracts,
5626
5667
  generateClient: resolved.generateClient,
5627
5668
  generatePermissions: resolved.generatePermissions,
5628
5669
  moduleBoundaryPreset: resolved.moduleBoundaryPreset,
@@ -5660,6 +5701,7 @@ export {
5660
5701
  generateFeatureSource,
5661
5702
  getModuleBoundaryPreset,
5662
5703
  getModuleBoundaryProfile,
5704
+ inspectRouteContracts,
5663
5705
  loadSupacloudConfig,
5664
5706
  renderApplication,
5665
5707
  resolveModuleBoundaries,
@@ -5668,5 +5710,6 @@ export {
5668
5710
  scanProductionSource,
5669
5711
  validateFeatureSpec,
5670
5712
  validateGraph,
5713
+ validateRouteContracts,
5671
5714
  watchProject
5672
5715
  };
@@ -0,0 +1,12 @@
1
+ import type { ApplicationGraph, Diagnostic } from "./types";
2
+ /** Declaration coverage only; runtime decoding and database behavior require separate tests. */
3
+ export declare function inspectRouteContracts(graph: ApplicationGraph): {
4
+ module: string;
5
+ controller: string;
6
+ handler: string;
7
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
8
+ path: string;
9
+ file: string;
10
+ missing: ("query" | "body" | "params" | "response")[];
11
+ }[];
12
+ export declare function validateRouteContracts(graph: ApplicationGraph): Diagnostic[];
package/dist/types.d.ts CHANGED
@@ -290,6 +290,8 @@ export interface ApplicationGraph {
290
290
  };
291
291
  }
292
292
  export interface CompileOptions {
293
+ /** Require schemas for bound route inputs and responses. Does not prove runtime validation. */
294
+ requireRouteContracts?: boolean;
293
295
  /** Project root directory (containing tsconfig). */
294
296
  rootDir: string;
295
297
  /** Glob patterns, defaults to ['**\/*.module.ts', '**\/*.ts']. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",