@azure-net/cli 5.0.0-1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bin/azure-net.js +12 -45
  2. package/bin/azure-net.ts +13 -5
  3. package/package.json +1 -1
  4. package/src/checks/checkDomain.js +8 -38
  5. package/src/checks/checkFoldersStructure.js +0 -14
  6. package/src/checks/checkHooks.js +0 -5
  7. package/src/checks/checkImports.js +0 -1
  8. package/src/checks/checkInternal.js +2 -1
  9. package/src/checks/checkInternal.ts +2 -0
  10. package/src/checks/checkLayerBoundaries.js +50 -54
  11. package/src/checks/checkLayerBoundaries.ts +55 -58
  12. package/src/checks/checkLayerImports.js +136 -0
  13. package/src/checks/checkLayerImports.ts +185 -0
  14. package/src/checks/checkPresenterNames.js +1 -37
  15. package/src/checks/checkPresenterNames.ts +1 -6
  16. package/src/checks/checkProject.js +0 -6
  17. package/src/checks/checkProviderGraph.js +1 -38
  18. package/src/checks/checkProviderNames.js +0 -28
  19. package/src/checks/index.js +1 -1
  20. package/src/checks/index.ts +1 -0
  21. package/src/generate/layers/generateRepository.js +4 -19
  22. package/src/generate/layers/generateRepositoryMethod.js +0 -12
  23. package/src/generate/layers/generateUseCases.js +0 -6
  24. package/src/generate/layers/repositoryShared.js +41 -104
  25. package/src/generate/layers/repositoryShared.ts +17 -12
  26. package/src/generate/module/generateDomain.js +0 -25
  27. package/src/generate/module/generateModule.js +0 -10
  28. package/src/generate/module/generateModulePreset.js +10 -51
  29. package/src/generate/module/generateModulePreset.ts +9 -8
  30. package/src/generate/module/generatePresenter.js +29 -76
  31. package/src/generate/module/generatePresenter.ts +15 -10
  32. package/src/generate/module/generateRepository.js +0 -17
  33. package/src/generate/module/generateSchemaFromType.js +9 -58
  34. package/src/generate/module/generateSchemaFromType.ts +1 -2
  35. package/src/generate/module/generateTypesJson.js +4 -45
  36. package/src/generate/module/generateUseCases.js +0 -9
  37. package/src/generate/module/repositoryModuleShared.js +21 -98
  38. package/src/generate/module/repositoryModuleShared.ts +14 -10
  39. package/src/generate/ui/generateComponent.js +0 -16
  40. package/src/generate/ui/generateDesignComponent.js +0 -12
  41. package/src/generate/ui/generateWidget.js +0 -12
  42. package/src/init/initAliases.js +42 -64
  43. package/src/init/initAliases.ts +47 -42
  44. package/src/init/initEdges.js +0 -15
  45. package/src/init/initStructure.js +48 -21
  46. package/src/init/initStructure.ts +51 -3
  47. package/src/install/installFresh.js +0 -16
  48. package/src/plugins/addLint.js +11 -46
  49. package/src/scaffolds/createCoreSchema.js +6 -40
  50. package/src/scaffolds/createCoreSchema.ts +6 -18
  51. package/src/scaffolds/createDatasource.js +26 -61
  52. package/src/scaffolds/createDatasource.ts +26 -40
  53. package/src/scaffolds/createDatasourceProvider.js +19 -56
  54. package/src/scaffolds/createDatasourceProvider.ts +20 -34
  55. package/src/scaffolds/createPresenterFactory.js +5 -34
  56. package/src/scaffolds/createPresenterFactory.ts +5 -17
  57. package/src/scaffolds/createProgram.js +1 -5
  58. package/src/scaffolds/createResponse.js +3 -11
  59. package/src/scaffolds/createResponse.ts +3 -1
  60. package/src/scaffolds/createSchemaRule.js +3 -8
  61. package/src/scaffolds/createSchemaRule.ts +3 -1
  62. package/src/scaffolds/createTranslationManager.js +3 -14
  63. package/src/scaffolds/createTranslationManager.ts +3 -1
  64. package/src/utils/contextUtils.js +5 -20
  65. package/src/utils/contextUtils.ts +2 -3
  66. package/src/utils/fileUtils.js +3 -38
  67. package/src/utils/fileUtils.ts +0 -26
  68. package/src/utils/loadConfig.js +1 -14
  69. package/src/utils/loadConfig.ts +1 -3
  70. package/src/utils/sharedFoundation.js +48 -0
  71. package/src/utils/sharedFoundation.ts +70 -0
