@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.
- package/bin/azure-net.js +12 -45
- package/bin/azure-net.ts +13 -5
- package/package.json +1 -1
- package/src/checks/checkDomain.js +8 -38
- package/src/checks/checkFoldersStructure.js +0 -14
- package/src/checks/checkHooks.js +0 -5
- package/src/checks/checkImports.js +0 -1
- package/src/checks/checkInternal.js +2 -1
- package/src/checks/checkInternal.ts +2 -0
- package/src/checks/checkLayerBoundaries.js +50 -54
- package/src/checks/checkLayerBoundaries.ts +55 -58
- package/src/checks/checkLayerImports.js +136 -0
- package/src/checks/checkLayerImports.ts +185 -0
- package/src/checks/checkPresenterNames.js +1 -37
- package/src/checks/checkPresenterNames.ts +1 -6
- package/src/checks/checkProject.js +0 -6
- package/src/checks/checkProviderGraph.js +1 -38
- package/src/checks/checkProviderNames.js +0 -28
- package/src/checks/index.js +1 -1
- package/src/checks/index.ts +1 -0
- package/src/generate/layers/generateRepository.js +4 -19
- package/src/generate/layers/generateRepositoryMethod.js +0 -12
- package/src/generate/layers/generateUseCases.js +0 -6
- package/src/generate/layers/repositoryShared.js +41 -104
- package/src/generate/layers/repositoryShared.ts +17 -12
- package/src/generate/module/generateDomain.js +0 -25
- package/src/generate/module/generateModule.js +0 -10
- package/src/generate/module/generateModulePreset.js +10 -51
- package/src/generate/module/generateModulePreset.ts +9 -8
- package/src/generate/module/generatePresenter.js +29 -76
- package/src/generate/module/generatePresenter.ts +15 -10
- package/src/generate/module/generateRepository.js +0 -17
- package/src/generate/module/generateSchemaFromType.js +9 -58
- package/src/generate/module/generateSchemaFromType.ts +1 -2
- package/src/generate/module/generateTypesJson.js +4 -45
- package/src/generate/module/generateUseCases.js +0 -9
- package/src/generate/module/repositoryModuleShared.js +21 -98
- package/src/generate/module/repositoryModuleShared.ts +14 -10
- package/src/generate/ui/generateComponent.js +0 -16
- package/src/generate/ui/generateDesignComponent.js +0 -12
- package/src/generate/ui/generateWidget.js +0 -12
- package/src/init/initAliases.js +42 -64
- package/src/init/initAliases.ts +47 -42
- package/src/init/initEdges.js +0 -15
- package/src/init/initStructure.js +48 -21
- package/src/init/initStructure.ts +51 -3
- package/src/install/installFresh.js +0 -16
- package/src/plugins/addLint.js +11 -46
- package/src/scaffolds/createCoreSchema.js +6 -40
- package/src/scaffolds/createCoreSchema.ts +6 -18
- package/src/scaffolds/createDatasource.js +26 -61
- package/src/scaffolds/createDatasource.ts +26 -40
- package/src/scaffolds/createDatasourceProvider.js +19 -56
- package/src/scaffolds/createDatasourceProvider.ts +20 -34
- package/src/scaffolds/createPresenterFactory.js +5 -34
- package/src/scaffolds/createPresenterFactory.ts +5 -17
- package/src/scaffolds/createProgram.js +1 -5
- package/src/scaffolds/createResponse.js +3 -11
- package/src/scaffolds/createResponse.ts +3 -1
- package/src/scaffolds/createSchemaRule.js +3 -8
- package/src/scaffolds/createSchemaRule.ts +3 -1
- package/src/scaffolds/createTranslationManager.js +3 -14
- package/src/scaffolds/createTranslationManager.ts +3 -1
- package/src/utils/contextUtils.js +5 -20
- package/src/utils/contextUtils.ts +2 -3
- package/src/utils/fileUtils.js +3 -38
- package/src/utils/fileUtils.ts +0 -26
- package/src/utils/loadConfig.js +1 -14
- package/src/utils/loadConfig.ts +1 -3
- package/src/utils/sharedFoundation.js +48 -0
- 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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
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 = [
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
152
|
-
if (
|
|
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 ===
|
|
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:
|
|
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:
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 = [
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
197
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
+
}
|