nuxt-openapi-hyperfetch 0.2.7-alpha.1 → 0.3.0-beta
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/.editorconfig +26 -26
- package/.prettierignore +17 -17
- package/CONTRIBUTING.md +291 -291
- package/INSTRUCTIONS.md +327 -327
- package/LICENSE +202 -202
- package/README.md +309 -231
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +1 -1
- package/dist/cli/logo.js +5 -5
- package/dist/cli/messages.d.ts +1 -0
- package/dist/cli/messages.js +2 -0
- package/dist/cli/prompts.d.ts +5 -0
- package/dist/cli/prompts.js +12 -0
- package/dist/cli/types.d.ts +1 -1
- package/dist/generators/components/connector-generator/templates.js +68 -19
- package/dist/generators/shared/runtime/useFormConnector.js +8 -1
- package/dist/generators/shared/runtime/useListConnector.js +13 -6
- package/dist/generators/use-async-data/generator.js +4 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +4 -4
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +4 -4
- package/dist/generators/use-async-data/templates.js +17 -17
- package/dist/generators/use-fetch/generator.js +4 -0
- package/dist/generators/use-fetch/templates.js +14 -14
- package/dist/index.js +40 -27
- package/dist/module/index.js +19 -0
- package/dist/module/types.d.ts +7 -0
- package/docs/API-REFERENCE.md +886 -886
- package/docs/generated-components.md +615 -615
- package/docs/headless-composables-ui.md +569 -569
- package/eslint.config.js +85 -85
- package/package.json +1 -1
- package/src/cli/config.ts +147 -140
- package/src/cli/logger.ts +124 -124
- package/src/cli/logo.ts +25 -25
- package/src/cli/messages.ts +4 -0
- package/src/cli/prompts.ts +14 -1
- package/src/cli/types.ts +50 -50
- package/src/generators/components/connector-generator/generator.ts +138 -138
- package/src/generators/components/connector-generator/templates.ts +307 -254
- package/src/generators/components/connector-generator/types.ts +34 -34
- package/src/generators/components/schema-analyzer/index.ts +44 -44
- package/src/generators/components/schema-analyzer/intent-detector.ts +187 -187
- package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -96
- package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -166
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -268
- package/src/generators/components/schema-analyzer/types.ts +177 -177
- package/src/generators/nuxt-server/generator.ts +272 -272
- package/src/generators/shared/runtime/apiHelpers.ts +535 -535
- package/src/generators/shared/runtime/pagination.ts +323 -323
- package/src/generators/shared/runtime/useDeleteConnector.ts +109 -109
- package/src/generators/shared/runtime/useDetailConnector.ts +64 -64
- package/src/generators/shared/runtime/useFormConnector.ts +147 -139
- package/src/generators/shared/runtime/useListConnector.ts +158 -148
- package/src/generators/shared/runtime/zod-error-merger.ts +119 -119
- package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -399
- package/src/generators/shared/templates/api-pagination-plugin.ts +158 -158
- package/src/generators/use-async-data/generator.ts +213 -205
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -329
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -324
- package/src/generators/use-async-data/templates.ts +257 -257
- package/src/generators/use-fetch/generator.ts +178 -170
- package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -354
- package/src/generators/use-fetch/templates.ts +214 -214
- package/src/index.ts +306 -303
- package/src/module/index.ts +158 -133
- package/src/module/types.ts +39 -31
- package/dist/generators/tanstack-query/generator.d.ts +0 -5
- package/dist/generators/tanstack-query/generator.js +0 -11
|
@@ -1,272 +1,272 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import { format } from 'prettier';
|
|
4
|
-
import {
|
|
5
|
-
getApiFiles as getApiFilesOfficial,
|
|
6
|
-
parseApiFile as parseApiFileOfficial,
|
|
7
|
-
} from './parser.js';
|
|
8
|
-
import {
|
|
9
|
-
getApiFiles as getApiFilesHeyApi,
|
|
10
|
-
parseApiFile as parseApiFileHeyApi,
|
|
11
|
-
} from '../shared/parsers/heyapi-parser.js';
|
|
12
|
-
import {
|
|
13
|
-
generateServerRouteFile,
|
|
14
|
-
generateRouteFilePath,
|
|
15
|
-
generateRoutesIndexFile,
|
|
16
|
-
} from './templates.js';
|
|
17
|
-
import {
|
|
18
|
-
generateAuthContextStub,
|
|
19
|
-
generateAuthTypesStub,
|
|
20
|
-
generateTransformerStub,
|
|
21
|
-
generateTransformerExamples,
|
|
22
|
-
generateBffReadme,
|
|
23
|
-
} from './bff-templates.js';
|
|
24
|
-
import type { MethodInfo } from './types.js';
|
|
25
|
-
import { type Logger, createClackLogger } from '../../cli/logger.js';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Main function to generate Nuxt Server Routes
|
|
29
|
-
*/
|
|
30
|
-
export async function generateNuxtServerRoutes(
|
|
31
|
-
inputDir: string,
|
|
32
|
-
serverRoutePath: string,
|
|
33
|
-
options?: {
|
|
34
|
-
enableBff?: boolean;
|
|
35
|
-
backend?: string;
|
|
36
|
-
},
|
|
37
|
-
logger: Logger = createClackLogger()
|
|
38
|
-
): Promise<void> {
|
|
39
|
-
const mainSpinner = logger.spinner();
|
|
40
|
-
|
|
41
|
-
// Select parser based on chosen backend
|
|
42
|
-
const getApiFiles = options?.backend === 'heyapi' ? getApiFilesHeyApi : getApiFilesOfficial;
|
|
43
|
-
const parseApiFile = options?.backend === 'heyapi' ? parseApiFileHeyApi : parseApiFileOfficial;
|
|
44
|
-
|
|
45
|
-
const enableBff = options?.enableBff ?? false;
|
|
46
|
-
|
|
47
|
-
if (enableBff) {
|
|
48
|
-
logger.log.info('BFF Mode: Enabled (transformers + auth)');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 1. Get all API files
|
|
52
|
-
mainSpinner.start('Scanning API files');
|
|
53
|
-
const apiFiles = getApiFiles(inputDir);
|
|
54
|
-
mainSpinner.stop(`Found ${apiFiles.length} API file(s)`);
|
|
55
|
-
|
|
56
|
-
if (apiFiles.length === 0) {
|
|
57
|
-
throw new Error('No API files found in the input directory');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 2. Parse each API file
|
|
61
|
-
mainSpinner.start('Parsing API files');
|
|
62
|
-
const allMethods: MethodInfo[] = [];
|
|
63
|
-
|
|
64
|
-
for (const file of apiFiles) {
|
|
65
|
-
const fileName = path.basename(file);
|
|
66
|
-
try {
|
|
67
|
-
const apiInfo = parseApiFile(file);
|
|
68
|
-
allMethods.push(...apiInfo.methods);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
logger.log.error(`Error parsing ${fileName}: ${String(error)}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
mainSpinner.stop(`Found ${allMethods.length} routes to generate`);
|
|
75
|
-
|
|
76
|
-
if (allMethods.length === 0) {
|
|
77
|
-
logger.log.warn('No methods found to generate');
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 3. Clean and create output directory
|
|
82
|
-
mainSpinner.start('Preparing output directory');
|
|
83
|
-
await fs.emptyDir(serverRoutePath);
|
|
84
|
-
mainSpinner.stop('Output directory ready');
|
|
85
|
-
|
|
86
|
-
// 4. Generate BFF structure if enabled
|
|
87
|
-
if (enableBff) {
|
|
88
|
-
await generateBffStructure(allMethods, serverRoutePath, inputDir, logger);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 5. Calculate relative import path from server routes to APIs
|
|
92
|
-
const relativePath = calculateRelativeImportPath(serverRoutePath, inputDir);
|
|
93
|
-
|
|
94
|
-
// 6. Generate each server route
|
|
95
|
-
mainSpinner.start('Generating server routes');
|
|
96
|
-
let successCount = 0;
|
|
97
|
-
let errorCount = 0;
|
|
98
|
-
|
|
99
|
-
for (const method of allMethods) {
|
|
100
|
-
try {
|
|
101
|
-
// Extract resource name from path
|
|
102
|
-
const resource = extractResourceFromPath(method.path);
|
|
103
|
-
|
|
104
|
-
const code = generateServerRouteFile(method, relativePath, {
|
|
105
|
-
enableBff: enableBff,
|
|
106
|
-
resource: resource,
|
|
107
|
-
});
|
|
108
|
-
const formattedCode = await formatCode(code, logger);
|
|
109
|
-
const routeFilePath = generateRouteFilePath(method);
|
|
110
|
-
const fullPath = path.join(serverRoutePath, routeFilePath);
|
|
111
|
-
|
|
112
|
-
// Ensure directory exists
|
|
113
|
-
await fs.ensureDir(path.dirname(fullPath));
|
|
114
|
-
|
|
115
|
-
await fs.writeFile(fullPath, formattedCode, 'utf-8');
|
|
116
|
-
successCount++;
|
|
117
|
-
} catch (error) {
|
|
118
|
-
logger.log.error(`Error generating ${method.path} [${method.httpMethod}]: ${String(error)}`);
|
|
119
|
-
errorCount++;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
mainSpinner.stop(`Generated ${successCount} server routes`);
|
|
123
|
-
|
|
124
|
-
// 7. Generate configuration files
|
|
125
|
-
mainSpinner.start('Generating configuration files');
|
|
126
|
-
|
|
127
|
-
// Generate routes index (documentation)
|
|
128
|
-
const routesIndexCode = generateRoutesIndexFile(allMethods);
|
|
129
|
-
const formattedRoutesIndex = await formatCode(routesIndexCode, logger);
|
|
130
|
-
await fs.writeFile(path.join(serverRoutePath, '_routes.ts'), formattedRoutesIndex, 'utf-8');
|
|
131
|
-
mainSpinner.stop('Configuration files generated');
|
|
132
|
-
|
|
133
|
-
// 8. Summary and Next Steps
|
|
134
|
-
if (errorCount > 0) {
|
|
135
|
-
logger.log.warn(`Completed with ${errorCount} error(s)`);
|
|
136
|
-
}
|
|
137
|
-
logger.log.success(`Generated ${successCount} server route(s) in ${serverRoutePath}`);
|
|
138
|
-
|
|
139
|
-
// Build next steps message
|
|
140
|
-
let nextSteps = '1. Configure API_BASE_URL and API_SECRET in your .env\n';
|
|
141
|
-
nextSteps += '2. Update nuxt.config.ts with runtimeConfig (add apiBaseUrl and apiSecret)';
|
|
142
|
-
|
|
143
|
-
if (enableBff) {
|
|
144
|
-
nextSteps += '\n3. Implement authentication in server/auth/context.ts';
|
|
145
|
-
nextSteps += '\n4. Add business logic to transformers in server/bff/transformers/';
|
|
146
|
-
nextSteps += '\n5. See server/bff/README.md for BFF documentation';
|
|
147
|
-
nextSteps += '\n6. Start your Nuxt dev server and test the routes';
|
|
148
|
-
} else {
|
|
149
|
-
nextSteps += '\n3. Start your Nuxt dev server and test the routes';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
logger.note(nextSteps, 'Next steps');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Calculate relative import path from server routes to APIs
|
|
157
|
-
*/
|
|
158
|
-
function calculateRelativeImportPath(serverRoutePath: string, inputDir: string): string {
|
|
159
|
-
// Use Nuxt's ~ alias (project root) so the path is stable regardless of serverRoutePath depth
|
|
160
|
-
const projectRoot = process.cwd();
|
|
161
|
-
const relativeInputDir = path.relative(projectRoot, path.resolve(inputDir));
|
|
162
|
-
// Convert Windows paths to Unix-style
|
|
163
|
-
return '~/' + relativeInputDir.replace(/\\/g, '/');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Format code with Prettier
|
|
168
|
-
*/
|
|
169
|
-
async function formatCode(code: string, logger: Logger): Promise<string> {
|
|
170
|
-
try {
|
|
171
|
-
return await format(code, {
|
|
172
|
-
parser: 'typescript',
|
|
173
|
-
semi: false,
|
|
174
|
-
singleQuote: true,
|
|
175
|
-
trailingComma: 'es5',
|
|
176
|
-
printWidth: 100,
|
|
177
|
-
});
|
|
178
|
-
} catch {
|
|
179
|
-
logger.log.warn('Prettier formatting failed, using unformatted code');
|
|
180
|
-
return code;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Generate BFF structure (auth + transformers)
|
|
186
|
-
*/
|
|
187
|
-
async function generateBffStructure(
|
|
188
|
-
allMethods: MethodInfo[],
|
|
189
|
-
serverRoutePath: string,
|
|
190
|
-
inputDir: string,
|
|
191
|
-
logger: Logger
|
|
192
|
-
): Promise<void> {
|
|
193
|
-
const bffSpinner = logger.spinner();
|
|
194
|
-
bffSpinner.start('Generating BFF structure (auth + transformers)');
|
|
195
|
-
|
|
196
|
-
const serverRoot = path.dirname(serverRoutePath);
|
|
197
|
-
|
|
198
|
-
// 1. Generate auth files (only if they don't exist)
|
|
199
|
-
const authDir = path.join(serverRoot, 'auth');
|
|
200
|
-
await fs.ensureDir(authDir);
|
|
201
|
-
|
|
202
|
-
const authContextPath = path.join(authDir, 'context.ts');
|
|
203
|
-
if (!fs.existsSync(authContextPath)) {
|
|
204
|
-
const authContextCode = generateAuthContextStub();
|
|
205
|
-
const formattedAuthContext = await formatCode(authContextCode, logger);
|
|
206
|
-
await fs.writeFile(authContextPath, formattedAuthContext, 'utf-8');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const authTypesPath = path.join(authDir, 'types.ts');
|
|
210
|
-
if (!fs.existsSync(authTypesPath)) {
|
|
211
|
-
const authTypesCode = generateAuthTypesStub();
|
|
212
|
-
const formattedAuthTypes = await formatCode(authTypesCode, logger);
|
|
213
|
-
await fs.writeFile(authTypesPath, formattedAuthTypes, 'utf-8');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 2. Generate transformer stubs (only if they don't exist)
|
|
217
|
-
const bffDir = path.join(serverRoot, 'bff');
|
|
218
|
-
const transformersDir = path.join(bffDir, 'transformers');
|
|
219
|
-
await fs.ensureDir(transformersDir);
|
|
220
|
-
|
|
221
|
-
// Group methods by resource
|
|
222
|
-
const methodsByResource = new Map<string, MethodInfo[]>();
|
|
223
|
-
for (const method of allMethods) {
|
|
224
|
-
const resource = extractResourceFromPath(method.path);
|
|
225
|
-
if (!methodsByResource.has(resource)) {
|
|
226
|
-
methodsByResource.set(resource, []);
|
|
227
|
-
}
|
|
228
|
-
methodsByResource.get(resource)!.push(method);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Generate transformer for each resource
|
|
232
|
-
for (const [resource, methods] of methodsByResource.entries()) {
|
|
233
|
-
const transformerPath = path.join(transformersDir, `${resource}.ts`);
|
|
234
|
-
if (!fs.existsSync(transformerPath)) {
|
|
235
|
-
const transformerCode = generateTransformerStub(resource, methods, inputDir);
|
|
236
|
-
const formattedTransformer = await formatCode(transformerCode, logger);
|
|
237
|
-
await fs.writeFile(transformerPath, formattedTransformer, 'utf-8');
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// 3. Generate examples file (always regenerated)
|
|
242
|
-
const examplesPath = path.join(bffDir, '_transformers.example.ts');
|
|
243
|
-
const examplesCode = generateTransformerExamples();
|
|
244
|
-
const formattedExamples = await formatCode(examplesCode, logger);
|
|
245
|
-
await fs.writeFile(examplesPath, formattedExamples, 'utf-8');
|
|
246
|
-
|
|
247
|
-
// 4. Generate BFF README (always regenerated)
|
|
248
|
-
const bffReadmePath = path.join(bffDir, 'README.md');
|
|
249
|
-
const bffReadmeCode = generateBffReadme();
|
|
250
|
-
await fs.writeFile(bffReadmePath, bffReadmeCode, 'utf-8');
|
|
251
|
-
|
|
252
|
-
bffSpinner.stop('BFF structure generated');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Extract resource name from API path
|
|
257
|
-
* Examples:
|
|
258
|
-
* /pet -> pet
|
|
259
|
-
* /pet/{id} -> pet
|
|
260
|
-
* /store/inventory -> store
|
|
261
|
-
* /user/login -> user
|
|
262
|
-
*/
|
|
263
|
-
function extractResourceFromPath(path: string): string {
|
|
264
|
-
// Remove leading slash
|
|
265
|
-
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
266
|
-
|
|
267
|
-
// Get first segment
|
|
268
|
-
const firstSegment = cleanPath.split('/')[0];
|
|
269
|
-
|
|
270
|
-
// Remove any path params
|
|
271
|
-
return firstSegment.replace(/\{[^}]+\}/g, '').toLowerCase();
|
|
272
|
-
}
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { format } from 'prettier';
|
|
4
|
+
import {
|
|
5
|
+
getApiFiles as getApiFilesOfficial,
|
|
6
|
+
parseApiFile as parseApiFileOfficial,
|
|
7
|
+
} from './parser.js';
|
|
8
|
+
import {
|
|
9
|
+
getApiFiles as getApiFilesHeyApi,
|
|
10
|
+
parseApiFile as parseApiFileHeyApi,
|
|
11
|
+
} from '../shared/parsers/heyapi-parser.js';
|
|
12
|
+
import {
|
|
13
|
+
generateServerRouteFile,
|
|
14
|
+
generateRouteFilePath,
|
|
15
|
+
generateRoutesIndexFile,
|
|
16
|
+
} from './templates.js';
|
|
17
|
+
import {
|
|
18
|
+
generateAuthContextStub,
|
|
19
|
+
generateAuthTypesStub,
|
|
20
|
+
generateTransformerStub,
|
|
21
|
+
generateTransformerExamples,
|
|
22
|
+
generateBffReadme,
|
|
23
|
+
} from './bff-templates.js';
|
|
24
|
+
import type { MethodInfo } from './types.js';
|
|
25
|
+
import { type Logger, createClackLogger } from '../../cli/logger.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Main function to generate Nuxt Server Routes
|
|
29
|
+
*/
|
|
30
|
+
export async function generateNuxtServerRoutes(
|
|
31
|
+
inputDir: string,
|
|
32
|
+
serverRoutePath: string,
|
|
33
|
+
options?: {
|
|
34
|
+
enableBff?: boolean;
|
|
35
|
+
backend?: string;
|
|
36
|
+
},
|
|
37
|
+
logger: Logger = createClackLogger()
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const mainSpinner = logger.spinner();
|
|
40
|
+
|
|
41
|
+
// Select parser based on chosen backend
|
|
42
|
+
const getApiFiles = options?.backend === 'heyapi' ? getApiFilesHeyApi : getApiFilesOfficial;
|
|
43
|
+
const parseApiFile = options?.backend === 'heyapi' ? parseApiFileHeyApi : parseApiFileOfficial;
|
|
44
|
+
|
|
45
|
+
const enableBff = options?.enableBff ?? false;
|
|
46
|
+
|
|
47
|
+
if (enableBff) {
|
|
48
|
+
logger.log.info('BFF Mode: Enabled (transformers + auth)');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 1. Get all API files
|
|
52
|
+
mainSpinner.start('Scanning API files');
|
|
53
|
+
const apiFiles = getApiFiles(inputDir);
|
|
54
|
+
mainSpinner.stop(`Found ${apiFiles.length} API file(s)`);
|
|
55
|
+
|
|
56
|
+
if (apiFiles.length === 0) {
|
|
57
|
+
throw new Error('No API files found in the input directory');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Parse each API file
|
|
61
|
+
mainSpinner.start('Parsing API files');
|
|
62
|
+
const allMethods: MethodInfo[] = [];
|
|
63
|
+
|
|
64
|
+
for (const file of apiFiles) {
|
|
65
|
+
const fileName = path.basename(file);
|
|
66
|
+
try {
|
|
67
|
+
const apiInfo = parseApiFile(file);
|
|
68
|
+
allMethods.push(...apiInfo.methods);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.log.error(`Error parsing ${fileName}: ${String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mainSpinner.stop(`Found ${allMethods.length} routes to generate`);
|
|
75
|
+
|
|
76
|
+
if (allMethods.length === 0) {
|
|
77
|
+
logger.log.warn('No methods found to generate');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Clean and create output directory
|
|
82
|
+
mainSpinner.start('Preparing output directory');
|
|
83
|
+
await fs.emptyDir(serverRoutePath);
|
|
84
|
+
mainSpinner.stop('Output directory ready');
|
|
85
|
+
|
|
86
|
+
// 4. Generate BFF structure if enabled
|
|
87
|
+
if (enableBff) {
|
|
88
|
+
await generateBffStructure(allMethods, serverRoutePath, inputDir, logger);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. Calculate relative import path from server routes to APIs
|
|
92
|
+
const relativePath = calculateRelativeImportPath(serverRoutePath, inputDir);
|
|
93
|
+
|
|
94
|
+
// 6. Generate each server route
|
|
95
|
+
mainSpinner.start('Generating server routes');
|
|
96
|
+
let successCount = 0;
|
|
97
|
+
let errorCount = 0;
|
|
98
|
+
|
|
99
|
+
for (const method of allMethods) {
|
|
100
|
+
try {
|
|
101
|
+
// Extract resource name from path
|
|
102
|
+
const resource = extractResourceFromPath(method.path);
|
|
103
|
+
|
|
104
|
+
const code = generateServerRouteFile(method, relativePath, {
|
|
105
|
+
enableBff: enableBff,
|
|
106
|
+
resource: resource,
|
|
107
|
+
});
|
|
108
|
+
const formattedCode = await formatCode(code, logger);
|
|
109
|
+
const routeFilePath = generateRouteFilePath(method);
|
|
110
|
+
const fullPath = path.join(serverRoutePath, routeFilePath);
|
|
111
|
+
|
|
112
|
+
// Ensure directory exists
|
|
113
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
114
|
+
|
|
115
|
+
await fs.writeFile(fullPath, formattedCode, 'utf-8');
|
|
116
|
+
successCount++;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.log.error(`Error generating ${method.path} [${method.httpMethod}]: ${String(error)}`);
|
|
119
|
+
errorCount++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
mainSpinner.stop(`Generated ${successCount} server routes`);
|
|
123
|
+
|
|
124
|
+
// 7. Generate configuration files
|
|
125
|
+
mainSpinner.start('Generating configuration files');
|
|
126
|
+
|
|
127
|
+
// Generate routes index (documentation)
|
|
128
|
+
const routesIndexCode = generateRoutesIndexFile(allMethods);
|
|
129
|
+
const formattedRoutesIndex = await formatCode(routesIndexCode, logger);
|
|
130
|
+
await fs.writeFile(path.join(serverRoutePath, '_routes.ts'), formattedRoutesIndex, 'utf-8');
|
|
131
|
+
mainSpinner.stop('Configuration files generated');
|
|
132
|
+
|
|
133
|
+
// 8. Summary and Next Steps
|
|
134
|
+
if (errorCount > 0) {
|
|
135
|
+
logger.log.warn(`Completed with ${errorCount} error(s)`);
|
|
136
|
+
}
|
|
137
|
+
logger.log.success(`Generated ${successCount} server route(s) in ${serverRoutePath}`);
|
|
138
|
+
|
|
139
|
+
// Build next steps message
|
|
140
|
+
let nextSteps = '1. Configure API_BASE_URL and API_SECRET in your .env\n';
|
|
141
|
+
nextSteps += '2. Update nuxt.config.ts with runtimeConfig (add apiBaseUrl and apiSecret)';
|
|
142
|
+
|
|
143
|
+
if (enableBff) {
|
|
144
|
+
nextSteps += '\n3. Implement authentication in server/auth/context.ts';
|
|
145
|
+
nextSteps += '\n4. Add business logic to transformers in server/bff/transformers/';
|
|
146
|
+
nextSteps += '\n5. See server/bff/README.md for BFF documentation';
|
|
147
|
+
nextSteps += '\n6. Start your Nuxt dev server and test the routes';
|
|
148
|
+
} else {
|
|
149
|
+
nextSteps += '\n3. Start your Nuxt dev server and test the routes';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
logger.note(nextSteps, 'Next steps');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Calculate relative import path from server routes to APIs
|
|
157
|
+
*/
|
|
158
|
+
function calculateRelativeImportPath(serverRoutePath: string, inputDir: string): string {
|
|
159
|
+
// Use Nuxt's ~ alias (project root) so the path is stable regardless of serverRoutePath depth
|
|
160
|
+
const projectRoot = process.cwd();
|
|
161
|
+
const relativeInputDir = path.relative(projectRoot, path.resolve(inputDir));
|
|
162
|
+
// Convert Windows paths to Unix-style
|
|
163
|
+
return '~/' + relativeInputDir.replace(/\\/g, '/');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Format code with Prettier
|
|
168
|
+
*/
|
|
169
|
+
async function formatCode(code: string, logger: Logger): Promise<string> {
|
|
170
|
+
try {
|
|
171
|
+
return await format(code, {
|
|
172
|
+
parser: 'typescript',
|
|
173
|
+
semi: false,
|
|
174
|
+
singleQuote: true,
|
|
175
|
+
trailingComma: 'es5',
|
|
176
|
+
printWidth: 100,
|
|
177
|
+
});
|
|
178
|
+
} catch {
|
|
179
|
+
logger.log.warn('Prettier formatting failed, using unformatted code');
|
|
180
|
+
return code;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate BFF structure (auth + transformers)
|
|
186
|
+
*/
|
|
187
|
+
async function generateBffStructure(
|
|
188
|
+
allMethods: MethodInfo[],
|
|
189
|
+
serverRoutePath: string,
|
|
190
|
+
inputDir: string,
|
|
191
|
+
logger: Logger
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const bffSpinner = logger.spinner();
|
|
194
|
+
bffSpinner.start('Generating BFF structure (auth + transformers)');
|
|
195
|
+
|
|
196
|
+
const serverRoot = path.dirname(serverRoutePath);
|
|
197
|
+
|
|
198
|
+
// 1. Generate auth files (only if they don't exist)
|
|
199
|
+
const authDir = path.join(serverRoot, 'auth');
|
|
200
|
+
await fs.ensureDir(authDir);
|
|
201
|
+
|
|
202
|
+
const authContextPath = path.join(authDir, 'context.ts');
|
|
203
|
+
if (!fs.existsSync(authContextPath)) {
|
|
204
|
+
const authContextCode = generateAuthContextStub();
|
|
205
|
+
const formattedAuthContext = await formatCode(authContextCode, logger);
|
|
206
|
+
await fs.writeFile(authContextPath, formattedAuthContext, 'utf-8');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const authTypesPath = path.join(authDir, 'types.ts');
|
|
210
|
+
if (!fs.existsSync(authTypesPath)) {
|
|
211
|
+
const authTypesCode = generateAuthTypesStub();
|
|
212
|
+
const formattedAuthTypes = await formatCode(authTypesCode, logger);
|
|
213
|
+
await fs.writeFile(authTypesPath, formattedAuthTypes, 'utf-8');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2. Generate transformer stubs (only if they don't exist)
|
|
217
|
+
const bffDir = path.join(serverRoot, 'bff');
|
|
218
|
+
const transformersDir = path.join(bffDir, 'transformers');
|
|
219
|
+
await fs.ensureDir(transformersDir);
|
|
220
|
+
|
|
221
|
+
// Group methods by resource
|
|
222
|
+
const methodsByResource = new Map<string, MethodInfo[]>();
|
|
223
|
+
for (const method of allMethods) {
|
|
224
|
+
const resource = extractResourceFromPath(method.path);
|
|
225
|
+
if (!methodsByResource.has(resource)) {
|
|
226
|
+
methodsByResource.set(resource, []);
|
|
227
|
+
}
|
|
228
|
+
methodsByResource.get(resource)!.push(method);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Generate transformer for each resource
|
|
232
|
+
for (const [resource, methods] of methodsByResource.entries()) {
|
|
233
|
+
const transformerPath = path.join(transformersDir, `${resource}.ts`);
|
|
234
|
+
if (!fs.existsSync(transformerPath)) {
|
|
235
|
+
const transformerCode = generateTransformerStub(resource, methods, inputDir);
|
|
236
|
+
const formattedTransformer = await formatCode(transformerCode, logger);
|
|
237
|
+
await fs.writeFile(transformerPath, formattedTransformer, 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Generate examples file (always regenerated)
|
|
242
|
+
const examplesPath = path.join(bffDir, '_transformers.example.ts');
|
|
243
|
+
const examplesCode = generateTransformerExamples();
|
|
244
|
+
const formattedExamples = await formatCode(examplesCode, logger);
|
|
245
|
+
await fs.writeFile(examplesPath, formattedExamples, 'utf-8');
|
|
246
|
+
|
|
247
|
+
// 4. Generate BFF README (always regenerated)
|
|
248
|
+
const bffReadmePath = path.join(bffDir, 'README.md');
|
|
249
|
+
const bffReadmeCode = generateBffReadme();
|
|
250
|
+
await fs.writeFile(bffReadmePath, bffReadmeCode, 'utf-8');
|
|
251
|
+
|
|
252
|
+
bffSpinner.stop('BFF structure generated');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Extract resource name from API path
|
|
257
|
+
* Examples:
|
|
258
|
+
* /pet -> pet
|
|
259
|
+
* /pet/{id} -> pet
|
|
260
|
+
* /store/inventory -> store
|
|
261
|
+
* /user/login -> user
|
|
262
|
+
*/
|
|
263
|
+
function extractResourceFromPath(path: string): string {
|
|
264
|
+
// Remove leading slash
|
|
265
|
+
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
266
|
+
|
|
267
|
+
// Get first segment
|
|
268
|
+
const firstSegment = cleanPath.split('/')[0];
|
|
269
|
+
|
|
270
|
+
// Remove any path params
|
|
271
|
+
return firstSegment.replace(/\{[^}]+\}/g, '').toLowerCase();
|
|
272
|
+
}
|