archforge-x 1.0.7 → 2.0.0-beta.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/analyzers/dependency.js +209 -20
- package/dist/cli/interactive.js +10 -4
- package/dist/core/VersionManifest.js +10 -0
- package/dist/core/architecture/parser.js +32 -4
- package/dist/core/architecture/schema.js +114 -4
- package/dist/generators/node/express.js +3292 -238
- package/dist/generators/node/nestjs.js +3137 -57
- package/dist/generators/readme/context.js +241 -0
- package/dist/generators/readme/index.js +158 -0
- package/dist/generators/readme/sections.js +1193 -0
- package/dist/generators/readme/types.js +20 -0
- package/dist/generators/templates/auth.js +771 -0
- package/dist/generators/templates/ci.js +139 -19
- package/dist/generators/templates/docker.js +195 -39
- package/dist/generators/templates/index.js +27 -1
- package/dist/generators/templates/typeorm.js +603 -0
- package/dist/rules/engine.js +177 -27
- package/package.json +15 -2
|
@@ -9,6 +9,11 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const typescript_1 = __importDefault(require("typescript"));
|
|
11
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const minimatch_1 = require("minimatch");
|
|
13
|
+
// Ignore comment patterns
|
|
14
|
+
const IGNORE_FILE_PATTERN = /@archforge-ignore-file/;
|
|
15
|
+
const IGNORE_LINE_PATTERN = /@archforge-ignore-line/;
|
|
16
|
+
const IGNORE_NEXT_LINE_PATTERN = /@archforge-ignore-next-line/;
|
|
12
17
|
// --- LANGUAGE PARSERS ---
|
|
13
18
|
const parseImportsTS = (content, filePath) => {
|
|
14
19
|
const imports = [];
|
|
@@ -16,21 +21,74 @@ const parseImportsTS = (content, filePath) => {
|
|
|
16
21
|
sourceFile.forEachChild((node) => {
|
|
17
22
|
if (typescript_1.default.isImportDeclaration(node) && node.moduleSpecifier) {
|
|
18
23
|
const importPath = node.moduleSpecifier.text;
|
|
19
|
-
|
|
24
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
25
|
+
imports.push({
|
|
26
|
+
path: importPath,
|
|
27
|
+
line: line + 1, // Convert to 1-based
|
|
28
|
+
column: character + 1
|
|
29
|
+
});
|
|
20
30
|
}
|
|
21
31
|
if (typescript_1.default.isVariableStatement(node)) {
|
|
22
32
|
const text = node.getText();
|
|
23
33
|
const match = text.match(/require\(['"](.+)['"]\)/);
|
|
24
|
-
if (match)
|
|
25
|
-
|
|
34
|
+
if (match) {
|
|
35
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
36
|
+
imports.push({
|
|
37
|
+
path: match[1],
|
|
38
|
+
line: line + 1,
|
|
39
|
+
column: character + 1
|
|
40
|
+
});
|
|
41
|
+
}
|
|
26
42
|
}
|
|
27
43
|
});
|
|
28
44
|
return imports;
|
|
29
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Check if a file should be completely ignored
|
|
48
|
+
*/
|
|
49
|
+
function shouldIgnoreFile(content, filePath, arch) {
|
|
50
|
+
// Check for @archforge-ignore-file comment
|
|
51
|
+
if (IGNORE_FILE_PATTERN.test(content)) {
|
|
52
|
+
const match = content.match(/@archforge-ignore-file(?:\s*:\s*(.+))?/);
|
|
53
|
+
return { ignored: true, reason: match?.[1]?.trim() || 'File-level ignore' };
|
|
54
|
+
}
|
|
55
|
+
// Check against config ignore patterns
|
|
56
|
+
if (arch.ignore?.files?.includes(filePath)) {
|
|
57
|
+
return { ignored: true, reason: 'Listed in ignore.files' };
|
|
58
|
+
}
|
|
59
|
+
if (arch.ignore?.patterns) {
|
|
60
|
+
for (const pattern of arch.ignore.patterns) {
|
|
61
|
+
if ((0, minimatch_1.minimatch)(filePath, pattern)) {
|
|
62
|
+
return { ignored: true, reason: `Matches ignore pattern: ${pattern}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ignored: false };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a specific line should be ignored
|
|
70
|
+
*/
|
|
71
|
+
function shouldIgnoreLine(lines, lineNumber) {
|
|
72
|
+
const lineIndex = lineNumber - 1;
|
|
73
|
+
const currentLine = lines[lineIndex] || '';
|
|
74
|
+
const previousLine = lines[lineIndex - 1] || '';
|
|
75
|
+
// Check current line for inline ignore
|
|
76
|
+
if (IGNORE_LINE_PATTERN.test(currentLine)) {
|
|
77
|
+
const match = currentLine.match(/@archforge-ignore-line(?:\s*:\s*(.+))?/);
|
|
78
|
+
return { ignored: true, reason: match?.[1]?.trim() || 'Inline ignore' };
|
|
79
|
+
}
|
|
80
|
+
// Check previous line for next-line ignore
|
|
81
|
+
if (IGNORE_NEXT_LINE_PATTERN.test(previousLine)) {
|
|
82
|
+
const match = previousLine.match(/@archforge-ignore-next-line(?:\s*:\s*(.+))?/);
|
|
83
|
+
return { ignored: true, reason: match?.[1]?.trim() || 'Next-line ignore' };
|
|
84
|
+
}
|
|
85
|
+
return { ignored: false };
|
|
86
|
+
}
|
|
30
87
|
// --- CORE ANALYZER ---
|
|
31
88
|
function analyzeDependencies(arch, options = {}) {
|
|
32
89
|
const violations = [];
|
|
33
90
|
const projectRoot = path_1.default.resolve(arch.project.root || ".");
|
|
91
|
+
const forbiddenPackages = arch.rules?.imports?.forbiddenPackages || [];
|
|
34
92
|
function getLayerByPath(filePath) {
|
|
35
93
|
const absoluteFile = path_1.default.resolve(filePath);
|
|
36
94
|
const sortedLayers = [...arch.layers].sort((a, b) => b.path.length - a.path.length);
|
|
@@ -85,37 +143,145 @@ function analyzeDependencies(arch, options = {}) {
|
|
|
85
143
|
if (!currentLayer)
|
|
86
144
|
return;
|
|
87
145
|
const content = fs_extra_1.default.readFileSync(filePath, "utf-8");
|
|
146
|
+
const relativeFilePath = path_1.default.relative(projectRoot, filePath);
|
|
147
|
+
const lines = content.split('\n');
|
|
148
|
+
// Check if entire file should be ignored
|
|
149
|
+
const fileIgnore = shouldIgnoreFile(content, relativeFilePath, arch);
|
|
150
|
+
if (fileIgnore.ignored && !options.includeIgnored) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
88
153
|
let imports = [];
|
|
89
|
-
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext))
|
|
154
|
+
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
90
155
|
imports = parseImportsTS(content, filePath);
|
|
91
|
-
|
|
92
|
-
|
|
156
|
+
}
|
|
157
|
+
for (const importInfo of imports) {
|
|
158
|
+
// Check for line-level ignore
|
|
159
|
+
const lineIgnore = shouldIgnoreLine(lines, importInfo.line);
|
|
160
|
+
const isIgnored = fileIgnore.ignored || lineIgnore.ignored;
|
|
161
|
+
const ignoreReason = fileIgnore.reason || lineIgnore.reason;
|
|
162
|
+
// Check forbidden packages first
|
|
163
|
+
const packageName = importInfo.path.split('/')[0];
|
|
164
|
+
const forbiddenPkg = forbiddenPackages.find(fp => fp.name === packageName || importInfo.path.startsWith(fp.name));
|
|
165
|
+
if (forbiddenPkg) {
|
|
166
|
+
const violation = {
|
|
167
|
+
file: relativeFilePath,
|
|
168
|
+
line: importInfo.line,
|
|
169
|
+
column: importInfo.column,
|
|
170
|
+
fromLayer: currentLayer.name,
|
|
171
|
+
importedLayer: packageName,
|
|
172
|
+
importPath: importInfo.path,
|
|
173
|
+
type: "forbidden-package",
|
|
174
|
+
severity: forbiddenPkg.severity || "error",
|
|
175
|
+
message: forbiddenPkg.message || `Package '${packageName}' is forbidden`,
|
|
176
|
+
ignored: isIgnored,
|
|
177
|
+
ignoreReason
|
|
178
|
+
};
|
|
179
|
+
if (!isIgnored || options.includeIgnored) {
|
|
180
|
+
violations.push(violation);
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Check layer violations
|
|
185
|
+
const importedLayer = resolveImportedLayer(filePath, importInfo.path);
|
|
93
186
|
if (!importedLayer || importedLayer.name === currentLayer.name)
|
|
94
187
|
continue;
|
|
95
188
|
const canImport = currentLayer.canImport || currentLayer.allowedImports || [];
|
|
96
189
|
if (!canImport.includes(importedLayer.name)) {
|
|
97
|
-
|
|
98
|
-
file:
|
|
190
|
+
const violation = {
|
|
191
|
+
file: relativeFilePath,
|
|
192
|
+
line: importInfo.line,
|
|
193
|
+
column: importInfo.column,
|
|
99
194
|
fromLayer: currentLayer.name,
|
|
100
195
|
importedLayer: importedLayer.name,
|
|
101
|
-
importPath:
|
|
196
|
+
importPath: importInfo.path,
|
|
102
197
|
type: "forbidden",
|
|
103
|
-
severity: "error"
|
|
104
|
-
|
|
198
|
+
severity: "error",
|
|
199
|
+
ignored: isIgnored,
|
|
200
|
+
ignoreReason
|
|
201
|
+
};
|
|
202
|
+
if (!isIgnored || options.includeIgnored) {
|
|
203
|
+
violations.push(violation);
|
|
204
|
+
}
|
|
105
205
|
}
|
|
106
206
|
}
|
|
107
207
|
}
|
|
208
|
+
// Check file location rules
|
|
209
|
+
function checkFileLocations() {
|
|
210
|
+
const locationRules = arch.rules?.fileLocations || [];
|
|
211
|
+
if (locationRules.length === 0)
|
|
212
|
+
return;
|
|
213
|
+
function scanForLocationViolations(folderPath) {
|
|
214
|
+
const absoluteFolderPath = path_1.default.resolve(projectRoot, folderPath);
|
|
215
|
+
if (!fs_extra_1.default.existsSync(absoluteFolderPath))
|
|
216
|
+
return;
|
|
217
|
+
const entries = fs_extra_1.default.readdirSync(absoluteFolderPath);
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const fullPath = path_1.default.join(absoluteFolderPath, entry);
|
|
220
|
+
const stat = fs_extra_1.default.statSync(fullPath);
|
|
221
|
+
if (stat.isDirectory()) {
|
|
222
|
+
if (["node_modules", ".git", "dist"].includes(entry))
|
|
223
|
+
continue;
|
|
224
|
+
scanForLocationViolations(fullPath);
|
|
225
|
+
}
|
|
226
|
+
else if (stat.isFile()) {
|
|
227
|
+
const relativeFilePath = path_1.default.relative(projectRoot, fullPath);
|
|
228
|
+
for (const rule of locationRules) {
|
|
229
|
+
if ((0, minimatch_1.minimatch)(entry, rule.pattern) || (0, minimatch_1.minimatch)(relativeFilePath, rule.pattern)) {
|
|
230
|
+
const isInAllowedPath = rule.allowedPaths.some(allowedPath => (0, minimatch_1.minimatch)(relativeFilePath, allowedPath));
|
|
231
|
+
if (!isInAllowedPath) {
|
|
232
|
+
const content = fs_extra_1.default.readFileSync(fullPath, "utf-8");
|
|
233
|
+
const fileIgnore = shouldIgnoreFile(content, relativeFilePath, arch);
|
|
234
|
+
if (!fileIgnore.ignored || options.includeIgnored) {
|
|
235
|
+
violations.push({
|
|
236
|
+
file: relativeFilePath,
|
|
237
|
+
fromLayer: "unknown",
|
|
238
|
+
importedLayer: "N/A",
|
|
239
|
+
importPath: "N/A",
|
|
240
|
+
type: "file-location",
|
|
241
|
+
severity: rule.severity,
|
|
242
|
+
message: rule.message || `File '${entry}' is not in an allowed location. Expected: ${rule.allowedPaths.join(' or ')}`,
|
|
243
|
+
ignored: fileIgnore.ignored,
|
|
244
|
+
ignoreReason: fileIgnore.reason
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
scanForLocationViolations("src");
|
|
254
|
+
}
|
|
108
255
|
arch.layers.forEach((layer) => scanFolder(layer.path));
|
|
109
|
-
|
|
256
|
+
checkFileLocations();
|
|
257
|
+
// Sort violations: errors first, then by file
|
|
258
|
+
return violations.sort((a, b) => {
|
|
259
|
+
if (a.ignored !== b.ignored)
|
|
260
|
+
return a.ignored ? 1 : -1;
|
|
261
|
+
if (a.severity !== b.severity) {
|
|
262
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
263
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
264
|
+
}
|
|
265
|
+
return a.file.localeCompare(b.file);
|
|
266
|
+
});
|
|
110
267
|
}
|
|
111
268
|
function reportViolations(violations, options = {}) {
|
|
112
|
-
|
|
269
|
+
const activeViolations = violations.filter(v => !v.ignored);
|
|
270
|
+
const ignoredViolations = violations.filter(v => v.ignored);
|
|
271
|
+
if (activeViolations.length === 0) {
|
|
113
272
|
console.log(chalk_1.default.green("✅ No architectural violations found. Architecture is clean."));
|
|
273
|
+
if (ignoredViolations.length > 0) {
|
|
274
|
+
console.log(chalk_1.default.gray(` (${ignoredViolations.length} violation(s) ignored via @archforge-ignore)`));
|
|
275
|
+
}
|
|
114
276
|
return;
|
|
115
277
|
}
|
|
116
|
-
console.log(chalk_1.default.red.bold(
|
|
117
|
-
const grouped =
|
|
118
|
-
const key =
|
|
278
|
+
console.log(chalk_1.default.red.bold(`\n❌ Found ${activeViolations.length} Architecture Violation(s):`));
|
|
279
|
+
const grouped = activeViolations.reduce((acc, v) => {
|
|
280
|
+
const key = v.type === "forbidden-package"
|
|
281
|
+
? `forbidden-package:${v.importedLayer}`
|
|
282
|
+
: v.type === "file-location"
|
|
283
|
+
? `file-location`
|
|
284
|
+
: `${v.fromLayer} -> ${v.importedLayer}`;
|
|
119
285
|
if (!acc[key])
|
|
120
286
|
acc[key] = [];
|
|
121
287
|
acc[key].push(v);
|
|
@@ -124,17 +290,40 @@ function reportViolations(violations, options = {}) {
|
|
|
124
290
|
Object.keys(grouped).forEach(key => {
|
|
125
291
|
const group = grouped[key];
|
|
126
292
|
const v = group[0];
|
|
127
|
-
const color = v.severity === "error" ? chalk_1.default.red : chalk_1.default.yellow;
|
|
128
|
-
|
|
293
|
+
const color = v.severity === "error" ? chalk_1.default.red : v.severity === "warning" ? chalk_1.default.yellow : chalk_1.default.blue;
|
|
294
|
+
const icon = v.severity === "error" ? "❌" : v.severity === "warning" ? "⚠️" : "ℹ️";
|
|
295
|
+
if (v.type === "forbidden-package") {
|
|
296
|
+
console.log(color.bold(`\n${icon} [FORBIDDEN PACKAGE] ${v.importedLayer}`));
|
|
297
|
+
}
|
|
298
|
+
else if (v.type === "file-location") {
|
|
299
|
+
console.log(color.bold(`\n${icon} [FILE LOCATION] Misplaced files`));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.log(color.bold(`\n${icon} [${v.type.toUpperCase()}] ${v.fromLayer} depends on ${v.importedLayer}`));
|
|
303
|
+
}
|
|
129
304
|
group.forEach(item => {
|
|
130
|
-
|
|
305
|
+
const location = item.line ? `:${item.line}:${item.column || 1}` : '';
|
|
306
|
+
console.log(chalk_1.default.gray(` • ${item.file}${location}`));
|
|
307
|
+
if (item.message) {
|
|
308
|
+
console.log(chalk_1.default.gray(` ${item.message}`));
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.log(chalk_1.default.gray(` imports '${item.importPath}'`));
|
|
312
|
+
}
|
|
131
313
|
});
|
|
132
314
|
});
|
|
315
|
+
if (ignoredViolations.length > 0) {
|
|
316
|
+
console.log(chalk_1.default.gray(`\n📝 ${ignoredViolations.length} violation(s) ignored via @archforge-ignore`));
|
|
317
|
+
}
|
|
133
318
|
if (options.outputFile) {
|
|
134
319
|
const absoluteOutput = path_1.default.resolve(options.outputFile);
|
|
135
320
|
const ext = path_1.default.extname(absoluteOutput).toLowerCase();
|
|
136
321
|
const content = ext === ".json" ? JSON.stringify(violations, null, 2) :
|
|
137
|
-
violations.map(v =>
|
|
322
|
+
violations.map(v => {
|
|
323
|
+
const status = v.ignored ? '[IGNORED] ' : '';
|
|
324
|
+
const location = v.line ? `:${v.line}` : '';
|
|
325
|
+
return `${status}[${v.severity.toUpperCase()}] ${v.fromLayer} -> ${v.importedLayer} in ${v.file}${location}`;
|
|
326
|
+
}).join("\n");
|
|
138
327
|
fs_extra_1.default.writeFileSync(absoluteOutput, content, "utf-8");
|
|
139
328
|
console.log(chalk_1.default.blueBright(`\n📄 Detailed report saved to ${absoluteOutput}`));
|
|
140
329
|
}
|
package/dist/cli/interactive.js
CHANGED
|
@@ -27,14 +27,20 @@ async function interactiveCLI() {
|
|
|
27
27
|
{ title: "JavaScript", value: "js" },
|
|
28
28
|
],
|
|
29
29
|
});
|
|
30
|
+
// NestJS only supports TypeScript
|
|
31
|
+
const frameworkChoices = langResponse.language === "ts"
|
|
32
|
+
? [
|
|
33
|
+
{ title: "NestJS", value: "nestjs" },
|
|
34
|
+
{ title: "Express", value: "express" },
|
|
35
|
+
]
|
|
36
|
+
: [
|
|
37
|
+
{ title: "Express", value: "express" },
|
|
38
|
+
];
|
|
30
39
|
const frameworkResponse = await (0, prompts_1.default)({
|
|
31
40
|
type: "select",
|
|
32
41
|
name: "framework",
|
|
33
42
|
message: "Select framework:",
|
|
34
|
-
choices:
|
|
35
|
-
{ title: "NestJS", value: "nestjs" },
|
|
36
|
-
{ title: "Express", value: "express" },
|
|
37
|
-
],
|
|
43
|
+
choices: frameworkChoices,
|
|
38
44
|
});
|
|
39
45
|
const archResponse = await (0, prompts_1.default)({
|
|
40
46
|
type: "select",
|
|
@@ -13,6 +13,16 @@ exports.NodePackageVersions = {
|
|
|
13
13
|
uuid: "9.0.1",
|
|
14
14
|
zod: "3.23.8",
|
|
15
15
|
"http-status-codes": "2.3.0",
|
|
16
|
+
// Authentication
|
|
17
|
+
jsonwebtoken: "^9.0.2",
|
|
18
|
+
bcrypt: "^5.1.1",
|
|
19
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
20
|
+
"@types/bcrypt": "^5.0.2",
|
|
21
|
+
"passport-jwt": "^4.0.1",
|
|
22
|
+
"@types/passport-jwt": "^4.0.1",
|
|
23
|
+
"@nestjs/jwt": "^10.2.0",
|
|
24
|
+
"@nestjs/passport": "^10.0.3",
|
|
25
|
+
passport: "^0.7.0",
|
|
16
26
|
// NestJS (v11)
|
|
17
27
|
"@nestjs/common": "^11.0.0",
|
|
18
28
|
"@nestjs/core": "^11.0.0",
|
|
@@ -41,6 +41,12 @@ function mergeArchitectures(base, child) {
|
|
|
41
41
|
project: { ...base.project, ...child.project },
|
|
42
42
|
metadata: { ...base.metadata, ...child.metadata },
|
|
43
43
|
layers: Array.from(layerMap.values()),
|
|
44
|
+
rules: { ...base.rules, ...child.rules },
|
|
45
|
+
naming: { ...base.naming, ...child.naming },
|
|
46
|
+
ignore: {
|
|
47
|
+
files: [...(base.ignore?.files || []), ...(child.ignore?.files || [])],
|
|
48
|
+
patterns: [...(base.ignore?.patterns || []), ...(child.ignore?.patterns || [])],
|
|
49
|
+
},
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
// --- MAIN FUNCTIONS ---
|
|
@@ -66,11 +72,27 @@ function loadArchitecture(filePath) {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
catch (e) {
|
|
69
|
-
throw new Error(`Syntax error in ${filePath}
|
|
75
|
+
throw new Error(`Syntax error in ${filePath}:\n` +
|
|
76
|
+
`${e.message}\n\n` +
|
|
77
|
+
chalk_1.default.yellow(`💡 Tip: Validate your YAML/JSON syntax at https://jsonlint.com or https://yamlvalidator.com`));
|
|
70
78
|
}
|
|
71
79
|
// 3. Handle Inheritance (Extends)
|
|
72
80
|
if (parsed.extends) {
|
|
73
|
-
|
|
81
|
+
let parentPath;
|
|
82
|
+
// Support npm package extends (e.g., "@company/archforge-config")
|
|
83
|
+
if (parsed.extends.startsWith('@') || parsed.extends.startsWith('npm:')) {
|
|
84
|
+
try {
|
|
85
|
+
const packageName = parsed.extends.replace('npm:', '');
|
|
86
|
+
parentPath = require.resolve(packageName);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new Error(`Cannot resolve extends package: ${parsed.extends}\n\n` +
|
|
90
|
+
chalk_1.default.yellow(`💡 Make sure the package is installed: npm install ${parsed.extends}`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
parentPath = path_1.default.resolve(path_1.default.dirname(absolutePath), parsed.extends);
|
|
95
|
+
}
|
|
74
96
|
console.log(chalk_1.default.gray(` ↳ Extending configuration from: ${parsed.extends}`));
|
|
75
97
|
const parentArch = loadArchitecture(parentPath);
|
|
76
98
|
const { extends: _, ...childConfig } = parsed;
|
|
@@ -79,12 +101,18 @@ function loadArchitecture(filePath) {
|
|
|
79
101
|
// 4. Validate Schema
|
|
80
102
|
try {
|
|
81
103
|
const validated = schema_1.architectureSchema.parse(parsed);
|
|
104
|
+
// 5. Validate layer cross-references (canImport, forbiddenImports)
|
|
105
|
+
(0, schema_1.validateLayerReferences)(validated);
|
|
82
106
|
return processArchitecturePlaceholders(validated);
|
|
83
107
|
}
|
|
84
108
|
catch (err) {
|
|
85
109
|
if (err instanceof zod_1.z.ZodError) {
|
|
86
|
-
const issues = err.issues.map(i =>
|
|
87
|
-
|
|
110
|
+
const issues = err.issues.map(i => {
|
|
111
|
+
const path = i.path.join('.');
|
|
112
|
+
return ` - ${chalk_1.default.red(path)}: ${i.message}`;
|
|
113
|
+
}).join('\n');
|
|
114
|
+
throw new Error(`${chalk_1.default.red.bold('Invalid architecture configuration:')}\n\n${issues}\n\n` +
|
|
115
|
+
chalk_1.default.yellow(`💡 Check the documentation: archforge docs`));
|
|
88
116
|
}
|
|
89
117
|
throw err;
|
|
90
118
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.architectureSchema = exports.customStructureSchema = exports.rulesSchema = exports.namingSchema = exports.metadataSchema = exports.layerSchema = void 0;
|
|
3
|
+
exports.architectureSchema = exports.ignoreSchema = exports.customStructureSchema = exports.rulesSchema = exports.importRulesSchema = exports.complexityRulesSchema = exports.forbiddenPackageSchema = exports.fileLocationRuleSchema = exports.namingSchema = exports.namingPatternSchema = exports.metadataSchema = exports.layerSchema = void 0;
|
|
4
|
+
exports.validateLayerReferences = validateLayerReferences;
|
|
4
5
|
// src/core/architecture/schema.ts
|
|
5
6
|
const zod_1 = require("zod");
|
|
7
|
+
// Layer schema with enhanced validation
|
|
6
8
|
exports.layerSchema = zod_1.z.object({
|
|
7
|
-
name: zod_1.z.string().min(1),
|
|
8
|
-
path: zod_1.z.string().min(1),
|
|
9
|
+
name: zod_1.z.string().min(1, "Layer name is required"),
|
|
10
|
+
path: zod_1.z.string().min(1, "Layer path is required").refine((p) => !p.startsWith('/'), "Layer path must be relative (no leading slash)"),
|
|
9
11
|
description: zod_1.z.string().optional(),
|
|
10
12
|
allowedImports: zod_1.z.array(zod_1.z.string()).optional(),
|
|
11
13
|
forbiddenImports: zod_1.z.array(zod_1.z.string()).optional(),
|
|
@@ -21,21 +23,62 @@ exports.metadataSchema = zod_1.z.object({
|
|
|
21
23
|
modules: zod_1.z.array(zod_1.z.string()).optional(),
|
|
22
24
|
version: zod_1.z.string().optional(),
|
|
23
25
|
});
|
|
26
|
+
// Enhanced naming schema with regex pattern support
|
|
27
|
+
exports.namingPatternSchema = zod_1.z.object({
|
|
28
|
+
type: zod_1.z.string(),
|
|
29
|
+
match: zod_1.z.string(),
|
|
30
|
+
message: zod_1.z.string().optional(),
|
|
31
|
+
});
|
|
24
32
|
exports.namingSchema = zod_1.z.object({
|
|
25
33
|
services: zod_1.z.string().optional(),
|
|
26
34
|
repositories: zod_1.z.string().optional(),
|
|
27
35
|
controllers: zod_1.z.string().optional(),
|
|
28
36
|
entities: zod_1.z.string().optional(),
|
|
37
|
+
enforcePatterns: zod_1.z.boolean().optional(),
|
|
38
|
+
patterns: zod_1.z.array(exports.namingPatternSchema).optional(),
|
|
39
|
+
});
|
|
40
|
+
// File location rules schema
|
|
41
|
+
exports.fileLocationRuleSchema = zod_1.z.object({
|
|
42
|
+
pattern: zod_1.z.string(),
|
|
43
|
+
allowedPaths: zod_1.z.array(zod_1.z.string()),
|
|
44
|
+
severity: zod_1.z.enum(['error', 'warning', 'info']).default('error'),
|
|
45
|
+
message: zod_1.z.string().optional(),
|
|
46
|
+
});
|
|
47
|
+
// Forbidden package rule schema
|
|
48
|
+
exports.forbiddenPackageSchema = zod_1.z.object({
|
|
49
|
+
name: zod_1.z.string(),
|
|
50
|
+
message: zod_1.z.string().optional(),
|
|
51
|
+
severity: zod_1.z.enum(['error', 'warning']).default('error'),
|
|
29
52
|
});
|
|
53
|
+
// Complexity rules schema
|
|
54
|
+
exports.complexityRulesSchema = zod_1.z.object({
|
|
55
|
+
maxFileLines: zod_1.z.number().positive().optional(),
|
|
56
|
+
maxFunctionLines: zod_1.z.number().positive().optional(),
|
|
57
|
+
maxCyclomaticComplexity: zod_1.z.number().positive().optional(),
|
|
58
|
+
});
|
|
59
|
+
// Import rules schema
|
|
60
|
+
exports.importRulesSchema = zod_1.z.object({
|
|
61
|
+
forbiddenPackages: zod_1.z.array(exports.forbiddenPackageSchema).optional(),
|
|
62
|
+
requireAbsoluteImports: zod_1.z.boolean().optional(),
|
|
63
|
+
});
|
|
64
|
+
// Enhanced rules schema
|
|
30
65
|
exports.rulesSchema = zod_1.z.object({
|
|
31
66
|
forbidDirectDbAccess: zod_1.z.boolean().optional(),
|
|
32
67
|
forbidFrameworkInDomain: zod_1.z.boolean().optional(),
|
|
33
68
|
enforceLayerBoundaries: zod_1.z.boolean().optional(),
|
|
69
|
+
fileLocations: zod_1.z.array(exports.fileLocationRuleSchema).optional(),
|
|
70
|
+
imports: exports.importRulesSchema.optional(),
|
|
71
|
+
complexity: exports.complexityRulesSchema.optional(),
|
|
34
72
|
});
|
|
35
73
|
exports.customStructureSchema = zod_1.z.object({
|
|
36
74
|
enabled: zod_1.z.boolean(),
|
|
37
75
|
folders: zod_1.z.array(zod_1.z.object({ path: zod_1.z.string() })),
|
|
38
76
|
});
|
|
77
|
+
// Ignore configuration schema
|
|
78
|
+
exports.ignoreSchema = zod_1.z.object({
|
|
79
|
+
files: zod_1.z.array(zod_1.z.string()).optional(),
|
|
80
|
+
patterns: zod_1.z.array(zod_1.z.string()).optional(),
|
|
81
|
+
});
|
|
39
82
|
exports.architectureSchema = zod_1.z.object({
|
|
40
83
|
version: zod_1.z.string().default("1.0"),
|
|
41
84
|
name: zod_1.z.string(),
|
|
@@ -45,9 +88,76 @@ exports.architectureSchema = zod_1.z.object({
|
|
|
45
88
|
root: zod_1.z.string().optional(),
|
|
46
89
|
}),
|
|
47
90
|
metadata: exports.metadataSchema,
|
|
48
|
-
layers: zod_1.z.array(exports.layerSchema),
|
|
91
|
+
layers: zod_1.z.array(exports.layerSchema).min(1, "At least one layer must be defined"),
|
|
49
92
|
naming: exports.namingSchema.optional(),
|
|
50
93
|
rules: exports.rulesSchema.optional(),
|
|
51
94
|
customStructure: exports.customStructureSchema.optional(),
|
|
52
95
|
strict: zod_1.z.boolean().optional(),
|
|
96
|
+
ignore: exports.ignoreSchema.optional(),
|
|
53
97
|
});
|
|
98
|
+
/**
|
|
99
|
+
* Validates that all layer references in canImport exist
|
|
100
|
+
*/
|
|
101
|
+
function validateLayerReferences(config) {
|
|
102
|
+
const layerNames = new Set(config.layers.map(l => l.name));
|
|
103
|
+
for (const layer of config.layers) {
|
|
104
|
+
const imports = layer.canImport || layer.allowedImports || [];
|
|
105
|
+
for (const importRef of imports) {
|
|
106
|
+
if (!layerNames.has(importRef)) {
|
|
107
|
+
const suggestions = findSimilarNames(importRef, [...layerNames]);
|
|
108
|
+
const suggestionText = suggestions.length > 0
|
|
109
|
+
? `\n\nDid you mean: "${suggestions[0]}"?`
|
|
110
|
+
: '';
|
|
111
|
+
throw new Error(`Layer "${layer.name}" references unknown layer "${importRef}" in canImport.${suggestionText}\n\n` +
|
|
112
|
+
`Available layers: ${[...layerNames].join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Also check forbiddenImports
|
|
116
|
+
for (const forbidden of layer.forbiddenImports || []) {
|
|
117
|
+
if (!layerNames.has(forbidden)) {
|
|
118
|
+
const suggestions = findSimilarNames(forbidden, [...layerNames]);
|
|
119
|
+
const suggestionText = suggestions.length > 0
|
|
120
|
+
? `\n\nDid you mean: "${suggestions[0]}"?`
|
|
121
|
+
: '';
|
|
122
|
+
throw new Error(`Layer "${layer.name}" references unknown layer "${forbidden}" in forbiddenImports.${suggestionText}\n\n` +
|
|
123
|
+
`Available layers: ${[...layerNames].join(', ')}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Find similar names using Levenshtein distance
|
|
130
|
+
*/
|
|
131
|
+
function findSimilarNames(target, candidates) {
|
|
132
|
+
return candidates
|
|
133
|
+
.map(candidate => ({
|
|
134
|
+
name: candidate,
|
|
135
|
+
distance: levenshteinDistance(target.toLowerCase(), candidate.toLowerCase())
|
|
136
|
+
}))
|
|
137
|
+
.filter(item => item.distance <= 3) // Max 3 character difference
|
|
138
|
+
.sort((a, b) => a.distance - b.distance)
|
|
139
|
+
.map(item => item.name);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Calculate Levenshtein distance between two strings
|
|
143
|
+
*/
|
|
144
|
+
function levenshteinDistance(a, b) {
|
|
145
|
+
const matrix = [];
|
|
146
|
+
for (let i = 0; i <= b.length; i++) {
|
|
147
|
+
matrix[i] = [i];
|
|
148
|
+
}
|
|
149
|
+
for (let j = 0; j <= a.length; j++) {
|
|
150
|
+
matrix[0][j] = j;
|
|
151
|
+
}
|
|
152
|
+
for (let i = 1; i <= b.length; i++) {
|
|
153
|
+
for (let j = 1; j <= a.length; j++) {
|
|
154
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
155
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return matrix[b.length][a.length];
|
|
163
|
+
}
|