@azure-net/cli 4.0.0-1 → 4.1.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/bin/azure-net.js CHANGED
@@ -5,7 +5,7 @@ import { addLint } from '../src/plugins/index.js';
5
5
 
6
6
  const program = new Command();
7
7
 
8
- program.name('azure-net').description('Azure-Net CLI generator').version('2.1.0');
8
+ program.name('azure-net').description('Azure-Net CLI generator').version('4.1.0');
9
9
 
10
10
  const addCommand = program.command('add').description('Add project integrations and plugins');
11
11
  const createCommand = program.command('create').description('Create project boilerplate artifacts');
@@ -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/bin/azure-net.ts CHANGED
@@ -5,7 +5,7 @@ import { addLint } from '../src/plugins/index.js';
5
5
 
6
6
  const program = new Command();
7
7
 
8
- program.name('azure-net').description('Azure-Net CLI generator').version('4.0.0');
8
+ program.name('azure-net').description('Azure-Net CLI generator').version('4.1.0');
9
9
 
10
10
  const addCommand = program.command('add').description('Add project integrations and plugins');
11
11
  const createCommand = program.command('create').description('Create project boilerplate artifacts');
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-net/cli",
3
- "version": "4.0.0-1",
3
+ "version": "4.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "azure-net": "bin/azure-net.js"
@@ -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,147 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
4
+ const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
5
+ const IMPORT_DECLARATION_PATTERN = /(^|\n)\s*import\s+(?:[\s\S]*?\s+from\s*)?['"]([^'"]+)['"]\s*;?/g;
6
+ const EXPORT_DECLARATION_PATTERN = /(^|\n)\s*export\s+(?:type\s+)?(?:[\s\S]*?\s+from\s*)['"]([^'"]+)['"]\s*;?/g;
7
+ const FORBIDDEN_IMPORT_EXTENSION_PATTERN = /\.(?:ts|js)(?:$|[?#])/;
8
+ const toPosixPath = (value) => value.split(path.sep).join('/');
9
+ const getRelativePath = (filePath) => toPosixPath(path.relative(process.cwd(), filePath));
10
+ const findLineByIndex = (content, index) => {
11
+ let line = 1;
12
+ for (let i = 0; i < index; i += 1) {
13
+ if (content[i] === '\n') line += 1;
14
+ }
15
+ return line;
16
+ };
17
+ const listFiles = async (rootDir) => {
18
+ const files = [];
19
+ const stack = [rootDir];
20
+ while (stack.length) {
21
+ const current = stack.pop();
22
+ if (!current) continue;
23
+ let entries = [];
24
+ try {
25
+ entries = await fs.readdir(current, { withFileTypes: true });
26
+ } catch {
27
+ continue;
28
+ }
29
+ for (const entry of entries) {
30
+ if (entry.isDirectory()) {
31
+ if (!IGNORED_DIRS.has(entry.name)) {
32
+ stack.push(path.join(current, entry.name));
33
+ }
34
+ continue;
35
+ }
36
+ if (!entry.isFile()) continue;
37
+ const filePath = path.join(current, entry.name);
38
+ if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
39
+ files.push(filePath);
40
+ }
41
+ }
42
+ return files;
43
+ };
44
+ const getImportKind = (raw) => {
45
+ const normalized = raw.trim();
46
+ if (/^import\s+type\b/.test(normalized)) return 'type';
47
+ if (/^import\s*['"]/.test(normalized)) return 'side-effect';
48
+ return 'value';
49
+ };
50
+ const getKindOrder = (kind) => {
51
+ if (kind === 'value') return 0;
52
+ if (kind === 'type') return 1;
53
+ return 2;
54
+ };
55
+ const extractImports = (content) => {
56
+ const imports = [];
57
+ const regex = new RegExp(IMPORT_DECLARATION_PATTERN.source, 'g');
58
+ let match;
59
+ while ((match = regex.exec(content)) !== null) {
60
+ const raw = match[0].trim();
61
+ const source = match[2];
62
+ if (!source) continue;
63
+ imports.push({
64
+ kind: getImportKind(raw),
65
+ line: findLineByIndex(content, match.index + (match[1] === '\n' ? 1 : 0)),
66
+ source,
67
+ raw
68
+ });
69
+ }
70
+ return imports.sort((a, b) => a.line - b.line);
71
+ };
72
+ const extractExportSources = (content) => {
73
+ const exports = [];
74
+ const regex = new RegExp(EXPORT_DECLARATION_PATTERN.source, 'g');
75
+ let match;
76
+ while ((match = regex.exec(content)) !== null) {
77
+ const source = match[2];
78
+ if (!source) continue;
79
+ exports.push({
80
+ source,
81
+ line: findLineByIndex(content, match.index + (match[1] === '\n' ? 1 : 0))
82
+ });
83
+ }
84
+ return exports;
85
+ };
86
+ const hasForbiddenExtension = (source) => {
87
+ const sourceWithoutQuery = source.split('?')[0] ?? source;
88
+ return FORBIDDEN_IMPORT_EXTENSION_PATTERN.test(sourceWithoutQuery);
89
+ };
90
+ const collectIssuesForFile = (filePath, content) => {
91
+ const issues = [];
92
+ const imports = extractImports(content);
93
+ let lastKindOrder = 0;
94
+ for (const importEntry of imports) {
95
+ if (hasForbiddenExtension(importEntry.source)) {
96
+ issues.push({
97
+ file: filePath,
98
+ line: importEntry.line,
99
+ message: `Import "${importEntry.source}" must not include .ts/.js extension`
100
+ });
101
+ }
102
+ const currentOrder = getKindOrder(importEntry.kind);
103
+ if (currentOrder < lastKindOrder) {
104
+ issues.push({
105
+ file: filePath,
106
+ line: importEntry.line,
107
+ message: 'Import order violation: value imports first, then type imports, then side-effect imports'
108
+ });
109
+ }
110
+ lastKindOrder = Math.max(lastKindOrder, currentOrder);
111
+ }
112
+ for (const exportEntry of extractExportSources(content)) {
113
+ if (!hasForbiddenExtension(exportEntry.source)) continue;
114
+ issues.push({
115
+ file: filePath,
116
+ line: exportEntry.line,
117
+ message: `Export source "${exportEntry.source}" must not include .ts/.js extension`
118
+ });
119
+ }
120
+ return issues;
121
+ };
122
+ export default async function checkImports() {
123
+ const srcRoot = path.join(process.cwd(), 'src');
124
+ try {
125
+ await fs.access(srcRoot);
126
+ } catch {
127
+ console.error('❌ src folder not found.');
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ const issues = [];
132
+ const files = await listFiles(srcRoot);
133
+ for (const filePath of files) {
134
+ const content = await fs.readFile(filePath, 'utf-8');
135
+ issues.push(...collectIssuesForFile(filePath, content));
136
+ }
137
+ if (!issues.length) {
138
+ console.log('✅ Imports check passed: extensions and import groups are valid.');
139
+ return;
140
+ }
141
+ console.error('❌ Imports check failed:');
142
+ for (const issue of issues) {
143
+ console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
144
+ }
145
+ process.exitCode = 1;
146
+ }
147
+ //# sourceMappingURL=checkImports.js.map
@@ -0,0 +1,193 @@
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 EXPORT_DECLARATION_PATTERN = /(^|\n)\s*export\s+(?:type\s+)?(?:[\s\S]*?\s+from\s*)['"]([^'"]+)['"]\s*;?/g;
23
+ const FORBIDDEN_IMPORT_EXTENSION_PATTERN = /\.(?:ts|js)(?:$|[?#])/;
24
+
25
+ const toPosixPath = (value: string): string => value.split(path.sep).join('/');
26
+
27
+ const getRelativePath = (filePath: string): string => toPosixPath(path.relative(process.cwd(), filePath));
28
+
29
+ const findLineByIndex = (content: string, index: number): number => {
30
+ let line = 1;
31
+ for (let i = 0; i < index; i += 1) {
32
+ if (content[i] === '\n') line += 1;
33
+ }
34
+ return line;
35
+ };
36
+
37
+ const listFiles = async (rootDir: string): Promise<string[]> => {
38
+ const files: string[] = [];
39
+ const stack: string[] = [rootDir];
40
+
41
+ while (stack.length) {
42
+ const current = stack.pop();
43
+ if (!current) continue;
44
+
45
+ let entries: import('node:fs').Dirent[] = [];
46
+ try {
47
+ entries = await fs.readdir(current, { withFileTypes: true });
48
+ } catch {
49
+ continue;
50
+ }
51
+
52
+ for (const entry of entries) {
53
+ if (entry.isDirectory()) {
54
+ if (!IGNORED_DIRS.has(entry.name)) {
55
+ stack.push(path.join(current, entry.name));
56
+ }
57
+ continue;
58
+ }
59
+
60
+ if (!entry.isFile()) continue;
61
+ const filePath = path.join(current, entry.name);
62
+ if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
63
+ files.push(filePath);
64
+ }
65
+ }
66
+
67
+ return files;
68
+ };
69
+
70
+ const getImportKind = (raw: string): ImportKind => {
71
+ const normalized = raw.trim();
72
+ if (/^import\s+type\b/.test(normalized)) return 'type';
73
+ if (/^import\s*['"]/.test(normalized)) return 'side-effect';
74
+ return 'value';
75
+ };
76
+
77
+ const getKindOrder = (kind: ImportKind): number => {
78
+ if (kind === 'value') return 0;
79
+ if (kind === 'type') return 1;
80
+ return 2;
81
+ };
82
+
83
+ const extractImports = (content: string): ImportEntry[] => {
84
+ const imports: ImportEntry[] = [];
85
+ const regex = new RegExp(IMPORT_DECLARATION_PATTERN.source, 'g');
86
+ let match: RegExpExecArray | null;
87
+
88
+ while ((match = regex.exec(content)) !== null) {
89
+ const raw = match[0].trim();
90
+ const source = match[2];
91
+ if (!source) continue;
92
+
93
+ imports.push({
94
+ kind: getImportKind(raw),
95
+ line: findLineByIndex(content, match.index + (match[1] === '\n' ? 1 : 0)),
96
+ source,
97
+ raw
98
+ });
99
+ }
100
+
101
+ return imports.sort((a, b) => a.line - b.line);
102
+ };
103
+
104
+ const extractExportSources = (content: string): Array<{ source: string; line: number }> => {
105
+ const exports: Array<{ source: string; line: number }> = [];
106
+ const regex = new RegExp(EXPORT_DECLARATION_PATTERN.source, 'g');
107
+ let match: RegExpExecArray | null;
108
+
109
+ while ((match = regex.exec(content)) !== null) {
110
+ const source = match[2];
111
+ if (!source) continue;
112
+ exports.push({
113
+ source,
114
+ line: findLineByIndex(content, match.index + (match[1] === '\n' ? 1 : 0))
115
+ });
116
+ }
117
+
118
+ return exports;
119
+ };
120
+
121
+ const hasForbiddenExtension = (source: string): boolean => {
122
+ const sourceWithoutQuery = source.split('?')[0] ?? source;
123
+ return FORBIDDEN_IMPORT_EXTENSION_PATTERN.test(sourceWithoutQuery);
124
+ };
125
+
126
+ const collectIssuesForFile = (filePath: string, content: string): ImportIssue[] => {
127
+ const issues: ImportIssue[] = [];
128
+ const imports = extractImports(content);
129
+ let lastKindOrder = 0;
130
+
131
+ for (const importEntry of imports) {
132
+ if (hasForbiddenExtension(importEntry.source)) {
133
+ issues.push({
134
+ file: filePath,
135
+ line: importEntry.line,
136
+ message: `Import "${importEntry.source}" must not include .ts/.js extension`
137
+ });
138
+ }
139
+
140
+ const currentOrder = getKindOrder(importEntry.kind);
141
+ if (currentOrder < lastKindOrder) {
142
+ issues.push({
143
+ file: filePath,
144
+ line: importEntry.line,
145
+ message: 'Import order violation: value imports first, then type imports, then side-effect imports'
146
+ });
147
+ }
148
+ lastKindOrder = Math.max(lastKindOrder, currentOrder);
149
+ }
150
+
151
+ for (const exportEntry of extractExportSources(content)) {
152
+ if (!hasForbiddenExtension(exportEntry.source)) continue;
153
+ issues.push({
154
+ file: filePath,
155
+ line: exportEntry.line,
156
+ message: `Export source "${exportEntry.source}" must not include .ts/.js extension`
157
+ });
158
+ }
159
+
160
+ return issues;
161
+ };
162
+
163
+ export default async function checkImports(): Promise<void> {
164
+ const srcRoot = path.join(process.cwd(), 'src');
165
+
166
+ try {
167
+ await fs.access(srcRoot);
168
+ } catch {
169
+ console.error('❌ src folder not found.');
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+
174
+ const issues: ImportIssue[] = [];
175
+ const files = await listFiles(srcRoot);
176
+
177
+ for (const filePath of files) {
178
+ const content = await fs.readFile(filePath, 'utf-8');
179
+ issues.push(...collectIssuesForFile(filePath, content));
180
+ }
181
+
182
+ if (!issues.length) {
183
+ console.log('✅ Imports check passed: extensions and import groups are valid.');
184
+ return;
185
+ }
186
+
187
+ console.error('❌ Imports check failed:');
188
+ for (const issue of issues) {
189
+ console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
190
+ }
191
+
192
+ process.exitCode = 1;
193
+ }
@@ -1,27 +1,27 @@
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';
6
-
7
+ import checkImports from './checkImports.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 },
13
+ { name: 'imports', run: checkImports },
11
14
  { name: 'layer-boundaries', run: checkLayerBoundaries },
12
15
  { name: 'folders-structure', run: checkFoldersStructure }
13
16
  ];
14
-
15
17
  const runTask = async (task) => {
16
18
  console.log(`\n▶ Running check ${task.name}...`);
17
19
  process.exitCode = 0;
18
20
  await task.run();
19
-
20
21
  if ((process.exitCode ?? 0) !== 0) {
21
22
  throw new Error(`Check "${task.name}" failed`);
22
23
  }
23
24
  };
24
-
25
25
  export default async function checkInternal() {
26
26
  try {
27
27
  for (const task of tasks) {
@@ -33,3 +33,4 @@ export default async function checkInternal() {
33
33
  process.exitCode = 1;
34
34
  }
35
35
  }
36
+ //# sourceMappingURL=checkInternal.js.map
@@ -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
  ];
@@ -2,22 +2,17 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { loadUserConfig } from '../utils/loadConfig.js';
4
4
  import { normalizeContexts } from '../init/initAliases.js';
5
-
6
5
  const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
7
6
  const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
8
-
9
7
  const IMPORT_FROM_PATTERN = /\bimport\s+(?:type\s+)?[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g;
10
8
  const EXPORT_FROM_PATTERN = /\bexport\s+(?:type\s+)?\{[\s\S]*?\}\sfrom\s*['"]([^'"]+)['"]/g;
11
9
  const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
12
-
13
10
  const toPosixPath = (value) => value.split(path.sep).join('/');
14
-
15
11
  const normalizeAlias = (value, fallback = '') => {
16
12
  const raw = String(value ?? '').trim();
17
13
  if (!raw.length) return fallback;
18
14
  return raw.startsWith('$') ? raw : `$${raw}`;
19
15
  };
20
-
21
16
  const findLineByIndex = (content, index) => {
22
17
  let line = 1;
23
18
  for (let i = 0; i < index; i += 1) {
@@ -25,24 +20,19 @@ const findLineByIndex = (content, index) => {
25
20
  }
26
21
  return line;
27
22
  };
28
-
29
23
  const getRelativePath = (filePath) => toPosixPath(path.relative(process.cwd(), filePath));
30
-
31
24
  const listFiles = async (rootDir) => {
32
25
  const files = [];
33
26
  const stack = [rootDir];
34
-
35
27
  while (stack.length) {
36
28
  const current = stack.pop();
37
29
  if (!current) continue;
38
-
39
30
  let entries = [];
40
31
  try {
41
32
  entries = await fs.readdir(current, { withFileTypes: true });
42
33
  } catch {
43
34
  continue;
44
35
  }
45
-
46
36
  for (const entry of entries) {
47
37
  if (entry.isDirectory()) {
48
38
  if (!IGNORED_DIRS.has(entry.name)) {
@@ -50,21 +40,17 @@ const listFiles = async (rootDir) => {
50
40
  }
51
41
  continue;
52
42
  }
53
-
54
43
  if (!entry.isFile()) continue;
55
44
  const filePath = path.join(current, entry.name);
56
45
  if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
57
46
  files.push(filePath);
58
47
  }
59
48
  }
60
-
61
49
  return files;
62
50
  };
63
-
64
51
  const extractImports = (content) => {
65
52
  const matches = [];
66
53
  const patterns = [IMPORT_FROM_PATTERN, EXPORT_FROM_PATTERN, DYNAMIC_IMPORT_PATTERN];
67
-
68
54
  for (const pattern of patterns) {
69
55
  const regex = new RegExp(pattern.source, 'g');
70
56
  let match;
@@ -74,10 +60,8 @@ const extractImports = (content) => {
74
60
  matches.push({ value, line: findLineByIndex(content, match.index) });
75
61
  }
76
62
  }
77
-
78
63
  return matches;
79
64
  };
80
-
81
65
  const resolveAliasFromImport = (importPath, aliases) => {
82
66
  const sorted = [...aliases].sort((a, b) => b.length - a.length);
83
67
  for (const alias of sorted) {
@@ -87,74 +71,117 @@ const resolveAliasFromImport = (importPath, aliases) => {
87
71
  }
88
72
  return null;
89
73
  };
90
-
74
+ const resolveContextFromRelativeImport = (filePath, importPath, contexts) => {
75
+ if (!importPath.startsWith('.')) return null;
76
+ const absoluteImportPath = path.resolve(path.dirname(filePath), importPath);
77
+ const appRoot = path.join(process.cwd(), 'src', 'app');
78
+ for (const context of contexts) {
79
+ const contextRoot = path.join(appRoot, context.name);
80
+ if (absoluteImportPath === contextRoot || absoluteImportPath.startsWith(`${contextRoot}${path.sep}`)) {
81
+ return context;
82
+ }
83
+ }
84
+ return null;
85
+ };
91
86
  const isSharedLikeContext = (contextName, alias) => {
92
87
  const normalizedName = contextName.toLowerCase();
93
88
  const normalizedAlias = alias.toLowerCase();
94
89
  return normalizedName.includes('shared') || normalizedAlias.includes('shared');
95
90
  };
96
-
97
91
  export default async function checkLayerBoundaries() {
98
92
  const config = await loadUserConfig();
99
93
  const contexts = normalizeContexts(config.contexts);
100
-
101
94
  if (!contexts.length) {
102
95
  console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
103
96
  process.exitCode = 1;
104
97
  return;
105
98
  }
106
-
107
99
  const coreAlias = normalizeAlias(config.coreAlias, '$core');
108
100
  const sharedAliasRaw = normalizeAlias(config.sharedAlias, '');
109
101
  const sharedAlias = sharedAliasRaw.length ? sharedAliasRaw : null;
110
-
111
102
  const aliasToContext = new Map();
112
103
  for (const context of contexts) {
113
104
  aliasToContext.set(context.alias, context);
114
105
  }
115
-
116
106
  const knownAliases = [coreAlias, ...contexts.map((context) => context.alias)];
117
107
  if (sharedAlias && !knownAliases.includes(sharedAlias)) {
118
108
  knownAliases.push(sharedAlias);
119
109
  }
120
-
121
110
  const issues = [];
122
-
111
+ const coreRoot = path.join(process.cwd(), 'src', 'core');
112
+ try {
113
+ await fs.access(coreRoot);
114
+ const coreFiles = await listFiles(coreRoot);
115
+ for (const filePath of coreFiles) {
116
+ const content = await fs.readFile(filePath, 'utf-8');
117
+ const imports = extractImports(content);
118
+ for (const entry of imports) {
119
+ const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
120
+ if (targetAlias && targetAlias !== coreAlias) {
121
+ const targetContext = aliasToContext.get(targetAlias);
122
+ issues.push({
123
+ file: filePath,
124
+ line: entry.line,
125
+ message: targetContext
126
+ ? `Layer boundary violation: core cannot import from context "${targetContext.name}" (${targetAlias})`
127
+ : `Layer boundary violation: core cannot import from "${targetAlias}"`
128
+ });
129
+ continue;
130
+ }
131
+ const relativeTargetContext = resolveContextFromRelativeImport(filePath, entry.value, contexts);
132
+ if (!relativeTargetContext) continue;
133
+ issues.push({
134
+ file: filePath,
135
+ line: entry.line,
136
+ message: `Layer boundary violation: core cannot import from context "${relativeTargetContext.name}" via relative path`
137
+ });
138
+ }
139
+ }
140
+ } catch {
141
+ // Core folder is optional for the check.
142
+ }
123
143
  for (const context of contexts) {
124
- const layersRoot = path.join(process.cwd(), 'src', 'app', context.name, 'layers');
125
-
144
+ const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
126
145
  try {
127
- await fs.access(layersRoot);
146
+ await fs.access(contextRoot);
128
147
  } catch {
129
148
  continue;
130
149
  }
131
-
132
- const files = await listFiles(layersRoot);
150
+ const files = await listFiles(contextRoot);
133
151
  const allowedAliases = new Set([context.alias, coreAlias]);
134
- if (sharedAlias) {
152
+ if (sharedAlias && context.alias !== sharedAlias) {
135
153
  allowedAliases.add(sharedAlias);
136
154
  }
137
-
138
155
  for (const filePath of files) {
139
156
  const content = await fs.readFile(filePath, 'utf-8');
140
157
  const imports = extractImports(content);
141
-
142
158
  for (const entry of imports) {
143
159
  const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
144
160
  if (!targetAlias) continue;
145
161
  if (targetAlias === coreAlias) continue;
146
162
  if (allowedAliases.has(targetAlias)) continue;
147
-
148
163
  const targetContext = aliasToContext.get(targetAlias);
149
164
  const baseMessage = targetContext
150
165
  ? `Layer boundary violation: context "${context.name}" cannot import from context "${targetContext.name}" (${targetAlias})`
151
166
  : `Layer boundary violation: context "${context.name}" cannot import from "${targetAlias}"`;
152
-
153
167
  const withSharedHint =
154
168
  !sharedAlias && targetContext && isSharedLikeContext(targetContext.name, targetAlias)
155
169
  ? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
156
170
  : baseMessage;
157
-
171
+ issues.push({
172
+ file: filePath,
173
+ line: entry.line,
174
+ message: withSharedHint
175
+ });
176
+ }
177
+ for (const entry of imports) {
178
+ const relativeTargetContext = resolveContextFromRelativeImport(filePath, entry.value, contexts);
179
+ if (!relativeTargetContext || relativeTargetContext.name === context.name) continue;
180
+ const baseMessage = `Layer boundary violation: context "${context.name}" cannot import from context "${relativeTargetContext.name}" via relative path`;
181
+ const withSharedHint =
182
+ !sharedAlias && isSharedLikeContext(relativeTargetContext.name, relativeTargetContext.alias)
183
+ ? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
184
+ : baseMessage;
158
185
  issues.push({
159
186
  file: filePath,
160
187
  line: entry.line,
@@ -163,16 +190,14 @@ export default async function checkLayerBoundaries() {
163
190
  }
164
191
  }
165
192
  }
166
-
167
193
  if (!issues.length) {
168
194
  console.log('✅ Layer boundaries check passed.');
169
195
  return;
170
196
  }
171
-
172
197
  console.error('❌ Layer boundaries check failed:');
173
198
  for (const issue of issues) {
174
199
  console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
175
200
  }
176
-
177
201
  process.exitCode = 1;
178
202
  }
203
+ //# sourceMappingURL=checkLayerBoundaries.js.map
@@ -99,6 +99,22 @@ const resolveAliasFromImport = (importPath: string, aliases: string[]): string |
99
99
  return null;
100
100
  };
101
101
 
102
+ const resolveContextFromRelativeImport = (filePath: string, importPath: string, contexts: ContextConfig[]): ContextConfig | null => {
103
+ if (!importPath.startsWith('.')) return null;
104
+
105
+ const absoluteImportPath = path.resolve(path.dirname(filePath), importPath);
106
+ const appRoot = path.join(process.cwd(), 'src', 'app');
107
+
108
+ for (const context of contexts) {
109
+ const contextRoot = path.join(appRoot, context.name);
110
+ if (absoluteImportPath === contextRoot || absoluteImportPath.startsWith(`${contextRoot}${path.sep}`)) {
111
+ return context;
112
+ }
113
+ }
114
+
115
+ return null;
116
+ };
117
+
102
118
  const isSharedLikeContext = (contextName: string, alias: string): boolean => {
103
119
  const normalizedName = contextName.toLowerCase();
104
120
  const normalizedAlias = alias.toLowerCase();
@@ -131,18 +147,54 @@ export default async function checkLayerBoundaries(): Promise<void> {
131
147
 
132
148
  const issues: BoundaryIssue[] = [];
133
149
 
150
+ const coreRoot = path.join(process.cwd(), 'src', 'core');
151
+ try {
152
+ await fs.access(coreRoot);
153
+ const coreFiles = await listFiles(coreRoot);
154
+ for (const filePath of coreFiles) {
155
+ const content = await fs.readFile(filePath, 'utf-8');
156
+ const imports = extractImports(content);
157
+
158
+ for (const entry of imports) {
159
+ const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
160
+ if (targetAlias && targetAlias !== coreAlias) {
161
+ const targetContext = aliasToContext.get(targetAlias);
162
+ issues.push({
163
+ file: filePath,
164
+ line: entry.line,
165
+ message: targetContext
166
+ ? `Layer boundary violation: core cannot import from context "${targetContext.name}" (${targetAlias})`
167
+ : `Layer boundary violation: core cannot import from "${targetAlias}"`
168
+ });
169
+ continue;
170
+ }
171
+
172
+ const relativeTargetContext = resolveContextFromRelativeImport(filePath, entry.value, contexts);
173
+ if (!relativeTargetContext) continue;
174
+
175
+ issues.push({
176
+ file: filePath,
177
+ line: entry.line,
178
+ message: `Layer boundary violation: core cannot import from context "${relativeTargetContext.name}" via relative path`
179
+ });
180
+ }
181
+ }
182
+ } catch {
183
+ // Core folder is optional for the check.
184
+ }
185
+
134
186
  for (const context of contexts) {
135
- const layersRoot = path.join(process.cwd(), 'src', 'app', context.name, 'layers');
187
+ const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
136
188
 
137
189
  try {
138
- await fs.access(layersRoot);
190
+ await fs.access(contextRoot);
139
191
  } catch {
140
192
  continue;
141
193
  }
142
194
 
143
- const files = await listFiles(layersRoot);
195
+ const files = await listFiles(contextRoot);
144
196
  const allowedAliases = new Set<string>([context.alias, coreAlias]);
145
- if (sharedAlias) {
197
+ if (sharedAlias && context.alias !== sharedAlias) {
146
198
  allowedAliases.add(sharedAlias);
147
199
  }
148
200
 
@@ -172,6 +224,23 @@ export default async function checkLayerBoundaries(): Promise<void> {
172
224
  message: withSharedHint
173
225
  });
174
226
  }
227
+
228
+ for (const entry of imports) {
229
+ const relativeTargetContext = resolveContextFromRelativeImport(filePath, entry.value, contexts);
230
+ if (!relativeTargetContext || relativeTargetContext.name === context.name) continue;
231
+
232
+ const baseMessage = `Layer boundary violation: context "${context.name}" cannot import from context "${relativeTargetContext.name}" via relative path`;
233
+ const withSharedHint =
234
+ !sharedAlias && isSharedLikeContext(relativeTargetContext.name, relativeTargetContext.alias)
235
+ ? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
236
+ : baseMessage;
237
+
238
+ issues.push({
239
+ file: filePath,
240
+ line: entry.line,
241
+ message: withSharedHint
242
+ });
243
+ }
175
244
  }
176
245
  }
177
246
 
@@ -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, 'typecheck');
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, 'typecheck');
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
+ }
@@ -1,7 +1,10 @@
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';
7
9
  export { default as checkProject } from './checkProject.js';
10
+ //# sourceMappingURL=index.js.map
@@ -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(resolveDomainRootPath(contextName, domainName), 'ports', `${meta.repositoryInterfaceName}.ts`);
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 {