@supacloud/compiler 0.1.0 → 0.2.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 +6 -2
- package/dist/index.js +72 -5
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
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.js
CHANGED
|
@@ -201,9 +201,9 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
201
201
|
className: cls.getName() ?? "<anonymous>",
|
|
202
202
|
name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
|
|
203
203
|
permission: stringLiteralProp(meta, "permission"),
|
|
204
|
-
transaction:
|
|
204
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
205
205
|
audit: stringLiteralProp(meta, "audit"),
|
|
206
|
-
idempotency:
|
|
206
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none"
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
}
|
|
@@ -231,6 +231,10 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
231
231
|
exports
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
+
function commandModeProp(object, name) {
|
|
235
|
+
const value = stringLiteralProp(object, name);
|
|
236
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
237
|
+
}
|
|
234
238
|
function parseProvider(el, exportsSet, ctx) {
|
|
235
239
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
|
|
236
240
|
const line = el.getStartLineNumber();
|
|
@@ -383,6 +387,11 @@ function parseController(el, ctx) {
|
|
|
383
387
|
schemaImports[schemaExpr.getText()] = importPath;
|
|
384
388
|
}
|
|
385
389
|
}
|
|
390
|
+
const commandExpr = getProp(optionsArg, "command");
|
|
391
|
+
if (commandExpr && Node.isIdentifier(commandExpr)) {
|
|
392
|
+
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
393
|
+
route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
|
|
394
|
+
}
|
|
386
395
|
}
|
|
387
396
|
routes.push(route);
|
|
388
397
|
}
|
|
@@ -625,6 +634,16 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
625
634
|
params?: unknown;
|
|
626
635
|
query?: unknown;
|
|
627
636
|
response?: unknown;
|
|
637
|
+
command?: string;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export interface CompiledCommand {
|
|
641
|
+
className: string;
|
|
642
|
+
name: string;
|
|
643
|
+
permission: string;
|
|
644
|
+
transaction: "required" | "none";
|
|
645
|
+
audit?: string;
|
|
646
|
+
idempotency: "required" | "none";
|
|
628
647
|
}
|
|
629
648
|
|
|
630
649
|
export interface CompiledController {
|
|
@@ -643,12 +662,15 @@ export interface CompiledModule {
|
|
|
643
662
|
createRequestScope?(
|
|
644
663
|
services: Record<string, unknown>,
|
|
645
664
|
ctx: unknown,
|
|
665
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
646
666
|
): Record<string, unknown>;
|
|
647
667
|
createJobScope?(
|
|
648
668
|
services: Record<string, unknown>,
|
|
649
669
|
ctx: unknown,
|
|
670
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
650
671
|
): Record<string, unknown>;
|
|
651
672
|
controllers: CompiledController[];
|
|
673
|
+
commands: CompiledCommand[];
|
|
652
674
|
}`;
|
|
653
675
|
async function generateApplication(graph, options) {
|
|
654
676
|
const modules = topoSortModules(graph.modules);
|
|
@@ -667,7 +689,7 @@ async function generateApplication(graph, options) {
|
|
|
667
689
|
...imports.size > 0 ? [""] : [],
|
|
668
690
|
INTERFACES,
|
|
669
691
|
"",
|
|
670
|
-
"export function createCompiledModules(
|
|
692
|
+
"export function createCompiledModules(): CompiledModule[] {",
|
|
671
693
|
" return [",
|
|
672
694
|
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
673
695
|
" ];",
|
|
@@ -798,6 +820,7 @@ class ModuleGenerator {
|
|
|
798
820
|
lines.push(` createJobScope: create${this.pascal}JobScope,`);
|
|
799
821
|
}
|
|
800
822
|
lines.push(` controllers: ${this.renderControllers()},`);
|
|
823
|
+
lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
|
|
801
824
|
lines.push(`}`);
|
|
802
825
|
return lines.join(`
|
|
803
826
|
`);
|
|
@@ -822,6 +845,8 @@ class ModuleGenerator {
|
|
|
822
845
|
fields.push(`${field}: ${local}`);
|
|
823
846
|
}
|
|
824
847
|
}
|
|
848
|
+
if (route.command)
|
|
849
|
+
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
825
850
|
return `{ ${fields.join(", ")} }`;
|
|
826
851
|
});
|
|
827
852
|
return [
|
|
@@ -855,6 +880,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
855
880
|
`function create${this.pascal}${suffix}(`,
|
|
856
881
|
` services: Record<string, unknown>,`,
|
|
857
882
|
` ctx: unknown,`,
|
|
883
|
+
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
858
884
|
`): Record<string, unknown> {`,
|
|
859
885
|
indent(this.renderFactoryBody(kind), 2),
|
|
860
886
|
`}`
|
|
@@ -953,7 +979,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
953
979
|
continue;
|
|
954
980
|
if (kind === "services")
|
|
955
981
|
return `imported.${importName}.${camelName(token)}`;
|
|
956
|
-
return `
|
|
982
|
+
return `imported.${importName}.${camelName(token)}`;
|
|
957
983
|
}
|
|
958
984
|
if (kind === "services")
|
|
959
985
|
return `deps.${camelName(token)}`;
|
|
@@ -1016,6 +1042,42 @@ function validateGraph(graph, strict = false) {
|
|
|
1016
1042
|
const warn2 = (code, message, file, line) => {
|
|
1017
1043
|
diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
|
|
1018
1044
|
};
|
|
1045
|
+
const modulesByName = new Map;
|
|
1046
|
+
const commandsByName = new Map;
|
|
1047
|
+
const routesByKey = new Map;
|
|
1048
|
+
for (const module of graph.modules) {
|
|
1049
|
+
const previousModule = modulesByName.get(module.name);
|
|
1050
|
+
if (previousModule) {
|
|
1051
|
+
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
1052
|
+
} else {
|
|
1053
|
+
modulesByName.set(module.name, module);
|
|
1054
|
+
}
|
|
1055
|
+
for (const command of module.commands) {
|
|
1056
|
+
const previousName = commandsByName.get(command.name);
|
|
1057
|
+
if (previousName) {
|
|
1058
|
+
error("duplicate-command", `command 名 ${command.name} 重复(首次由模块 ${previousName.module.name} 的 ${previousName.className} 声明)`, module.file, module.line);
|
|
1059
|
+
} else {
|
|
1060
|
+
commandsByName.set(command.name, { module, className: command.className });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
for (const module of graph.modules) {
|
|
1065
|
+
for (const controller of module.controllers) {
|
|
1066
|
+
for (const route of controller.routes) {
|
|
1067
|
+
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1068
|
+
const key = `${route.method} ${fullPath}`;
|
|
1069
|
+
const previous = routesByKey.get(key);
|
|
1070
|
+
if (previous) {
|
|
1071
|
+
error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
|
|
1072
|
+
} else {
|
|
1073
|
+
routesByKey.set(key, { module, controller });
|
|
1074
|
+
}
|
|
1075
|
+
if (route.command && !module.commands.some((command) => command.className === route.command)) {
|
|
1076
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1019
1081
|
for (const module of graph.modules) {
|
|
1020
1082
|
const seen = new Map;
|
|
1021
1083
|
for (const provider of module.providers) {
|
|
@@ -1047,13 +1109,18 @@ function validateGraph(graph, strict = false) {
|
|
|
1047
1109
|
}
|
|
1048
1110
|
for (const command of module.commands) {
|
|
1049
1111
|
if (!command.permission) {
|
|
1050
|
-
|
|
1112
|
+
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
|
|
1051
1113
|
}
|
|
1052
1114
|
}
|
|
1053
1115
|
}
|
|
1054
1116
|
diagnostics.push(...detectCycles(graph, resolveDep));
|
|
1055
1117
|
return diagnostics;
|
|
1056
1118
|
}
|
|
1119
|
+
function joinRoutePaths(prefix, path) {
|
|
1120
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1121
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1122
|
+
return normalized.replace(/:[^/]+/g, ":param");
|
|
1123
|
+
}
|
|
1057
1124
|
function detectCycles(graph, resolveDep) {
|
|
1058
1125
|
const diagnostics = [];
|
|
1059
1126
|
const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|