@supacloud/compiler 0.1.0 → 0.3.1
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 +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +110 -7
- package/dist/types.d.ts +16 -2
- package/dist/validate.d.ts +5 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ interface ApplicationGraph {
|
|
|
47
47
|
`<outDir>/application.ts`(头注释 `// GENERATED BY @supacloud/compiler — do not edit`):
|
|
48
48
|
|
|
49
49
|
- 文件顶部本地声明 `CompiledRoute` / `CompiledController` / `CompiledModule` 接口,不 import 任何外部包。
|
|
50
|
-
- `createCompiledModules(
|
|
50
|
+
- `createCompiledModules()` 按 imports 拓扑序返回模块描述:`{ name, createServices, createRequestScope?, createJobScope?, controllers, commands }`。平台依赖统一由 `createApplication({ deps })` 传入生成工厂。
|
|
51
51
|
- 每个模块一个 `create<Name>Services(deps, imported)`:实例化 application 级 provider;dep 解析顺序为本模块 services > imports 模块导出的 services(`imported.<module>.<key>`)> 平台注入(`deps.<camelName>`)。
|
|
52
52
|
- 含 request 级 provider/controller 的模块额外生成 `create<Name>RequestScope(services, ctx)`:依赖 `REQUEST_CONTEXT`(或 token name `supacloud.request-context`)的参数传 `ctx`,其余经 `services` 解析(运行期负责把 imports 模块导出的 application 服务合并进 `services`);job 级同理生成 `create<Name>JobScope`。
|
|
53
53
|
- services 对象的 key 为 token 名的 camelCase:`CaseService → caseService`、`CASE_REPOSITORY → caseRepository`、`LOGGER → logger`。
|
|
@@ -64,7 +64,11 @@ interface ApplicationGraph {
|
|
|
64
64
|
| `module-boundary` | error | 依赖的 token 由未 import 的模块提供 |
|
|
65
65
|
| `unresolved-token` | error | 依赖的 token 无法解析且不属于平台注入 |
|
|
66
66
|
| `duplicate-token` | error | 同一 token 在同模块重复注册 |
|
|
67
|
-
| `
|
|
67
|
+
| `duplicate-module` | error | 应用中存在重复模块名 |
|
|
68
|
+
| `duplicate-command` | error | 应用中存在重复业务 command 名 |
|
|
69
|
+
| `duplicate-route` | error | 规范化后 HTTP method + path 冲突 |
|
|
70
|
+
| `route-command-unresolved` | error | 路由绑定了本模块未声明的 command 类 |
|
|
71
|
+
| `command-missing-permission` | error | `@Command` 未声明 permission |
|
|
68
72
|
| `missing-deps` | warn(strict 时 error) | 构造/工厂依赖无法静态解析 |
|
|
69
73
|
|
|
70
74
|
依赖的 token 全图都无 provider 时不报错,记入 `externalTokens`(平台注入)。
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { generateApplication } from "./generate";
|
|
|
4
4
|
export type { GenerateOptions } from "./generate";
|
|
5
5
|
export { validateGraph } from "./validate";
|
|
6
6
|
export { camelName } from "./util";
|
|
7
|
-
export type { ApplicationGraph, CommandNode, CompileOptions, CompileResult, ControllerNode, Diagnostic, ModuleNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, } from "./types";
|
|
7
|
+
export type { ApplicationGraph, CommandNode, CompileOptions, CompileResult, ControllerNode, Diagnostic, ModuleBoundaryRule, ModuleNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -137,6 +137,7 @@ function parseTokenVariable(decl, file) {
|
|
|
137
137
|
function parseModule(candidate, nameByNode, ctx) {
|
|
138
138
|
const { options, className, file, line } = candidate;
|
|
139
139
|
const name = nameByNode.get(candidate.node) ?? className;
|
|
140
|
+
const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
|
|
140
141
|
const imports = arrayProp(options, "imports").map((el) => {
|
|
141
142
|
const decl = Node.isIdentifier(el) ? resolveDeclaration(el)[0] : undefined;
|
|
142
143
|
if (decl) {
|
|
@@ -201,9 +202,9 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
201
202
|
className: cls.getName() ?? "<anonymous>",
|
|
202
203
|
name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
|
|
203
204
|
permission: stringLiteralProp(meta, "permission"),
|
|
204
|
-
transaction:
|
|
205
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
205
206
|
audit: stringLiteralProp(meta, "audit"),
|
|
206
|
-
idempotency:
|
|
207
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none"
|
|
207
208
|
});
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -221,6 +222,7 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
221
222
|
return {
|
|
222
223
|
name,
|
|
223
224
|
className,
|
|
225
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
224
226
|
file: sourcePath(ctx.rootDir, file),
|
|
225
227
|
line,
|
|
226
228
|
imports,
|
|
@@ -231,6 +233,10 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
231
233
|
exports
|
|
232
234
|
};
|
|
233
235
|
}
|
|
236
|
+
function commandModeProp(object, name) {
|
|
237
|
+
const value = stringLiteralProp(object, name);
|
|
238
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
239
|
+
}
|
|
234
240
|
function parseProvider(el, exportsSet, ctx) {
|
|
235
241
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
|
|
236
242
|
const line = el.getStartLineNumber();
|
|
@@ -383,6 +389,11 @@ function parseController(el, ctx) {
|
|
|
383
389
|
schemaImports[schemaExpr.getText()] = importPath;
|
|
384
390
|
}
|
|
385
391
|
}
|
|
392
|
+
const commandExpr = getProp(optionsArg, "command");
|
|
393
|
+
if (commandExpr && Node.isIdentifier(commandExpr)) {
|
|
394
|
+
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
395
|
+
route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
|
|
396
|
+
}
|
|
386
397
|
}
|
|
387
398
|
routes.push(route);
|
|
388
399
|
}
|
|
@@ -625,6 +636,16 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
625
636
|
params?: unknown;
|
|
626
637
|
query?: unknown;
|
|
627
638
|
response?: unknown;
|
|
639
|
+
command?: string;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export interface CompiledCommand {
|
|
643
|
+
className: string;
|
|
644
|
+
name: string;
|
|
645
|
+
permission: string;
|
|
646
|
+
transaction: "required" | "none";
|
|
647
|
+
audit?: string;
|
|
648
|
+
idempotency: "required" | "none";
|
|
628
649
|
}
|
|
629
650
|
|
|
630
651
|
export interface CompiledController {
|
|
@@ -643,12 +664,15 @@ export interface CompiledModule {
|
|
|
643
664
|
createRequestScope?(
|
|
644
665
|
services: Record<string, unknown>,
|
|
645
666
|
ctx: unknown,
|
|
667
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
646
668
|
): Record<string, unknown>;
|
|
647
669
|
createJobScope?(
|
|
648
670
|
services: Record<string, unknown>,
|
|
649
671
|
ctx: unknown,
|
|
672
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
650
673
|
): Record<string, unknown>;
|
|
651
674
|
controllers: CompiledController[];
|
|
675
|
+
commands: CompiledCommand[];
|
|
652
676
|
}`;
|
|
653
677
|
async function generateApplication(graph, options) {
|
|
654
678
|
const modules = topoSortModules(graph.modules);
|
|
@@ -667,7 +691,7 @@ async function generateApplication(graph, options) {
|
|
|
667
691
|
...imports.size > 0 ? [""] : [],
|
|
668
692
|
INTERFACES,
|
|
669
693
|
"",
|
|
670
|
-
"export function createCompiledModules(
|
|
694
|
+
"export function createCompiledModules(): CompiledModule[] {",
|
|
671
695
|
" return [",
|
|
672
696
|
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
673
697
|
" ];",
|
|
@@ -798,6 +822,7 @@ class ModuleGenerator {
|
|
|
798
822
|
lines.push(` createJobScope: create${this.pascal}JobScope,`);
|
|
799
823
|
}
|
|
800
824
|
lines.push(` controllers: ${this.renderControllers()},`);
|
|
825
|
+
lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
|
|
801
826
|
lines.push(`}`);
|
|
802
827
|
return lines.join(`
|
|
803
828
|
`);
|
|
@@ -822,6 +847,8 @@ class ModuleGenerator {
|
|
|
822
847
|
fields.push(`${field}: ${local}`);
|
|
823
848
|
}
|
|
824
849
|
}
|
|
850
|
+
if (route.command)
|
|
851
|
+
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
825
852
|
return `{ ${fields.join(", ")} }`;
|
|
826
853
|
});
|
|
827
854
|
return [
|
|
@@ -855,6 +882,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
855
882
|
`function create${this.pascal}${suffix}(`,
|
|
856
883
|
` services: Record<string, unknown>,`,
|
|
857
884
|
` ctx: unknown,`,
|
|
885
|
+
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
858
886
|
`): Record<string, unknown> {`,
|
|
859
887
|
indent(this.renderFactoryBody(kind), 2),
|
|
860
888
|
`}`
|
|
@@ -953,7 +981,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
953
981
|
continue;
|
|
954
982
|
if (kind === "services")
|
|
955
983
|
return `imported.${importName}.${camelName(token)}`;
|
|
956
|
-
return `
|
|
984
|
+
return `imported.${importName}.${camelName(token)}`;
|
|
957
985
|
}
|
|
958
986
|
if (kind === "services")
|
|
959
987
|
return `deps.${camelName(token)}`;
|
|
@@ -986,7 +1014,9 @@ var SCOPE_LIFETIME_RANK = {
|
|
|
986
1014
|
request: 1,
|
|
987
1015
|
job: 1
|
|
988
1016
|
};
|
|
989
|
-
function validateGraph(graph,
|
|
1017
|
+
function validateGraph(graph, options = false) {
|
|
1018
|
+
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
1019
|
+
const moduleBoundaries = typeof options === "object" ? options.moduleBoundaries : undefined;
|
|
990
1020
|
const diagnostics = [];
|
|
991
1021
|
const globalProviders = new Map;
|
|
992
1022
|
for (const module of graph.modules) {
|
|
@@ -1016,6 +1046,42 @@ function validateGraph(graph, strict = false) {
|
|
|
1016
1046
|
const warn2 = (code, message, file, line) => {
|
|
1017
1047
|
diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
|
|
1018
1048
|
};
|
|
1049
|
+
const modulesByName = new Map;
|
|
1050
|
+
const commandsByName = new Map;
|
|
1051
|
+
const routesByKey = new Map;
|
|
1052
|
+
for (const module of graph.modules) {
|
|
1053
|
+
const previousModule = modulesByName.get(module.name);
|
|
1054
|
+
if (previousModule) {
|
|
1055
|
+
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
1056
|
+
} else {
|
|
1057
|
+
modulesByName.set(module.name, module);
|
|
1058
|
+
}
|
|
1059
|
+
for (const command of module.commands) {
|
|
1060
|
+
const previousName = commandsByName.get(command.name);
|
|
1061
|
+
if (previousName) {
|
|
1062
|
+
error("duplicate-command", `command 名 ${command.name} 重复(首次由模块 ${previousName.module.name} 的 ${previousName.className} 声明)`, module.file, module.line);
|
|
1063
|
+
} else {
|
|
1064
|
+
commandsByName.set(command.name, { module, className: command.className });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
for (const module of graph.modules) {
|
|
1069
|
+
for (const controller of module.controllers) {
|
|
1070
|
+
for (const route of controller.routes) {
|
|
1071
|
+
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1072
|
+
const key = `${route.method} ${fullPath}`;
|
|
1073
|
+
const previous = routesByKey.get(key);
|
|
1074
|
+
if (previous) {
|
|
1075
|
+
error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
|
|
1076
|
+
} else {
|
|
1077
|
+
routesByKey.set(key, { module, controller });
|
|
1078
|
+
}
|
|
1079
|
+
if (route.command && !module.commands.some((command) => command.className === route.command)) {
|
|
1080
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1019
1085
|
for (const module of graph.modules) {
|
|
1020
1086
|
const seen = new Map;
|
|
1021
1087
|
for (const provider of module.providers) {
|
|
@@ -1047,13 +1113,47 @@ function validateGraph(graph, strict = false) {
|
|
|
1047
1113
|
}
|
|
1048
1114
|
for (const command of module.commands) {
|
|
1049
1115
|
if (!command.permission) {
|
|
1050
|
-
|
|
1116
|
+
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (moduleBoundaries && moduleBoundaries.length > 0) {
|
|
1121
|
+
for (const module of graph.modules) {
|
|
1122
|
+
const sourceTags = module.tags ?? [];
|
|
1123
|
+
for (const importName of module.imports) {
|
|
1124
|
+
const targetModule = graph.modules.find((m) => m.name === importName);
|
|
1125
|
+
if (!targetModule)
|
|
1126
|
+
continue;
|
|
1127
|
+
const targetTags = targetModule.tags ?? [];
|
|
1128
|
+
for (const rule of moduleBoundaries) {
|
|
1129
|
+
const matchesSource = rule.sourceTag === "*" || sourceTags.includes(rule.sourceTag);
|
|
1130
|
+
if (!matchesSource)
|
|
1131
|
+
continue;
|
|
1132
|
+
if (rule.bannedDependenciesWithTags) {
|
|
1133
|
+
for (const bannedTag of rule.bannedDependenciesWithTags) {
|
|
1134
|
+
if (targetTags.includes(bannedTag)) {
|
|
1135
|
+
error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 禁止依赖带有标签 '${bannedTag}' 的模块 ${targetModule.name} (tags: [${targetTags.join(", ")}])`, module.file, module.line);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
|
|
1140
|
+
const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
|
|
1141
|
+
if (!hasAllowed && targetTags.length > 0) {
|
|
1142
|
+
error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1051
1146
|
}
|
|
1052
1147
|
}
|
|
1053
1148
|
}
|
|
1054
1149
|
diagnostics.push(...detectCycles(graph, resolveDep));
|
|
1055
1150
|
return diagnostics;
|
|
1056
1151
|
}
|
|
1152
|
+
function joinRoutePaths(prefix, path) {
|
|
1153
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1154
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1155
|
+
return normalized.replace(/:[^/]+/g, ":param");
|
|
1156
|
+
}
|
|
1057
1157
|
function detectCycles(graph, resolveDep) {
|
|
1058
1158
|
const diagnostics = [];
|
|
1059
1159
|
const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
|
|
@@ -1102,7 +1202,10 @@ async function compileProject(options) {
|
|
|
1102
1202
|
const graph = await analyzeProject(options.rootDir, options.include);
|
|
1103
1203
|
const diagnostics = [
|
|
1104
1204
|
...graph.diagnostics ?? [],
|
|
1105
|
-
...validateGraph(graph,
|
|
1205
|
+
...validateGraph(graph, {
|
|
1206
|
+
strict: options.strict,
|
|
1207
|
+
moduleBoundaries: options.moduleBoundaries
|
|
1208
|
+
})
|
|
1106
1209
|
];
|
|
1107
1210
|
if (options.strict) {
|
|
1108
1211
|
for (const diagnostic of diagnostics) {
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface RouteNode {
|
|
|
38
38
|
params?: string;
|
|
39
39
|
query?: string;
|
|
40
40
|
response?: string;
|
|
41
|
+
/** @Command-decorated class explicitly bound by the route. */
|
|
42
|
+
command?: string;
|
|
41
43
|
}
|
|
42
44
|
export interface ControllerNode {
|
|
43
45
|
className: string;
|
|
@@ -54,9 +56,9 @@ export interface CommandNode {
|
|
|
54
56
|
className: string;
|
|
55
57
|
name: string;
|
|
56
58
|
permission?: string;
|
|
57
|
-
transaction
|
|
59
|
+
transaction: "required" | "none";
|
|
58
60
|
audit?: string;
|
|
59
|
-
idempotency
|
|
61
|
+
idempotency: "required" | "none";
|
|
60
62
|
}
|
|
61
63
|
export interface QueryNode {
|
|
62
64
|
className: string;
|
|
@@ -66,6 +68,8 @@ export interface ModuleNode {
|
|
|
66
68
|
/** @Module({ name }) 或 defineModule 的 name。 */
|
|
67
69
|
name: string;
|
|
68
70
|
className: string;
|
|
71
|
+
/** 模块标签(如 ['scope:case', 'type:feature']),用于架构边界治理。 */
|
|
72
|
+
tags?: string[];
|
|
69
73
|
file: string;
|
|
70
74
|
line: number;
|
|
71
75
|
/** 被 import 模块的 name。 */
|
|
@@ -102,6 +106,16 @@ export interface CompileOptions {
|
|
|
102
106
|
outDir: string;
|
|
103
107
|
/** warn 级诊断升级为 error。 */
|
|
104
108
|
strict?: boolean;
|
|
109
|
+
/** 模块边界与架构治理规则(受 Nx enforce-module-boundaries 启发)。 */
|
|
110
|
+
moduleBoundaries?: ModuleBoundaryRule[];
|
|
111
|
+
}
|
|
112
|
+
export interface ModuleBoundaryRule {
|
|
113
|
+
/** 源模块标签模式或标签(如 'type:ui', 'scope:case', '*')。 */
|
|
114
|
+
sourceTag: string;
|
|
115
|
+
/** 源模块仅允许依赖具有这些标签的模块。 */
|
|
116
|
+
onlyDependOnLibsWithTags?: string[];
|
|
117
|
+
/** 源模块禁止依赖具有这些标签的模块。 */
|
|
118
|
+
bannedDependenciesWithTags?: string[];
|
|
105
119
|
}
|
|
106
120
|
export interface CompileResult {
|
|
107
121
|
diagnostics: Diagnostic[];
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { ApplicationGraph, Diagnostic } from "./types";
|
|
1
|
+
import type { ApplicationGraph, Diagnostic, ModuleBoundaryRule } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* 校验 ApplicationGraph,产出诊断列表。
|
|
4
4
|
* strict 时 warn 级诊断升级为 error。
|
|
5
5
|
*/
|
|
6
|
-
export declare function validateGraph(graph: ApplicationGraph,
|
|
6
|
+
export declare function validateGraph(graph: ApplicationGraph, options?: boolean | {
|
|
7
|
+
strict?: boolean;
|
|
8
|
+
moduleBoundaries?: ModuleBoundaryRule[];
|
|
9
|
+
}): Diagnostic[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/compiler",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"directory": "packages/compiler"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"ts-morph": "^
|
|
44
|
+
"ts-morph": "^28.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/bun": "^1.4.0",
|