@azure-net/cli 2.3.62 ā 4.0.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/.prettierrc +7 -0
- package/bin/azure-net.js +247 -157
- package/bin/azure-net.ts +253 -0
- package/eslint.config.js +19 -0
- package/package.json +12 -5
- package/src/commands/generate/generateCrudBase.js +95 -114
- package/src/commands/generate/generateCrudPresenter.js +135 -142
- package/src/commands/generate/generateModule.js +21 -21
- package/src/commands/generate/generateModuleBase.js +92 -114
- package/src/commands/generate/generateModulePresenter.js +73 -80
- package/src/commands/generate/generatePresenter.js +38 -44
- package/src/commands/generate/generateRepository.js +41 -47
- package/src/commands/generate/generateSchema.js +74 -70
- package/src/commands/generate/generateService.js +32 -37
- package/src/commands/generate/index.js +0 -5
- package/src/generate/layers/generateRepository.js +124 -0
- package/src/generate/layers/generateRepositoryMethod.js +102 -0
- package/src/generate/layers/generateUseCases.js +50 -0
- package/src/generate/layers/repositoryShared.js +569 -0
- package/src/generate/module/generateDomain.js +146 -0
- package/src/generate/module/generateDomain.ts +147 -0
- package/src/generate/module/generateModule.js +84 -0
- package/src/generate/module/generateModule.ts +84 -0
- package/src/generate/module/generatePresenter.js +419 -0
- package/src/generate/module/generatePresenter.ts +442 -0
- package/src/generate/module/generateRepository.js +144 -0
- package/src/generate/module/generateRepository.ts +158 -0
- package/src/generate/module/generateSchemaFromType.js +320 -0
- package/src/generate/module/generateSchemaFromType.ts +325 -0
- package/src/generate/module/generateTypesJson.js +252 -0
- package/src/generate/module/generateTypesJson.ts +259 -0
- package/src/generate/module/generateUseCases.js +67 -0
- package/src/generate/module/generateUseCases.ts +67 -0
- package/src/generate/module/repositoryModuleShared.js +598 -0
- package/src/generate/ui/generateComponent.js +98 -0
- package/src/generate/ui/generateComponent.ts +100 -0
- package/src/generate/ui/generateDesignComponent.js +71 -0
- package/src/generate/ui/generateDesignComponent.ts +71 -0
- package/src/generate/ui/generateWidget.js +71 -0
- package/src/generate/ui/generateWidget.ts +71 -0
- package/src/init/initAliases.js +159 -0
- package/src/init/initAliases.ts +167 -0
- package/src/init/initEdges.js +74 -0
- package/src/init/initEdges.ts +74 -0
- package/src/init/initStructure.js +122 -0
- package/src/init/initStructure.ts +124 -0
- package/src/install/installFresh.js +101 -0
- package/src/install/installFresh.ts +101 -0
- package/src/plugins/addLint.js +257 -0
- package/src/plugins/addLint.ts +274 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/index.ts +1 -0
- package/src/scaffolds/createCoreMiddleware.js +27 -0
- package/src/scaffolds/createCoreMiddleware.ts +27 -0
- package/src/scaffolds/createCoreSchema.js +141 -0
- package/src/scaffolds/createCoreSchema.ts +142 -0
- package/src/scaffolds/createDatasource.js +173 -0
- package/src/scaffolds/createDatasource.ts +175 -0
- package/src/scaffolds/createDatasourceProvider.js +164 -0
- package/src/scaffolds/createDatasourceProvider.ts +175 -0
- package/src/scaffolds/createMiddleware.js +105 -0
- package/src/scaffolds/createMiddleware.ts +105 -0
- package/src/scaffolds/createPresenterFactory.js +112 -0
- package/src/scaffolds/createPresenterFactory.ts +114 -0
- package/src/scaffolds/createResponse.js +64 -0
- package/src/scaffolds/createResponse.ts +64 -0
- package/src/scaffolds/createSchemaRule.js +40 -0
- package/src/scaffolds/createSchemaRule.ts +42 -0
- package/src/scaffolds/createTranslationManager.js +78 -0
- package/src/scaffolds/createTranslationManager.ts +78 -0
- package/src/scaffolds/index.js +9 -0
- package/src/scaffolds/index.ts +9 -0
- package/src/utils/contextUtils.js +56 -49
- package/src/utils/fileUtils.js +72 -84
- package/src/utils/loadConfig.js +57 -13
- package/tsconfig.json +19 -0
- package/src/commands/generate/generateComponent.js +0 -53
- package/src/commands/generate/generateDatasource.js +0 -84
- package/src/commands/generate/generateDesignComponent.js +0 -53
- package/src/commands/generate/generateResponse.js +0 -41
- package/src/commands/generate/generateWidget.js +0 -53
- package/src/commands/init/index.js +0 -9
- package/src/commands/init/initAliases.js +0 -52
- package/src/commands/init/initEdges.js +0 -42
- package/src/commands/init/initLint.js +0 -138
- package/src/commands/init/initMiddleware.js +0 -21
- package/src/commands/init/initPresenter.js +0 -25
- package/src/commands/init/initSchema.js +0 -22
- package/src/commands/init/initStructure.js +0 -140
- package/src/commands/init/initTranslations.js +0 -59
- package/src/commands/init/migrateStructure.js +0 -144
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { loadUserConfig } from '../../utils/loadConfig.js';
|
|
5
|
+
import { normalizeContexts } from '../../init/initAliases.js';
|
|
6
|
+
import { toCamelCase, toPascalCase } from '../../utils/contextUtils.js';
|
|
7
|
+
import { updateIndexTs } from '../../utils/fileUtils.js';
|
|
8
|
+
|
|
9
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'];
|
|
10
|
+
const MUTATION_METHODS = new Set(['put', 'patch', 'delete']);
|
|
11
|
+
const RESERVED_IDENTIFIERS = new Set([
|
|
12
|
+
'break',
|
|
13
|
+
'case',
|
|
14
|
+
'catch',
|
|
15
|
+
'class',
|
|
16
|
+
'const',
|
|
17
|
+
'continue',
|
|
18
|
+
'debugger',
|
|
19
|
+
'default',
|
|
20
|
+
'delete',
|
|
21
|
+
'do',
|
|
22
|
+
'else',
|
|
23
|
+
'enum',
|
|
24
|
+
'export',
|
|
25
|
+
'extends',
|
|
26
|
+
'false',
|
|
27
|
+
'finally',
|
|
28
|
+
'for',
|
|
29
|
+
'function',
|
|
30
|
+
'if',
|
|
31
|
+
'import',
|
|
32
|
+
'in',
|
|
33
|
+
'instanceof',
|
|
34
|
+
'new',
|
|
35
|
+
'null',
|
|
36
|
+
'return',
|
|
37
|
+
'super',
|
|
38
|
+
'switch',
|
|
39
|
+
'this',
|
|
40
|
+
'throw',
|
|
41
|
+
'true',
|
|
42
|
+
'try',
|
|
43
|
+
'typeof',
|
|
44
|
+
'var',
|
|
45
|
+
'void',
|
|
46
|
+
'while',
|
|
47
|
+
'with',
|
|
48
|
+
'as',
|
|
49
|
+
'implements',
|
|
50
|
+
'interface',
|
|
51
|
+
'let',
|
|
52
|
+
'package',
|
|
53
|
+
'private',
|
|
54
|
+
'protected',
|
|
55
|
+
'public',
|
|
56
|
+
'static',
|
|
57
|
+
'yield'
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const normalizeAlias = (value, fallback) => {
|
|
61
|
+
const raw = String(value ?? '').trim();
|
|
62
|
+
if (!raw.length) return fallback;
|
|
63
|
+
return raw.startsWith('$') ? raw : `$${raw}`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const toTypeFileName = (typeName) => String(typeName ?? '').replace(/^I/, '') || 'Type';
|
|
67
|
+
|
|
68
|
+
export const ensureRepositoryName = (rawName) => {
|
|
69
|
+
const normalized = toPascalCase(String(rawName ?? '').trim() || 'Repository');
|
|
70
|
+
return normalized.endsWith('Repository') ? normalized : `${normalized}Repository`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const getRepositoryMeta = (repositoryName) => {
|
|
74
|
+
const className = ensureRepositoryName(repositoryName);
|
|
75
|
+
const baseName = className.replace(/Repository$/, '') || 'Repository';
|
|
76
|
+
const repositoryInterfaceName = `I${baseName}Repository`;
|
|
77
|
+
const useCasesClassName = `${baseName}UseCases`;
|
|
78
|
+
const repositoryDependencyNameRaw = toCamelCase(baseName || 'repository');
|
|
79
|
+
const repositoryDependencyName = RESERVED_IDENTIFIERS.has(repositoryDependencyNameRaw)
|
|
80
|
+
? `${repositoryDependencyNameRaw}Repository`
|
|
81
|
+
: repositoryDependencyNameRaw;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
className,
|
|
85
|
+
baseName,
|
|
86
|
+
repositoryInterfaceName,
|
|
87
|
+
useCasesClassName,
|
|
88
|
+
repositoryDependencyName
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const getConfigState = async () => {
|
|
93
|
+
const config = await loadUserConfig();
|
|
94
|
+
const contexts = normalizeContexts(config.contexts);
|
|
95
|
+
const coreAlias = normalizeAlias(config.coreAlias, '$core');
|
|
96
|
+
return { contexts, coreAlias };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const selectContext = async (message = 'Select context:') => {
|
|
100
|
+
const { contexts } = await getConfigState();
|
|
101
|
+
if (!contexts.length) {
|
|
102
|
+
console.error('ā Cannot continue: `contexts` is missing or empty in azure-net.config.ts/js');
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { context } = await prompts({
|
|
107
|
+
type: 'select',
|
|
108
|
+
name: 'context',
|
|
109
|
+
message,
|
|
110
|
+
choices: contexts.map((item) => ({ title: item.name, value: item.name })),
|
|
111
|
+
initial: 0
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return context ? String(context) : null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const resolveContextAlias = (contexts, contextName) => {
|
|
118
|
+
const found = contexts.find((item) => item.name === contextName);
|
|
119
|
+
if (!found) return `$${contextName}`;
|
|
120
|
+
return normalizeAlias(found.alias, `$${contextName}`);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const resolveRepositoriesPath = (contextName) =>
|
|
124
|
+
path.join(process.cwd(), 'src', 'app', contextName, 'layers', 'infrastructure', 'http', 'repositories');
|
|
125
|
+
|
|
126
|
+
export const resolveDatasourcesPath = (contextName) =>
|
|
127
|
+
path.join(process.cwd(), 'src', 'app', contextName, 'layers', 'infrastructure', 'http', 'datasources');
|
|
128
|
+
|
|
129
|
+
export const resolveCoreDatasourcesPath = () => path.join(process.cwd(), 'src', 'core', 'datasource');
|
|
130
|
+
|
|
131
|
+
export const resolveDomainRootPath = (contextName, domainName) =>
|
|
132
|
+
path.join(process.cwd(), 'src', 'app', contextName, 'layers', 'domain', domainName);
|
|
133
|
+
|
|
134
|
+
export const resolveUseCasesPath = (contextName) =>
|
|
135
|
+
path.join(process.cwd(), 'src', 'app', contextName, 'layers', 'application', 'use-cases');
|
|
136
|
+
|
|
137
|
+
export const getAvailableTsNames = async (targetPath) => {
|
|
138
|
+
try {
|
|
139
|
+
const files = await fs.readdir(targetPath);
|
|
140
|
+
return files.filter((item) => item.endsWith('.ts') && item !== 'index.ts').map((item) => item.replace(/\.ts$/, ''));
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const selectDatasource = async (contextName) => {
|
|
147
|
+
const { contexts, coreAlias } = await getConfigState();
|
|
148
|
+
const contextAlias = resolveContextAlias(contexts, contextName);
|
|
149
|
+
const coreDatasources = await getAvailableTsNames(resolveCoreDatasourcesPath());
|
|
150
|
+
const contextDatasources = await getAvailableTsNames(resolveDatasourcesPath(contextName));
|
|
151
|
+
|
|
152
|
+
const choices = [];
|
|
153
|
+
for (const datasource of coreDatasources) {
|
|
154
|
+
choices.push({
|
|
155
|
+
title: `${datasource} (core)`,
|
|
156
|
+
value: { name: datasource, importPath: `${coreAlias}/datasource` }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const datasource of contextDatasources) {
|
|
161
|
+
choices.push({
|
|
162
|
+
title: `${datasource} (${contextName})`,
|
|
163
|
+
value: { name: datasource, importPath: `${contextAlias}/layers/infrastructure/http/datasources` }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!choices.length) {
|
|
168
|
+
console.error(`ā No datasources found in core or context "${contextName}".`);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { datasource } = await prompts({
|
|
173
|
+
type: 'select',
|
|
174
|
+
name: 'datasource',
|
|
175
|
+
message: 'Select datasource:',
|
|
176
|
+
choices,
|
|
177
|
+
initial: 0
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return datasource ?? null;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const selectDomain = async (contextName) => {
|
|
184
|
+
const domainPath = path.join(process.cwd(), 'src', 'app', contextName, 'layers', 'domain');
|
|
185
|
+
try {
|
|
186
|
+
const items = await fs.readdir(domainPath, { withFileTypes: true });
|
|
187
|
+
const domains = items.filter((item) => item.isDirectory()).map((item) => item.name);
|
|
188
|
+
if (!domains.length) {
|
|
189
|
+
console.error(`ā No domain folders found in ${domainPath}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { domain } = await prompts({
|
|
194
|
+
type: 'select',
|
|
195
|
+
name: 'domain',
|
|
196
|
+
message: 'Select domain:',
|
|
197
|
+
choices: domains.map((item) => ({ title: item, value: item })),
|
|
198
|
+
initial: 0
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return domain ? String(domain) : null;
|
|
202
|
+
} catch {
|
|
203
|
+
console.error(`ā Domain path not found: ${domainPath}`);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const extractInterfaceName = (content, fallback) => {
|
|
209
|
+
const match = content.match(/export\s+interface\s+([A-Za-z0-9_]+)/);
|
|
210
|
+
return match?.[1] ?? fallback;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const getDomainTypes = async (contextName, domainName) => {
|
|
214
|
+
const root = resolveDomainRootPath(contextName, domainName);
|
|
215
|
+
const result = [];
|
|
216
|
+
const readLayer = async (layer) => {
|
|
217
|
+
const layerPath = path.join(root, layer);
|
|
218
|
+
try {
|
|
219
|
+
const files = await fs.readdir(layerPath);
|
|
220
|
+
for (const fileName of files) {
|
|
221
|
+
if (!fileName.endsWith('.ts') || fileName === 'index.ts') continue;
|
|
222
|
+
const absolute = path.join(layerPath, fileName);
|
|
223
|
+
// eslint-disable-next-line no-await-in-loop
|
|
224
|
+
const content = await fs.readFile(absolute, 'utf-8');
|
|
225
|
+
const fallback = fileName.replace(/\.ts$/, '');
|
|
226
|
+
const interfaceName = extractInterfaceName(content, fallback);
|
|
227
|
+
result.push({ name: interfaceName, layer });
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// ignore
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
await readLayer('ports');
|
|
235
|
+
await readLayer('model');
|
|
236
|
+
return result;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const isPrimitiveType = (value) =>
|
|
240
|
+
['string', 'number', 'boolean', 'unknown', 'void', 'null', 'undefined', 'bigint', 'symbol', 'never'].includes(String(value ?? '').trim());
|
|
241
|
+
|
|
242
|
+
const buildResponseSelection = async (domainTypes) => {
|
|
243
|
+
const { isPrimitive } = await prompts({
|
|
244
|
+
type: 'confirm',
|
|
245
|
+
name: 'isPrimitive',
|
|
246
|
+
message: 'Is response type primitive?',
|
|
247
|
+
initial: false
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (isPrimitive) {
|
|
251
|
+
const { primitiveType } = await prompts({
|
|
252
|
+
type: 'text',
|
|
253
|
+
name: 'primitiveType',
|
|
254
|
+
message: 'Primitive response type:',
|
|
255
|
+
validate: (value) => (String(value ?? '').trim().length > 0 ? true : 'Type is required')
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return { kind: 'primitive', type: String(primitiveType).trim(), layer: null };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!domainTypes.length) {
|
|
262
|
+
console.error('ā Domain has no model/ports types to choose from.');
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const { selectedType } = await prompts({
|
|
267
|
+
type: 'select',
|
|
268
|
+
name: 'selectedType',
|
|
269
|
+
message: 'Select response type:',
|
|
270
|
+
choices: domainTypes.map((item) => ({
|
|
271
|
+
title: `${item.name} (${item.layer})`,
|
|
272
|
+
value: item
|
|
273
|
+
})),
|
|
274
|
+
initial: 0
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (!selectedType) return null;
|
|
278
|
+
return { kind: 'domain', type: selectedType.name, layer: selectedType.layer };
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const buildDomainRequestTypeSelection = async (question, domainTypes) => {
|
|
282
|
+
if (!domainTypes.length) {
|
|
283
|
+
console.error('ā Domain has no model/ports types to choose from.');
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { enabled } = await prompts({
|
|
288
|
+
type: 'confirm',
|
|
289
|
+
name: 'enabled',
|
|
290
|
+
message: question,
|
|
291
|
+
initial: false
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!enabled) return null;
|
|
295
|
+
|
|
296
|
+
const { selectedType } = await prompts({
|
|
297
|
+
type: 'select',
|
|
298
|
+
name: 'selectedType',
|
|
299
|
+
message: 'Select type:',
|
|
300
|
+
choices: domainTypes.map((item) => ({
|
|
301
|
+
title: `${item.name} (${item.layer})`,
|
|
302
|
+
value: item
|
|
303
|
+
})),
|
|
304
|
+
initial: 0
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return selectedType ?? null;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const buildPathParamTypeSelection = async () => {
|
|
311
|
+
const { enabled } = await prompts({
|
|
312
|
+
type: 'confirm',
|
|
313
|
+
name: 'enabled',
|
|
314
|
+
message: 'Has path param?',
|
|
315
|
+
initial: false
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (!enabled) return null;
|
|
319
|
+
|
|
320
|
+
const { type } = await prompts({
|
|
321
|
+
type: 'select',
|
|
322
|
+
name: 'type',
|
|
323
|
+
message: 'Path param type:',
|
|
324
|
+
choices: [
|
|
325
|
+
{ title: 'string', value: 'string' },
|
|
326
|
+
{ title: 'number', value: 'number' }
|
|
327
|
+
],
|
|
328
|
+
initial: 0
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return type ?? 'string';
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const promptMethodDefinition = async (domainTypes) => {
|
|
335
|
+
const { methodNameRaw } = await prompts({
|
|
336
|
+
type: 'text',
|
|
337
|
+
name: 'methodNameRaw',
|
|
338
|
+
message: 'Method name:',
|
|
339
|
+
validate: (value) => (String(value ?? '').trim().length > 0 ? true : 'Method name is required')
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const { httpMethod } = await prompts({
|
|
343
|
+
type: 'select',
|
|
344
|
+
name: 'httpMethod',
|
|
345
|
+
message: 'HTTP method:',
|
|
346
|
+
choices: HTTP_METHODS.map((item) => ({ title: item, value: item })),
|
|
347
|
+
initial: 0
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const { routeRaw } = await prompts({
|
|
351
|
+
type: 'text',
|
|
352
|
+
name: 'routeRaw',
|
|
353
|
+
message: 'Request path (e.g. /public-route):',
|
|
354
|
+
initial: '/public-route',
|
|
355
|
+
validate: (value) => {
|
|
356
|
+
const raw = String(value ?? '').trim();
|
|
357
|
+
if (!raw.length) return 'Path is required';
|
|
358
|
+
return raw.startsWith('/') ? true : 'Path must start with "/"';
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const response = await buildResponseSelection(domainTypes);
|
|
363
|
+
if (!response) return null;
|
|
364
|
+
|
|
365
|
+
const normalizedMethod = String(httpMethod ?? 'get').toLowerCase();
|
|
366
|
+
let queryType = null;
|
|
367
|
+
let bodyType = null;
|
|
368
|
+
let pathParamType = null;
|
|
369
|
+
|
|
370
|
+
if (normalizedMethod === 'get') {
|
|
371
|
+
queryType = await buildDomainRequestTypeSelection('Has query params?', domainTypes);
|
|
372
|
+
} else if (normalizedMethod === 'post') {
|
|
373
|
+
bodyType = await buildDomainRequestTypeSelection('Has request body?', domainTypes);
|
|
374
|
+
} else if (MUTATION_METHODS.has(normalizedMethod)) {
|
|
375
|
+
pathParamType = await buildPathParamTypeSelection();
|
|
376
|
+
bodyType = await buildDomainRequestTypeSelection('Has request body?', domainTypes);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
name: toCamelCase(String(methodNameRaw ?? '').trim() || 'method'),
|
|
381
|
+
httpMethod: normalizedMethod,
|
|
382
|
+
route: String(routeRaw ?? '/public-route').trim() || '/public-route',
|
|
383
|
+
response,
|
|
384
|
+
queryType,
|
|
385
|
+
bodyType,
|
|
386
|
+
pathParamType
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const getResponseTypeName = (method) => method.response.type;
|
|
391
|
+
|
|
392
|
+
const getMethodParams = (method) => {
|
|
393
|
+
const params = [];
|
|
394
|
+
if (method.pathParamType) params.push(`pathParam: ${method.pathParamType}`);
|
|
395
|
+
if (method.queryType) params.push(`request: ${method.queryType.name}`);
|
|
396
|
+
if (method.bodyType) params.push(`request: ${method.bodyType.name}`);
|
|
397
|
+
return params.join(', ');
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const getRequestMapping = (method) => {
|
|
401
|
+
const routeWithPath = method.pathParamType ? `${method.route}/\${pathParam}` : method.route;
|
|
402
|
+
|
|
403
|
+
if (method.httpMethod === 'get') {
|
|
404
|
+
if (method.queryType) {
|
|
405
|
+
return {
|
|
406
|
+
callbackParams: '{ http, query }',
|
|
407
|
+
call: `http.get(\`${routeWithPath}\`, { searchParams: query.toSearchParams(request) })`
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
callbackParams: '{ http }',
|
|
413
|
+
call: `http.get(\`${routeWithPath}\`)`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (method.httpMethod === 'post') {
|
|
418
|
+
if (method.bodyType) {
|
|
419
|
+
return {
|
|
420
|
+
callbackParams: '{ http }',
|
|
421
|
+
call: `http.post(\`${routeWithPath}\`, { json: request })`
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
callbackParams: '{ http }',
|
|
427
|
+
call: `http.post(\`${routeWithPath}\`)`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (method.httpMethod === 'put' || method.httpMethod === 'patch' || method.httpMethod === 'delete') {
|
|
432
|
+
const callMethod = method.httpMethod;
|
|
433
|
+
if (method.bodyType) {
|
|
434
|
+
return {
|
|
435
|
+
callbackParams: '{ http }',
|
|
436
|
+
call: `http.${callMethod}(\`${routeWithPath}\`, { json: request })`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
callbackParams: '{ http }',
|
|
442
|
+
call: `http.${callMethod}(\`${routeWithPath}\`)`
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
callbackParams: '{ http }',
|
|
448
|
+
call: `http.${method.httpMethod}(\`${routeWithPath}\`)`
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export const buildRepositoryMethodCode = (method) => {
|
|
453
|
+
const params = getMethodParams(method);
|
|
454
|
+
const responseType = getResponseTypeName(method);
|
|
455
|
+
const mapping = getRequestMapping(method);
|
|
456
|
+
|
|
457
|
+
return `\tpublic async ${method.name}(${params}) {\n\t\treturn this.datasource\n\t\t\t.createRequest<${responseType}>((${mapping.callbackParams}) => ${mapping.call})\n\t\t\t.then((res) => res.getData());\n\t}`;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
export const buildRepositoryContent = ({ meta, contextAlias, domainName, datasource, methods }) => {
|
|
461
|
+
const typeImports = new Set([meta.repositoryInterfaceName]);
|
|
462
|
+
for (const method of methods) {
|
|
463
|
+
if (!isPrimitiveType(method.response.type)) typeImports.add(method.response.type);
|
|
464
|
+
if (method.queryType) typeImports.add(method.queryType.name);
|
|
465
|
+
if (method.bodyType) typeImports.add(method.bodyType.name);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const methodsCode = methods.map((method) => buildRepositoryMethodCode(method)).join('\n\n');
|
|
469
|
+
const typeImportLine = typeImports.size
|
|
470
|
+
? `import type { ${Array.from(typeImports).join(', ')} } from '${contextAlias}/layers/domain/${domainName}';\n`
|
|
471
|
+
: '';
|
|
472
|
+
|
|
473
|
+
return `import { ${datasource.name} } from '${datasource.importPath}';
|
|
474
|
+
${typeImportLine}
|
|
475
|
+
export class ${meta.className} implements ${meta.repositoryInterfaceName} {
|
|
476
|
+
\tconstructor(private readonly datasource: ${datasource.name}) {}
|
|
477
|
+
|
|
478
|
+
${methodsCode ? `${methodsCode}\n` : ''}
|
|
479
|
+
}
|
|
480
|
+
`;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export const writeRepositoryInterface = async ({ contextName, domainName, meta, methods }) => {
|
|
484
|
+
const domainPath = resolveDomainRootPath(contextName, domainName);
|
|
485
|
+
const portsPath = path.join(domainPath, 'ports');
|
|
486
|
+
await fs.mkdir(portsPath, { recursive: true });
|
|
487
|
+
|
|
488
|
+
const portsImports = new Set();
|
|
489
|
+
const modelImports = new Set();
|
|
490
|
+
|
|
491
|
+
for (const method of methods) {
|
|
492
|
+
const allTypes = [];
|
|
493
|
+
if (!isPrimitiveType(method.response.type) && method.response.layer) {
|
|
494
|
+
allTypes.push({ name: method.response.type, layer: method.response.layer });
|
|
495
|
+
}
|
|
496
|
+
if (method.queryType) allTypes.push(method.queryType);
|
|
497
|
+
if (method.bodyType) allTypes.push(method.bodyType);
|
|
498
|
+
|
|
499
|
+
for (const typeEntry of allTypes) {
|
|
500
|
+
if (typeEntry.layer === 'ports') portsImports.add(typeEntry.name);
|
|
501
|
+
if (typeEntry.layer === 'model') modelImports.add(typeEntry.name);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const imports = [];
|
|
506
|
+
for (const name of Array.from(portsImports)) {
|
|
507
|
+
imports.push(`import type { ${name} } from './${toTypeFileName(name)}';`);
|
|
508
|
+
}
|
|
509
|
+
for (const name of Array.from(modelImports)) {
|
|
510
|
+
imports.push(`import type { ${name} } from '../model/${toTypeFileName(name)}';`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const signatures = methods
|
|
514
|
+
.map((method) => {
|
|
515
|
+
const responseType = getResponseTypeName(method);
|
|
516
|
+
const params = getMethodParams(method);
|
|
517
|
+
return `\t${method.name}(${params}): Promise<${responseType}>;`;
|
|
518
|
+
})
|
|
519
|
+
.join('\n');
|
|
520
|
+
|
|
521
|
+
const content = `${imports.join('\n')}${imports.length ? '\n\n' : ''}export interface ${meta.repositoryInterfaceName} {\n${signatures ? `${signatures}\n` : ''}}\n`;
|
|
522
|
+
await fs.writeFile(path.join(portsPath, `${meta.repositoryInterfaceName}.ts`), content, 'utf-8');
|
|
523
|
+
await updateIndexTs(portsPath);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const parseRepositoryFileForDomain = (content, contextAlias) => {
|
|
527
|
+
const escapedAlias = contextAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
528
|
+
const regex = new RegExp(`from\\s+['"]${escapedAlias}/layers/domain/([^'"]+)['"]`);
|
|
529
|
+
const match = content.match(regex);
|
|
530
|
+
return match?.[1] ?? null;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const parseRepositoryMethodsFromInterface = (content) => {
|
|
534
|
+
const methods = [];
|
|
535
|
+
const regex = /(\w+)\(([^)]*)\)\s*:\s*Promise<([^>]+)>;/g;
|
|
536
|
+
let match;
|
|
537
|
+
while ((match = regex.exec(content)) !== null) {
|
|
538
|
+
methods.push(match[1]);
|
|
539
|
+
}
|
|
540
|
+
return methods;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
export const createUseCasesForRepository = async ({ contextName, contextAlias, domainName, repositoryName }) => {
|
|
544
|
+
const meta = getRepositoryMeta(repositoryName);
|
|
545
|
+
const interfacePath = path.join(resolveDomainRootPath(contextName, domainName), 'ports', `${meta.repositoryInterfaceName}.ts`);
|
|
546
|
+
|
|
547
|
+
let interfaceContent = '';
|
|
548
|
+
try {
|
|
549
|
+
interfaceContent = await fs.readFile(interfacePath, 'utf-8');
|
|
550
|
+
} catch {
|
|
551
|
+
console.error(`ā Repository interface not found: ${interfacePath}`);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const methods = parseRepositoryMethodsFromInterface(interfaceContent);
|
|
556
|
+
const useCasesPath = resolveUseCasesPath(contextName);
|
|
557
|
+
await fs.mkdir(useCasesPath, { recursive: true });
|
|
558
|
+
|
|
559
|
+
const declares = methods.map((methodName) => `\tdeclare ${methodName}: ${meta.repositoryInterfaceName}['${methodName}'];`).join('\n');
|
|
560
|
+
|
|
561
|
+
const content = `import { ClassMirror } from '@azure-net/kit';
|
|
562
|
+
import type { ${meta.repositoryInterfaceName} } from '${contextAlias}/layers/domain/${domainName}';
|
|
563
|
+
|
|
564
|
+
export class ${meta.useCasesClassName} extends ClassMirror {
|
|
565
|
+
\tconstructor(private ${meta.repositoryDependencyName}: ${meta.repositoryInterfaceName}) {
|
|
566
|
+
\t\tsuper(${meta.repositoryDependencyName});
|
|
567
|
+
\t}
|
|
568
|
+
|
|
569
|
+
${declares ? `${declares}\n` : ''}
|
|
570
|
+
}
|
|
571
|
+
`;
|
|
572
|
+
|
|
573
|
+
await fs.writeFile(path.join(useCasesPath, `${meta.useCasesClassName}.ts`), content, 'utf-8');
|
|
574
|
+
await updateIndexTs(useCasesPath);
|
|
575
|
+
return true;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export const resolveDomainForRepository = async ({ contextName, contextAlias, repositoryName }) => {
|
|
579
|
+
const repositoryPath = path.join(resolveRepositoriesPath(contextName), `${ensureRepositoryName(repositoryName)}.ts`);
|
|
580
|
+
try {
|
|
581
|
+
const content = await fs.readFile(repositoryPath, 'utf-8');
|
|
582
|
+
return parseRepositoryFileForDomain(content, contextAlias);
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
export const promptMethodsBatch = async (domainTypes, count) => {
|
|
589
|
+
const methods = [];
|
|
590
|
+
for (let index = 0; index < count; index += 1) {
|
|
591
|
+
console.log(`\nš§© Method ${index + 1} of ${count}`);
|
|
592
|
+
// eslint-disable-next-line no-await-in-loop
|
|
593
|
+
const method = await promptMethodDefinition(domainTypes);
|
|
594
|
+
if (!method) return null;
|
|
595
|
+
methods.push(method);
|
|
596
|
+
}
|
|
597
|
+
return methods;
|
|
598
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { toPascalCase } from '../../utils/contextUtils.js';
|
|
5
|
+
import { normalizeContexts } from '../../init/initAliases.js';
|
|
6
|
+
import { loadUserConfig } from '../../utils/loadConfig.js';
|
|
7
|
+
import { writeIfNotExists } from '../../utils/fileUtils.js';
|
|
8
|
+
|
|
9
|
+
const createComponentContent = (propsName) => `<script lang="ts">
|
|
10
|
+
import type { ${propsName} } from './types';
|
|
11
|
+
const {}: ${propsName} = $props();
|
|
12
|
+
</script>
|
|
13
|
+
<!--Your HTML here-->
|
|
14
|
+
<style lang="scss">
|
|
15
|
+
\t@use './style';
|
|
16
|
+
</style>
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const createTypesContent = (propsName) => `export interface ${propsName} {}
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const createIndexContent = (name) => `export { default as ${name} } from './Component.svelte';
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const printContextsError = () => {
|
|
26
|
+
console.error('ā Cannot generate UI artifact: `contexts` is missing or empty in azure-net.config.ts/js');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const selectContext = async () => {
|
|
30
|
+
const config = await loadUserConfig();
|
|
31
|
+
const contexts = normalizeContexts(config.contexts);
|
|
32
|
+
if (!contexts.length) return null;
|
|
33
|
+
|
|
34
|
+
const { context } = await prompts({
|
|
35
|
+
type: 'select',
|
|
36
|
+
name: 'context',
|
|
37
|
+
message: 'Select context:',
|
|
38
|
+
choices: contexts.map((item) => ({ title: item.name, value: item.name })),
|
|
39
|
+
initial: 0
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return context ? String(context) : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default async function generateComponent() {
|
|
46
|
+
const context = await selectContext();
|
|
47
|
+
if (!context) {
|
|
48
|
+
printContextsError();
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { rawName } = await prompts({
|
|
54
|
+
type: 'text',
|
|
55
|
+
name: 'rawName',
|
|
56
|
+
message: 'Component name:',
|
|
57
|
+
validate: (value) => (String(value).trim().length > 0 ? true : 'Name is required')
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { componentKind } = await prompts({
|
|
61
|
+
type: 'select',
|
|
62
|
+
name: 'componentKind',
|
|
63
|
+
message: 'Component kind:',
|
|
64
|
+
choices: [
|
|
65
|
+
{ title: 'shared', value: 'shared' },
|
|
66
|
+
{ title: 'layout', value: 'layout' },
|
|
67
|
+
{ title: 'page', value: 'page' }
|
|
68
|
+
],
|
|
69
|
+
initial: 0
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const name = toPascalCase(String(rawName ?? 'Component')) || 'Component';
|
|
73
|
+
const propsName = `IComponent${name}Props`;
|
|
74
|
+
const kind = componentKind ?? 'shared';
|
|
75
|
+
let targetDir = path.join(process.cwd(), 'src', 'app', context, 'ui', 'components', kind);
|
|
76
|
+
|
|
77
|
+
if (kind === 'page') {
|
|
78
|
+
const { pageFolderRaw } = await prompts({
|
|
79
|
+
type: 'text',
|
|
80
|
+
name: 'pageFolderRaw',
|
|
81
|
+
message: 'Page folder name:',
|
|
82
|
+
validate: (value) => (String(value).trim().length > 0 ? true : 'Page folder name is required')
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const pageFolder = String(pageFolderRaw ?? '').trim();
|
|
86
|
+
targetDir = path.join(targetDir, pageFolder);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
targetDir = path.join(targetDir, name);
|
|
90
|
+
|
|
91
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
92
|
+
await writeIfNotExists(path.join(targetDir, 'Component.svelte'), createComponentContent(propsName));
|
|
93
|
+
await writeIfNotExists(path.join(targetDir, 'style.scss'), '');
|
|
94
|
+
await writeIfNotExists(path.join(targetDir, 'types.ts'), createTypesContent(propsName));
|
|
95
|
+
await writeIfNotExists(path.join(targetDir, 'index.ts'), createIndexContent(name));
|
|
96
|
+
|
|
97
|
+
console.log(`ā
Component generated: ${targetDir}`);
|
|
98
|
+
}
|