@supacloud/compiler 0.2.0 → 0.4.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/dist/compile.d.ts CHANGED
@@ -1,6 +1,11 @@
1
- import type { CompileOptions, CompileResult } from "./types";
1
+ import type { CheckProjectResult, CompileOptions, CompileResult } from "./types";
2
2
  /**
3
3
  * 完整编译流程:AST 分析 → 校验 → 生成静态工厂代码与 manifest。
4
4
  * 即使存在 error 级诊断也会照常写出文件,由调用方根据 diagnostics 决定是否采用。
5
5
  */
6
6
  export declare function compileProject(options: CompileOptions): Promise<CompileResult>;
7
+ /**
8
+ * Check generated artifacts without writing files to disk.
9
+ * Analyze the AST, run governance checks, and compare application.ts and app.manifest.json.
10
+ */
11
+ export declare function checkProject(options: CompileOptions): Promise<CheckProjectResult>;
@@ -3,8 +3,14 @@ export interface GenerateOptions {
3
3
  rootDir: string;
4
4
  outDir: string;
5
5
  }
6
+ export interface RenderedArtifacts {
7
+ applicationCode: string;
8
+ manifestJson: string;
9
+ }
10
+ /** Render application.ts and app.manifest.json content without file I/O. */
11
+ export declare function renderApplication(graph: ApplicationGraph, options: GenerateOptions): RenderedArtifacts;
6
12
  /**
7
- * 生成静态工厂代码(application.ts)与 app.manifest.json,返回写入的绝对路径。
8
- * 生成代码只 import 业务类/schema,不 import @supacloud/app,运行期无反射、无容器。
13
+ * Generate static factory code (application.ts) and app.manifest.json.
14
+ * Generated code imports only application classes and schemas, with no runtime reflection or container lookup.
9
15
  */
10
16
  export declare function generateApplication(graph: ApplicationGraph, options: GenerateOptions): Promise<string[]>;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { analyzeProject } from "./analyze";
2
- export { compileProject } from "./compile";
3
- export { generateApplication } from "./generate";
4
- export type { GenerateOptions } from "./generate";
2
+ export { checkProject, compileProject } from "./compile";
3
+ export { generateApplication, renderApplication } from "./generate";
4
+ export type { GenerateOptions, RenderedArtifacts } 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 { ANGULAR_ENTERPRISE_RULES, CLEAN_ARCHITECTURE_RULES, MODULAR_MONOLITH_RULES, MODULE_BOUNDARY_PROFILES, getModuleBoundaryPreset, getModuleBoundaryProfile, resolveModuleBoundaries, } from "./profiles";
8
+ export type { ApplicationGraph, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, ControllerNode, Diagnostic, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, ValidateOptions, } from "./types";
package/dist/index.js CHANGED
@@ -12,7 +12,9 @@ var ROUTE_DECORATORS = {
12
12
  Post: "POST",
13
13
  Put: "PUT",
14
14
  Patch: "PATCH",
15
- Delete: "DELETE"
15
+ Delete: "DELETE",
16
+ Head: "HEAD",
17
+ Options: "OPTIONS"
16
18
  };
17
19
  var SCOPES = ["application", "request", "job"];