@@ -2,6 +2,7 @@ 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
+ import { SHARED_ESSENTIALS_DIR_NAME } from '../utils/sharedFoundation.js';
5
6
  const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
6
7
  const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
7
8
  const IMPORT_FROM_PATTERN = /\bimport\s+(?:type\s+)?[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g;
@@ -83,10 +84,10 @@ const resolveContextFromRelativeImport = (filePath, importPath, contexts) => {
83
84
  }
84
85
  return null;
85
86
  };
86
- const isSharedLikeContext = (contextName, alias) => {
87
- const normalizedName = contextName.toLowerCase();
88
- const normalizedAlias = alias.toLowerCase();
89
- return normalizedName.includes('shared') || normalizedAlias.includes('shared');
87
+ const isInside = (targetPath, rootPath) => targetPath === rootPath || targetPath.startsWith(`${rootPath}${path.sep}`);
88
+ const resolveAbsoluteRelativeImport = (filePath, importPath) => {
89
+ if (!importPath.startsWith('.')) return null;
90
+ return path.resolve(path.dirname(filePath), importPath);
90
91
  };
91
92
  export default async function checkLayerBoundaries() {
92
93
  const config = await loadUserConfig();
@@ -96,96 +97,92 @@ export default async function checkLayerBoundaries() {
96
97
  process.exitCode = 1;
97
98
  return;
98
99
  }
99
- const coreAlias = normalizeAlias(config.coreAlias, '$core');
100
100
  const sharedAliasRaw = normalizeAlias(config.sharedAlias, '');
101
101
  const sharedAlias = sharedAliasRaw.length ? sharedAliasRaw : null;
102
+ if (!sharedAlias) {
103
+ console.error('❌ sharedAlias is missing in azure-net.config.ts/js. Set it explicitly to the shared context alias.');
104
+ process.exitCode = 1;
105
+ return;
106
+ }
102
107
  const aliasToContext = new Map();
103
108
  for (const context of contexts) {
104
109
  aliasToContext.set(context.alias, context);
105
110
  }
106
- const knownAliases = [coreAlias, ...contexts.map((context) => context.alias)];
107
- if (sharedAlias && !knownAliases.includes(sharedAlias)) {
108
- knownAliases.push(sharedAlias);
111
+ const knownAliases = [...contexts.map((context) => context.alias)];
112
+ const sharedContext = aliasToContext.get(sharedAlias);
113
+ if (!sharedContext) {
114
+ console.error(`❌ sharedAlias "${sharedAlias}" does not point to any configured context.`);
115
+ process.exitCode = 1;
116
+ return;
109
117
  }
110
118
  const issues = [];
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
- }
143
119
  for (const context of contexts) {
144
120
  const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
121
+ const isSharedContext = context.alias === sharedAlias;
122
+ const sharedEssentialsRoot = isSharedContext ? path.join(contextRoot, SHARED_ESSENTIALS_DIR_NAME) : null;
145
123
  try {
146
124
  await fs.access(contextRoot);
147
125
  } catch {
148
126
  continue;
149
127
  }
150
128
  const files = await listFiles(contextRoot);
151
- const allowedAliases = new Set([context.alias, coreAlias]);
152
- if (sharedAlias && context.alias !== sharedAlias) {
129
+ const allowedAliases = new Set([context.alias]);
130
+ if (context.alias !== sharedAlias) {
153
131
  allowedAliases.add(sharedAlias);
154
132
  }
155
133
  for (const filePath of files) {
156
134
  const content = await fs.readFile(filePath, 'utf-8');
157
135
  const imports = extractImports(content);
136
+ const isSharedEssentialsFile = Boolean(sharedEssentialsRoot && isInside(filePath, sharedEssentialsRoot));
158
137
  for (const entry of imports) {
159
138
  const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
160
139
  if (!targetAlias) continue;
161
- if (targetAlias === coreAlias) continue;
140
+ if (isSharedEssentialsFile && targetAlias === sharedAlias) {
141
+ const importRest = entry.value.slice(sharedAlias.length).replace(/^\/+/, '');
142
+ if (importRest.length && !importRest.startsWith(`${SHARED_ESSENTIALS_DIR_NAME}/`)) {
143
+ issues.push({
144
+ file: filePath,
145
+ line: entry.line,
146
+ message: `Layer boundary violation: shared essentials cannot import from shared "${importRest}". Essentials may depend only on shared essentials code.`
147
+ });
148
+ continue;
149
+ }
150
+ }
162
151
  if (allowedAliases.has(targetAlias)) continue;
163
152
  const targetContext = aliasToContext.get(targetAlias);
164
153
  const baseMessage = targetContext
165
154
  ? `Layer boundary violation: context "${context.name}" cannot import from context "${targetContext.name}" (${targetAlias})`
166
155
  : `Layer boundary violation: context "${context.name}" cannot import from "${targetAlias}"`;
167
- const withSharedHint =
168
- !sharedAlias && targetContext && isSharedLikeContext(targetContext.name, targetAlias)
169
- ? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
170
- : baseMessage;
171
156
  issues.push({
172
157
  file: filePath,
173
158
  line: entry.line,
174
- message: withSharedHint
159
+ message: baseMessage
175
160
  });
176
161
  }
177
162
  for (const entry of imports) {
163
+ const absoluteRelativeImport = resolveAbsoluteRelativeImport(filePath, entry.value);
164
+ if (
165
+ isSharedEssentialsFile &&
166
+ absoluteRelativeImport &&
167
+ sharedEssentialsRoot &&
168
+ isInside(absoluteRelativeImport, contextRoot) &&
169
+ !isInside(absoluteRelativeImport, sharedEssentialsRoot)
170
+ ) {
171
+ issues.push({
172
+ file: filePath,
173
+ line: entry.line,
174
+ message: 'Layer boundary violation: shared essentials cannot import from shared layers/ui via relative path.'
175
+ });
176
+ continue;
177
+ }
178
178
  const relativeTargetContext = resolveContextFromRelativeImport(filePath, entry.value, contexts);
179
179
  if (!relativeTargetContext || relativeTargetContext.name === context.name) continue;
180
+ if (context.alias !== sharedAlias && relativeTargetContext.alias === sharedAlias) continue;
180
181
  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;
185
182
  issues.push({
186
183
  file: filePath,
187
184
  line: entry.line,
188
- message: withSharedHint
185
+ message: baseMessage
189
186
  });
190
187
  }
191
188
  }
@@ -200,4 +197,3 @@ export default async function checkLayerBoundaries() {
200
197
  }
201
198
  process.exitCode = 1;
202
199
  }
203
- //# sourceMappingURL=checkLayerBoundaries.js.map
@@ -2,6 +2,7 @@ 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
+ import { SHARED_ESSENTIALS_DIR_NAME } from '../utils/sharedFoundation.js';
5
6
 
6
7
  type BoundaryIssue = {
7
8
  file: string;
@@ -115,10 +116,12 @@ const resolveContextFromRelativeImport = (filePath: string, importPath: string,
115
116
  return null;
116
117
  };
117
118
 
118
- const isSharedLikeContext = (contextName: string, alias: string): boolean => {
119
- const normalizedName = contextName.toLowerCase();
120
- const normalizedAlias = alias.toLowerCase();
121
- return normalizedName.includes('shared') || normalizedAlias.includes('shared');
119
+ const isInside = (targetPath: string, rootPath: string): boolean =>
120
+ targetPath === rootPath || targetPath.startsWith(`${rootPath}${path.sep}`);
121
+
122
+ const resolveAbsoluteRelativeImport = (filePath: string, importPath: string): string | null => {
123
+ if (!importPath.startsWith('.')) return null;
124
+ return path.resolve(path.dirname(filePath), importPath);
122
125
  };
123
126
 
124
127
  export default async function checkLayerBoundaries(): Promise<void> {
@@ -131,60 +134,33 @@ export default async function checkLayerBoundaries(): Promise<void> {
131
134
  return;
132
135
  }
133
136
 
134
- const coreAlias = normalizeAlias(config.coreAlias, '$core');
135
137
  const sharedAliasRaw = normalizeAlias(config.sharedAlias, '');
136
138
  const sharedAlias = sharedAliasRaw.length ? sharedAliasRaw : null;
139
+ if (!sharedAlias) {
140
+ console.error('❌ sharedAlias is missing in azure-net.config.ts/js. Set it explicitly to the shared context alias.');
141
+ process.exitCode = 1;
142
+ return;
143
+ }
137
144
 
138
145
  const aliasToContext = new Map<string, ContextConfig>();
139
146
  for (const context of contexts) {
140
147
  aliasToContext.set(context.alias, context);
141
148
  }
142
149
 
143
- const knownAliases = [coreAlias, ...contexts.map((context) => context.alias)];
144
- if (sharedAlias && !knownAliases.includes(sharedAlias)) {
145
- knownAliases.push(sharedAlias);
150
+ const knownAliases = [...contexts.map((context) => context.alias)];
151
+ const sharedContext = aliasToContext.get(sharedAlias);
152
+ if (!sharedContext) {
153
+ console.error(`❌ sharedAlias "${sharedAlias}" does not point to any configured context.`);
154
+ process.exitCode = 1;
155
+ return;
146
156
  }
147
157
 
148
158
  const issues: BoundaryIssue[] = [];
149
159
 
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
-
186
160
  for (const context of contexts) {
187
161
  const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
162
+ const isSharedContext = context.alias === sharedAlias;
163
+ const sharedEssentialsRoot = isSharedContext ? path.join(contextRoot, SHARED_ESSENTIALS_DIR_NAME) : null;
188
164
 
189
165
  try {
190
166
  await fs.access(contextRoot);
@@ -193,19 +169,32 @@ export default async function checkLayerBoundaries(): Promise<void> {
193
169
  }
194
170
 
195
171
  const files = await listFiles(contextRoot);
196
- const allowedAliases = new Set<string>([context.alias, coreAlias]);
197
- if (sharedAlias && context.alias !== sharedAlias) {
172
+ const allowedAliases = new Set<string>([context.alias]);
173
+ if (context.alias !== sharedAlias) {
198
174
  allowedAliases.add(sharedAlias);
199
175
  }
200
176
 
201
177
  for (const filePath of files) {
202
178
  const content = await fs.readFile(filePath, 'utf-8');
203
179
  const imports = extractImports(content);
180
+ const isSharedEssentialsFile = Boolean(sharedEssentialsRoot && isInside(filePath, sharedEssentialsRoot));
204
181
 
205
182
  for (const entry of imports) {
206
183
  const targetAlias = resolveAliasFromImport(entry.value, knownAliases);
207
184
  if (!targetAlias) continue;
208
- if (targetAlias === coreAlias) continue;
185
+
186
+ if (isSharedEssentialsFile && targetAlias === sharedAlias) {
187
+ const importRest = entry.value.slice(sharedAlias.length).replace(/^\/+/, '');
188
+ if (importRest.length && !importRest.startsWith(`${SHARED_ESSENTIALS_DIR_NAME}/`)) {
189
+ issues.push({
190
+ file: filePath,
191
+ line: entry.line,
192
+ message: `Layer boundary violation: shared essentials cannot import from shared "${importRest}". Essentials may depend only on shared essentials code.`
193
+ });
194
+ continue;
195
+ }
196
+ }
197
+
209
198
  if (allowedAliases.has(targetAlias)) continue;
210
199
 
211
200
  const targetContext = aliasToContext.get(targetAlias);
@@ -213,32 +202,40 @@ export default async function checkLayerBoundaries(): Promise<void> {
213
202
  ? `Layer boundary violation: context "${context.name}" cannot import from context "${targetContext.name}" (${targetAlias})`
214
203
  : `Layer boundary violation: context "${context.name}" cannot import from "${targetAlias}"`;
215
204
 
216
- const withSharedHint =
217
- !sharedAlias && targetContext && isSharedLikeContext(targetContext.name, targetAlias)
218
- ? `${baseMessage}. sharedAlias is not configured in azure-net.config.ts/js; if this context is shared, set sharedAlias explicitly`
219
- : baseMessage;
220
-
221
205
  issues.push({
222
206
  file: filePath,
223
207
  line: entry.line,
224
- message: withSharedHint
208
+ message: baseMessage
225
209
  });
226
210
  }
227
211
 
228
212
  for (const entry of imports) {
213
+ const absoluteRelativeImport = resolveAbsoluteRelativeImport(filePath, entry.value);
214
+ if (
215
+ isSharedEssentialsFile &&
216
+ absoluteRelativeImport &&
217
+ sharedEssentialsRoot &&
218
+ isInside(absoluteRelativeImport, contextRoot) &&
219
+ !isInside(absoluteRelativeImport, sharedEssentialsRoot)
220
+ ) {
221
+ issues.push({
222
+ file: filePath,
223
+ line: entry.line,
224
+ message: 'Layer boundary violation: shared essentials cannot import from shared layers/ui via relative path.'
225
+ });
226
+ continue;
227
+ }
228
+
229
229
  const relativeTargetContext = resolveContextFromRelativeImport(filePath, entry.value, contexts);
230
230
  if (!relativeTargetContext || relativeTargetContext.name === context.name) continue;
231
+ if (context.alias !== sharedAlias && relativeTargetContext.alias === sharedAlias) continue;
231
232
 
232
233
  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
234
 
238
235
  issues.push({
239
236
  file: filePath,
240
237
  line: entry.line,
241
- message: withSharedHint
238
+ message: baseMessage
242
239
  });
243
240
  }
244
241
  }
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { normalizeContexts } from '../init/initAliases.js';
4
+ import { loadUserConfig } from '../utils/loadConfig.js';
5
+ const TARGET_EXTENSIONS = new Set(['.ts', '.js', '.svelte']);
6
+ const IGNORED_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', '.git']);
7
+ const LAYERS = ['domain', 'application', 'infrastructure', 'presentation'];
8
+ const FORBIDDEN_LAYER_IMPORTS = {
9
+ infrastructure: ['application', 'presentation'],
10
+ application: ['presentation'],
11
+ domain: ['application', 'infrastructure', 'presentation'],
12
+ presentation: []
13
+ };
14
+ const IMPORT_FROM_PATTERN = /\bimport\s+(?:type\s+)?[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g;
15
+ const EXPORT_FROM_PATTERN = /\bexport\s+(?:type\s+)?\{[\s\S]*?\}\sfrom\s*['"]([^'"]+)['"]/g;
16
+ const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
17
+ const toPosixPath = (value) => value.split(path.sep).join('/');
18
+ const findLineByIndex = (content, index) => {
19
+ let line = 1;
20
+ for (let i = 0; i < index; i += 1) {
21
+ if (content[i] === '\n') line += 1;
22
+ }
23
+ return line;
24
+ };
25
+ const getRelativePath = (filePath) => toPosixPath(path.relative(process.cwd(), filePath));
26
+ const listFiles = async (rootDir) => {
27
+ const files = [];
28
+ const stack = [rootDir];
29
+ while (stack.length) {
30
+ const current = stack.pop();
31
+ if (!current) continue;
32
+ let entries = [];
33
+ try {
34
+ entries = await fs.readdir(current, { withFileTypes: true });
35
+ } catch {
36
+ continue;
37
+ }
38
+ for (const entry of entries) {
39
+ if (entry.isDirectory()) {
40
+ if (!IGNORED_DIRS.has(entry.name)) {
41
+ stack.push(path.join(current, entry.name));
42
+ }
43
+ continue;
44
+ }
45
+ if (!entry.isFile()) continue;
46
+ const filePath = path.join(current, entry.name);
47
+ if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
48
+ files.push(filePath);
49
+ }
50
+ }
51
+ return files;
52
+ };
53
+ const extractImports = (content) => {
54
+ const matches = [];
55
+ const patterns = [IMPORT_FROM_PATTERN, EXPORT_FROM_PATTERN, DYNAMIC_IMPORT_PATTERN];
56
+ for (const pattern of patterns) {
57
+ const regex = new RegExp(pattern.source, 'g');
58
+ let match;
59
+ while ((match = regex.exec(content)) !== null) {
60
+ const value = match[1];
61
+ if (!value) continue;
62
+ matches.push({ value, line: findLineByIndex(content, match.index) });
63
+ }
64
+ }
65
+ return matches;
66
+ };
67
+ const getLayerFromPath = (filePath, contextRoot) => {
68
+ for (const layer of LAYERS) {
69
+ const layerRoot = path.join(contextRoot, 'layers', layer);
70
+ if (filePath === layerRoot || filePath.startsWith(`${layerRoot}${path.sep}`)) {
71
+ return layer;
72
+ }
73
+ }
74
+ return null;
75
+ };
76
+ const getLayerFromAliasImport = (importPath, context) => {
77
+ if (importPath !== context.alias && !importPath.startsWith(`${context.alias}/`)) return null;
78
+ const withoutAlias = importPath.slice(context.alias.length).replace(/^\/+/, '');
79
+ for (const layer of LAYERS) {
80
+ if (withoutAlias === `layers/${layer}` || withoutAlias.startsWith(`layers/${layer}/`)) return layer;
81
+ if (withoutAlias === layer || withoutAlias.startsWith(`${layer}/`)) return layer;
82
+ }
83
+ return null;
84
+ };
85
+ const getLayerFromRelativeImport = (filePath, importPath, contextRoot) => {
86
+ if (!importPath.startsWith('.')) return null;
87
+ const absoluteImportPath = path.resolve(path.dirname(filePath), importPath);
88
+ return getLayerFromPath(absoluteImportPath, contextRoot);
89
+ };
90
+ const createMessage = (sourceLayer, targetLayer, context) =>
91
+ `Layer import violation: "${context.name}" ${sourceLayer} layer cannot import from ${targetLayer} layer`;
92
+ export default async function checkLayerImports() {
93
+ const config = await loadUserConfig();
94
+ const contexts = normalizeContexts(config.contexts);
95
+ if (!contexts.length) {
96
+ console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ const issues = [];
101
+ for (const context of contexts) {
102
+ const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
103
+ try {
104
+ await fs.access(contextRoot);
105
+ } catch {
106
+ continue;
107
+ }
108
+ const files = await listFiles(path.join(contextRoot, 'layers'));
109
+ for (const filePath of files) {
110
+ const sourceLayer = getLayerFromPath(filePath, contextRoot);
111
+ if (!sourceLayer) continue;
112
+ const forbiddenLayers = FORBIDDEN_LAYER_IMPORTS[sourceLayer];
113
+ if (!forbiddenLayers.length) continue;
114
+ const content = await fs.readFile(filePath, 'utf-8');
115
+ const imports = extractImports(content);
116
+ for (const entry of imports) {
117
+ const targetLayer = getLayerFromAliasImport(entry.value, context) ?? getLayerFromRelativeImport(filePath, entry.value, contextRoot);
118
+ if (!targetLayer || !forbiddenLayers.includes(targetLayer)) continue;
119
+ issues.push({
120
+ file: filePath,
121
+ line: entry.line,
122
+ message: createMessage(sourceLayer, targetLayer, context)
123
+ });
124
+ }
125
+ }
126
+ }
127
+ if (!issues.length) {
128
+ console.log('✅ Layer imports check passed.');
129
+ return;
130
+ }
131
+ console.error('❌ Layer imports check failed:');
132
+ for (const issue of issues) {
133
+ console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
134
+ }
135
+ process.exitCode = 1;
136
+ }
@@ -0,0 +1,185 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { normalizeContexts } from '../init/initAliases.js';
4
+ import { loadUserConfig } from '../utils/loadConfig.js';
5
+
6
+ type LayerName = 'domain' | 'application' | 'infrastructure' | 'presentation';
7
+
8
+ type ContextConfig = {
9
+ name: string;
10
+ alias: string;
11
+ };
12
+
13
+ type LayerImportIssue = {
14
+ file: string;
15
+ line: number;
16
+ message: 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 LAYERS: LayerName[] = ['domain', 'application', 'infrastructure', 'presentation'];
22
+ const FORBIDDEN_LAYER_IMPORTS: Record<LayerName, LayerName[]> = {
23
+ infrastructure: ['application', 'presentation'],
24
+ application: ['presentation'],
25
+ domain: ['application', 'infrastructure', 'presentation'],
26
+ presentation: []
27
+ };
28
+
29
+ const IMPORT_FROM_PATTERN = /\bimport\s+(?:type\s+)?[\s\S]*?\sfrom\s*['"]([^'"]+)['"]/g;
30
+ const EXPORT_FROM_PATTERN = /\bexport\s+(?:type\s+)?\{[\s\S]*?\}\sfrom\s*['"]([^'"]+)['"]/g;
31
+ const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
32
+
33
+ const toPosixPath = (value: string): string => value.split(path.sep).join('/');
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 getRelativePath = (filePath: string): string => toPosixPath(path.relative(process.cwd(), filePath));
44
+
45
+ const listFiles = async (rootDir: string): Promise<string[]> => {
46
+ const files: string[] = [];
47
+ const stack: string[] = [rootDir];
48
+
49
+ while (stack.length) {
50
+ const current = stack.pop();
51
+ if (!current) continue;
52
+
53
+ let entries: import('node:fs').Dirent[] = [];
54
+ try {
55
+ entries = await fs.readdir(current, { withFileTypes: true });
56
+ } catch {
57
+ continue;
58
+ }
59
+
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ if (!IGNORED_DIRS.has(entry.name)) {
63
+ stack.push(path.join(current, entry.name));
64
+ }
65
+ continue;
66
+ }
67
+
68
+ if (!entry.isFile()) continue;
69
+ const filePath = path.join(current, entry.name);
70
+ if (!TARGET_EXTENSIONS.has(path.extname(filePath))) continue;
71
+ files.push(filePath);
72
+ }
73
+ }
74
+
75
+ return files;
76
+ };
77
+
78
+ const extractImports = (content: string): Array<{ value: string; line: number }> => {
79
+ const matches: Array<{ value: string; line: number }> = [];
80
+ const patterns = [IMPORT_FROM_PATTERN, EXPORT_FROM_PATTERN, DYNAMIC_IMPORT_PATTERN];
81
+
82
+ for (const pattern of patterns) {
83
+ const regex = new RegExp(pattern.source, 'g');
84
+ let match: RegExpExecArray | null;
85
+ while ((match = regex.exec(content)) !== null) {
86
+ const value = match[1];
87
+ if (!value) continue;
88
+ matches.push({ value, line: findLineByIndex(content, match.index) });
89
+ }
90
+ }
91
+
92
+ return matches;
93
+ };
94
+
95
+ const getLayerFromPath = (filePath: string, contextRoot: string): LayerName | null => {
96
+ for (const layer of LAYERS) {
97
+ const layerRoot = path.join(contextRoot, 'layers', layer);
98
+ if (filePath === layerRoot || filePath.startsWith(`${layerRoot}${path.sep}`)) {
99
+ return layer;
100
+ }
101
+ }
102
+
103
+ return null;
104
+ };
105
+
106
+ const getLayerFromAliasImport = (importPath: string, context: ContextConfig): LayerName | null => {
107
+ if (importPath !== context.alias && !importPath.startsWith(`${context.alias}/`)) return null;
108
+
109
+ const withoutAlias = importPath.slice(context.alias.length).replace(/^\/+/, '');
110
+ for (const layer of LAYERS) {
111
+ if (withoutAlias === `layers/${layer}` || withoutAlias.startsWith(`layers/${layer}/`)) return layer;
112
+ if (withoutAlias === layer || withoutAlias.startsWith(`${layer}/`)) return layer;
113
+ }
114
+
115
+ return null;
116
+ };
117
+
118
+ const getLayerFromRelativeImport = (filePath: string, importPath: string, contextRoot: string): LayerName | null => {
119
+ if (!importPath.startsWith('.')) return null;
120
+
121
+ const absoluteImportPath = path.resolve(path.dirname(filePath), importPath);
122
+ return getLayerFromPath(absoluteImportPath, contextRoot);
123
+ };
124
+
125
+ const createMessage = (sourceLayer: LayerName, targetLayer: LayerName, context: ContextConfig): string =>
126
+ `Layer import violation: "${context.name}" ${sourceLayer} layer cannot import from ${targetLayer} layer`;
127
+
128
+ export default async function checkLayerImports(): Promise<void> {
129
+ const config = await loadUserConfig();
130
+ const contexts = normalizeContexts(config.contexts) as ContextConfig[];
131
+
132
+ if (!contexts.length) {
133
+ console.error('❌ No contexts found in azure-net.config.ts/js. Fill `contexts` first.');
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+
138
+ const issues: LayerImportIssue[] = [];
139
+
140
+ for (const context of contexts) {
141
+ const contextRoot = path.join(process.cwd(), 'src', 'app', context.name);
142
+
143
+ try {
144
+ await fs.access(contextRoot);
145
+ } catch {
146
+ continue;
147
+ }
148
+
149
+ const files = await listFiles(path.join(contextRoot, 'layers'));
150
+
151
+ for (const filePath of files) {
152
+ const sourceLayer = getLayerFromPath(filePath, contextRoot);
153
+ if (!sourceLayer) continue;
154
+
155
+ const forbiddenLayers = FORBIDDEN_LAYER_IMPORTS[sourceLayer];
156
+ if (!forbiddenLayers.length) continue;
157
+
158
+ const content = await fs.readFile(filePath, 'utf-8');
159
+ const imports = extractImports(content);
160
+
161
+ for (const entry of imports) {
162
+ const targetLayer = getLayerFromAliasImport(entry.value, context) ?? getLayerFromRelativeImport(filePath, entry.value, contextRoot);
163
+ if (!targetLayer || !forbiddenLayers.includes(targetLayer)) continue;
164
+
165
+ issues.push({
166
+ file: filePath,
167
+ line: entry.line,
168
+ message: createMessage(sourceLayer, targetLayer, context)
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ if (!issues.length) {
175
+ console.log('✅ Layer imports check passed.');
176
+ return;
177
+ }
178
+
179
+ console.error('❌ Layer imports check failed:');
180
+ for (const issue of issues) {
181
+ console.error(`• ${getRelativePath(issue.file)}:${issue.line} — ${issue.message}`);
182
+ }
183
+
184
+ process.exitCode = 1;
185
+ }