@azure-net/cli 5.0.1 → 5.1.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.
@@ -0,0 +1,217 @@
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 { getSharedContextRootPath, getSharedState } from '../../utils/sharedFoundation.js';
6
+ import { toCamelCase } from '../../utils/contextUtils.js';
7
+ const ROUTES_DIR = path.join(process.cwd(), 'src', 'routes');
8
+ const ROUTE_FILE_PATTERN = /^\+(page|server)(?:\.server)?\.(?:svelte|ts|js)$/;
9
+ const PAGE_FILE_PATTERN = /^\+page(?:\.server)?\.(?:svelte|ts|js)$/;
10
+ const SERVER_FILE_PATTERN = /^\+server\.(?:ts|js)$/;
11
+ const isRouteGroup = (segment) => /^\(.+\)$/.test(segment);
12
+ const getRouteGroupName = (segment) => segment.slice(1, -1).trim();
13
+ const extractParamName = (rawParam) => {
14
+ const withoutRest = rawParam.startsWith('...') ? rawParam.slice(3) : rawParam;
15
+ const matcherIndex = withoutRest.indexOf('=');
16
+ return matcherIndex === -1 ? withoutRest : withoutRest.slice(0, matcherIndex);
17
+ };
18
+ const convertDynamicSegments = (segment) => {
19
+ const optionalMatch = segment.match(/^\[\[(.+)\]\]$/);
20
+ if (optionalMatch?.[1]) {
21
+ const paramName = extractParamName(optionalMatch[1]);
22
+ return { variants: ['', `{${paramName}}`], params: [paramName] };
23
+ }
24
+ const params = [];
25
+ const converted = segment.replace(/\[(.+?)\]/g, (_match, rawParam) => {
26
+ const paramName = extractParamName(rawParam);
27
+ if (paramName.length) params.push(paramName);
28
+ return `{${paramName}}`;
29
+ });
30
+ return { variants: [converted], params };
31
+ };
32
+ const normalizeUrl = (segments) => {
33
+ const filtered = segments.filter(Boolean);
34
+ return filtered.length ? `/${filtered.join('/')}` : '/';
35
+ };
36
+ const createRouteVariants = (segments) => {
37
+ let variants = [[]];
38
+ const dynamicParams = new Set();
39
+ for (const segment of segments) {
40
+ if (isRouteGroup(segment)) continue;
41
+ const converted = convertDynamicSegments(segment);
42
+ for (const param of converted.params) {
43
+ dynamicParams.add(param);
44
+ }
45
+ const nextVariants = [];
46
+ for (const variant of variants) {
47
+ for (const nextSegment of converted.variants) {
48
+ nextVariants.push(nextSegment ? [...variant, nextSegment] : [...variant]);
49
+ }
50
+ }
51
+ variants = nextVariants;
52
+ }
53
+ return {
54
+ urls: [...new Set(variants.map(normalizeUrl))],
55
+ dynamicParams: [...dynamicParams]
56
+ };
57
+ };
58
+ const hasRouteFile = (files) => files.some((file) => ROUTE_FILE_PATTERN.test(file));
59
+ const hasPageFile = (files) => files.some((file) => PAGE_FILE_PATTERN.test(file));
60
+ const hasServerFile = (files) => files.some((file) => SERVER_FILE_PATTERN.test(file));
61
+ const readDirSafe = async (dir) => {
62
+ try {
63
+ return await fs.readdir(dir, { withFileTypes: true });
64
+ } catch {
65
+ return [];
66
+ }
67
+ };
68
+ const collectRoutes = async (dir, segments = [], groupStack = [], rootGroup) => {
69
+ const entries = (await readDirSafe(dir)).sort((a, b) => a.name.localeCompare(b.name));
70
+ const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
71
+ const dirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
72
+ const routes = [];
73
+ const routeVariants = createRouteVariants(segments);
74
+ if (hasRouteFile(files)) {
75
+ for (const url of routeVariants.urls) {
76
+ routes.push({
77
+ url,
78
+ routeDir: dir,
79
+ relativeDir: path.relative(ROUTES_DIR, dir) || '.',
80
+ segments,
81
+ dynamicParams: routeVariants.dynamicParams,
82
+ rootGroup,
83
+ groupStack,
84
+ depth: segments.length,
85
+ hasChildren: dirs.length > 0,
86
+ hasPage: hasPageFile(files),
87
+ hasServer: hasServerFile(files)
88
+ });
89
+ }
90
+ }
91
+ for (const child of dirs) {
92
+ const childGroupName = isRouteGroup(child.name) ? getRouteGroupName(child.name) : undefined;
93
+ const nextGroupStack = childGroupName ? [...groupStack, childGroupName] : groupStack;
94
+ const nextRootGroup = rootGroup ?? (segments.length === 0 ? childGroupName : undefined);
95
+ const childRoutes = await collectRoutes(path.join(dir, child.name), [...segments, child.name], nextGroupStack, nextRootGroup);
96
+ routes.push(...childRoutes);
97
+ }
98
+ return routes;
99
+ };
100
+ const sanitizeGroupName = (value) => {
101
+ const trimmed = value.trim();
102
+ return trimmed.length ? trimmed : 'routes';
103
+ };
104
+ const toRouteVariableName = (groupName) => {
105
+ const base = toCamelCase(sanitizeGroupName(groupName).replace(/[^\p{L}\p{N}_-]+/gu, '-'));
106
+ return `${base || 'routes'}Routes`;
107
+ };
108
+ const ensureUniqueVariableName = (name, used) => {
109
+ if (!used.has(name)) {
110
+ used.add(name);
111
+ return name;
112
+ }
113
+ let index = 2;
114
+ let nextName = `${name}${index}`;
115
+ while (used.has(nextName)) {
116
+ index += 1;
117
+ nextName = `${name}${index}`;
118
+ }
119
+ used.add(nextName);
120
+ return nextName;
121
+ };
122
+ const createGroupMap = (routes, mode, resolver) => {
123
+ const groups = new Map();
124
+ for (const route of routes) {
125
+ const groupName =
126
+ mode === 'none'
127
+ ? 'routes'
128
+ : mode === 'root-groups'
129
+ ? (route.rootGroup ?? 'routes')
130
+ : (sanitizeGroupName(String(resolver?.(route) ?? 'routes')) ?? 'routes');
131
+ const list = groups.get(groupName) ?? [];
132
+ list.push(route);
133
+ groups.set(groupName, list);
134
+ }
135
+ return groups;
136
+ };
137
+ const createStringArray = (items, indent = '\t') => {
138
+ if (!items.length) return '[]';
139
+ return `[\n${items.map((item) => `${indent}'${item}'`).join(',\n')}\n]`;
140
+ };
141
+ const createGeneratedFile = (groups) => {
142
+ const usedVariables = new Set();
143
+ const groupEntries = [...groups.entries()].map(([groupName, routes]) => ({
144
+ groupName,
145
+ variableName: ensureUniqueVariableName(
146
+ groupName === 'routes' ? (groups.size === 1 ? 'routes' : 'ungroupedRoutes') : toRouteVariableName(groupName),
147
+ usedVariables
148
+ ),
149
+ routes: [...new Set(routes.map((route) => route.url))].sort((a, b) => a.localeCompare(b))
150
+ }));
151
+ const lines = ['// This file is generated by `azure-net generate routes`.', '// Do not edit it manually.', ''];
152
+ for (const group of groupEntries) {
153
+ lines.push(`export const ${group.variableName} = ${createStringArray(group.routes)} as const;`);
154
+ lines.push('');
155
+ }
156
+ if (groupEntries.length !== 1 || groupEntries[0]?.variableName !== 'routes') {
157
+ lines.push(`export const routes = [${groupEntries.map((group) => `...${group.variableName}`).join(', ')}] as const;`);
158
+ lines.push('');
159
+ }
160
+ lines.push('export const routeGroups = {');
161
+ lines.push(
162
+ ...groupEntries.map((group, index) => `\t'${group.groupName}': ${group.variableName}${index === groupEntries.length - 1 ? '' : ','}`)
163
+ );
164
+ lines.push('} as const;');
165
+ lines.push('');
166
+ lines.push('export type Route = (typeof routes)[number];');
167
+ lines.push('export type RouteGroupName = keyof typeof routeGroups;');
168
+ lines.push('');
169
+ return `${lines.join('\n')}\n`;
170
+ };
171
+ const selectGroupingMode = async () => {
172
+ const { mode } = await prompts({
173
+ type: 'select',
174
+ name: 'mode',
175
+ message: 'How should routes be grouped?',
176
+ choices: [
177
+ { title: 'By root route groups: (account), (landing), ...', value: 'root-groups' },
178
+ { title: 'By azure-net.config.ts groupRoutesFunc', value: 'config-function' },
179
+ { title: 'Do not group, generate one routes array', value: 'none' }
180
+ ]
181
+ });
182
+ return mode ? mode : null;
183
+ };
184
+ const getConfigRouteGroupResolver = async () => {
185
+ const config = await loadUserConfig();
186
+ if (typeof config.groupRoutesFunc !== 'function') {
187
+ console.error('❌ `groupRoutesFunc` is missing in azure-net.config.ts/js or is not a function.');
188
+ console.error('Example:');
189
+ console.error('export default {');
190
+ console.error("\tgroupRoutesFunc: (route) => route.rootGroup ?? 'app'");
191
+ console.error('};');
192
+ return null;
193
+ }
194
+ return config.groupRoutesFunc;
195
+ };
196
+ export default async function generateRoutes() {
197
+ const mode = await selectGroupingMode();
198
+ if (!mode) return;
199
+ const { sharedContext } = await getSharedState();
200
+ const resolver = mode === 'config-function' ? await getConfigRouteGroupResolver() : undefined;
201
+ if (mode === 'config-function' && !resolver) {
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ const routes = await collectRoutes(ROUTES_DIR);
206
+ if (!routes.length) {
207
+ console.warn('⚠️ No SvelteKit routes found in src/routes');
208
+ return;
209
+ }
210
+ const groups = createGroupMap(routes, mode, resolver ?? undefined);
211
+ const outputDir = path.join(getSharedContextRootPath(sharedContext.name), 'runtime', 'routes');
212
+ const outputFile = path.join(outputDir, 'index.ts');
213
+ await fs.mkdir(outputDir, { recursive: true });
214
+ await fs.writeFile(outputFile, createGeneratedFile(groups), 'utf-8');
215
+ console.log(`✅ Routes generated: ${outputFile}`);
216
+ }
217
+ //# sourceMappingURL=generateRoutes.js.map
@@ -0,0 +1,282 @@
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 { getSharedContextRootPath, getSharedState } from '../../utils/sharedFoundation.js';
6
+ import { toCamelCase } from '../../utils/contextUtils.js';
7
+
8
+ type RouteGroupingMode = 'root-groups' | 'config-function' | 'none';
9
+
10
+ export type RouteGenerationMeta = {
11
+ url: string;
12
+ routeDir: string;
13
+ relativeDir: string;
14
+ segments: string[];
15
+ dynamicParams: string[];
16
+ rootGroup?: string;
17
+ groupStack: string[];
18
+ depth: number;
19
+ hasChildren: boolean;
20
+ hasPage: boolean;
21
+ hasServer: boolean;
22
+ };
23
+
24
+ type RouteRecord = RouteGenerationMeta;
25
+ type RouteGroupResolver = (route: RouteGenerationMeta) => string | null | undefined;
26
+
27
+ const ROUTES_DIR = path.join(process.cwd(), 'src', 'routes');
28
+ const ROUTE_FILE_PATTERN = /^\+(page|server)(?:\.server)?\.(?:svelte|ts|js)$/;
29
+ const PAGE_FILE_PATTERN = /^\+page(?:\.server)?\.(?:svelte|ts|js)$/;
30
+ const SERVER_FILE_PATTERN = /^\+server\.(?:ts|js)$/;
31
+
32
+ const isRouteGroup = (segment: string): boolean => /^\(.+\)$/.test(segment);
33
+ const getRouteGroupName = (segment: string): string => segment.slice(1, -1).trim();
34
+
35
+ const extractParamName = (rawParam: string): string => {
36
+ const withoutRest = rawParam.startsWith('...') ? rawParam.slice(3) : rawParam;
37
+ const matcherIndex = withoutRest.indexOf('=');
38
+ return matcherIndex === -1 ? withoutRest : withoutRest.slice(0, matcherIndex);
39
+ };
40
+
41
+ const convertDynamicSegments = (segment: string): { variants: string[]; params: string[] } => {
42
+ const optionalMatch = segment.match(/^\[\[(.+)\]\]$/);
43
+ if (optionalMatch?.[1]) {
44
+ const paramName = extractParamName(optionalMatch[1]);
45
+ return { variants: ['', `{${paramName}}`], params: [paramName] };
46
+ }
47
+
48
+ const params: string[] = [];
49
+ const converted = segment.replace(/\[(.+?)\]/g, (_match, rawParam: string) => {
50
+ const paramName = extractParamName(rawParam);
51
+ if (paramName.length) params.push(paramName);
52
+ return `{${paramName}}`;
53
+ });
54
+
55
+ return { variants: [converted], params };
56
+ };
57
+
58
+ const normalizeUrl = (segments: string[]): string => {
59
+ const filtered = segments.filter(Boolean);
60
+ return filtered.length ? `/${filtered.join('/')}` : '/';
61
+ };
62
+
63
+ const createRouteVariants = (segments: string[]): { urls: string[]; dynamicParams: string[] } => {
64
+ let variants: string[][] = [[]];
65
+ const dynamicParams = new Set<string>();
66
+
67
+ for (const segment of segments) {
68
+ if (isRouteGroup(segment)) continue;
69
+
70
+ const converted = convertDynamicSegments(segment);
71
+ for (const param of converted.params) {
72
+ dynamicParams.add(param);
73
+ }
74
+
75
+ const nextVariants: string[][] = [];
76
+ for (const variant of variants) {
77
+ for (const nextSegment of converted.variants) {
78
+ nextVariants.push(nextSegment ? [...variant, nextSegment] : [...variant]);
79
+ }
80
+ }
81
+ variants = nextVariants;
82
+ }
83
+
84
+ return {
85
+ urls: [...new Set(variants.map(normalizeUrl))],
86
+ dynamicParams: [...dynamicParams]
87
+ };
88
+ };
89
+
90
+ const hasRouteFile = (files: string[]): boolean => files.some((file) => ROUTE_FILE_PATTERN.test(file));
91
+ const hasPageFile = (files: string[]): boolean => files.some((file) => PAGE_FILE_PATTERN.test(file));
92
+ const hasServerFile = (files: string[]): boolean => files.some((file) => SERVER_FILE_PATTERN.test(file));
93
+
94
+ const readDirSafe = async (dir: string): Promise<import('node:fs').Dirent[]> => {
95
+ try {
96
+ return await fs.readdir(dir, { withFileTypes: true });
97
+ } catch {
98
+ return [];
99
+ }
100
+ };
101
+
102
+ const collectRoutes = async (
103
+ dir: string,
104
+ segments: string[] = [],
105
+ groupStack: string[] = [],
106
+ rootGroup?: string
107
+ ): Promise<RouteRecord[]> => {
108
+ const entries = (await readDirSafe(dir)).sort((a, b) => a.name.localeCompare(b.name));
109
+ const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
110
+ const dirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
111
+ const routes: RouteRecord[] = [];
112
+ const routeVariants = createRouteVariants(segments);
113
+
114
+ if (hasRouteFile(files)) {
115
+ for (const url of routeVariants.urls) {
116
+ routes.push({
117
+ url,
118
+ routeDir: dir,
119
+ relativeDir: path.relative(ROUTES_DIR, dir) || '.',
120
+ segments,
121
+ dynamicParams: routeVariants.dynamicParams,
122
+ rootGroup,
123
+ groupStack,
124
+ depth: segments.length,
125
+ hasChildren: dirs.length > 0,
126
+ hasPage: hasPageFile(files),
127
+ hasServer: hasServerFile(files)
128
+ });
129
+ }
130
+ }
131
+
132
+ for (const child of dirs) {
133
+ const childGroupName = isRouteGroup(child.name) ? getRouteGroupName(child.name) : undefined;
134
+ const nextGroupStack = childGroupName ? [...groupStack, childGroupName] : groupStack;
135
+ const nextRootGroup = rootGroup ?? (segments.length === 0 ? childGroupName : undefined);
136
+ const childRoutes = await collectRoutes(path.join(dir, child.name), [...segments, child.name], nextGroupStack, nextRootGroup);
137
+ routes.push(...childRoutes);
138
+ }
139
+
140
+ return routes;
141
+ };
142
+
143
+ const sanitizeGroupName = (value: string): string => {
144
+ const trimmed = value.trim();
145
+ return trimmed.length ? trimmed : 'routes';
146
+ };
147
+
148
+ const toRouteVariableName = (groupName: string): string => {
149
+ const base = toCamelCase(sanitizeGroupName(groupName).replace(/[^\p{L}\p{N}_-]+/gu, '-'));
150
+ return `${base || 'routes'}Routes`;
151
+ };
152
+
153
+ const ensureUniqueVariableName = (name: string, used: Set<string>): string => {
154
+ if (!used.has(name)) {
155
+ used.add(name);
156
+ return name;
157
+ }
158
+
159
+ let index = 2;
160
+ let nextName = `${name}${index}`;
161
+ while (used.has(nextName)) {
162
+ index += 1;
163
+ nextName = `${name}${index}`;
164
+ }
165
+ used.add(nextName);
166
+ return nextName;
167
+ };
168
+
169
+ const createGroupMap = (routes: RouteRecord[], mode: RouteGroupingMode, resolver?: RouteGroupResolver): Map<string, RouteRecord[]> => {
170
+ const groups = new Map<string, RouteRecord[]>();
171
+
172
+ for (const route of routes) {
173
+ const groupName =
174
+ mode === 'none'
175
+ ? 'routes'
176
+ : mode === 'root-groups'
177
+ ? (route.rootGroup ?? 'routes')
178
+ : (sanitizeGroupName(String(resolver?.(route) ?? 'routes')) ?? 'routes');
179
+ const list = groups.get(groupName) ?? [];
180
+ list.push(route);
181
+ groups.set(groupName, list);
182
+ }
183
+
184
+ return groups;
185
+ };
186
+
187
+ const createStringArray = (items: string[], indent = '\t'): string => {
188
+ if (!items.length) return '[]';
189
+ return `[\n${items.map((item) => `${indent}'${item}'`).join(',\n')}\n]`;
190
+ };
191
+
192
+ const createGeneratedFile = (groups: Map<string, RouteRecord[]>): string => {
193
+ const usedVariables = new Set<string>();
194
+ const groupEntries = [...groups.entries()].map(([groupName, routes]) => ({
195
+ groupName,
196
+ variableName: ensureUniqueVariableName(
197
+ groupName === 'routes' ? (groups.size === 1 ? 'routes' : 'ungroupedRoutes') : toRouteVariableName(groupName),
198
+ usedVariables
199
+ ),
200
+ routes: [...new Set(routes.map((route) => route.url))].sort((a, b) => a.localeCompare(b))
201
+ }));
202
+
203
+ const lines: string[] = ['// This file is generated by `azure-net generate routes`.', '// Do not edit it manually.', ''];
204
+
205
+ for (const group of groupEntries) {
206
+ lines.push(`export const ${group.variableName} = ${createStringArray(group.routes)} as const;`);
207
+ lines.push('');
208
+ }
209
+
210
+ if (groupEntries.length !== 1 || groupEntries[0]?.variableName !== 'routes') {
211
+ lines.push(`export const routes = [${groupEntries.map((group) => `...${group.variableName}`).join(', ')}] as const;`);
212
+ lines.push('');
213
+ }
214
+
215
+ lines.push('export const routeGroups = {');
216
+ lines.push(
217
+ ...groupEntries.map((group, index) => `\t'${group.groupName}': ${group.variableName}${index === groupEntries.length - 1 ? '' : ','}`)
218
+ );
219
+ lines.push('} as const;');
220
+ lines.push('');
221
+ lines.push('export type Route = (typeof routes)[number];');
222
+ lines.push('export type RouteGroupName = keyof typeof routeGroups;');
223
+ lines.push('');
224
+
225
+ return `${lines.join('\n')}\n`;
226
+ };
227
+
228
+ const selectGroupingMode = async (): Promise<RouteGroupingMode | null> => {
229
+ const { mode } = await prompts({
230
+ type: 'select',
231
+ name: 'mode',
232
+ message: 'How should routes be grouped?',
233
+ choices: [
234
+ { title: 'By root route groups: (account), (landing), ...', value: 'root-groups' },
235
+ { title: 'By azure-net.config.ts groupRoutesFunc', value: 'config-function' },
236
+ { title: 'Do not group, generate one routes array', value: 'none' }
237
+ ]
238
+ });
239
+
240
+ return mode ? (mode as RouteGroupingMode) : null;
241
+ };
242
+
243
+ const getConfigRouteGroupResolver = async (): Promise<RouteGroupResolver | null> => {
244
+ const config = await loadUserConfig();
245
+ if (typeof config.groupRoutesFunc !== 'function') {
246
+ console.error('❌ `groupRoutesFunc` is missing in azure-net.config.ts/js or is not a function.');
247
+ console.error('Example:');
248
+ console.error('export default {');
249
+ console.error("\tgroupRoutesFunc: (route) => route.rootGroup ?? 'app'");
250
+ console.error('};');
251
+ return null;
252
+ }
253
+
254
+ return config.groupRoutesFunc as RouteGroupResolver;
255
+ };
256
+
257
+ export default async function generateRoutes(): Promise<void> {
258
+ const mode = await selectGroupingMode();
259
+ if (!mode) return;
260
+
261
+ const { sharedContext } = await getSharedState();
262
+ const resolver = mode === 'config-function' ? await getConfigRouteGroupResolver() : undefined;
263
+ if (mode === 'config-function' && !resolver) {
264
+ process.exitCode = 1;
265
+ return;
266
+ }
267
+
268
+ const routes = await collectRoutes(ROUTES_DIR);
269
+ if (!routes.length) {
270
+ console.warn('⚠️ No SvelteKit routes found in src/routes');
271
+ return;
272
+ }
273
+
274
+ const groups = createGroupMap(routes, mode, resolver ?? undefined);
275
+ const outputDir = path.join(getSharedContextRootPath(sharedContext.name), 'runtime', 'routes');
276
+ const outputFile = path.join(outputDir, 'index.ts');
277
+
278
+ await fs.mkdir(outputDir, { recursive: true });
279
+ await fs.writeFile(outputFile, createGeneratedFile(groups), 'utf-8');
280
+
281
+ console.log(`✅ Routes generated: ${outputFile}`);
282
+ }
@@ -5,81 +5,79 @@ import prompts from 'prompts';
5
5
  import { writeIfNotExists, updateIndexTs } from '../utils/fileUtils.js';
6
6
  import { toPascalCase } from '../utils/contextUtils.js';
7
7
  import { getFoundationConstructPath, getSharedState } from '../utils/sharedFoundation.js';
8
- const createPresenterFactoryTemplate = (factoryName) => `import { createAsyncHelpers, createPresenterFactory } from '@azure-net/kit/delivery';
8
+ const createPresenterFactoryTemplate = (
9
+ factoryName
10
+ ) => `import { createAsyncHelpers, createPresenterFactory } from '@azure-net/kit/delivery';
9
11
 
10
12
  export const AsyncHelpers = createAsyncHelpers();
11
13
 
12
14
  export const ${factoryName} = createPresenterFactory({ ...AsyncHelpers });
13
15
  `;
14
16
  const resolveConfigFile = (cwd) => {
15
- const tsPath = path.join(cwd, 'azure-net.config.ts');
16
- const jsPath = path.join(cwd, 'azure-net.config.js');
17
- if (fsSync.existsSync(tsPath)) {
18
- return { filepath: tsPath, type: 'ts', exists: true, content: fsSync.readFileSync(tsPath, 'utf-8') };
19
- }
20
- if (fsSync.existsSync(jsPath)) {
21
- return { filepath: jsPath, type: 'js', exists: true, content: fsSync.readFileSync(jsPath, 'utf-8') };
22
- }
23
- return { filepath: tsPath, type: 'ts', exists: false, content: '' };
17
+ const tsPath = path.join(cwd, 'azure-net.config.ts');
18
+ const jsPath = path.join(cwd, 'azure-net.config.js');
19
+ if (fsSync.existsSync(tsPath)) {
20
+ return { filepath: tsPath, type: 'ts', exists: true, content: fsSync.readFileSync(tsPath, 'utf-8') };
21
+ }
22
+ if (fsSync.existsSync(jsPath)) {
23
+ return { filepath: jsPath, type: 'js', exists: true, content: fsSync.readFileSync(jsPath, 'utf-8') };
24
+ }
25
+ return { filepath: tsPath, type: 'ts', exists: false, content: '' };
24
26
  };
25
27
  const buildDefaultPresenterFactoryConfigEntry = (factoryName) => {
26
- return `defaultPresenterFactory: '${factoryName}'`;
28
+ return `defaultPresenterFactory: '${factoryName}'`;
27
29
  };
28
30
  const upsertDefaultPresenterFactoryConfig = (content, factoryName) => {
29
- const hasExportDefaultObject = /export\s+default\s*\{[\s\S]*\}\s*;?/.test(content);
30
- const defaultFactoryEntry = buildDefaultPresenterFactoryConfigEntry(factoryName);
31
- if (!hasExportDefaultObject) {
32
- return `export default {\n\t${defaultFactoryEntry}\n};\n`;
33
- }
34
- let nextContent = content;
35
- if (/defaultPresenterFactory\s*:/.test(nextContent)) {
36
- nextContent = nextContent.replace(/defaultPresenterFactory\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
37
- }
38
- else if (/defaultPresenter\s*:/.test(nextContent)) {
39
- nextContent = nextContent.replace(/defaultPresenter\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
40
- }
41
- else {
42
- nextContent = nextContent.replace(/export\s+default\s*\{([\s\S]*)\}\s*;?/, (_full, body) => {
43
- const trimmedRight = body.replace(/\s*$/, '');
44
- if (!trimmedRight.length) {
45
- return `export default {\n\t${defaultFactoryEntry}\n};`;
46
- }
47
- const withComma = trimmedRight.endsWith(',') ? trimmedRight : `${trimmedRight},`;
48
- return `export default {${withComma}\n\t${defaultFactoryEntry}\n};`;
49
- });
50
- }
51
- return nextContent;
31
+ const hasExportDefaultObject = /export\s+default\s*\{[\s\S]*\}\s*;?/.test(content);
32
+ const defaultFactoryEntry = buildDefaultPresenterFactoryConfigEntry(factoryName);
33
+ if (!hasExportDefaultObject) {
34
+ return `export default {\n\t${defaultFactoryEntry}\n};\n`;
35
+ }
36
+ let nextContent = content;
37
+ if (/defaultPresenterFactory\s*:/.test(nextContent)) {
38
+ nextContent = nextContent.replace(/defaultPresenterFactory\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
39
+ } else if (/defaultPresenter\s*:/.test(nextContent)) {
40
+ nextContent = nextContent.replace(/defaultPresenter\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
41
+ } else {
42
+ nextContent = nextContent.replace(/export\s+default\s*\{([\s\S]*)\}\s*;?/, (_full, body) => {
43
+ const trimmedRight = body.replace(/\s*$/, '');
44
+ if (!trimmedRight.length) {
45
+ return `export default {\n\t${defaultFactoryEntry}\n};`;
46
+ }
47
+ const withComma = trimmedRight.endsWith(',') ? trimmedRight : `${trimmedRight},`;
48
+ return `export default {${withComma}\n\t${defaultFactoryEntry}\n};`;
49
+ });
50
+ }
51
+ return nextContent;
52
52
  };
53
53
  export default async function createPresenterFactory() {
54
- const { presenterFactoryNameRaw } = await prompts({
55
- type: 'text',
56
- name: 'presenterFactoryNameRaw',
57
- message: 'Presenter factory name:',
58
- initial: 'AppPresenter'
59
- });
60
- const presenterFactoryName = toPascalCase(String(presenterFactoryNameRaw ?? 'AppPresenter')) || 'AppPresenter';
61
- const { sharedContext } = await getSharedState();
62
- const presenterRootPath = getFoundationConstructPath(sharedContext.name, 'presenter');
63
- const filePath = path.join(presenterRootPath, `${presenterFactoryName}.ts`);
64
- await fs.mkdir(presenterRootPath, { recursive: true });
65
- const created = await writeIfNotExists(filePath, createPresenterFactoryTemplate(presenterFactoryName));
66
- if (!created) {
67
- console.log(`⚠️ Presenter factory "${presenterFactoryName}" already exists. File was not overwritten.`);
68
- }
69
- else {
70
- await updateIndexTs(presenterRootPath);
71
- console.log(`✅ Presenter factory created: ${filePath}`);
72
- }
73
- const { useAsDefault } = await prompts({
74
- type: 'confirm',
75
- name: 'useAsDefault',
76
- message: 'Use this presenter factory as default for the project?',
77
- initial: true
78
- });
79
- if (!useAsDefault)
80
- return;
81
- const config = resolveConfigFile(process.cwd());
82
- const nextContent = upsertDefaultPresenterFactoryConfig(config.content, presenterFactoryName);
83
- await fs.writeFile(config.filepath, nextContent, 'utf-8');
84
- console.log(`✅ Default presenter factory saved to ${path.basename(config.filepath)}`);
54
+ const { presenterFactoryNameRaw } = await prompts({
55
+ type: 'text',
56
+ name: 'presenterFactoryNameRaw',
57
+ message: 'Presenter factory name:',
58
+ initial: 'AppPresenter'
59
+ });
60
+ const presenterFactoryName = toPascalCase(String(presenterFactoryNameRaw ?? 'AppPresenter')) || 'AppPresenter';
61
+ const { sharedContext } = await getSharedState();
62
+ const presenterRootPath = getFoundationConstructPath(sharedContext.name, 'presenter');
63
+ const filePath = path.join(presenterRootPath, `${presenterFactoryName}.ts`);
64
+ await fs.mkdir(presenterRootPath, { recursive: true });
65
+ const created = await writeIfNotExists(filePath, createPresenterFactoryTemplate(presenterFactoryName));
66
+ if (!created) {
67
+ console.log(`⚠️ Presenter factory "${presenterFactoryName}" already exists. File was not overwritten.`);
68
+ } else {
69
+ await updateIndexTs(presenterRootPath);
70
+ console.log(`✅ Presenter factory created: ${filePath}`);
71
+ }
72
+ const { useAsDefault } = await prompts({
73
+ type: 'confirm',
74
+ name: 'useAsDefault',
75
+ message: 'Use this presenter factory as default for the project?',
76
+ initial: true
77
+ });
78
+ if (!useAsDefault) return;
79
+ const config = resolveConfigFile(process.cwd());
80
+ const nextContent = upsertDefaultPresenterFactoryConfig(config.content, presenterFactoryName);
81
+ await fs.writeFile(config.filepath, nextContent, 'utf-8');
82
+ console.log(`✅ Default presenter factory saved to ${path.basename(config.filepath)}`);
85
83
  }
@@ -9,6 +9,7 @@ export type AzureNetConfig = {
9
9
  packageManager?: string;
10
10
  defaultSchemaFactory?: string;
11
11
  defaultPresenterFactory?: string;
12
+ groupRoutesFunc?: unknown;
12
13
  [key: string]: unknown;
13
14
  };
14
15