18
20
  async function analyzeProject(rootDir, include) {
@@ -137,6 +139,7 @@ function parseTokenVariable(decl, file) {
137
139
  function parseModule(candidate, nameByNode, ctx) {
138
140
  const { options, className, file, line } = candidate;
139
141
  const name = nameByNode.get(candidate.node) ?? className;
142
+ const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
140
143
  const imports = arrayProp(options, "imports").map((el) => {
141
144
  const decl = Node.isIdentifier(el) ? resolveDeclaration(el)[0] : undefined;
142
145
  if (decl) {
@@ -221,6 +224,7 @@ function parseModule(candidate, nameByNode, ctx) {
221
224
  return {
222
225
  name,
223
226
  className,
227
+ tags: tags.length > 0 ? tags : undefined,
224
228
  file: sourcePath(ctx.rootDir, file),
225
229
  line,
226
230
  imports,
@@ -672,7 +676,7 @@ export interface CompiledModule {
672
676
  controllers: CompiledController[];
673
677
  commands: CompiledCommand[];
674
678
  }`;
675
- async function generateApplication(graph, options) {
679
+ function renderApplication(graph, options) {
676
680
  const modules = topoSortModules(graph.modules);
677
681
  const imports = new ImportManager;
678
682
  const factorySections = [];
@@ -704,12 +708,19 @@ async function generateApplication(graph, options) {
704
708
  modules: graph.modules,
705
709
  externalTokens: graph.externalTokens
706
710
  };
711
+ return {
712
+ applicationCode: code,
713
+ manifestJson: JSON.stringify(manifest, null, 2) + `
714
+ `
715
+ };
716
+ }
717
+ async function generateApplication(graph, options) {
718
+ const rendered = renderApplication(graph, options);
707
719
  await mkdir(options.outDir, { recursive: true });
708
720
  const applicationPath = join2(options.outDir, "application.ts");
709
721
  const manifestPath = join2(options.outDir, "app.manifest.json");
710
- await writeFile(applicationPath, code, "utf8");
711
- await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + `
712
- `, "utf8");
722
+ await writeFile(applicationPath, rendered.applicationCode, "utf8");
723
+ await writeFile(manifestPath, rendered.manifestJson, "utf8");
713
724
  return [applicationPath, manifestPath];
714
725
  }
715
726
  function factoryOfScope(scope) {
@@ -1006,14 +1017,199 @@ function orderProviders(providers) {
1006
1017
  return result;
1007
1018
  }
1008
1019
 
1020
+ // src/profiles.ts
1021
+ var MODULAR_MONOLITH_RULES = [
1022
+ {
1023
+ sourceTag: "type:feature",
1024
+ bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
1025
+ },
1026
+ {
1027
+ sourceTag: "type:root",
1028
+ onlyDependOnLibsWithTags: ["type:feature", "type:core", "type:shared", "type:domain"]
1029
+ },
1030
+ {
1031
+ sourceTag: "type:app",
1032
+ onlyDependOnLibsWithTags: ["type:feature", "type:core", "type:shared", "type:domain"]
1033
+ },
1034
+ {
1035
+ sourceTag: "type:core",
1036
+ bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
1037
+ },
1038
+ {
1039
+ sourceTag: "type:shared",
1040
+ bannedDependenciesWithTags: ["type:feature", "type:core", "type:root", "type:app"]
1041
+ }
1042
+ ];
1043
+ var ANGULAR_ENTERPRISE_RULES = [
1044
+ {
1045
+ sourceTag: "type:feature",
1046
+ bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
1047
+ },
1048
+ {
1049
+ sourceTag: "type:ui",
1050
+ bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
1051
+ },
1052
+ {
1053
+ sourceTag: "type:data-access",
1054
+ bannedDependenciesWithTags: ["type:feature", "type:ui", "type:root", "type:app"]
1055
+ },
1056
+ {
1057
+ sourceTag: "type:util",
1058
+ bannedDependenciesWithTags: ["type:feature", "type:ui", "type:data-access", "type:root", "type:app"]
1059
+ },
1060
+ {
1061
+ sourceTag: "type:shared",
1062
+ bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
1063
+ },
1064
+ {
1065
+ sourceTag: "type:core",
1066
+ bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
1067
+ },
1068
+ {
1069
+ sourceTag: "type:root",
1070
+ onlyDependOnLibsWithTags: ["type:feature", "type:ui", "type:data-access", "type:core", "type:shared", "type:util"]
1071
+ },
1072
+ {
1073
+ sourceTag: "type:app",
1074
+ onlyDependOnLibsWithTags: ["type:feature", "type:ui", "type:data-access", "type:core", "type:shared", "type:util"]
1075
+ }
1076
+ ];
1077
+ var CLEAN_ARCHITECTURE_RULES = [
1078
+ {
1079
+ sourceTag: "type:api",
1080
+ onlyDependOnLibsWithTags: ["type:application", "type:domain", "type:shared", "type:common"]
1081
+ },
1082
+ {
1083
+ sourceTag: "type:controller",
1084
+ onlyDependOnLibsWithTags: ["type:application", "type:domain", "type:shared", "type:common"]
1085
+ },
1086
+ {
1087
+ sourceTag: "type:presentation",
1088
+ onlyDependOnLibsWithTags: ["type:application", "type:domain", "type:shared", "type:common"]
1089
+ },
1090
+ {
1091
+ sourceTag: "type:application",
1092
+ onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
1093
+ },
1094
+ {
1095
+ sourceTag: "type:service",
1096
+ onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
1097
+ },
1098
+ {
1099
+ sourceTag: "type:domain",
1100
+ bannedDependenciesWithTags: [
1101
+ "type:api",
1102
+ "type:controller",
1103
+ "type:presentation",
1104
+ "type:application",
1105
+ "type:service",
1106
+ "type:infrastructure",
1107
+ "type:infra",
1108
+ "type:root",
1109
+ "type:app"
1110
+ ]
1111
+ },
1112
+ {
1113
+ sourceTag: "type:infrastructure",
1114
+ onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
1115
+ },
1116
+ {
1117
+ sourceTag: "type:infra",
1118
+ onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
1119
+ }
1120
+ ];
1121
+ var MODULE_BOUNDARY_PROFILES = {
1122
+ "modular-monolith": {
1123
+ name: "modular-monolith",
1124
+ description: "Modular monolith / vertical slice preset (blocks cross-feature dependencies and limits root aggregation to feature/core/shared/domain)",
1125
+ rules: MODULAR_MONOLITH_RULES
1126
+ },
1127
+ "feature-slices": {
1128
+ name: "feature-slices",
1129
+ description: "Vertical slice preset (alias for modular-monolith)",
1130
+ rules: MODULAR_MONOLITH_RULES
1131
+ },
1132
+ "vertical-slices": {
1133
+ name: "vertical-slices",
1134
+ description: "Vertical slice architecture preset (alias for modular-monolith)",
1135
+ rules: MODULAR_MONOLITH_RULES
1136
+ },
1137
+ "angular-enterprise": {
1138
+ name: "angular-enterprise",
1139
+ description: "Angular / Nx enterprise monorepo preset (enforces one-way layering across feature, UI, data-access, util, shared, and root modules)",
1140
+ rules: ANGULAR_ENTERPRISE_RULES
1141
+ },
1142
+ angular: {
1143
+ name: "angular",
1144
+ description: "Angular enterprise layering preset (alias for angular-enterprise)",
1145
+ rules: ANGULAR_ENTERPRISE_RULES
1146
+ },
1147
+ "clean-architecture": {
1148
+ name: "clean-architecture",
1149
+ description: "Clean Architecture / DDD layering preset (API/presentation -> application -> domain <- infrastructure)",
1150
+ rules: CLEAN_ARCHITECTURE_RULES
1151
+ },
1152
+ "domain-driven": {
1153
+ name: "domain-driven",
1154
+ description: "DDD layering preset (alias for clean-architecture)",
1155
+ rules: CLEAN_ARCHITECTURE_RULES
1156
+ }
1157
+ };
1158
+ function getModuleBoundaryProfile(name) {
1159
+ const profile = MODULE_BOUNDARY_PROFILES[name];
1160
+ if (!profile) {
1161
+ throw new Error(`Unknown module boundary preset: '${name}'. Supported presets: ${Object.keys(MODULE_BOUNDARY_PROFILES).join(", ")}`);
1162
+ }
1163
+ return {
1164
+ ...profile,
1165
+ rules: profile.rules.map((rule) => ({
1166
+ ...rule,
1167
+ onlyDependOnLibsWithTags: rule.onlyDependOnLibsWithTags ? [...rule.onlyDependOnLibsWithTags] : undefined,
1168
+ bannedDependenciesWithTags: rule.bannedDependenciesWithTags ? [...rule.bannedDependenciesWithTags] : undefined
1169
+ }))
1170
+ };
1171
+ }
1172
+ function getModuleBoundaryPreset(name) {
1173
+ return getModuleBoundaryProfile(name).rules;
1174
+ }
1175
+ function resolveModuleBoundaries(options) {
1176
+ if (!options)
1177
+ return;
1178
+ const { preset, rules } = options;
1179
+ if (!preset && !rules)
1180
+ return;
1181
+ const presetRules = preset ? getModuleBoundaryPreset(preset) : [];
1182
+ const customRules = rules ?? [];
1183
+ const merged = [...presetRules, ...customRules];
1184
+ return merged.length > 0 ? merged : undefined;
1185
+ }
1186
+
1009
1187
  // src/validate.ts
1010
1188
  var SCOPE_LIFETIME_RANK = {
1011
1189
  application: 0,
1012
1190
  request: 1,
1013
1191
  job: 1
1014
1192
  };
1015
- function validateGraph(graph, strict = false) {
1193
+ function validateGraph(graph, options = false) {
1194
+ const strict = typeof options === "boolean" ? options : options.strict ?? false;
1016
1195
  const diagnostics = [];
1196
+ let moduleBoundaries;
1197
+ if (typeof options === "object") {
1198
+ try {
1199
+ moduleBoundaries = resolveModuleBoundaries({
1200
+ preset: options.moduleBoundaryPreset,
1201
+ rules: options.moduleBoundaries
1202
+ });
1203
+ } catch (err) {
1204
+ diagnostics.push({
1205
+ severity: "error",
1206
+ code: "invalid-boundary-preset",
1207
+ message: err instanceof Error ? err.message : String(err),
1208
+ file: graph.modules[0]?.file,
1209
+ line: graph.modules[0]?.line
1210
+ });
1211
+ }
1212
+ }
1017
1213
  const globalProviders = new Map;
1018
1214
  for (const module of graph.modules) {
1019
1215
  for (const provider of module.providers) {
@@ -1075,6 +1271,17 @@ function validateGraph(graph, strict = false) {
1075
1271
  if (route.command && !module.commands.some((command) => command.className === route.command)) {
1076
1272
  error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
1077
1273
  }
1274
+ if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
1275
+ error("route-command-binding-disallowed", `Route ${key} binds command ${route.command}, but route-level command bindings are disabled by policy. Use an application service (${controller.className}.${route.handler}, ${controller.file}).`, controller.file);
1276
+ }
1277
+ }
1278
+ if (typeof options === "object" && options.disallowControllerDirectDb) {
1279
+ for (const dep of controller.deps) {
1280
+ const isDbClient = dep === "DB_CLIENT" || dep === "DatabaseClient" || graph.tokenNames?.[dep] === "supacloud.db-client";
1281
+ if (isDbClient) {
1282
+ error("controller-direct-db-access", `Controller ${controller.className} directly injects database client '${dep}', violating presentation layer separation (${controller.file})`, controller.file);
1283
+ }
1284
+ }
1078
1285
  }
1079
1286
  }
1080
1287
  }
@@ -1111,9 +1318,62 @@ function validateGraph(graph, strict = false) {
1111
1318
  if (!command.permission) {
1112
1319
  error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
1113
1320
  }
1321
+ if (typeof options === "object" && options.commandCapabilities) {
1322
+ const caps = options.commandCapabilities;
1323
+ const location = `${command.className} (${module.file})`;
1324
+ if (command.permission && caps.permission === false) {
1325
+ error("command-permission-unsupported", `Command ${command.name} declares permission, but runtime permission checks are unavailable (${location}).`, module.file, module.line);
1326
+ }
1327
+ if (command.audit && caps.audit === false) {
1328
+ error("command-audit-unsupported", `Command ${command.name} declares audit, but audit persistence is unavailable (${location}).`, module.file, module.line);
1329
+ }
1330
+ if (command.idempotency === "required" && caps.idempotency === false) {
1331
+ error("command-idempotency-unsupported", `Command ${command.name} declares idempotency, but idempotency receipt persistence is unavailable (${location}).`, module.file, module.line);
1332
+ }
1333
+ if (command.transaction === "required") {
1334
+ if (caps.transaction === "rpc-only") {
1335
+ warn2("command-transaction-rpc-only", `Command ${command.name} declares transaction: 'required', but only DB RPC transactions are available; multi-table writes must use one DB RPC (${location}).`, module.file, module.line);
1336
+ } else if (caps.transaction === false) {
1337
+ error("command-transaction-unsupported", `Command ${command.name} declares transaction: 'required', but transaction support is unavailable (${location}).`, module.file, module.line);
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+ if (moduleBoundaries && moduleBoundaries.length > 0) {
1344
+ for (const module of graph.modules) {
1345
+ const sourceTags = module.tags ?? [];
1346
+ for (const importName of module.imports) {
1347
+ const targetModule = graph.modules.find((m) => m.name === importName);
1348
+ if (!targetModule)
1349
+ continue;
1350
+ const targetTags = targetModule.tags ?? [];
1351
+ for (const rule of moduleBoundaries) {
1352
+ const matchesSource = rule.sourceTag === "*" || sourceTags.includes(rule.sourceTag);
1353
+ if (!matchesSource)
1354
+ continue;
1355
+ if (rule.bannedDependenciesWithTags) {
1356
+ for (const bannedTag of rule.bannedDependenciesWithTags) {
1357
+ if (targetTags.includes(bannedTag)) {
1358
+ error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 禁止依赖带有标签 '${bannedTag}' 的模块 ${targetModule.name} (tags: [${targetTags.join(", ")}])`, module.file, module.line);
1359
+ }
1360
+ }
1361
+ }
1362
+ if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
1363
+ const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
1364
+ if (!hasAllowed && targetTags.length > 0) {
1365
+ error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1114
1370
  }
1115
1371
  }
1116
1372
  diagnostics.push(...detectCycles(graph, resolveDep));
1373
+ diagnostics.push(...detectModuleCycles(graph));
1374
+ if (typeof options === "object" && options.detectOrphanModules) {
1375
+ diagnostics.push(...detectOrphanModules(graph));
1376
+ }
1117
1377
  return diagnostics;
1118
1378
  }
1119
1379
  function joinRoutePaths(prefix, path) {
@@ -1163,13 +1423,103 @@ function detectCycles(graph, resolveDep) {
1163
1423
  visit(ref);
1164
1424
  return diagnostics;
1165
1425
  }
1426
+ function detectModuleCycles(graph) {
1427
+ const diagnostics = [];
1428
+ const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
1429
+ const state = new Map;
1430
+ const stack = [];
1431
+ const reported = new Set;
1432
+ const visit = (name) => {
1433
+ if (state.get(name) === "done")
1434
+ return;
1435
+ if (state.get(name) === "visiting") {
1436
+ const cycleStart = stack.indexOf(name);
1437
+ const cycle = [...stack.slice(cycleStart), name];
1438
+ const cycleKey = [...cycle].sort().join("|");
1439
+ if (!reported.has(cycleKey)) {
1440
+ reported.add(cycleKey);
1441
+ const mod2 = moduleMap.get(name);
1442
+ diagnostics.push({
1443
+ severity: "error",
1444
+ code: "circular-module-import",
1445
+ message: `Module circular import detected: ${cycle.join(" -> ")}`,
1446
+ file: mod2?.file,
1447
+ line: mod2?.line
1448
+ });
1449
+ }
1450
+ return;
1451
+ }
1452
+ state.set(name, "visiting");
1453
+ stack.push(name);
1454
+ const mod = moduleMap.get(name);
1455
+ if (mod) {
1456
+ for (const importName of mod.imports) {
1457
+ if (moduleMap.has(importName)) {
1458
+ visit(importName);
1459
+ }
1460
+ }
1461
+ }
1462
+ stack.pop();
1463
+ state.set(name, "done");
1464
+ };
1465
+ for (const mod of graph.modules) {
1466
+ visit(mod.name);
1467
+ }
1468
+ return diagnostics;
1469
+ }
1470
+ function detectOrphanModules(graph) {
1471
+ const diagnostics = [];
1472
+ const rootModules = graph.modules.filter((m) => m.tags && (m.tags.includes("type:root") || m.tags.includes("type:app")) || m.name === "app" || m.name === "root");
1473
+ if (rootModules.length === 0)
1474
+ return diagnostics;
1475
+ const reachable = new Set;
1476
+ const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
1477
+ const queue = rootModules.map((m) => m.name);
1478
+ for (const root of rootModules) {
1479
+ reachable.add(root.name);
1480
+ }
1481
+ while (queue.length > 0) {
1482
+ const current = queue.shift();
1483
+ const mod = moduleMap.get(current);
1484
+ if (!mod)
1485
+ continue;
1486
+ for (const imp of mod.imports) {
1487
+ if (!reachable.has(imp)) {
1488
+ reachable.add(imp);
1489
+ queue.push(imp);
1490
+ }
1491
+ }
1492
+ }
1493
+ for (const mod of graph.modules) {
1494
+ if (!reachable.has(mod.name)) {
1495
+ diagnostics.push({
1496
+ severity: "warn",
1497
+ code: "orphan-module",
1498
+ message: `Module '${mod.name}' is declared but not reachable from any root module (${rootModules.map((r) => r.name).join(", ")})`,
1499
+ file: mod.file,
1500
+ line: mod.line
1501
+ });
1502
+ }
1503
+ }
1504
+ return diagnostics;
1505
+ }
1166
1506
 
1167
1507
  // src/compile.ts
1508
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
1509
+ import { join as join3 } from "node:path";
1168
1510
  async function compileProject(options) {
1169
1511
  const graph = await analyzeProject(options.rootDir, options.include);
1170
1512
  const diagnostics = [
1171
1513
  ...graph.diagnostics ?? [],
1172
- ...validateGraph(graph, options.strict)
1514
+ ...validateGraph(graph, {
1515
+ strict: options.strict,
1516
+ moduleBoundaryPreset: options.moduleBoundaryPreset,
1517
+ moduleBoundaries: options.moduleBoundaries,
1518
+ allowRouteCommandBindings: options.allowRouteCommandBindings,
1519
+ commandCapabilities: options.commandCapabilities,
1520
+ disallowControllerDirectDb: options.disallowControllerDirectDb,
1521
+ detectOrphanModules: options.detectOrphanModules
1522
+ })
1173
1523
  ];
1174
1524
  if (options.strict) {
1175
1525
  for (const diagnostic of diagnostics) {
@@ -1183,10 +1533,66 @@ async function compileProject(options) {
1183
1533
  });
1184
1534
  return { diagnostics, graph, written };
1185
1535
  }
1536
+ async function checkProject(options) {
1537
+ const graph = await analyzeProject(options.rootDir, options.include);
1538
+ const diagnostics = [
1539
+ ...graph.diagnostics ?? [],
1540
+ ...validateGraph(graph, {
1541
+ strict: options.strict,
1542
+ moduleBoundaryPreset: options.moduleBoundaryPreset,
1543
+ moduleBoundaries: options.moduleBoundaries,
1544
+ allowRouteCommandBindings: options.allowRouteCommandBindings,
1545
+ commandCapabilities: options.commandCapabilities,
1546
+ disallowControllerDirectDb: options.disallowControllerDirectDb,
1547
+ detectOrphanModules: options.detectOrphanModules
1548
+ })
1549
+ ];
1550
+ if (options.strict) {
1551
+ for (const diagnostic of diagnostics) {
1552
+ if (diagnostic.severity === "warn")
1553
+ diagnostic.severity = "error";
1554
+ }
1555
+ }
1556
+ const rendered = renderApplication(graph, {
1557
+ rootDir: options.rootDir,
1558
+ outDir: options.outDir
1559
+ });
1560
+ const expectedFiles = {
1561
+ "application.ts": rendered.applicationCode,
1562
+ "app.manifest.json": rendered.manifestJson
1563
+ };
1564
+ const mismatches = [];
1565
+ for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
1566
+ const diskPath = join3(options.outDir, filename);
1567
+ if (!existsSync2(diskPath)) {
1568
+ mismatches.push(`${filename}: generated artifact is missing from disk`);
1569
+ continue;
1570
+ }
1571
+ const diskContent = readFileSync(diskPath, "utf8");
1572
+ if (diskContent !== expectedContent) {
1573
+ mismatches.push(`${filename}: disk artifact differs from current compiler output`);
1574
+ }
1575
+ }
1576
+ return {
1577
+ upToDate: mismatches.length === 0,
1578
+ mismatches,
1579
+ diagnostics,
1580
+ graph
1581
+ };
1582
+ }
1186
1583
  export {
1584
+ ANGULAR_ENTERPRISE_RULES,
1585
+ CLEAN_ARCHITECTURE_RULES,
1586
+ MODULAR_MONOLITH_RULES,
1587
+ MODULE_BOUNDARY_PROFILES,
1187
1588
  analyzeProject,
1188
1589
  camelName,
1590
+ checkProject,
1189
1591
  compileProject,
1190
1592
  generateApplication,
1593
+ getModuleBoundaryPreset,
1594
+ getModuleBoundaryProfile,
1595
+ renderApplication,
1596
+ resolveModuleBoundaries,
1191
1597
  validateGraph
1192
1598
  };
@@ -0,0 +1,36 @@
1
+ import type { ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule } from "./types";
2
+ /**
3
+ * Modular monolith / vertical slice architecture rules:
4
+ * - type:feature slices cannot depend on one another or on root/app modules;
5
+ * - type:root / type:app entry modules may aggregate feature, core, shared, and domain modules;
6
+ * - type:core foundational modules cannot depend upward on feature or root/app modules;
7
+ * - type:shared common utilities/components cannot depend on feature or root/app modules.
8
+ */
9
+ export declare const MODULAR_MONOLITH_RULES: ModuleBoundaryRule[];
10
+ /**
11
+ * Angular / Nx enterprise monorepo layering rules:
12
+ * follows the recommended Nx / Angular enterprise workspace conventions:
13
+ * - feature slices cannot directly depend on one another;
14
+ * - ui / data-access / util / shared libraries cannot depend upward on feature or root modules;
15
+ * - root / app modules serve only as composition entry points.
16
+ */
17
+ export declare const ANGULAR_ENTERPRISE_RULES: ModuleBoundaryRule[];
18
+ /**
19
+ * Clean Architecture / DDD layering rules (similar to Spring Boot DDD conventions):
20
+ * - presentation layers (api/controller/presentation) may depend only on application, domain, and shared layers;
21
+ * - application layers (application/service) orchestrate use cases and depend only on domain and shared layers;
22
+ * - the domain layer remains pure and cannot depend on presentation, application, infrastructure, or root modules;
23
+ * - infrastructure layers (infrastructure/infra) implement domain ports and may depend only on domain and shared layers.
24
+ */
25
+ export declare const CLEAN_ARCHITECTURE_RULES: ModuleBoundaryRule[];
26
+ /** Registry of built-in module boundary profiles. */
27
+ export declare const MODULE_BOUNDARY_PROFILES: Record<ModuleBoundaryPresetName, ModuleBoundaryProfile>;
28
+ /** Return the architecture governance rules for a preset name. */
29
+ export declare function getModuleBoundaryProfile(name: ModuleBoundaryPresetName): ModuleBoundaryProfile;
30
+ /** Return a copy of the rules included in a preset. */
31
+ export declare function getModuleBoundaryPreset(name: ModuleBoundaryPresetName): ModuleBoundaryRule[];
32
+ /** Resolve and merge preset rules with optional user-defined rules. */
33
+ export declare function resolveModuleBoundaries(options?: {
34
+ preset?: ModuleBoundaryPresetName;
35
+ rules?: ModuleBoundaryRule[];
36
+ }): ModuleBoundaryRule[] | undefined;
package/dist/types.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface ProviderNode {
31
31
  importPath?: string;
32
32
  }
33
33
  export interface RouteNode {
34
- method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
34
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
35
35
  path: string;
36
36
  handler: string;
37
37
  body?: string;
@@ -68,6 +68,8 @@ export interface ModuleNode {
68
68
  /** @Module({ name }) 或 defineModule 的 name。 */
69
69
  name: string;
70
70
  className: string;
71
+ /** 模块标签(如 ['scope:case', 'type:feature']),用于架构边界治理。 */
72
+ tags?: string[];
71
73
  file: string;
72
74
  line: number;
73
75
  /** 被 import 模块的 name。 */
@@ -104,9 +106,73 @@ export interface CompileOptions {
104
106
  outDir: string;
105
107
  /** warn 级诊断升级为 error。 */
106
108
  strict?: boolean;
109
+ /** Built-in architecture boundary preset (for example, 'modular-monolith'). */
110
+ moduleBoundaryPreset?: ModuleBoundaryPresetName;
111
+ /** Module boundary and architecture governance rules inspired by Nx enforce-module-boundaries. */
112
+ moduleBoundaries?: ModuleBoundaryRule[];
113
+ /** Allow routes to bind directly to @Command (defaults to true). */
114
+ allowRouteCommandBindings?: boolean;
115
+ /** Runtime Command executor capabilities used to validate declared governance metadata. */
116
+ commandCapabilities?: CommandExecutionCapabilities;
117
+ /** Disallow controllers from directly injecting DB clients (enforces presentation layer separation). */
118
+ disallowControllerDirectDb?: boolean;
119
+ /** Detect modules declared in the project that are unreachable from any root module. */
120
+ detectOrphanModules?: boolean;
121
+ }
122
+ export interface ModuleBoundaryRule {
123
+ /** Source module tag pattern or tag (for example, 'type:ui', 'scope:case', or '*'). */
124
+ sourceTag: string;
125
+ /** Tags allowed for modules imported by the source module. */
126
+ onlyDependOnLibsWithTags?: string[];
127
+ /** Tags forbidden for modules imported by the source module. */
128
+ bannedDependenciesWithTags?: string[];
129
+ }
130
+ /** Names of built-in module boundary presets. */
131
+ export type ModuleBoundaryPresetName = "modular-monolith" | "feature-slices" | "vertical-slices" | "angular-enterprise" | "angular" | "clean-architecture" | "domain-driven";
132
+ export interface ModuleBoundaryProfile {
133
+ name: ModuleBoundaryPresetName;
134
+ description: string;
135
+ rules: ModuleBoundaryRule[];
136
+ }
137
+ export interface ValidateOptions {
138
+ strict?: boolean;
139
+ moduleBoundaryPreset?: ModuleBoundaryPresetName;
140
+ moduleBoundaries?: ModuleBoundaryRule[];
141
+ /** Allow routes to bind directly to @Command (defaults to true). */
142
+ allowRouteCommandBindings?: boolean;
143
+ /** Runtime Command executor capabilities used to validate declared governance metadata. */
144
+ commandCapabilities?: CommandExecutionCapabilities;
145
+ /** Disallow controllers from directly injecting DB clients. */
146
+ disallowControllerDirectDb?: boolean;
147
+ /** Detect modules declared in the project that are unreachable from any root module. */
148
+ detectOrphanModules?: boolean;
149
+ }
150
+ /** Runtime capabilities declared by the Command executor. */
151
+ export interface CommandExecutionCapabilities {
152
+ /** Whether runtime permission checks are supported. */
153
+ permission?: boolean;
154
+ /** Whether runtime audit persistence is supported. */
155
+ audit?: boolean;
156
+ /** Whether idempotency receipts are supported. */
157
+ idempotency?: boolean;
158
+ /**
159
+ * Transaction execution capability:
160
+ * - true: full transaction boundaries are supported;
161
+ * - 'rpc-only': application-level transactions are unavailable and multi-table writes must use one DB RPC (warn);
162
+ * - false: transaction support is disabled (error).
163
+ */
164
+ transaction?: boolean | "rpc-only";
107
165
  }
108
166
  export interface CompileResult {
109
167
  diagnostics: Diagnostic[];
110
168
  graph: ApplicationGraph;
111
169
  written: string[];
112
170
  }
171
+ export interface CheckProjectResult {
172
+ /** Whether generated artifacts exactly match the files on disk. */
173
+ upToDate: boolean;
174
+ /** Relative paths of missing or mismatched artifacts. */
175
+ mismatches: string[];
176
+ diagnostics: Diagnostic[];
177
+ graph: ApplicationGraph;
178
+ }