@supacloud/compiler 0.3.1 → 0.4.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/dist/analyze.d.ts +3 -3
- package/dist/cli.js +1698 -0
- package/dist/compile.d.ts +8 -3
- package/dist/generate.d.ts +8 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +377 -7
- package/dist/profiles.d.ts +36 -0
- package/dist/types.d.ts +79 -25
- package/dist/util.d.ts +7 -7
- package/dist/validate.d.ts +4 -7
- package/package.json +5 -2
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
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Complete compilation pipeline: AST analysis -> validation -> generate static factory code and manifest.
|
|
4
|
+
* Files are emitted even when error-level diagnostics exist; caller decides adoption based on 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>;
|
package/dist/generate.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
-
|
|
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,
|
|
713
|
-
await writeFile(manifestPath,
|
|
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
|
-
|
|
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;
|