@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 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.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: stringLiteralProp(meta, "transaction"),
205
+ transaction: commandModeProp(meta, "transaction") ?? "none",
205
206
  audit: stringLiteralProp(meta, "audit"),
206
- idempotency: stringLiteralProp(meta, "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(deps: Record<string, unknown>): CompiledModule[] {",
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 `services.${camelName(token)}`;
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, strict = false) {
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
- warn2("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
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, options.strict)
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?: 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;
@@ -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[];
@@ -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, strict?: boolean): Diagnostic[];
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.0",
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": "^26.0.0"
44
+ "ts-morph": "^28.0.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/bun": "^1.4.0",