@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 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(deps)` 按 imports 拓扑序返回模块描述:`{ name, createServices, createRequestScope?, createJobScope?, controllers }`。
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
- | `command-missing-permission` | warn(strict 时 error) | `@Command` 未声明 permission |
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: stringLiteralProp(meta, "transaction"),
204
+ transaction: commandModeProp(meta, "transaction") ?? "none",
205
205
  audit: stringLiteralProp(meta, "audit"),
206
- idempotency: stringLiteralProp(meta, "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(deps: Record<string, unknown>): CompiledModule[] {",
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 `services.${camelName(token)}`;
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
- warn2("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
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?: string;
59
+ transaction: "required" | "none";
58
60
  audit?: string;
59
- idempotency?: string;
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.1.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",