@supacloud/compiler 0.3.1 → 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, ModuleBoundaryRule, 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) {
@@ -674,7 +676,7 @@ export interface CompiledModule {
674
676
  controllers: CompiledController[];
675
677
  commands: CompiledCommand[];
676
678
  }`;
677
- async function generateApplication(graph, options) {
679
+ function renderApplication(graph, options) {
678
680
  const modules = topoSortModules(graph.modules);
679
681
  const imports = new ImportManager;
680
682
  const factorySections = [];
@@ -706,12 +708,19 @@ async function generateApplication(graph, options) {
706
708
  modules: graph.modules,
707
709
  externalTokens: graph.externalTokens
708
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);
709
719
  await mkdir(options.outDir, { recursive: true });
710
720
  const applicationPath = join2(options.outDir, "application.ts");
711
721
  const manifestPath = join2(options.outDir, "app.manifest.json");
712
- await writeFile(applicationPath, code, "utf8");
713
- await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + `
714
- `, "utf8");
722
+ await writeFile(applicationPath, rendered.applicationCode, "utf8");
723
+ await writeFile(manifestPath, rendered.manifestJson, "utf8");
715
724
  return [applicationPath, manifestPath];
716
725
  }
717
726
  function factoryOfScope(scope) {
@@ -1008,6 +1017,173 @@ function orderProviders(providers) {
1008
1017
  return result;
1009
1018
  }
1010
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
+
1011
1187
  // src/validate.ts
1012
1188
  var SCOPE_LIFETIME_RANK = {
1013
1189
  application: 0,
@@ -1016,8 +1192,24 @@ var SCOPE_LIFETIME_RANK = {
1016
1192
  };
1017
1193
  function validateGraph(graph, options = false) {
1018
1194
  const strict = typeof options === "boolean" ? options : options.strict ?? false;
1019
- const moduleBoundaries = typeof options === "object" ? options.moduleBoundaries : undefined;
1020
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
+ }
1021
1213
  const globalProviders = new Map;
1022
1214
  for (const module of graph.modules) {
1023
1215
  for (const provider of module.providers) {
@@ -1079,6 +1271,17 @@ function validateGraph(graph, options = false) {
1079
1271
  if (route.command && !module.commands.some((command) => command.className === route.command)) {
1080
1272
  error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
1081
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
+ }
1082
1285
  }
1083
1286
  }
1084
1287
  }
@@ -1115,6 +1318,26 @@ function validateGraph(graph, options = false) {
1115
1318
  if (!command.permission) {
1116
1319
  error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
1117
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
+ }
1118
1341
  }
1119
1342
  }
1120
1343
  if (moduleBoundaries && moduleBoundaries.length > 0) {
@@ -1147,6 +1370,10 @@ function validateGraph(graph, options = false) {
1147
1370
  }
1148
1371
  }
1149
1372
  diagnostics.push(...detectCycles(graph, resolveDep));
1373
+ diagnostics.push(...detectModuleCycles(graph));
1374
+ if (typeof options === "object" && options.detectOrphanModules) {
1375
+ diagnostics.push(...detectOrphanModules(graph));
1376
+ }
1150
1377
  return diagnostics;
1151
1378
  }
1152
1379
  function joinRoutePaths(prefix, path) {
@@ -1196,15 +1423,102 @@ function detectCycles(graph, resolveDep) {
1196
1423
  visit(ref);
1197
1424
  return diagnostics;
1198
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
+ }
1199
1506
 
1200
1507
  // src/compile.ts
1508
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
1509
+ import { join as join3 } from "node:path";
1201
1510
  async function compileProject(options) {
1202
1511
  const graph = await analyzeProject(options.rootDir, options.include);
1203
1512
  const diagnostics = [
1204
1513
  ...graph.diagnostics ?? [],
1205
1514
  ...validateGraph(graph, {
1206
1515
  strict: options.strict,
1207
- moduleBoundaries: options.moduleBoundaries
1516
+ moduleBoundaryPreset: options.moduleBoundaryPreset,
1517
+ moduleBoundaries: options.moduleBoundaries,
1518
+ allowRouteCommandBindings: options.allowRouteCommandBindings,
1519
+ commandCapabilities: options.commandCapabilities,
1520
+ disallowControllerDirectDb: options.disallowControllerDirectDb,
1521
+ detectOrphanModules: options.detectOrphanModules
1208
1522
  })
1209
1523
  ];
1210
1524
  if (options.strict) {
@@ -1219,10 +1533,66 @@ async function compileProject(options) {
1219
1533
  });
1220
1534
  return { diagnostics, graph, written };
1221
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
+ }
1222
1583
  export {
1584
+ ANGULAR_ENTERPRISE_RULES,
1585
+ CLEAN_ARCHITECTURE_RULES,
1586
+ MODULAR_MONOLITH_RULES,
1587
+ MODULE_BOUNDARY_PROFILES,
1223
1588
  analyzeProject,
1224
1589
  camelName,
1590
+ checkProject,
1225
1591
  compileProject,
1226
1592
  generateApplication,
1593
+ getModuleBoundaryPreset,
1594
+ getModuleBoundaryProfile,
1595
+ renderApplication,
1596
+ resolveModuleBoundaries,
1227
1597
  validateGraph
1228
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;
@@ -106,19 +106,73 @@ export interface CompileOptions {
106
106
  outDir: string;
107
107
  /** warn 级诊断升级为 error。 */
108
108
  strict?: boolean;
109
- /** 模块边界与架构治理规则(受 Nx enforce-module-boundaries 启发)。 */
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. */
110
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;
111
121
  }
112
122
  export interface ModuleBoundaryRule {
113
- /** 源模块标签模式或标签(如 'type:ui', 'scope:case', '*')。 */
123
+ /** Source module tag pattern or tag (for example, 'type:ui', 'scope:case', or '*'). */
114
124
  sourceTag: string;
115
- /** 源模块仅允许依赖具有这些标签的模块。 */
125
+ /** Tags allowed for modules imported by the source module. */
116
126
  onlyDependOnLibsWithTags?: string[];
117
- /** 源模块禁止依赖具有这些标签的模块。 */
127
+ /** Tags forbidden for modules imported by the source module. */
118
128
  bannedDependenciesWithTags?: string[];
119
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";
165
+ }
120
166
  export interface CompileResult {
121
167
  diagnostics: Diagnostic[];
122
168
  graph: ApplicationGraph;
123
169
  written: string[];
124
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
+ }
@@ -1,9 +1,6 @@
1
- import type { ApplicationGraph, Diagnostic, ModuleBoundaryRule } from "./types";
1
+ import type { ApplicationGraph, Diagnostic, ValidateOptions } from "./types";
2
2
  /**
3
3
  * 校验 ApplicationGraph,产出诊断列表。
4
4
  * strict 时 warn 级诊断升级为 error。
5
5
  */
6
- export declare function validateGraph(graph: ApplicationGraph, options?: boolean | {
7
- strict?: boolean;
8
- moduleBoundaries?: ModuleBoundaryRule[];
9
- }): Diagnostic[];
6
+ export declare function validateGraph(graph: ApplicationGraph, options?: boolean | ValidateOptions): Diagnostic[];
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "supacloud-compiler": "./dist/cli.js"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "types": "./dist/index.d.ts",
@@ -19,7 +22,7 @@
19
22
  ],
20
23
  "scripts": {
21
24
  "build": "bun run clean && bun run build:js && bun run build:types",
22
- "build:js": "bun build src/index.ts --outdir dist --target node --external ts-morph",
25
+ "build:js": "bun build src/index.ts src/cli.ts --outdir dist --target node --external ts-morph",
23
26
  "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
24
27
  "clean": "rm -rf dist",
25
28
  "prepublishOnly": "bun run build",