@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 +18 -0
- package/dist/cli.js +41 -0
- package/dist/config.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +43 -0
- package/dist/route-contracts.d.ts +12 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
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.
|
|
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",
|