@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
package/bin/azure-net.js
CHANGED
|
@@ -12,6 +12,7 @@ const createCommand = program.command('create').description('Create project boil
|
|
|
12
12
|
const initCommand = program.command('init').description('Initialize project scaffolding');
|
|
13
13
|
const installCommand = program.command('install').description('Install project presets and flows');
|
|
14
14
|
const generateCommand = program.command('generate').description('Generate UI artifacts');
|
|
15
|
+
const checkCommand = program.command('check').description('Run project checks');
|
|
15
16
|
|
|
16
17
|
addCommand
|
|
17
18
|
.command('commit-tools')
|
|
@@ -91,16 +92,6 @@ createCommand
|
|
|
91
92
|
await handler();
|
|
92
93
|
});
|
|
93
94
|
|
|
94
|
-
const lazyCommand = (command, description, importer) => {
|
|
95
|
-
program
|
|
96
|
-
.command(command)
|
|
97
|
-
.description(description)
|
|
98
|
-
.action(async (...args) => {
|
|
99
|
-
const handler = await importer();
|
|
100
|
-
await handler(...args);
|
|
101
|
-
});
|
|
102
|
-
};
|
|
103
|
-
|
|
104
95
|
initCommand
|
|
105
96
|
.command('edges')
|
|
106
97
|
.description('Initialize edges plugin in vite config')
|
|
@@ -226,28 +217,68 @@ generateCommand
|
|
|
226
217
|
await handler();
|
|
227
218
|
});
|
|
228
219
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
'
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
220
|
+
generateCommand
|
|
221
|
+
.command('module-preset')
|
|
222
|
+
.description('Generate preset CRUD-like module flow')
|
|
223
|
+
.action(async () => {
|
|
224
|
+
const handler = (await import('../src/generate/module/generateModulePreset.js')).default;
|
|
225
|
+
await handler();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
checkCommand
|
|
229
|
+
.command('presenter-names')
|
|
230
|
+
.description('Check duplicate presenter names in src')
|
|
231
|
+
.action(async () => {
|
|
232
|
+
const handler = (await import('../src/checks/checkPresenterNames.js')).default;
|
|
233
|
+
await handler();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
checkCommand
|
|
237
|
+
.command('provider-names')
|
|
238
|
+
.description('Check duplicate provider names in src')
|
|
239
|
+
.action(async () => {
|
|
240
|
+
const handler = (await import('../src/checks/checkProviderNames.js')).default;
|
|
241
|
+
await handler();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
checkCommand
|
|
245
|
+
.command('domain')
|
|
246
|
+
.description('Check domain naming and structure rules in all contexts')
|
|
247
|
+
.action(async () => {
|
|
248
|
+
const handler = (await import('../src/checks/checkDomain.js')).default;
|
|
249
|
+
await handler();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
checkCommand
|
|
253
|
+
.command('layer-boundaries')
|
|
254
|
+
.description('Check layer boundaries between contexts by aliases')
|
|
255
|
+
.action(async () => {
|
|
256
|
+
const handler = (await import('../src/checks/checkLayerBoundaries.js')).default;
|
|
257
|
+
await handler();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
checkCommand
|
|
261
|
+
.command('folders-structure')
|
|
262
|
+
.description('Check allowed folders structure in context layers/ui/components')
|
|
263
|
+
.action(async () => {
|
|
264
|
+
const handler = (await import('../src/checks/checkFoldersStructure.js')).default;
|
|
265
|
+
await handler();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
checkCommand
|
|
269
|
+
.command('internal')
|
|
270
|
+
.description('Run all internal azure-net checks')
|
|
271
|
+
.action(async () => {
|
|
272
|
+
const handler = (await import('../src/checks/checkInternal.js')).default;
|
|
273
|
+
await handler();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
checkCommand
|
|
277
|
+
.command('project')
|
|
278
|
+
.description('Run full project checks: format, internal, lint, typecheck')
|
|
279
|
+
.action(async () => {
|
|
280
|
+
const handler = (await import('../src/checks/checkProject.js')).default;
|
|
281
|
+
await handler();
|
|
282
|
+
});
|
|
283
|
+
|
|
253
284
|
program.parse();
|
package/bin/azure-net.ts
CHANGED
|
@@ -12,6 +12,7 @@ const createCommand = program.command('create').description('Create project boil
|
|
|
12
12
|
const initCommand = program.command('init').description('Initialize project scaffolding');
|
|
13
13
|
const installCommand = program.command('install').description('Install project presets and flows');
|
|
14
14
|
const generateCommand = program.command('generate').description('Generate UI artifacts');
|
|
15
|
+
const checkCommand = program.command('check').description('Run project checks');
|
|
15
16
|
|
|
16
17
|
addCommand
|
|
17
18
|
.command('commit-tools')
|
|
@@ -91,16 +92,6 @@ createCommand
|
|
|
91
92
|
await handler();
|
|
92
93
|
});
|
|
93
94
|
|
|
94
|
-
const lazyCommand = (command: string, description: string, importer: () => Promise<(...args: unknown[]) => Promise<void> | void>) => {
|
|
95
|
-
program
|
|
96
|
-
.command(command)
|
|
97
|
-
.description(description)
|
|
98
|
-
.action(async (...args: unknown[]) => {
|
|
99
|
-
const handler = await importer();
|
|
100
|
-
await handler(...args);
|
|
101
|
-
});
|
|
102
|
-
};
|
|
103
|
-
|
|
104
95
|
initCommand
|
|
105
96
|
.command('edges')
|
|
106
97
|
.description('Initialize edges plugin in vite config')
|
|
@@ -226,28 +217,68 @@ generateCommand
|
|
|
226
217
|
await handler();
|
|
227
218
|
});
|
|
228
219
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
'
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
220
|
+
generateCommand
|
|
221
|
+
.command('module-preset')
|
|
222
|
+
.description('Generate preset CRUD-like module flow')
|
|
223
|
+
.action(async () => {
|
|
224
|
+
const handler = (await import('../src/generate/module/generateModulePreset.js')).default;
|
|
225
|
+
await handler();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
checkCommand
|
|
229
|
+
.command('presenter-names')
|
|
230
|
+
.description('Check duplicate presenter names in src')
|
|
231
|
+
.action(async () => {
|
|
232
|
+
const handler = (await import('../src/checks/checkPresenterNames.js')).default;
|
|
233
|
+
await handler();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
checkCommand
|
|
237
|
+
.command('provider-names')
|
|
238
|
+
.description('Check duplicate provider names in src')
|
|
239
|
+
.action(async () => {
|
|
240
|
+
const handler = (await import('../src/checks/checkProviderNames.js')).default;
|
|
241
|
+
await handler();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
checkCommand
|
|
245
|
+
.command('domain')
|
|
246
|
+
.description('Check domain naming and structure rules in all contexts')
|
|
247
|
+
.action(async () => {
|
|
248
|
+
const handler = (await import('../src/checks/checkDomain.js')).default;
|
|
249
|
+
await handler();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
checkCommand
|
|
253
|
+
.command('layer-boundaries')
|
|
254
|
+
.description('Check layer boundaries between contexts by aliases')
|
|
255
|
+
.action(async () => {
|
|
256
|
+
const handler = (await import('../src/checks/checkLayerBoundaries.js')).default;
|
|
257
|
+
await handler();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
checkCommand
|
|
261
|
+
.command('folders-structure')
|
|
262
|
+
.description('Check allowed folders structure in context layers/ui/components')
|
|
263
|
+
.action(async () => {
|
|
264
|
+
const handler = (await import('../src/checks/checkFoldersStructure.js')).default;
|
|
265
|
+
await handler();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
checkCommand
|
|
269
|
+
.command('internal')
|
|
270
|
+
.description('Run all internal azure-net checks')
|
|
271
|
+
.action(async () => {
|
|
272
|
+
const handler = (await import('../src/checks/checkInternal.js')).default;
|
|
273
|
+
await handler();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
checkCommand
|
|
277
|
+
.command('project')
|
|
278
|
+
.description('Run full project checks: format, internal, lint, typecheck')
|
|
279
|
+
.action(async () => {
|
|
280
|
+
const handler = (await import('../src/checks/checkProject.js')).default;
|
|
281
|
+
await handler();
|
|
282
|
+
});
|
|
283
|
+
|
|
253
284
|
program.parse();
|
package/eslint.config.js
CHANGED
|
@@ -15,5 +15,20 @@ export default tseslint.config(
|
|
|
15
15
|
process: 'readonly'
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
files: [
|
|
21
|
+
'src/generate/module/repositoryModuleShared.ts',
|
|
22
|
+
'src/generate/layers/repositoryShared.ts',
|
|
23
|
+
'src/generate/layers/generateUseCases.ts',
|
|
24
|
+
'src/generate/layers/generateRepositoryMethod.ts',
|
|
25
|
+
'src/generate/layers/generateRepository.ts',
|
|
26
|
+
'src/utils/contextUtils.ts',
|
|
27
|
+
'src/utils/loadConfig.ts',
|
|
28
|
+
'src/utils/fileUtils.ts'
|
|
29
|
+
],
|
|
30
|
+
rules: {
|
|
31
|
+
'@typescript-eslint/ban-ts-comment': 'off'
|
|
32
|
+
}
|
|
18
33
|
}
|
|
19
34
|
);
|
package/package.json
CHANGED
|
@@ -0,0 +1,201 @@
|
|
|
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 DOMAIN_FILE_EXTENSIONS = new Set(['.ts']);
|
|
7
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
8
|
+
|
|
9
|
+
const INTERFACE_DECLARATION_PATTERN = /(?:^|\n)\s*(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
|
|
10
|
+
const TYPE_DECLARATION_PATTERN = /(?:^|\n)\s*(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g;
|
|
11
|
+
|
|
12
|
+
const EXPORT_STAR_PATTERN = /export\s+\*\s+from\s*['"][^'"]+['"]\s*;?/g;
|
|
13
|
+
const EXPORT_FROM_PATTERN = /export\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s*['"][^'"]+['"]\s*;?/g;
|
|
14
|
+
const EXPORT_LOCAL_PATTERN = /export\s+(?:type\s+)?\{[\s\S]*?\}\s*;?/g;
|
|
15
|
+
|
|
16
|
+
const toPosixPath = (value) => value.split(path.sep).join('/');
|
|
17
|
+
|
|
18
|
+
const stripComments = (content) => content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^\\])\/\/.*$/gm, '$1');
|
|
19
|
+
|
|
20
|
+
const stripExportStatements = (content) =>
|
|
21
|
+
content.replace(EXPORT_STAR_PATTERN, '').replace(EXPORT_FROM_PATTERN, '').replace(EXPORT_LOCAL_PATTERN, '');
|
|
22
|
+
|
|
23
|
+
const findLineByIndex = (content, index) => {
|
|
24
|
+
let line = 1;
|
|
25
|
+
for (let i = 0; i < index; i += 1) {
|
|
26
|
+
if (content[i] === '\n') line += 1;
|
|
27
|
+
}
|
|
28
|
+
return line;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getRelativeFilePath = (filePath) => toPosixPath(path.relative(process.cwd(), filePath));
|
|
32
|
+
|
|
33
|
+
const isEnumsPath = (filePath) => {
|
|
34
|
+
const normalized = toPosixPath(filePath);
|
|
35
|
+
return normalized.includes('/enums/');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isIndexFile = (filePath) => path.basename(filePath) === 'index.ts';
|
|
39
|
+
|
|
40
|
+
const isExportOnlyIndex = (content) => {
|
|
41
|
+
const withoutComments = stripComments(content);
|
|
42
|
+
const withoutExports = stripExportStatements(withoutComments);
|
|
43
|
+
return withoutExports.trim().length === 0;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const validateName = (declaration) => {
|
|
47
|
+
if (declaration.kind === 'interface') {
|
|
48
|
+
return /^I[A-Z]/.test(declaration.name) ? null : `Interface "${declaration.name}" must start with "I" and have uppercase second letter`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return /^[IT][A-Z]/.test(declaration.name)
|
|
52
|
+
? null
|
|
53
|
+
: `Type "${declaration.name}" must start with "I" or "T" and have uppercase second letter`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const extractDeclarations = (content) => {
|
|
57
|
+
const cleaned = stripComments(content);
|
|
58
|
+
const declarations = [];
|
|
59
|
+
|
|
60
|
+
const interfaceRegex = new RegExp(INTERFACE_DECLARATION_PATTERN.source, 'g');
|
|
61
|
+
let interfaceMatch;
|
|
62
|
+
while ((interfaceMatch = interfaceRegex.exec(cleaned)) !== null) {
|
|
63
|
+
const name = interfaceMatch[1];
|
|
64
|
+
if (!name) continue;
|
|
65
|
+
declarations.push({
|
|
66
|
+
kind: 'interface',
|
|
67
|
+
name,
|
|
68
|
+
line: findLineByIndex(cleaned, interfaceMatch.index)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const typeRegex = new RegExp(TYPE_DECLARATION_PATTERN.source, 'g');
|
|
73
|
+
let typeMatch;
|
|
74
|
+
while ((typeMatch = typeRegex.exec(cleaned)) !== null) {
|
|
75
|
+
const name = typeMatch[1];
|
|
76
|
+
if (!name) continue;
|
|
77
|
+
declarations.push({
|
|
78
|
+
kind: 'type',
|
|
79
|
+
name,
|
|
80
|
+
line: findLineByIndex(cleaned, typeMatch.index)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return declarations.sort((a, b) => a.line - b.line);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const listDomainFiles = async (domainRoot) => {
|
|
88
|
+
const files = [];
|
|
89
|
+
const stack = [domainRoot];
|
|
90
|
+
|
|
91
|
+
while (stack.length) {
|
|
92
|
+
const current = stack.pop();
|
|
93
|
+
if (!current) continue;
|
|
94
|
+
|
|
95
|
+
let entries = [];
|
|
96
|
+
try {
|
|
97
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
105
|
+
stack.push(path.join(current, entry.name));
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!entry.isFile()) continue;
|
|
111
|
+
const filePath = path.join(current, entry.name);
|
|
112
|
+
if (!DOMAIN_FILE_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
113
|
+
files.push(filePath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return files;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const collectIssuesForFile = (filePath, content) => {
|
|
121
|
+
const issues = [];
|
|
122
|
+
|
|
123
|
+
if (isIndexFile(filePath) && !isExportOnlyIndex(content)) {
|
|
124
|
+
issues.push({
|
|
125
|
+
file: filePath,
|
|
126
|
+
line: 1,
|
|
127
|
+
message: 'index.ts must contain only exports. Move declarations to separate files'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isEnumsPath(filePath)) {
|
|
132
|
+
return issues;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const declarations = extractDeclarations(content);
|
|
136
|
+
if (declarations.length > 5) {
|
|
137
|
+
issues.push({
|
|
138
|
+
file: filePath,
|
|
139
|
+
line: declarations[5]?.line ?? 1,
|
|
140
|
+
message: `Too many declarations in one file (${declarations.length}). Maximum is 5`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const declaration of declarations) {
|
|
145
|
+
const validationError = validateName(declaration);
|
|
146
|
+
if (!validationError) continue;
|
|
147
|
+
issues.push({
|
|
148
|
+
file: filePath,
|
|
149
|
+
line: declaration.line,
|
|
150
|
+
message: validationError
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return issues;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default async function checkDomain() {
|
|
158
|
+
const config = await loadUserConfig();
|
|
159
|
+
const contexts = normalizeContexts(config.contexts);
|
|
160
|
+
|
|
161
|
+
if (!contexts.length) {
|
|
162
|
+
console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const issues = [];
|
|
168
|
+
|
|
169
|
+
for (const context of contexts) {
|
|
170
|
+
const domainRoot = path.join(process.cwd(), 'src', 'app', context.name, 'layers', 'domain');
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(domainRoot);
|
|
174
|
+
} catch {
|
|
175
|
+
issues.push({
|
|
176
|
+
file: domainRoot,
|
|
177
|
+
line: 1,
|
|
178
|
+
message: `Domain folder not found for context "${context.name}"`
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const files = await listDomainFiles(domainRoot);
|
|
184
|
+
for (const filePath of files) {
|
|
185
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
186
|
+
issues.push(...collectIssuesForFile(filePath, content));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!issues.length) {
|
|
191
|
+
console.log('✅ Domain check passed: naming, per-file limits and index exports are valid.');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.error('❌ Domain check failed:');
|
|
196
|
+
for (const issue of issues) {
|
|
197
|
+
console.error(`• ${getRelativeFilePath(issue.file)}:${issue.line} — ${issue.message}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
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 DomainIssue = {
|
|
7
|
+
file: string;
|
|
8
|
+
line: number;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Declaration = {
|
|
13
|
+
kind: 'interface' | 'type';
|
|
14
|
+
name: string;
|
|
15
|
+
line: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DOMAIN_FILE_EXTENSIONS = new Set(['.ts']);
|
|
19
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
|
|
20
|
+
|
|
21
|
+
const INTERFACE_DECLARATION_PATTERN = /(?:^|\n)\s*(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
|
|
22
|
+
const TYPE_DECLARATION_PATTERN = /(?:^|\n)\s*(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g;
|
|
23
|
+
|
|
24
|
+
const EXPORT_STAR_PATTERN = /export\s+\*\s+from\s*['"][^'"]+['"]\s*;?/g;
|
|
25
|
+
const EXPORT_FROM_PATTERN = /export\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s*['"][^'"]+['"]\s*;?/g;
|
|
26
|
+
const EXPORT_LOCAL_PATTERN = /export\s+(?:type\s+)?\{[\s\S]*?\}\s*;?/g;
|
|
27
|
+
|
|
28
|
+
const toPosixPath = (value: string): string => value.split(path.sep).join('/');
|
|
29
|
+
|
|
30
|
+
const stripComments = (content: string): string => content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^\\])\/\/.*$/gm, '$1');
|
|
31
|
+
|
|
32
|
+
const stripExportStatements = (content: string): string =>
|
|
33
|
+
content.replace(EXPORT_STAR_PATTERN, '').replace(EXPORT_FROM_PATTERN, '').replace(EXPORT_LOCAL_PATTERN, '');
|
|
34
|
+
|
|
35
|
+
const findLineByIndex = (content: string, index: number): number => {
|
|
36
|
+
let line = 1;
|
|
37
|
+
for (let i = 0; i < index; i += 1) {
|
|
38
|
+
if (content[i] === '\n') line += 1;
|
|
39
|
+
}
|
|
40
|
+
return line;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getRelativeFilePath = (filePath: string): string => toPosixPath(path.relative(process.cwd(), filePath));
|
|
44
|
+
|
|
45
|
+
const isEnumsPath = (filePath: string): boolean => {
|
|
46
|
+
const normalized = toPosixPath(filePath);
|
|
47
|
+
return normalized.includes('/enums/');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isIndexFile = (filePath: string): boolean => path.basename(filePath) === 'index.ts';
|
|
51
|
+
|
|
52
|
+
const isExportOnlyIndex = (content: string): boolean => {
|
|
53
|
+
const withoutComments = stripComments(content);
|
|
54
|
+
const withoutExports = stripExportStatements(withoutComments);
|
|
55
|
+
return withoutExports.trim().length === 0;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const validateName = (declaration: Declaration): string | null => {
|
|
59
|
+
if (declaration.kind === 'interface') {
|
|
60
|
+
return /^I[A-Z]/.test(declaration.name) ? null : `Interface "${declaration.name}" must start with "I" and have uppercase second letter`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return /^[IT][A-Z]/.test(declaration.name)
|
|
64
|
+
? null
|
|
65
|
+
: `Type "${declaration.name}" must start with "I" or "T" and have uppercase second letter`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const extractDeclarations = (content: string): Declaration[] => {
|
|
69
|
+
const cleaned = stripComments(content);
|
|
70
|
+
const declarations: Declaration[] = [];
|
|
71
|
+
|
|
72
|
+
const interfaceRegex = new RegExp(INTERFACE_DECLARATION_PATTERN.source, 'g');
|
|
73
|
+
let interfaceMatch: RegExpExecArray | null;
|
|
74
|
+
while ((interfaceMatch = interfaceRegex.exec(cleaned)) !== null) {
|
|
75
|
+
const name = interfaceMatch[1];
|
|
76
|
+
if (!name) continue;
|
|
77
|
+
declarations.push({
|
|
78
|
+
kind: 'interface',
|
|
79
|
+
name,
|
|
80
|
+
line: findLineByIndex(cleaned, interfaceMatch.index)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const typeRegex = new RegExp(TYPE_DECLARATION_PATTERN.source, 'g');
|
|
85
|
+
let typeMatch: RegExpExecArray | null;
|
|
86
|
+
while ((typeMatch = typeRegex.exec(cleaned)) !== null) {
|
|
87
|
+
const name = typeMatch[1];
|
|
88
|
+
if (!name) continue;
|
|
89
|
+
declarations.push({
|
|
90
|
+
kind: 'type',
|
|
91
|
+
name,
|
|
92
|
+
line: findLineByIndex(cleaned, typeMatch.index)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return declarations.sort((a, b) => a.line - b.line);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const listDomainFiles = async (domainRoot: string): Promise<string[]> => {
|
|
100
|
+
const files: string[] = [];
|
|
101
|
+
const stack: string[] = [domainRoot];
|
|
102
|
+
|
|
103
|
+
while (stack.length) {
|
|
104
|
+
const current = stack.pop();
|
|
105
|
+
if (!current) continue;
|
|
106
|
+
|
|
107
|
+
let entries: import('node:fs').Dirent[] = [];
|
|
108
|
+
try {
|
|
109
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
117
|
+
stack.push(path.join(current, entry.name));
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!entry.isFile()) continue;
|
|
123
|
+
const filePath = path.join(current, entry.name);
|
|
124
|
+
if (!DOMAIN_FILE_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
125
|
+
files.push(filePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return files;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const collectIssuesForFile = (filePath: string, content: string): DomainIssue[] => {
|
|
133
|
+
const issues: DomainIssue[] = [];
|
|
134
|
+
|
|
135
|
+
if (isIndexFile(filePath) && !isExportOnlyIndex(content)) {
|
|
136
|
+
issues.push({
|
|
137
|
+
file: filePath,
|
|
138
|
+
line: 1,
|
|
139
|
+
message: 'index.ts must contain only exports. Move declarations to separate files'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isEnumsPath(filePath)) {
|
|
144
|
+
return issues;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const declarations = extractDeclarations(content);
|
|
148
|
+
if (declarations.length > 5) {
|
|
149
|
+
issues.push({
|
|
150
|
+
file: filePath,
|
|
151
|
+
line: declarations[5]?.line ?? 1,
|
|
152
|
+
message: `Too many declarations in one file (${declarations.length}). Maximum is 5`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const declaration of declarations) {
|
|
157
|
+
const validationError = validateName(declaration);
|
|
158
|
+
if (!validationError) continue;
|
|
159
|
+
issues.push({
|
|
160
|
+
file: filePath,
|
|
161
|
+
line: declaration.line,
|
|
162
|
+
message: validationError
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return issues;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default async function checkDomain(): Promise<void> {
|
|
170
|
+
const config = await loadUserConfig();
|
|
171
|
+
const contexts = normalizeContexts(config.contexts);
|
|
172
|
+
|
|
173
|
+
if (!contexts.length) {
|
|
174
|
+
console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const issues: DomainIssue[] = [];
|
|
180
|
+
|
|
181
|
+
for (const context of contexts) {
|
|
182
|
+
const domainRoot = path.join(process.cwd(), 'src', 'app', context.name, 'layers', 'domain');
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await fs.access(domainRoot);
|
|
186
|
+
} catch {
|
|
187
|
+
issues.push({
|
|
188
|
+
file: domainRoot,
|
|
189
|
+
line: 1,
|
|
190
|
+
message: `Domain folder not found for context "${context.name}"`
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const files = await listDomainFiles(domainRoot);
|
|
196
|
+
for (const filePath of files) {
|
|
197
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
198
|
+
issues.push(...collectIssuesForFile(filePath, content));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!issues.length) {
|
|
203
|
+
console.log('✅ Domain check passed: naming, per-file limits and index exports are valid.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.error('❌ Domain check failed:');
|
|
208
|
+
for (const issue of issues) {
|
|
209
|
+
console.error(`• ${getRelativeFilePath(issue.file)}:${issue.line} — ${issue.message}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
}
|