@azure-net/cli 4.0.0-0 → 4.0.0-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 +65 -34
- package/bin/azure-net.ts +65 -34
- package/eslint.config.js +15 -0
- package/package.json +1 -1
- package/src/checks/checkDomain.js +201 -0
- package/src/checks/checkDomain.ts +213 -0
- package/src/checks/checkFoldersStructure.js +70 -0
- package/src/checks/checkFoldersStructure.ts +85 -0
- package/src/checks/checkInternal.js +35 -0
- package/src/checks/checkInternal.ts +40 -0
- package/src/checks/checkLayerBoundaries.js +178 -0
- package/src/checks/checkLayerBoundaries.ts +189 -0
- package/src/checks/checkPresenterNames.js +202 -0
- package/src/checks/checkPresenterNames.ts +208 -0
- package/src/checks/checkProject.js +42 -0
- package/src/checks/checkProject.ts +45 -0
- package/src/checks/checkProviderNames.js +158 -0
- package/src/checks/checkProviderNames.ts +165 -0
- package/src/checks/index.js +7 -0
- package/src/checks/index.ts +7 -0
- package/src/generate/layers/generateRepository.ts +123 -0
- package/src/generate/layers/generateRepositoryMethod.ts +102 -0
- package/src/generate/layers/generateUseCases.ts +50 -0
- package/src/generate/layers/repositoryShared.ts +657 -0
- package/src/generate/module/generateModulePreset.js +307 -0
- package/src/generate/module/generateModulePreset.ts +337 -0
- package/src/generate/module/generateSchemaFromType.js +46 -8
- package/src/generate/module/generateSchemaFromType.ts +52 -8
- package/src/generate/module/repositoryModuleShared.ts +661 -0
- package/src/utils/contextUtils.ts +89 -0
- package/src/utils/fileUtils.ts +100 -0
- package/src/utils/loadConfig.ts +69 -0
- package/src/commands/generate/generateCrudBase.js +0 -188
- package/src/commands/generate/generateCrudPresenter.js +0 -225
- package/src/commands/generate/generateModule.js +0 -31
- package/src/commands/generate/generateModuleBase.js +0 -130
- package/src/commands/generate/generateModulePresenter.js +0 -103
- package/src/commands/generate/generatePresenter.js +0 -80
- package/src/commands/generate/generateRepository.js +0 -70
- package/src/commands/generate/generateSchema.js +0 -100
- package/src/commands/generate/generateService.js +0 -66
- package/src/commands/generate/index.js +0 -9
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadUserConfig } from '../utils/loadConfig.js';
|
|
4
|
+
import { normalizeContexts } from '../init/initAliases.js';
|
|
5
|
+
|
|
6
|
+
const ALLOWED_LAYERS_FOLDERS = new Set(['domain', 'presentation', 'infrastructure', 'application']);
|
|
7
|
+
const ALLOWED_UI_FOLDERS = new Set(['components', 'widgets', 'design', 'style']);
|
|
8
|
+
const ALLOWED_COMPONENTS_FOLDERS = new Set(['page', 'layout', 'shared']);
|
|
9
|
+
|
|
10
|
+
const toPosixPath = (value) => value.split(path.sep).join('/');
|
|
11
|
+
|
|
12
|
+
const getRelativePath = (filePath) => toPosixPath(path.relative(process.cwd(), filePath));
|
|
13
|
+
|
|
14
|
+
const getDirectories = async (dirPath) => {
|
|
15
|
+
try {
|
|
16
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
17
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const checkOnlyAllowedFolders = async (basePath, allowed, scopeLabel, issues) => {
|
|
24
|
+
const directories = await getDirectories(basePath);
|
|
25
|
+
if (!directories) return;
|
|
26
|
+
|
|
27
|
+
for (const dir of directories) {
|
|
28
|
+
if (allowed.has(dir)) continue;
|
|
29
|
+
issues.push({
|
|
30
|
+
file: basePath,
|
|
31
|
+
message: `${scopeLabel} can contain only: ${Array.from(allowed).join(', ')}. Found: ${dir}`
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default async function checkFoldersStructure() {
|
|
37
|
+
const config = await loadUserConfig();
|
|
38
|
+
const contexts = normalizeContexts(config.contexts);
|
|
39
|
+
|
|
40
|
+
if (!contexts.length) {
|
|
41
|
+
console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const issues = [];
|
|
47
|
+
|
|
48
|
+
for (const context of contexts) {
|
|
49
|
+
const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
|
|
50
|
+
const layersPath = path.join(contextRoot, 'layers');
|
|
51
|
+
const uiPath = path.join(contextRoot, 'ui');
|
|
52
|
+
const componentsPath = path.join(uiPath, 'components');
|
|
53
|
+
|
|
54
|
+
await checkOnlyAllowedFolders(layersPath, ALLOWED_LAYERS_FOLDERS, `layers (${context.name})`, issues);
|
|
55
|
+
await checkOnlyAllowedFolders(uiPath, ALLOWED_UI_FOLDERS, `ui (${context.name})`, issues);
|
|
56
|
+
await checkOnlyAllowedFolders(componentsPath, ALLOWED_COMPONENTS_FOLDERS, `ui/components (${context.name})`, issues);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!issues.length) {
|
|
60
|
+
console.log('✅ Folders structure check passed.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error('❌ Folders structure check failed:');
|
|
65
|
+
for (const issue of issues) {
|
|
66
|
+
console.error(`• ${getRelativePath(issue.file)} — ${issue.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadUserConfig } from '../utils/loadConfig.js';
|
|
4
|
+
import { normalizeContexts } from '../init/initAliases.js';
|
|
5
|
+
|
|
6
|
+
type StructureIssue = {
|
|
7
|
+
file: string;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ContextConfig = {
|
|
12
|
+
name: string;
|
|
13
|
+
alias: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ALLOWED_LAYERS_FOLDERS = new Set(['domain', 'presentation', 'infrastructure', 'application']);
|
|
17
|
+
const ALLOWED_UI_FOLDERS = new Set(['components', 'widgets', 'design', 'style']);
|
|
18
|
+
const ALLOWED_COMPONENTS_FOLDERS = new Set(['page', 'layout', 'shared']);
|
|
19
|
+
|
|
20
|
+
const toPosixPath = (value: string): string => value.split(path.sep).join('/');
|
|
21
|
+
|
|
22
|
+
const getRelativePath = (filePath: string): string => toPosixPath(path.relative(process.cwd(), filePath));
|
|
23
|
+
|
|
24
|
+
const getDirectories = async (dirPath: string): Promise<string[] | null> => {
|
|
25
|
+
try {
|
|
26
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
27
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const checkOnlyAllowedFolders = async (
|
|
34
|
+
basePath: string,
|
|
35
|
+
allowed: Set<string>,
|
|
36
|
+
scopeLabel: string,
|
|
37
|
+
issues: StructureIssue[]
|
|
38
|
+
): Promise<void> => {
|
|
39
|
+
const directories = await getDirectories(basePath);
|
|
40
|
+
if (!directories) return;
|
|
41
|
+
|
|
42
|
+
for (const dir of directories) {
|
|
43
|
+
if (allowed.has(dir)) continue;
|
|
44
|
+
issues.push({
|
|
45
|
+
file: basePath,
|
|
46
|
+
message: `${scopeLabel} can contain only: ${Array.from(allowed).join(', ')}. Found: ${dir}`
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default async function checkFoldersStructure(): Promise<void> {
|
|
52
|
+
const config = await loadUserConfig();
|
|
53
|
+
const contexts = normalizeContexts(config.contexts) as ContextConfig[];
|
|
54
|
+
|
|
55
|
+
if (!contexts.length) {
|
|
56
|
+
console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const issues: StructureIssue[] = [];
|
|
62
|
+
|
|
63
|
+
for (const context of contexts) {
|
|
64
|
+
const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
|
|
65
|
+
const layersPath = path.join(contextRoot, 'layers');
|
|
66
|
+
const uiPath = path.join(contextRoot, 'ui');
|
|
67
|
+
const componentsPath = path.join(uiPath, 'components');
|
|
68
|
+
|
|
69
|
+
await checkOnlyAllowedFolders(layersPath, ALLOWED_LAYERS_FOLDERS, `layers (${context.name})`, issues);
|
|
70
|
+
await checkOnlyAllowedFolders(uiPath, ALLOWED_UI_FOLDERS, `ui (${context.name})`, issues);
|
|
71
|
+
await checkOnlyAllowedFolders(componentsPath, ALLOWED_COMPONENTS_FOLDERS, `ui/components (${context.name})`, issues);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!issues.length) {
|
|
75
|
+
console.log('✅ Folders structure check passed.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.error('❌ Folders structure check failed:');
|
|
80
|
+
for (const issue of issues) {
|
|
81
|
+
console.error(`• ${getRelativePath(issue.file)} — ${issue.message}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import checkPresenterNames from './checkPresenterNames.js';
|
|
2
|
+
import checkProviderNames from './checkProviderNames.js';
|
|
3
|
+
import checkDomain from './checkDomain.js';
|
|
4
|
+
import checkLayerBoundaries from './checkLayerBoundaries.js';
|
|
5
|
+
import checkFoldersStructure from './checkFoldersStructure.js';
|
|
6
|
+
|
|
7
|
+
const tasks = [
|
|
8
|
+
{ name: 'presenter-names', run: checkPresenterNames },
|
|
9
|
+
{ name: 'provider-names', run: checkProviderNames },
|
|
10
|
+
{ name: 'domain', run: checkDomain },
|
|
11
|
+
{ name: 'layer-boundaries', run: checkLayerBoundaries },
|
|
12
|
+
{ name: 'folders-structure', run: checkFoldersStructure }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const runTask = async (task) => {
|
|
16
|
+
console.log(`\n▶ Running check ${task.name}...`);
|
|
17
|
+
process.exitCode = 0;
|
|
18
|
+
await task.run();
|
|
19
|
+
|
|
20
|
+
if ((process.exitCode ?? 0) !== 0) {
|
|
21
|
+
throw new Error(`Check "${task.name}" failed`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default async function checkInternal() {
|
|
26
|
+
try {
|
|
27
|
+
for (const task of tasks) {
|
|
28
|
+
await runTask(task);
|
|
29
|
+
}
|
|
30
|
+
console.log('\n✅ Internal checks passed.');
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(`\n❌ Internal checks failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import checkPresenterNames from './checkPresenterNames.js';
|
|
2
|
+
import checkProviderNames from './checkProviderNames.js';
|
|
3
|
+
import checkDomain from './checkDomain.js';
|
|
4
|
+
import checkLayerBoundaries from './checkLayerBoundaries.js';
|
|
5
|
+
import checkFoldersStructure from './checkFoldersStructure.js';
|
|
6
|
+
|
|
7
|
+
type CheckTask = {
|
|
8
|
+
name: string;
|
|
9
|
+
run: () => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const tasks: CheckTask[] = [
|
|
13
|
+
{ name: 'presenter-names', run: checkPresenterNames },
|
|
14
|
+
{ name: 'provider-names', run: checkProviderNames },
|
|
15
|
+
{ name: 'domain', run: checkDomain },
|
|
16
|
+
{ name: 'layer-boundaries', run: checkLayerBoundaries },
|
|
17
|
+
{ name: 'folders-structure', run: checkFoldersStructure }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const runTask = async (task: CheckTask): Promise<void> => {
|
|
21
|
+
console.log(`\n▶ Running check ${task.name}...`);
|
|
22
|
+
process.exitCode = 0;
|
|
23
|
+
await task.run();
|
|
24
|
+
|
|
25
|
+
if ((process.exitCode ?? 0) !== 0) {
|
|
26
|
+
throw new Error(`Check "${task.name}" failed`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default async function checkInternal(): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
for (const task of tasks) {
|
|
33
|
+
await runTask(task);
|
|
34
|
+
}
|
|
35
|
+
console.log('\n✅ Internal checks passed.');
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`\n❌ Internal checks failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadUserConfig } from '../utils/loadConfig.js';
|
|
4
|
+
import { normalizeContexts } from '../init/initAliases.js';
|
|
5
|
+
|
|
6
|
+
const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
|
|
7
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
8
|
+
|
|
9
|
+
const IMPORT_FROM_PATTERN = /\bimport\s+(?:type\s+)?[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g;
|
|
10
|
+
const EXPORT_FROM_PATTERN = /\bexport\s+(?:type\s+)?\{[\s\S]*?\}\sfrom\s*['"]([^'"]+)['"]/g;
|
|
11
|
+
const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
12
|
+
|
|
13
|
+
const toPosixPath = (value) => value.split(path.sep).join('/');
|
|
14
|
+
|
|
15
|
+
const normalizeAlias = (value, fallback = '') => {
|
|
16
|
+
const raw = String(value ?? '').trim();
|
|
17
|
+
if (!raw.length) return fallback;
|
|
18
|
+
return raw.startsWith('$') ? raw : `$${raw}`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const findLineByIndex = (content, index) => {
|
|
22
|
+
let line = 1;
|
|
23
|
+
for (let i = 0; i < index; i += 1) {
|
|
24
|
+
if (content[i] === '\n') line += 1;
|
|
25
|
+
}
|
|
26
|
+
return line;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getRelativePath = (filePath) => toPosixPath(path.relative(process.cwd(), filePath));
|
|
30
|
+
|
|
31
|
+
const listFiles = async (rootDir) => {
|
|
32
|
+
const files = [];
|
|
33
|
+
const stack = [rootDir];
|
|
34
|
+
|
|
35
|
+
while (stack.length) {
|
|
36
|
+
const current = stack.pop();
|
|
37
|
+
if (!current) continue;
|
|
38
|
+
|
|
39
|
+
let entries = [];
|
|
40
|
+
try {
|
|
41
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
49
|
+
stack.push(path.join(current, entry.name));
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!entry.isFile()) continue;
|
|
55
|
+
const filePath = path.join(current, entry.name);
|
|
56
|
+
if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
57
|
+
files.push(filePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return files;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const extractImports = (content) => {
|
|
65
|
+
const matches = [];
|
|
66
|
+
const patterns = [IMPORT_FROM_PATTERN, EXPORT_FROM_PATTERN, DYNAMIC_IMPORT_PATTERN];
|
|
67
|
+
|
|
68
|
+
for (const pattern of patterns) {
|
|
69
|
+
const regex = new RegExp(pattern.source, 'g');
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = regex.exec(content)) !== null) {
|
|
72
|
+
const value = match[1];
|
|
73
|
+
if (!value) continue;
|
|
74
|
+
matches.push({ value, line: findLineByIndex(content, match.index) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return matches;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const resolveAliasFromImport = (importPath, aliases) => {
|
|
82
|
+
const sorted = [...aliases].sort((a, b) => b.length - a.length);
|
|
83
|
+
for (const alias of sorted) {
|
|
84
|
+
if (importPath === alias || importPath.startsWith(`${alias}/`)) {
|
|
85
|
+
return alias;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const isSharedLikeContext = (contextName, alias) => {
|
|
92
|
+
const normalizedName = contextName.toLowerCase();
|
|
93
|
+
const normalizedAlias = alias.toLowerCase();
|
|
94
|
+
return normalizedName.includes('shared') || normalizedAlias.includes('shared');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default async function checkLayerBoundaries() {
|
|
98
|
+
const config = await loadUserConfig();
|
|
99
|
+
const contexts = normalizeContexts(config.contexts);
|
|
100
|
+
|
|
101
|
+
if (!contexts.length) {
|
|
102
|
+
console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const coreAlias = normalizeAlias(config.coreAlias, '$core');
|
|
108
|
+
const sharedAliasRaw = normalizeAlias(config.sharedAlias, '');
|
|
109
|
+
const sharedAlias = sharedAliasRaw.length ? sharedAliasRaw : null;
|
|
110
|
+
|
|
111
|
+
const aliasToContext = new Map();
|
|
112
|
+
for (const context of contexts) {
|
|
113
|
+
aliasToContext.set(context.alias, context);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const knownAliases = [coreAlias, ...contexts.map((context) => context.alias)];
|
|
117
|
+
if (sharedAlias && !knownAliases.includes(sharedAlias)) {
|
|
118
|
+
knownAliases.push(sharedAlias);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const issues = [];
|
|
122
|
+
|
|
123
|
+
for (const context of contexts) {
|
|
124
|
+
const layersRoot = path.join(process.cwd(), 'src', 'app', context.name, 'layers');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await fs.access(layersRoot);
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const files = await listFiles(layersRoot);
|
|
133
|
+
const allowedAliases = new Set([context.alias, coreAlias]);
|
|
134
|
+
if (sharedAlias) {
|
|
135
|
+
allowedAliases.add(sharedAlias);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const filePath of files) {
|
|
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) continue;
|
|
145
|
+
if (targetAlias === coreAlias) continue;
|
|
146
|
+
if (allowedAliases.has(targetAlias)) continue;
|
|
147
|
+
|
|
148
|
+
const targetContext = aliasToContext.get(targetAlias);
|
|
149
|
+
const baseMessage = targetContext
|
|
150
|
+
? `Layer boundary violation: context "${context.name}" cannot import from context "${targetContext.name}" (${targetAlias})`
|
|
151
|
+
: `Layer boundary violation: context "${context.name}" cannot import from "${targetAlias}"`;
|
|
152
|
+
|
|
153
|
+
const withSharedHint =
|
|
154
|
+
!sharedAlias && targetContext && isSharedLikeContext(targetContext.name, targetAlias)
|
|
155
|
+
? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
|
|
156
|
+
: baseMessage;
|
|
157
|
+
|
|
158
|
+
issues.push({
|
|
159
|
+
file: filePath,
|
|
160
|
+
line: entry.line,
|
|
161
|
+
message: withSharedHint
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!issues.length) {
|
|
168
|
+
console.log('✅ Layer boundaries check passed.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.error('❌ Layer boundaries check failed:');
|
|
173
|
+
for (const issue of issues) {
|
|
174
|
+
console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadUserConfig } from '../utils/loadConfig.js';
|
|
4
|
+
import { normalizeContexts } from '../init/initAliases.js';
|
|
5
|
+
|
|
6
|
+
type BoundaryIssue = {
|
|
7
|
+
file: string;
|
|
8
|
+
line: number;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ContextConfig = {
|
|
13
|
+
name: string;
|
|
14
|
+
alias: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
|
|
18
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
19
|
+
|
|
20
|
+
const IMPORT_FROM_PATTERN = /\bimport\s+(?:type\s+)?[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g;
|
|
21
|
+
const EXPORT_FROM_PATTERN = /\bexport\s+(?:type\s+)?\{[\s\S]*?\}\sfrom\s*['"]([^'"]+)['"]/g;
|
|
22
|
+
const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
23
|
+
|
|
24
|
+
const toPosixPath = (value: string): string => value.split(path.sep).join('/');
|
|
25
|
+
|
|
26
|
+
const normalizeAlias = (value: unknown, fallback = ''): string => {
|
|
27
|
+
const raw = String(value ?? '').trim();
|
|
28
|
+
if (!raw.length) return fallback;
|
|
29
|
+
return raw.startsWith('$') ? raw : `$${raw}`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const findLineByIndex = (content: string, index: number): number => {
|
|
33
|
+
let line = 1;
|
|
34
|
+
for (let i = 0; i < index; i += 1) {
|
|
35
|
+
if (content[i] === '\n') line += 1;
|
|
36
|
+
}
|
|
37
|
+
return line;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getRelativePath = (filePath: string): string => toPosixPath(path.relative(process.cwd(), filePath));
|
|
41
|
+
|
|
42
|
+
const listFiles = async (rootDir: string): Promise<string[]> => {
|
|
43
|
+
const files: string[] = [];
|
|
44
|
+
const stack: string[] = [rootDir];
|
|
45
|
+
|
|
46
|
+
while (stack.length) {
|
|
47
|
+
const current = stack.pop();
|
|
48
|
+
if (!current) continue;
|
|
49
|
+
|
|
50
|
+
let entries: import('node:fs').Dirent[] = [];
|
|
51
|
+
try {
|
|
52
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
60
|
+
stack.push(path.join(current, entry.name));
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!entry.isFile()) continue;
|
|
66
|
+
const filePath = path.join(current, entry.name);
|
|
67
|
+
if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
68
|
+
files.push(filePath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const extractImports = (content: string): Array<{ value: string; line: number }> => {
|
|
76
|
+
const matches: Array<{ value: string; line: number }> = [];
|
|
77
|
+
const patterns = [IMPORT_FROM_PATTERN, EXPORT_FROM_PATTERN, DYNAMIC_IMPORT_PATTERN];
|
|
78
|
+
|
|
79
|
+
for (const pattern of patterns) {
|
|
80
|
+
const regex = new RegExp(pattern.source, 'g');
|
|
81
|
+
let match: RegExpExecArray | null;
|
|
82
|
+
while ((match = regex.exec(content)) !== null) {
|
|
83
|
+
const value = match[1];
|
|
84
|
+
if (!value) continue;
|
|
85
|
+
matches.push({ value, line: findLineByIndex(content, match.index) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return matches;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const resolveAliasFromImport = (importPath: string, aliases: string[]): string | null => {
|
|
93
|
+
const sorted = [...aliases].sort((a, b) => b.length - a.length);
|
|
94
|
+
for (const alias of sorted) {
|
|
95
|
+
if (importPath === alias || importPath.startsWith(`${alias}/`)) {
|
|
96
|
+
return alias;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isSharedLikeContext = (contextName: string, alias: string): boolean => {
|
|
103
|
+
const normalizedName = contextName.toLowerCase();
|
|
104
|
+
const normalizedAlias = alias.toLowerCase();
|
|
105
|
+
return normalizedName.includes('shared') || normalizedAlias.includes('shared');
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default async function checkLayerBoundaries(): Promise<void> {
|
|
109
|
+
const config = await loadUserConfig();
|
|
110
|
+
const contexts = normalizeContexts(config.contexts) as ContextConfig[];
|
|
111
|
+
|
|
112
|
+
if (!contexts.length) {
|
|
113
|
+
console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const coreAlias = normalizeAlias(config.coreAlias, '$core');
|
|
119
|
+
const sharedAliasRaw = normalizeAlias(config.sharedAlias, '');
|
|
120
|
+
const sharedAlias = sharedAliasRaw.length ? sharedAliasRaw : null;
|
|
121
|
+
|
|
122
|
+
const aliasToContext = new Map<string, ContextConfig>();
|
|
123
|
+
for (const context of contexts) {
|
|
124
|
+
aliasToContext.set(context.alias, context);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const knownAliases = [coreAlias, ...contexts.map((context) => context.alias)];
|
|
128
|
+
if (sharedAlias && !knownAliases.includes(sharedAlias)) {
|
|
129
|
+
knownAliases.push(sharedAlias);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const issues: BoundaryIssue[] = [];
|
|
133
|
+
|
|
134
|
+
for (const context of contexts) {
|
|
135
|
+
const layersRoot = path.join(process.cwd(), 'src', 'app', context.name, 'layers');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await fs.access(layersRoot);
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const files = await listFiles(layersRoot);
|
|
144
|
+
const allowedAliases = new Set<string>([context.alias, coreAlias]);
|
|
145
|
+
if (sharedAlias) {
|
|
146
|
+
allowedAliases.add(sharedAlias);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const filePath of files) {
|
|
150
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
151
|
+
const imports = extractImports(content);
|
|
152
|
+
|
|
153
|
+
for (const entry of imports) {
|
|
154
|
+
const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
|
|
155
|
+
if (!targetAlias) continue;
|
|
156
|
+
if (targetAlias === coreAlias) continue;
|
|
157
|
+
if (allowedAliases.has(targetAlias)) continue;
|
|
158
|
+
|
|
159
|
+
const targetContext = aliasToContext.get(targetAlias);
|
|
160
|
+
const baseMessage = targetContext
|
|
161
|
+
? `Layer boundary violation: context "${context.name}" cannot import from context "${targetContext.name}" (${targetAlias})`
|
|
162
|
+
: `Layer boundary violation: context "${context.name}" cannot import from "${targetAlias}"`;
|
|
163
|
+
|
|
164
|
+
const withSharedHint =
|
|
165
|
+
!sharedAlias && targetContext && isSharedLikeContext(targetContext.name, targetAlias)
|
|
166
|
+
? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
|
|
167
|
+
: baseMessage;
|
|
168
|
+
|
|
169
|
+
issues.push({
|
|
170
|
+
file: filePath,
|
|
171
|
+
line: entry.line,
|
|
172
|
+
message: withSharedHint
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!issues.length) {
|
|
179
|
+
console.log('✅ Layer boundaries check passed.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.error('❌ Layer boundaries check failed:');
|
|
184
|
+
for (const issue of issues) {
|
|
185
|
+
console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
}
|