@supacloud/compiler 0.9.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 +23 -0
- package/dist/cli.js +53 -5
- package/dist/config.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +55 -5
- package/dist/route-contracts.d.ts +12 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,11 @@ bun add @supacloud/compiler
|
|
|
12
12
|
|
|
13
13
|
## 零配置项目
|
|
14
14
|
|
|
15
|
+
需要完整运行入口时,使用 `supacloud-cli app init --root ./orders --name orders`。
|
|
16
|
+
模板将本包放在 `devDependencies`,预置状态规格、治理能力、类型检查和本地测试。
|
|
17
|
+
编译产物的 HTTP method / scope 保留字面量联合类型,可直接传给 Elysia 适配器;
|
|
18
|
+
跨运行时依赖字典使用构造器/工厂参数类型连接,局部依赖保留类型推断和错误检查。
|
|
19
|
+
|
|
15
20
|
在项目根目录执行:
|
|
16
21
|
|
|
17
22
|
```bash
|
|
@@ -252,6 +257,24 @@ bun test
|
|
|
252
257
|
bun run build
|
|
253
258
|
```
|
|
254
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
|
+
|
|
255
278
|
## License
|
|
256
279
|
|
|
257
280
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -2264,7 +2264,7 @@ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
|
2264
2264
|
import { join as join2 } from "node:path";
|
|
2265
2265
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
2266
2266
|
var INTERFACES = `export interface CompiledRoute {
|
|
2267
|
-
method:
|
|
2267
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
2268
2268
|
path: string;
|
|
2269
2269
|
handler: string;
|
|
2270
2270
|
body?: unknown;
|
|
@@ -2335,7 +2335,7 @@ export type CompiledAspect = (
|
|
|
2335
2335
|
export interface CompiledController {
|
|
2336
2336
|
path: string;
|
|
2337
2337
|
serviceKey: string;
|
|
2338
|
-
scope:
|
|
2338
|
+
scope: "application" | "request" | "job";
|
|
2339
2339
|
routes: CompiledRoute[];
|
|
2340
2340
|
}
|
|
2341
2341
|
|
|
@@ -2949,7 +2949,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2949
2949
|
switch (provider.kind) {
|
|
2950
2950
|
case "class": {
|
|
2951
2951
|
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
|
|
2952
|
-
const args = provider.deps.map((dep) => this.
|
|
2952
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `ConstructorParameters<typeof ${useClass}>[${index}]`)).join(", ");
|
|
2953
2953
|
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
2954
2954
|
return {
|
|
2955
2955
|
constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
|
|
@@ -2970,7 +2970,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2970
2970
|
return { constLine, key, expr: local2 };
|
|
2971
2971
|
}
|
|
2972
2972
|
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
|
|
2973
|
-
const args = provider.deps.map((dep) => this.
|
|
2973
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `Parameters<typeof ${factory}>[${index}]`)).join(", ");
|
|
2974
2974
|
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
2975
2975
|
return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
|
|
2976
2976
|
}
|
|
@@ -2984,7 +2984,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2984
2984
|
}
|
|
2985
2985
|
emitController(controller, kind) {
|
|
2986
2986
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
2987
|
-
const args = controller.deps.map((dep) => this.
|
|
2987
|
+
const args = controller.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(controller, dep), `ConstructorParameters<typeof ${className}>[${index}]`)).join(", ");
|
|
2988
2988
|
const key = camelName(controller.className);
|
|
2989
2989
|
const local = this.localVar(controller.className, kind);
|
|
2990
2990
|
return {
|
|
@@ -3042,6 +3042,13 @@ ${indent(item, 2)}`).join(",")}
|
|
|
3042
3042
|
host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
|
|
3043
3043
|
};
|
|
3044
3044
|
}
|
|
3045
|
+
typedDepExpr(token, kind, options, type) {
|
|
3046
|
+
const expression = this.depExpr(token, kind, options);
|
|
3047
|
+
const localProvider = this.module.providers.find((provider) => this.locals[kind].get(provider.token) === expression);
|
|
3048
|
+
if (expression === "undefined" || localProvider && !(localProvider.kind === "factory" && !localProvider.useFactoryName))
|
|
3049
|
+
return expression;
|
|
3050
|
+
return `${expression} as ${type}`;
|
|
3051
|
+
}
|
|
3045
3052
|
depExpr(token, kind, options = {}) {
|
|
3046
3053
|
const isOptional = options.optional ?? false;
|
|
3047
3054
|
const isSelf = options.self ?? false;
|
|
@@ -4675,6 +4682,39 @@ function isAnyKeyword(node) {
|
|
|
4675
4682
|
return node.kind === ts4.SyntaxKind.AnyKeyword;
|
|
4676
4683
|
}
|
|
4677
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
|
+
|
|
4678
4718
|
// src/compile.ts
|
|
4679
4719
|
async function compileProject(options) {
|
|
4680
4720
|
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
|
|
@@ -4697,6 +4737,8 @@ async function compileProject(options) {
|
|
|
4697
4737
|
}
|
|
4698
4738
|
}
|
|
4699
4739
|
const typeSafety = resolveTypeSafety(options);
|
|
4740
|
+
if (options.requireRouteContracts)
|
|
4741
|
+
diagnostics.push(...validateRouteContracts(graph));
|
|
4700
4742
|
const rendered = renderApplication(graph, {
|
|
4701
4743
|
rootDir: options.rootDir,
|
|
4702
4744
|
outDir: options.outDir,
|
|
@@ -4760,6 +4802,8 @@ async function checkProject(options) {
|
|
|
4760
4802
|
}
|
|
4761
4803
|
}
|
|
4762
4804
|
const typeSafety = resolveTypeSafety(options);
|
|
4805
|
+
if (options.requireRouteContracts)
|
|
4806
|
+
diagnostics.push(...validateRouteContracts(graph));
|
|
4763
4807
|
const rendered = renderApplication(graph, {
|
|
4764
4808
|
rootDir: options.rootDir,
|
|
4765
4809
|
outDir: options.outDir,
|
|
@@ -5137,6 +5181,7 @@ function optionsKeyOf(options) {
|
|
|
5137
5181
|
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
5138
5182
|
commandCapabilities: options.commandCapabilities,
|
|
5139
5183
|
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
5184
|
+
requireRouteContracts: options.requireRouteContracts,
|
|
5140
5185
|
detectOrphanModules: options.detectOrphanModules,
|
|
5141
5186
|
generateClient: options.generateClient,
|
|
5142
5187
|
generatePermissions: options.generatePermissions,
|
|
@@ -5392,6 +5437,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
5392
5437
|
outDir: "generated",
|
|
5393
5438
|
include: ["**/*.module.ts", "**/*.ts"],
|
|
5394
5439
|
strict: true,
|
|
5440
|
+
requireRouteContracts: false,
|
|
5395
5441
|
generateClient: true,
|
|
5396
5442
|
generatePermissions: true,
|
|
5397
5443
|
treeShakeUnusedProviders: true,
|
|
@@ -5411,6 +5457,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
5411
5457
|
outDir: resolve5(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
5412
5458
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
5413
5459
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
5460
|
+
requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
|
|
5414
5461
|
generateClient: resolved.generateClient ?? DEFAULT_SUPACLOUD_CONFIG.generateClient,
|
|
5415
5462
|
generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
|
|
5416
5463
|
moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
|
|
@@ -5438,6 +5485,7 @@ function compileOptionsFromConfig(config, cwd = process.cwd()) {
|
|
|
5438
5485
|
outDir: resolved.outDir,
|
|
5439
5486
|
include: resolved.include,
|
|
5440
5487
|
strict: resolved.strict,
|
|
5488
|
+
requireRouteContracts: resolved.requireRouteContracts,
|
|
5441
5489
|
generateClient: resolved.generateClient,
|
|
5442
5490
|
generatePermissions: resolved.generatePermissions,
|
|
5443
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
|
@@ -2545,7 +2545,7 @@ import { access, mkdir, rename as rename2, unlink as unlink2, writeFile as write
|
|
|
2545
2545
|
import { join as join2 } from "node:path";
|
|
2546
2546
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
2547
2547
|
var INTERFACES = `export interface CompiledRoute {
|
|
2548
|
-
method:
|
|
2548
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
2549
2549
|
path: string;
|
|
2550
2550
|
handler: string;
|
|
2551
2551
|
body?: unknown;
|
|
@@ -2616,7 +2616,7 @@ export type CompiledAspect = (
|
|
|
2616
2616
|
export interface CompiledController {
|
|
2617
2617
|
path: string;
|
|
2618
2618
|
serviceKey: string;
|
|
2619
|
-
scope:
|
|
2619
|
+
scope: "application" | "request" | "job";
|
|
2620
2620
|
routes: CompiledRoute[];
|
|
2621
2621
|
}
|
|
2622
2622
|
|
|
@@ -3230,7 +3230,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
3230
3230
|
switch (provider.kind) {
|
|
3231
3231
|
case "class": {
|
|
3232
3232
|
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
|
|
3233
|
-
const args = provider.deps.map((dep) => this.
|
|
3233
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `ConstructorParameters<typeof ${useClass}>[${index}]`)).join(", ");
|
|
3234
3234
|
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
3235
3235
|
return {
|
|
3236
3236
|
constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
|
|
@@ -3251,7 +3251,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
3251
3251
|
return { constLine, key, expr: local2 };
|
|
3252
3252
|
}
|
|
3253
3253
|
const factory2 = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
|
|
3254
|
-
const args = provider.deps.map((dep) => this.
|
|
3254
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `Parameters<typeof ${factory2}>[${index}]`)).join(", ");
|
|
3255
3255
|
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
3256
3256
|
return { constLine: `const ${local} = ${factory2}(${args});`, key, expr: local };
|
|
3257
3257
|
}
|
|
@@ -3265,7 +3265,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
3265
3265
|
}
|
|
3266
3266
|
emitController(controller, kind) {
|
|
3267
3267
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
3268
|
-
const args = controller.deps.map((dep) => this.
|
|
3268
|
+
const args = controller.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(controller, dep), `ConstructorParameters<typeof ${className}>[${index}]`)).join(", ");
|
|
3269
3269
|
const key = camelName(controller.className);
|
|
3270
3270
|
const local = this.localVar(controller.className, kind);
|
|
3271
3271
|
return {
|
|
@@ -3323,6 +3323,13 @@ ${indent(item, 2)}`).join(",")}
|
|
|
3323
3323
|
host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
|
|
3324
3324
|
};
|
|
3325
3325
|
}
|
|
3326
|
+
typedDepExpr(token, kind, options, type) {
|
|
3327
|
+
const expression = this.depExpr(token, kind, options);
|
|
3328
|
+
const localProvider = this.module.providers.find((provider) => this.locals[kind].get(provider.token) === expression);
|
|
3329
|
+
if (expression === "undefined" || localProvider && !(localProvider.kind === "factory" && !localProvider.useFactoryName))
|
|
3330
|
+
return expression;
|
|
3331
|
+
return `${expression} as ${type}`;
|
|
3332
|
+
}
|
|
3326
3333
|
depExpr(token, kind, options = {}) {
|
|
3327
3334
|
const isOptional = options.optional ?? false;
|
|
3328
3335
|
const isSelf = options.self ?? false;
|
|
@@ -4856,6 +4863,39 @@ function isAnyKeyword(node) {
|
|
|
4856
4863
|
return node.kind === ts5.SyntaxKind.AnyKeyword;
|
|
4857
4864
|
}
|
|
4858
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
|
+
|
|
4859
4899
|
// src/compile.ts
|
|
4860
4900
|
async function compileProject(options) {
|
|
4861
4901
|
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
|
|
@@ -4878,6 +4918,8 @@ async function compileProject(options) {
|
|
|
4878
4918
|
}
|
|
4879
4919
|
}
|
|
4880
4920
|
const typeSafety = resolveTypeSafety(options);
|
|
4921
|
+
if (options.requireRouteContracts)
|
|
4922
|
+
diagnostics.push(...validateRouteContracts(graph));
|
|
4881
4923
|
const rendered = renderApplication(graph, {
|
|
4882
4924
|
rootDir: options.rootDir,
|
|
4883
4925
|
outDir: options.outDir,
|
|
@@ -4941,6 +4983,8 @@ async function checkProject(options) {
|
|
|
4941
4983
|
}
|
|
4942
4984
|
}
|
|
4943
4985
|
const typeSafety = resolveTypeSafety(options);
|
|
4986
|
+
if (options.requireRouteContracts)
|
|
4987
|
+
diagnostics.push(...validateRouteContracts(graph));
|
|
4944
4988
|
const rendered = renderApplication(graph, {
|
|
4945
4989
|
rootDir: options.rootDir,
|
|
4946
4990
|
outDir: options.outDir,
|
|
@@ -5125,6 +5169,7 @@ function optionsKeyOf(options) {
|
|
|
5125
5169
|
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
5126
5170
|
commandCapabilities: options.commandCapabilities,
|
|
5127
5171
|
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
5172
|
+
requireRouteContracts: options.requireRouteContracts,
|
|
5128
5173
|
detectOrphanModules: options.detectOrphanModules,
|
|
5129
5174
|
generateClient: options.generateClient,
|
|
5130
5175
|
generatePermissions: options.generatePermissions,
|
|
@@ -5570,6 +5615,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
5570
5615
|
outDir: "generated",
|
|
5571
5616
|
include: ["**/*.module.ts", "**/*.ts"],
|
|
5572
5617
|
strict: true,
|
|
5618
|
+
requireRouteContracts: false,
|
|
5573
5619
|
generateClient: true,
|
|
5574
5620
|
generatePermissions: true,
|
|
5575
5621
|
treeShakeUnusedProviders: true,
|
|
@@ -5589,6 +5635,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
5589
5635
|
outDir: resolve6(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
5590
5636
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
5591
5637
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
5638
|
+
requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
|
|
5592
5639
|
generateClient: resolved.generateClient ?? DEFAULT_SUPACLOUD_CONFIG.generateClient,
|
|
5593
5640
|
generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
|
|
5594
5641
|
moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
|
|
@@ -5616,6 +5663,7 @@ function compileOptionsFromConfig(config, cwd = process.cwd()) {
|
|
|
5616
5663
|
outDir: resolved.outDir,
|
|
5617
5664
|
include: resolved.include,
|
|
5618
5665
|
strict: resolved.strict,
|
|
5666
|
+
requireRouteContracts: resolved.requireRouteContracts,
|
|
5619
5667
|
generateClient: resolved.generateClient,
|
|
5620
5668
|
generatePermissions: resolved.generatePermissions,
|
|
5621
5669
|
moduleBoundaryPreset: resolved.moduleBoundaryPreset,
|
|
@@ -5653,6 +5701,7 @@ export {
|
|
|
5653
5701
|
generateFeatureSource,
|
|
5654
5702
|
getModuleBoundaryPreset,
|
|
5655
5703
|
getModuleBoundaryProfile,
|
|
5704
|
+
inspectRouteContracts,
|
|
5656
5705
|
loadSupacloudConfig,
|
|
5657
5706
|
renderApplication,
|
|
5658
5707
|
resolveModuleBoundaries,
|
|
@@ -5661,5 +5710,6 @@ export {
|
|
|
5661
5710
|
scanProductionSource,
|
|
5662
5711
|
validateFeatureSpec,
|
|
5663
5712
|
validateGraph,
|
|
5713
|
+
validateRouteContracts,
|
|
5664
5714
|
watchProject
|
|
5665
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",
|