@azure-net/cli 4.0.0-1 → 4.1.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/bin/azure-net.js +8 -0
- package/bin/azure-net.ts +16 -0
- package/package.json +1 -1
- package/src/checks/checkDomain.ts +10 -0
- package/src/checks/checkImports.ts +166 -0
- package/src/checks/checkInternal.js +2 -0
- package/src/checks/checkInternal.ts +4 -0
- package/src/checks/checkLayerBoundaries.ts +31 -4
- package/src/checks/checkProject.js +1 -1
- package/src/checks/checkProject.ts +1 -1
- package/src/checks/checkProviderGraph.js +221 -0
- package/src/checks/checkProviderGraph.ts +234 -0
- package/src/checks/index.js +1 -0
- package/src/checks/index.ts +2 -0
- package/src/generate/module/repositoryModuleShared.ts +6 -2
package/bin/azure-net.js
CHANGED
|
@@ -241,6 +241,14 @@ checkCommand
|
|
|
241
241
|
await handler();
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
checkCommand
|
|
245
|
+
.command('provider-graph')
|
|
246
|
+
.description('Check provider dependency graph for cycles')
|
|
247
|
+
.action(async () => {
|
|
248
|
+
const handler = (await import('../src/checks/checkProviderGraph.js')).default;
|
|
249
|
+
await handler();
|
|
250
|
+
});
|
|
251
|
+
|
|
244
252
|
checkCommand
|
|
245
253
|
.command('domain')
|
|
246
254
|
.description('Check domain naming and structure rules in all contexts')
|
package/bin/azure-net.ts
CHANGED
|
@@ -241,6 +241,14 @@ checkCommand
|
|
|
241
241
|
await handler();
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
checkCommand
|
|
245
|
+
.command('provider-graph')
|
|
246
|
+
.description('Check provider dependency graph for cycles')
|
|
247
|
+
.action(async () => {
|
|
248
|
+
const handler = (await import('../src/checks/checkProviderGraph.js')).default;
|
|
249
|
+
await handler();
|
|
250
|
+
});
|
|
251
|
+
|
|
244
252
|
checkCommand
|
|
245
253
|
.command('domain')
|
|
246
254
|
.description('Check domain naming and structure rules in all contexts')
|
|
@@ -249,6 +257,14 @@ checkCommand
|
|
|
249
257
|
await handler();
|
|
250
258
|
});
|
|
251
259
|
|
|
260
|
+
checkCommand
|
|
261
|
+
.command('imports')
|
|
262
|
+
.description('Check import extensions and import group order in src')
|
|
263
|
+
.action(async () => {
|
|
264
|
+
const handler = (await import('../src/checks/checkImports.js')).default;
|
|
265
|
+
await handler();
|
|
266
|
+
});
|
|
267
|
+
|
|
252
268
|
checkCommand
|
|
253
269
|
.command('layer-boundaries')
|
|
254
270
|
.description('Check layer boundaries between contexts by aliases')
|
package/package.json
CHANGED
|
@@ -49,6 +49,8 @@ const isEnumsPath = (filePath: string): boolean => {
|
|
|
49
49
|
|
|
50
50
|
const isIndexFile = (filePath: string): boolean => path.basename(filePath) === 'index.ts';
|
|
51
51
|
|
|
52
|
+
const startsWithInterfacePrefix = (filePath: string): boolean => /^I[A-Z]/.test(path.basename(filePath, '.ts'));
|
|
53
|
+
|
|
52
54
|
const isExportOnlyIndex = (content: string): boolean => {
|
|
53
55
|
const withoutComments = stripComments(content);
|
|
54
56
|
const withoutExports = stripExportStatements(withoutComments);
|
|
@@ -144,6 +146,14 @@ const collectIssuesForFile = (filePath: string, content: string): DomainIssue[]
|
|
|
144
146
|
return issues;
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
if (!isIndexFile(filePath) && startsWithInterfacePrefix(filePath)) {
|
|
150
|
+
issues.push({
|
|
151
|
+
file: filePath,
|
|
152
|
+
line: 1,
|
|
153
|
+
message: 'Domain file names must not start with "I". Keep "I" only in interface/type names'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
147
157
|
const declarations = extractDeclarations(content);
|
|
148
158
|
if (declarations.length > 5) {
|
|
149
159
|
issues.push({
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
type ImportIssue = {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ImportKind = 'value' | 'type' | 'side-effect';
|
|
11
|
+
|
|
12
|
+
type ImportEntry = {
|
|
13
|
+
kind: ImportKind;
|
|
14
|
+
line: number;
|
|
15
|
+
source: string;
|
|
16
|
+
raw: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
|
|
20
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
21
|
+
const IMPORT_DECLARATION_PATTERN = /(^|\n)\s*import\s+(?:[\s\S]*?\s+from\s*)?['"]([^'"]+)['"]\s*;?/g;
|
|
22
|
+
const FORBIDDEN_IMPORT_EXTENSION_PATTERN = /\.(?:ts|js)$/;
|
|
23
|
+
|
|
24
|
+
const toPosixPath = (value: string): string => value.split(path.sep).join('/');
|
|
25
|
+
|
|
26
|
+
const getRelativePath = (filePath: string): string => toPosixPath(path.relative(process.cwd(), filePath));
|
|
27
|
+
|
|
28
|
+
const findLineByIndex = (content: string, index: number): number => {
|
|
29
|
+
let line = 1;
|
|
30
|
+
for (let i = 0; i < index; i += 1) {
|
|
31
|
+
if (content[i] === '\n') line += 1;
|
|
32
|
+
}
|
|
33
|
+
return line;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const listFiles = async (rootDir: string): Promise<string[]> => {
|
|
37
|
+
const files: string[] = [];
|
|
38
|
+
const stack: string[] = [rootDir];
|
|
39
|
+
|
|
40
|
+
while (stack.length) {
|
|
41
|
+
const current = stack.pop();
|
|
42
|
+
if (!current) continue;
|
|
43
|
+
|
|
44
|
+
let entries: import('node:fs').Dirent[] = [];
|
|
45
|
+
try {
|
|
46
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
47
|
+
} catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
54
|
+
stack.push(path.join(current, entry.name));
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!entry.isFile()) continue;
|
|
60
|
+
const filePath = path.join(current, entry.name);
|
|
61
|
+
if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
62
|
+
files.push(filePath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return files;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getImportKind = (raw: string): ImportKind => {
|
|
70
|
+
const normalized = raw.trim();
|
|
71
|
+
if (/^import\s+type\b/.test(normalized)) return 'type';
|
|
72
|
+
if (/^import\s*['"]/.test(normalized)) return 'side-effect';
|
|
73
|
+
return 'value';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getKindOrder = (kind: ImportKind): number => {
|
|
77
|
+
if (kind === 'value') return 0;
|
|
78
|
+
if (kind === 'type') return 1;
|
|
79
|
+
return 2;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const extractImports = (content: string): ImportEntry[] => {
|
|
83
|
+
const imports: ImportEntry[] = [];
|
|
84
|
+
const regex = new RegExp(IMPORT_DECLARATION_PATTERN.source, 'g');
|
|
85
|
+
let match: RegExpExecArray | null;
|
|
86
|
+
|
|
87
|
+
while ((match = regex.exec(content)) !== null) {
|
|
88
|
+
const raw = match[0].trim();
|
|
89
|
+
const source = match[2];
|
|
90
|
+
if (!source) continue;
|
|
91
|
+
|
|
92
|
+
imports.push({
|
|
93
|
+
kind: getImportKind(raw),
|
|
94
|
+
line: findLineByIndex(content, match.index + (match[1] === '\n' ? 1 : 0)),
|
|
95
|
+
source,
|
|
96
|
+
raw
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return imports.sort((a, b) => a.line - b.line);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const hasForbiddenExtension = (source: string): boolean => {
|
|
104
|
+
const sourceWithoutQuery = source.split('?')[0] ?? source;
|
|
105
|
+
return FORBIDDEN_IMPORT_EXTENSION_PATTERN.test(sourceWithoutQuery);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const collectIssuesForFile = (filePath: string, content: string): ImportIssue[] => {
|
|
109
|
+
const issues: ImportIssue[] = [];
|
|
110
|
+
const imports = extractImports(content);
|
|
111
|
+
let lastKindOrder = 0;
|
|
112
|
+
|
|
113
|
+
for (const importEntry of imports) {
|
|
114
|
+
if (hasForbiddenExtension(importEntry.source)) {
|
|
115
|
+
issues.push({
|
|
116
|
+
file: filePath,
|
|
117
|
+
line: importEntry.line,
|
|
118
|
+
message: `Import "${importEntry.source}" must not include .ts/.js extension`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const currentOrder = getKindOrder(importEntry.kind);
|
|
123
|
+
if (currentOrder < lastKindOrder) {
|
|
124
|
+
issues.push({
|
|
125
|
+
file: filePath,
|
|
126
|
+
line: importEntry.line,
|
|
127
|
+
message: 'Import order violation: value imports first, then type imports, then side-effect imports'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
lastKindOrder = Math.max(lastKindOrder, currentOrder);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return issues;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default async function checkImports(): Promise<void> {
|
|
137
|
+
const srcRoot = path.join(process.cwd(), 'src');
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await fs.access(srcRoot);
|
|
141
|
+
} catch {
|
|
142
|
+
console.error('❌ src folder not found.');
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const issues: ImportIssue[] = [];
|
|
148
|
+
const files = await listFiles(srcRoot);
|
|
149
|
+
|
|
150
|
+
for (const filePath of files) {
|
|
151
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
152
|
+
issues.push(...collectIssuesForFile(filePath, content));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!issues.length) {
|
|
156
|
+
console.log('✅ Imports check passed: extensions and import groups are valid.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.error('❌ Imports check failed:');
|
|
161
|
+
for (const issue of issues) {
|
|
162
|
+
console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import checkPresenterNames from './checkPresenterNames.js';
|
|
2
2
|
import checkProviderNames from './checkProviderNames.js';
|
|
3
|
+
import checkProviderGraph from './checkProviderGraph.js';
|
|
3
4
|
import checkDomain from './checkDomain.js';
|
|
4
5
|
import checkLayerBoundaries from './checkLayerBoundaries.js';
|
|
5
6
|
import checkFoldersStructure from './checkFoldersStructure.js';
|
|
@@ -7,6 +8,7 @@ import checkFoldersStructure from './checkFoldersStructure.js';
|
|
|
7
8
|
const tasks = [
|
|
8
9
|
{ name: 'presenter-names', run: checkPresenterNames },
|
|
9
10
|
{ name: 'provider-names', run: checkProviderNames },
|
|
11
|
+
{ name: 'provider-graph', run: checkProviderGraph },
|
|
10
12
|
{ name: 'domain', run: checkDomain },
|
|
11
13
|
{ name: 'layer-boundaries', run: checkLayerBoundaries },
|
|
12
14
|
{ name: 'folders-structure', run: checkFoldersStructure }
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import checkPresenterNames from './checkPresenterNames.js';
|
|
2
2
|
import checkProviderNames from './checkProviderNames.js';
|
|
3
|
+
import checkProviderGraph from './checkProviderGraph.js';
|
|
3
4
|
import checkDomain from './checkDomain.js';
|
|
4
5
|
import checkLayerBoundaries from './checkLayerBoundaries.js';
|
|
5
6
|
import checkFoldersStructure from './checkFoldersStructure.js';
|
|
7
|
+
import checkImports from './checkImports.js';
|
|
6
8
|
|
|
7
9
|
type CheckTask = {
|
|
8
10
|
name: string;
|
|
@@ -12,7 +14,9 @@ type CheckTask = {
|
|
|
12
14
|
const tasks: CheckTask[] = [
|
|
13
15
|
{ name: 'presenter-names', run: checkPresenterNames },
|
|
14
16
|
{ name: 'provider-names', run: checkProviderNames },
|
|
17
|
+
{ name: 'provider-graph', run: checkProviderGraph },
|
|
15
18
|
{ name: 'domain', run: checkDomain },
|
|
19
|
+
{ name: 'imports', run: checkImports },
|
|
16
20
|
{ name: 'layer-boundaries', run: checkLayerBoundaries },
|
|
17
21
|
{ name: 'folders-structure', run: checkFoldersStructure }
|
|
18
22
|
];
|
|
@@ -131,18 +131,45 @@ export default async function checkLayerBoundaries(): Promise<void> {
|
|
|
131
131
|
|
|
132
132
|
const issues: BoundaryIssue[] = [];
|
|
133
133
|
|
|
134
|
+
const coreRoot = path.join(process.cwd(), 'src', 'core');
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(coreRoot);
|
|
137
|
+
const coreFiles = await listFiles(coreRoot);
|
|
138
|
+
for (const filePath of coreFiles) {
|
|
139
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
140
|
+
const imports = extractImports(content);
|
|
141
|
+
|
|
142
|
+
for (const entry of imports) {
|
|
143
|
+
const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
|
|
144
|
+
if (!targetAlias || targetAlias === coreAlias) continue;
|
|
145
|
+
if (!aliasToContext.has(targetAlias)) continue;
|
|
146
|
+
|
|
147
|
+
const targetContext = aliasToContext.get(targetAlias);
|
|
148
|
+
issues.push({
|
|
149
|
+
file: filePath,
|
|
150
|
+
line: entry.line,
|
|
151
|
+
message: targetContext
|
|
152
|
+
? `Layer boundary violation: core cannot import from context "${targetContext.name}" (${targetAlias})`
|
|
153
|
+
: `Layer boundary violation: core cannot import from "${targetAlias}"`
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Core folder is optional for the check.
|
|
159
|
+
}
|
|
160
|
+
|
|
134
161
|
for (const context of contexts) {
|
|
135
|
-
const
|
|
162
|
+
const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
|
|
136
163
|
|
|
137
164
|
try {
|
|
138
|
-
await fs.access(
|
|
165
|
+
await fs.access(contextRoot);
|
|
139
166
|
} catch {
|
|
140
167
|
continue;
|
|
141
168
|
}
|
|
142
169
|
|
|
143
|
-
const files = await listFiles(
|
|
170
|
+
const files = await listFiles(contextRoot);
|
|
144
171
|
const allowedAliases = new Set<string>([context.alias, coreAlias]);
|
|
145
|
-
if (sharedAlias) {
|
|
172
|
+
if (sharedAlias && context.alias !== sharedAlias) {
|
|
146
173
|
allowedAliases.add(sharedAlias);
|
|
147
174
|
}
|
|
148
175
|
|
|
@@ -33,7 +33,7 @@ export default async function checkProject() {
|
|
|
33
33
|
runScript(manager, 'format');
|
|
34
34
|
await runInternalChecks();
|
|
35
35
|
runScript(manager, 'lint');
|
|
36
|
-
runScript(manager, '
|
|
36
|
+
runScript(manager, 'check');
|
|
37
37
|
console.log('\n✅ Project checks passed.');
|
|
38
38
|
} catch (error) {
|
|
39
39
|
console.error(`\n❌ Project checks failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -36,7 +36,7 @@ export default async function checkProject(): Promise<void> {
|
|
|
36
36
|
runScript(manager, 'format');
|
|
37
37
|
await runInternalChecks();
|
|
38
38
|
runScript(manager, 'lint');
|
|
39
|
-
runScript(manager, '
|
|
39
|
+
runScript(manager, 'check');
|
|
40
40
|
console.log('\n✅ Project checks passed.');
|
|
41
41
|
} catch (error) {
|
|
42
42
|
console.error(`\n❌ Project checks failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const TARGET_EXTENSIONS = new Set(['.ts', '.js']);
|
|
5
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
6
|
+
|
|
7
|
+
const toPosixPath = (value) => value.split(path.sep).join('/');
|
|
8
|
+
|
|
9
|
+
const getLineByIndex = (content, index) => {
|
|
10
|
+
let line = 1;
|
|
11
|
+
for (let i = 0; i < index; i += 1) {
|
|
12
|
+
if (content[i] === '\n') line += 1;
|
|
13
|
+
}
|
|
14
|
+
return line;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const listSourceFiles = async (rootDir) => {
|
|
18
|
+
const files = [];
|
|
19
|
+
const stack = [rootDir];
|
|
20
|
+
|
|
21
|
+
while (stack.length) {
|
|
22
|
+
const current = stack.pop();
|
|
23
|
+
if (!current) continue;
|
|
24
|
+
|
|
25
|
+
let entries = [];
|
|
26
|
+
try {
|
|
27
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
28
|
+
} catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (!IGNORED_DIRS.has(entry.name)) stack.push(path.join(current, entry.name));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!entry.isFile()) continue;
|
|
39
|
+
const filePath = path.join(current, entry.name);
|
|
40
|
+
if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
41
|
+
files.push(filePath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return files;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const findMatchingBracket = (content, startIndex, openChar, closeChar) => {
|
|
49
|
+
let depth = 0;
|
|
50
|
+
for (let i = startIndex; i < content.length; i += 1) {
|
|
51
|
+
const ch = content[i];
|
|
52
|
+
if (ch === openChar) depth += 1;
|
|
53
|
+
if (ch === closeChar) {
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth === 0) return i;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return -1;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const extractDependsOn = (configBlock) => {
|
|
62
|
+
const dependsOnMatch = configBlock.match(/dependsOn\s*:\s*\{([\s\S]*?)\}/m);
|
|
63
|
+
if (!dependsOnMatch?.[1]) return [];
|
|
64
|
+
|
|
65
|
+
const block = dependsOnMatch[1];
|
|
66
|
+
const deps = new Set();
|
|
67
|
+
const keyRegex = /([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = keyRegex.exec(block)) !== null) {
|
|
70
|
+
const key = match[1];
|
|
71
|
+
if (key) deps.add(key);
|
|
72
|
+
}
|
|
73
|
+
return [...deps];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const parseProviderGraphFromFile = (content, filePath) => {
|
|
77
|
+
const nodes = [];
|
|
78
|
+
const token = 'createBoundaryProvider(';
|
|
79
|
+
let fromIndex = 0;
|
|
80
|
+
|
|
81
|
+
while (fromIndex < content.length) {
|
|
82
|
+
const start = content.indexOf(token, fromIndex);
|
|
83
|
+
if (start === -1) break;
|
|
84
|
+
|
|
85
|
+
const openParenIndex = start + token.length - 1;
|
|
86
|
+
const closeParenIndex = findMatchingBracket(content, openParenIndex, '(', ')');
|
|
87
|
+
if (closeParenIndex === -1) break;
|
|
88
|
+
|
|
89
|
+
const fullCall = content.slice(openParenIndex + 1, closeParenIndex);
|
|
90
|
+
const idMatch = fullCall.match(/^\s*(['"`])([^'"`]+)\1\s*,/m);
|
|
91
|
+
const providerName = idMatch?.[2];
|
|
92
|
+
if (providerName) {
|
|
93
|
+
const objectStartInCall = fullCall.indexOf('{');
|
|
94
|
+
if (objectStartInCall !== -1) {
|
|
95
|
+
const absoluteObjectStart = openParenIndex + 1 + objectStartInCall;
|
|
96
|
+
const absoluteObjectEnd = findMatchingBracket(content, absoluteObjectStart, '{', '}');
|
|
97
|
+
const configBlock =
|
|
98
|
+
absoluteObjectEnd !== -1 ? content.slice(absoluteObjectStart, absoluteObjectEnd + 1) : fullCall.slice(objectStartInCall);
|
|
99
|
+
|
|
100
|
+
nodes.push({
|
|
101
|
+
name: providerName,
|
|
102
|
+
file: filePath,
|
|
103
|
+
line: getLineByIndex(content, start),
|
|
104
|
+
dependsOn: extractDependsOn(configBlock)
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
nodes.push({
|
|
108
|
+
name: providerName,
|
|
109
|
+
file: filePath,
|
|
110
|
+
line: getLineByIndex(content, start),
|
|
111
|
+
dependsOn: []
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fromIndex = closeParenIndex + 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return nodes;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const findCycles = (graph) => {
|
|
123
|
+
const cycles = [];
|
|
124
|
+
const visited = new Set();
|
|
125
|
+
const inStack = new Set();
|
|
126
|
+
const stack = [];
|
|
127
|
+
|
|
128
|
+
const dfs = (node) => {
|
|
129
|
+
visited.add(node);
|
|
130
|
+
inStack.add(node);
|
|
131
|
+
stack.push(node);
|
|
132
|
+
|
|
133
|
+
const neighbors = graph.get(node) ?? [];
|
|
134
|
+
for (const next of neighbors) {
|
|
135
|
+
if (!graph.has(next)) continue;
|
|
136
|
+
if (!visited.has(next)) {
|
|
137
|
+
dfs(next);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inStack.has(next)) {
|
|
141
|
+
const idx = stack.indexOf(next);
|
|
142
|
+
if (idx !== -1) cycles.push([...stack.slice(idx), next]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stack.pop();
|
|
147
|
+
inStack.delete(node);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
for (const node of graph.keys()) {
|
|
151
|
+
if (!visited.has(node)) dfs(node);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return cycles;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default async function checkProviderGraph() {
|
|
158
|
+
const srcPath = path.join(process.cwd(), 'src');
|
|
159
|
+
try {
|
|
160
|
+
await fs.access(srcPath);
|
|
161
|
+
} catch {
|
|
162
|
+
console.error('❌ src folder not found in current workspace.');
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const files = await listSourceFiles(srcPath);
|
|
168
|
+
const providers = [];
|
|
169
|
+
|
|
170
|
+
for (const filePath of files) {
|
|
171
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
172
|
+
providers.push(...parseProviderGraphFromFile(content, filePath));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!providers.length) {
|
|
176
|
+
console.log('✅ No createBoundaryProvider declarations found.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const graph = new Map();
|
|
181
|
+
const issues = [];
|
|
182
|
+
const byName = new Map();
|
|
183
|
+
|
|
184
|
+
for (const provider of providers) {
|
|
185
|
+
if (byName.has(provider.name)) {
|
|
186
|
+
const existing = byName.get(provider.name);
|
|
187
|
+
issues.push({
|
|
188
|
+
file: provider.file,
|
|
189
|
+
line: provider.line,
|
|
190
|
+
message: `Provider graph ambiguity: duplicate provider id "${provider.name}" (first declared at ${toPosixPath(
|
|
191
|
+
path.relative(process.cwd(), existing.file)
|
|
192
|
+
)}:${existing.line})`
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
byName.set(provider.name, provider);
|
|
197
|
+
graph.set(provider.name, provider.dependsOn);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const cycles = findCycles(graph);
|
|
201
|
+
for (const cycle of cycles) {
|
|
202
|
+
const firstNode = byName.get(cycle[0] ?? '');
|
|
203
|
+
issues.push({
|
|
204
|
+
file: firstNode?.file ?? srcPath,
|
|
205
|
+
line: firstNode?.line ?? 1,
|
|
206
|
+
message: `Circular provider dependency detected: ${cycle.join(' -> ')}`
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!issues.length) {
|
|
211
|
+
console.log(`✅ Provider graph check passed (${providers.length} providers).`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.error('❌ Provider graph check failed:');
|
|
216
|
+
for (const issue of issues) {
|
|
217
|
+
console.error(`• ${toPosixPath(path.relative(process.cwd(), issue.file))}:${issue.line} — ${issue.message}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
type ProviderNode = {
|
|
5
|
+
name: string;
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
dependsOn: string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type GraphIssue = {
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TARGET_EXTENSIONS = new Set(['.ts', '.js']);
|
|
18
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
19
|
+
|
|
20
|
+
const toPosixPath = (value: string): string => value.split(path.sep).join('/');
|
|
21
|
+
|
|
22
|
+
const getLineByIndex = (content: string, index: number): number => {
|
|
23
|
+
let line = 1;
|
|
24
|
+
for (let i = 0; i < index; i += 1) {
|
|
25
|
+
if (content[i] === '\n') line += 1;
|
|
26
|
+
}
|
|
27
|
+
return line;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const listSourceFiles = async (rootDir: string): Promise<string[]> => {
|
|
31
|
+
const files: string[] = [];
|
|
32
|
+
const stack: string[] = [rootDir];
|
|
33
|
+
|
|
34
|
+
while (stack.length) {
|
|
35
|
+
const current = stack.pop();
|
|
36
|
+
if (!current) continue;
|
|
37
|
+
|
|
38
|
+
let entries: import('node:fs').Dirent[] = [];
|
|
39
|
+
try {
|
|
40
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
if (!IGNORED_DIRS.has(entry.name)) stack.push(path.join(current, entry.name));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!entry.isFile()) continue;
|
|
52
|
+
const filePath = path.join(current, entry.name);
|
|
53
|
+
if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
54
|
+
files.push(filePath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return files;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const findMatchingBracket = (content: string, startIndex: number, openChar: string, closeChar: string): number => {
|
|
62
|
+
let depth = 0;
|
|
63
|
+
for (let i = startIndex; i < content.length; i += 1) {
|
|
64
|
+
const ch = content[i];
|
|
65
|
+
if (ch === openChar) depth += 1;
|
|
66
|
+
if (ch === closeChar) {
|
|
67
|
+
depth -= 1;
|
|
68
|
+
if (depth === 0) return i;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const extractDependsOn = (configBlock: string): string[] => {
|
|
75
|
+
const dependsOnMatch = configBlock.match(/dependsOn\s*:\s*\{([\s\S]*?)\}/m);
|
|
76
|
+
if (!dependsOnMatch?.[1]) return [];
|
|
77
|
+
|
|
78
|
+
const block = dependsOnMatch[1];
|
|
79
|
+
const deps = new Set<string>();
|
|
80
|
+
const keyRegex = /([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g;
|
|
81
|
+
let match: RegExpExecArray | null;
|
|
82
|
+
while ((match = keyRegex.exec(block)) !== null) {
|
|
83
|
+
const key = match[1];
|
|
84
|
+
if (key) deps.add(key);
|
|
85
|
+
}
|
|
86
|
+
return [...deps];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const parseProviderGraphFromFile = (content: string, filePath: string): ProviderNode[] => {
|
|
90
|
+
const nodes: ProviderNode[] = [];
|
|
91
|
+
const token = 'createBoundaryProvider(';
|
|
92
|
+
let fromIndex = 0;
|
|
93
|
+
|
|
94
|
+
while (fromIndex < content.length) {
|
|
95
|
+
const start = content.indexOf(token, fromIndex);
|
|
96
|
+
if (start === -1) break;
|
|
97
|
+
|
|
98
|
+
const openParenIndex = start + token.length - 1;
|
|
99
|
+
const closeParenIndex = findMatchingBracket(content, openParenIndex, '(', ')');
|
|
100
|
+
if (closeParenIndex === -1) break;
|
|
101
|
+
|
|
102
|
+
const fullCall = content.slice(openParenIndex + 1, closeParenIndex);
|
|
103
|
+
const idMatch = fullCall.match(/^\s*(['"`])([^'"`]+)\1\s*,/m);
|
|
104
|
+
const providerName = idMatch?.[2];
|
|
105
|
+
if (providerName) {
|
|
106
|
+
const objectStartInCall = fullCall.indexOf('{');
|
|
107
|
+
if (objectStartInCall !== -1) {
|
|
108
|
+
const absoluteObjectStart = openParenIndex + 1 + objectStartInCall;
|
|
109
|
+
const absoluteObjectEnd = findMatchingBracket(content, absoluteObjectStart, '{', '}');
|
|
110
|
+
const configBlock =
|
|
111
|
+
absoluteObjectEnd !== -1 ? content.slice(absoluteObjectStart, absoluteObjectEnd + 1) : fullCall.slice(objectStartInCall);
|
|
112
|
+
|
|
113
|
+
nodes.push({
|
|
114
|
+
name: providerName,
|
|
115
|
+
file: filePath,
|
|
116
|
+
line: getLineByIndex(content, start),
|
|
117
|
+
dependsOn: extractDependsOn(configBlock)
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
nodes.push({
|
|
121
|
+
name: providerName,
|
|
122
|
+
file: filePath,
|
|
123
|
+
line: getLineByIndex(content, start),
|
|
124
|
+
dependsOn: []
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fromIndex = closeParenIndex + 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return nodes;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const findCycles = (graph: Map<string, string[]>): string[][] => {
|
|
136
|
+
const cycles: string[][] = [];
|
|
137
|
+
const visited = new Set<string>();
|
|
138
|
+
const inStack = new Set<string>();
|
|
139
|
+
const stack: string[] = [];
|
|
140
|
+
|
|
141
|
+
const dfs = (node: string): void => {
|
|
142
|
+
visited.add(node);
|
|
143
|
+
inStack.add(node);
|
|
144
|
+
stack.push(node);
|
|
145
|
+
|
|
146
|
+
const neighbors = graph.get(node) ?? [];
|
|
147
|
+
for (const next of neighbors) {
|
|
148
|
+
if (!graph.has(next)) continue;
|
|
149
|
+
if (!visited.has(next)) {
|
|
150
|
+
dfs(next);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (inStack.has(next)) {
|
|
154
|
+
const idx = stack.indexOf(next);
|
|
155
|
+
if (idx !== -1) cycles.push([...stack.slice(idx), next]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
stack.pop();
|
|
160
|
+
inStack.delete(node);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
for (const node of graph.keys()) {
|
|
164
|
+
if (!visited.has(node)) dfs(node);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return cycles;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default async function checkProviderGraph(): Promise<void> {
|
|
171
|
+
const srcPath = path.join(process.cwd(), 'src');
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(srcPath);
|
|
174
|
+
} catch {
|
|
175
|
+
console.error('❌ src folder not found in current workspace.');
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const files = await listSourceFiles(srcPath);
|
|
181
|
+
const providers: ProviderNode[] = [];
|
|
182
|
+
|
|
183
|
+
for (const filePath of files) {
|
|
184
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
185
|
+
providers.push(...parseProviderGraphFromFile(content, filePath));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!providers.length) {
|
|
189
|
+
console.log('✅ No createBoundaryProvider declarations found.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const graph = new Map<string, string[]>();
|
|
194
|
+
const issues: GraphIssue[] = [];
|
|
195
|
+
const byName = new Map<string, ProviderNode>();
|
|
196
|
+
|
|
197
|
+
for (const provider of providers) {
|
|
198
|
+
if (byName.has(provider.name)) {
|
|
199
|
+
const existing = byName.get(provider.name)!;
|
|
200
|
+
issues.push({
|
|
201
|
+
file: provider.file,
|
|
202
|
+
line: provider.line,
|
|
203
|
+
message: `Provider graph ambiguity: duplicate provider id "${provider.name}" (first declared at ${toPosixPath(
|
|
204
|
+
path.relative(process.cwd(), existing.file)
|
|
205
|
+
)}:${existing.line})`
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
byName.set(provider.name, provider);
|
|
210
|
+
graph.set(provider.name, provider.dependsOn);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cycles = findCycles(graph);
|
|
214
|
+
for (const cycle of cycles) {
|
|
215
|
+
const firstNode = byName.get(cycle[0] ?? '');
|
|
216
|
+
issues.push({
|
|
217
|
+
file: firstNode?.file ?? srcPath,
|
|
218
|
+
line: firstNode?.line ?? 1,
|
|
219
|
+
message: `Circular provider dependency detected: ${cycle.join(' -> ')}`
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!issues.length) {
|
|
224
|
+
console.log(`✅ Provider graph check passed (${providers.length} providers).`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.error('❌ Provider graph check failed:');
|
|
229
|
+
for (const issue of issues) {
|
|
230
|
+
console.error(`• ${toPosixPath(path.relative(process.cwd(), issue.file))}:${issue.line} — ${issue.message}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
}
|
package/src/checks/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as checkPresenterNames } from './checkPresenterNames.js';
|
|
2
2
|
export { default as checkProviderNames } from './checkProviderNames.js';
|
|
3
|
+
export { default as checkProviderGraph } from './checkProviderGraph.js';
|
|
3
4
|
export { default as checkDomain } from './checkDomain.js';
|
|
4
5
|
export { default as checkLayerBoundaries } from './checkLayerBoundaries.js';
|
|
5
6
|
export { default as checkFoldersStructure } from './checkFoldersStructure.js';
|
package/src/checks/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default as checkPresenterNames } from './checkPresenterNames.js';
|
|
2
2
|
export { default as checkProviderNames } from './checkProviderNames.js';
|
|
3
|
+
export { default as checkProviderGraph } from './checkProviderGraph.js';
|
|
3
4
|
export { default as checkDomain } from './checkDomain.js';
|
|
5
|
+
export { default as checkImports } from './checkImports.js';
|
|
4
6
|
export { default as checkLayerBoundaries } from './checkLayerBoundaries.js';
|
|
5
7
|
export { default as checkFoldersStructure } from './checkFoldersStructure.js';
|
|
6
8
|
export { default as checkInternal } from './checkInternal.js';
|
|
@@ -564,7 +564,7 @@ export const writeRepositoryInterface = async ({
|
|
|
564
564
|
.join('\n');
|
|
565
565
|
|
|
566
566
|
const content = `${imports.join('\n')}${imports.length ? '\n\n' : ''}export interface ${meta.repositoryInterfaceName} {\n${signatures ? `${signatures}\n` : ''}}\n`;
|
|
567
|
-
await fs.writeFile(path.join(portsPath, `${meta.repositoryInterfaceName}.ts`), content, 'utf-8');
|
|
567
|
+
await fs.writeFile(path.join(portsPath, `${toTypeFileName(meta.repositoryInterfaceName)}.ts`), content, 'utf-8');
|
|
568
568
|
await updateIndexTs(portsPath);
|
|
569
569
|
};
|
|
570
570
|
|
|
@@ -598,7 +598,11 @@ export const createUseCasesForRepository = async ({
|
|
|
598
598
|
repositoryName: string;
|
|
599
599
|
}): Promise<boolean> => {
|
|
600
600
|
const meta = getRepositoryMeta(repositoryName);
|
|
601
|
-
const interfacePath = path.join(
|
|
601
|
+
const interfacePath = path.join(
|
|
602
|
+
resolveDomainRootPath(contextName, domainName),
|
|
603
|
+
'ports',
|
|
604
|
+
`${toTypeFileName(meta.repositoryInterfaceName)}.ts`
|
|
605
|
+
);
|
|
602
606
|
|
|
603
607
|
let interfaceContent = '';
|
|
604
608
|
try {
|