nuxt-openapi-hyperfetch 0.1.0-alpha.1
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 -0
- package/.prettierignore +17 -0
- package/.prettierrc.json +12 -0
- package/CONTRIBUTING.md +292 -0
- package/INSTRUCTIONS.md +327 -0
- package/LICENSE +202 -0
- package/README.md +202 -0
- package/dist/cli/config.d.ts +57 -0
- package/dist/cli/config.js +85 -0
- package/dist/cli/logger.d.ts +44 -0
- package/dist/cli/logger.js +58 -0
- package/dist/cli/logo.d.ts +6 -0
- package/dist/cli/logo.js +21 -0
- package/dist/cli/messages.d.ts +65 -0
- package/dist/cli/messages.js +86 -0
- package/dist/cli/prompts.d.ts +30 -0
- package/dist/cli/prompts.js +118 -0
- package/dist/cli/types.d.ts +43 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli/utils.d.ts +26 -0
- package/dist/cli/utils.js +45 -0
- package/dist/generate.d.ts +6 -0
- package/dist/generate.js +48 -0
- package/dist/generators/nuxt-server/bff-templates.d.ts +25 -0
- package/dist/generators/nuxt-server/bff-templates.js +737 -0
- package/dist/generators/nuxt-server/generator.d.ts +7 -0
- package/dist/generators/nuxt-server/generator.js +206 -0
- package/dist/generators/nuxt-server/parser.d.ts +5 -0
- package/dist/generators/nuxt-server/parser.js +5 -0
- package/dist/generators/nuxt-server/templates.d.ts +35 -0
- package/dist/generators/nuxt-server/templates.js +412 -0
- package/dist/generators/nuxt-server/types.d.ts +5 -0
- package/dist/generators/nuxt-server/types.js +5 -0
- package/dist/generators/shared/parsers/heyapi-parser.d.ts +11 -0
- package/dist/generators/shared/parsers/heyapi-parser.js +248 -0
- package/dist/generators/shared/parsers/official-parser.d.ts +5 -0
- package/dist/generators/shared/parsers/official-parser.js +5 -0
- package/dist/generators/shared/runtime/apiHelpers.d.ts +183 -0
- package/dist/generators/shared/runtime/apiHelpers.js +268 -0
- package/dist/generators/shared/templates/api-callbacks-plugin.d.ts +178 -0
- package/dist/generators/shared/templates/api-callbacks-plugin.js +338 -0
- package/dist/generators/shared/types.d.ts +25 -0
- package/dist/generators/shared/types.js +4 -0
- package/dist/generators/tanstack-query/generator.d.ts +5 -0
- package/dist/generators/tanstack-query/generator.js +11 -0
- package/dist/generators/use-async-data/generator.d.ts +5 -0
- package/dist/generators/use-async-data/generator.js +156 -0
- package/dist/generators/use-async-data/parser.d.ts +5 -0
- package/dist/generators/use-async-data/parser.js +5 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncData.d.ts +38 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +122 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.d.ts +54 -0
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +126 -0
- package/dist/generators/use-async-data/templates.d.ts +20 -0
- package/dist/generators/use-async-data/templates.js +191 -0
- package/dist/generators/use-async-data/types.d.ts +4 -0
- package/dist/generators/use-async-data/types.js +4 -0
- package/dist/generators/use-fetch/generator.d.ts +5 -0
- package/dist/generators/use-fetch/generator.js +131 -0
- package/dist/generators/use-fetch/parser.d.ts +9 -0
- package/dist/generators/use-fetch/parser.js +282 -0
- package/dist/generators/use-fetch/runtime/useApiRequest.d.ts +46 -0
- package/dist/generators/use-fetch/runtime/useApiRequest.js +158 -0
- package/dist/generators/use-fetch/templates.d.ts +16 -0
- package/dist/generators/use-fetch/templates.js +169 -0
- package/dist/generators/use-fetch/types.d.ts +5 -0
- package/dist/generators/use-fetch/types.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +213 -0
- package/docs/API-REFERENCE.md +887 -0
- package/docs/ARCHITECTURE.md +649 -0
- package/docs/DEVELOPMENT.md +918 -0
- package/docs/QUICK-START.md +323 -0
- package/docs/README.md +155 -0
- package/docs/TROUBLESHOOTING.md +881 -0
- package/eslint.config.js +72 -0
- package/package.json +65 -0
- package/src/cli/config.ts +140 -0
- package/src/cli/logger.ts +66 -0
- package/src/cli/logo.ts +25 -0
- package/src/cli/messages.ts +97 -0
- package/src/cli/prompts.ts +143 -0
- package/src/cli/types.ts +50 -0
- package/src/cli/utils.ts +49 -0
- package/src/generate.ts +57 -0
- package/src/generators/nuxt-server/bff-templates.ts +754 -0
- package/src/generators/nuxt-server/generator.ts +270 -0
- package/src/generators/nuxt-server/parser.ts +5 -0
- package/src/generators/nuxt-server/templates.ts +483 -0
- package/src/generators/nuxt-server/types.ts +5 -0
- package/src/generators/shared/parsers/heyapi-parser.ts +307 -0
- package/src/generators/shared/parsers/official-parser.ts +5 -0
- package/src/generators/shared/runtime/apiHelpers.ts +466 -0
- package/src/generators/shared/templates/api-callbacks-plugin.ts +352 -0
- package/src/generators/shared/types.ts +27 -0
- package/src/generators/tanstack-query/generator.ts +11 -0
- package/src/generators/use-async-data/generator.ts +204 -0
- package/src/generators/use-async-data/parser.ts +5 -0
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +220 -0
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +236 -0
- package/src/generators/use-async-data/templates.ts +250 -0
- package/src/generators/use-async-data/types.ts +4 -0
- package/src/generators/use-fetch/generator.ts +169 -0
- package/src/generators/use-fetch/parser.ts +341 -0
- package/src/generators/use-fetch/runtime/useApiRequest.ts +223 -0
- package/src/generators/use-fetch/templates.ts +214 -0
- package/src/generators/use-fetch/types.ts +5 -0
- package/src/index.ts +265 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { format } from 'prettier';
|
|
4
|
+
import { getApiFiles as getApiFilesOfficial, parseApiFile as parseApiFileOfficial, } from './parser.js';
|
|
5
|
+
import { getApiFiles as getApiFilesHeyApi, parseApiFile as parseApiFileHeyApi, } from '../shared/parsers/heyapi-parser.js';
|
|
6
|
+
import { generateServerRouteFile, generateRouteFilePath, generateRoutesIndexFile, } from './templates.js';
|
|
7
|
+
import { generateAuthContextStub, generateAuthTypesStub, generateTransformerStub, generateTransformerExamples, generateBffReadme, } from './bff-templates.js';
|
|
8
|
+
import { p, logSuccess, logError, logNote } from '../../cli/logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* Main function to generate Nuxt Server Routes
|
|
11
|
+
*/
|
|
12
|
+
export async function generateNuxtServerRoutes(inputDir, serverRoutePath, options) {
|
|
13
|
+
const mainSpinner = p.spinner();
|
|
14
|
+
// Select parser based on chosen backend
|
|
15
|
+
const getApiFiles = options?.backend === 'heyapi' ? getApiFilesHeyApi : getApiFilesOfficial;
|
|
16
|
+
const parseApiFile = options?.backend === 'heyapi' ? parseApiFileHeyApi : parseApiFileOfficial;
|
|
17
|
+
const enableBff = options?.enableBff ?? false;
|
|
18
|
+
if (enableBff) {
|
|
19
|
+
p.log.info('BFF Mode: Enabled (transformers + auth)');
|
|
20
|
+
}
|
|
21
|
+
// 1. Get all API files
|
|
22
|
+
mainSpinner.start('Scanning API files');
|
|
23
|
+
const apiFiles = getApiFiles(inputDir);
|
|
24
|
+
mainSpinner.stop(`Found ${apiFiles.length} API file(s)`);
|
|
25
|
+
if (apiFiles.length === 0) {
|
|
26
|
+
throw new Error('No API files found in the input directory');
|
|
27
|
+
}
|
|
28
|
+
// 2. Parse each API file
|
|
29
|
+
mainSpinner.start('Parsing API files');
|
|
30
|
+
const allMethods = [];
|
|
31
|
+
for (const file of apiFiles) {
|
|
32
|
+
const fileName = path.basename(file);
|
|
33
|
+
try {
|
|
34
|
+
const apiInfo = parseApiFile(file);
|
|
35
|
+
allMethods.push(...apiInfo.methods);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logError(`Error parsing ${fileName}: ${String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
mainSpinner.stop(`Found ${allMethods.length} routes to generate`);
|
|
42
|
+
if (allMethods.length === 0) {
|
|
43
|
+
p.log.warn('No methods found to generate');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// 3. Clean and create output directory
|
|
47
|
+
mainSpinner.start('Preparing output directory');
|
|
48
|
+
await fs.emptyDir(serverRoutePath);
|
|
49
|
+
mainSpinner.stop('Output directory ready');
|
|
50
|
+
// 4. Generate BFF structure if enabled
|
|
51
|
+
if (enableBff) {
|
|
52
|
+
await generateBffStructure(allMethods, serverRoutePath, inputDir);
|
|
53
|
+
}
|
|
54
|
+
// 5. Calculate relative import path from server routes to APIs
|
|
55
|
+
const relativePath = calculateRelativeImportPath(serverRoutePath, inputDir);
|
|
56
|
+
// 6. Generate each server route
|
|
57
|
+
mainSpinner.start('Generating server routes');
|
|
58
|
+
let successCount = 0;
|
|
59
|
+
let errorCount = 0;
|
|
60
|
+
for (const method of allMethods) {
|
|
61
|
+
try {
|
|
62
|
+
// Extract resource name from path
|
|
63
|
+
const resource = extractResourceFromPath(method.path);
|
|
64
|
+
const code = generateServerRouteFile(method, relativePath, {
|
|
65
|
+
enableBff: enableBff,
|
|
66
|
+
resource: resource,
|
|
67
|
+
});
|
|
68
|
+
const formattedCode = await formatCode(code);
|
|
69
|
+
const routeFilePath = generateRouteFilePath(method);
|
|
70
|
+
const fullPath = path.join(serverRoutePath, routeFilePath);
|
|
71
|
+
// Ensure directory exists
|
|
72
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
73
|
+
await fs.writeFile(fullPath, formattedCode, 'utf-8');
|
|
74
|
+
successCount++;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logError(`Error generating ${method.path} [${method.httpMethod}]: ${String(error)}`);
|
|
78
|
+
errorCount++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
mainSpinner.stop(`Generated ${successCount} server routes`);
|
|
82
|
+
// 7. Generate configuration files
|
|
83
|
+
mainSpinner.start('Generating configuration files');
|
|
84
|
+
// Generate routes index (documentation)
|
|
85
|
+
const routesIndexCode = generateRoutesIndexFile(allMethods);
|
|
86
|
+
const formattedRoutesIndex = await formatCode(routesIndexCode);
|
|
87
|
+
await fs.writeFile(path.join(serverRoutePath, '_routes.ts'), formattedRoutesIndex, 'utf-8');
|
|
88
|
+
mainSpinner.stop('Configuration files generated');
|
|
89
|
+
// 8. Summary and Next Steps
|
|
90
|
+
if (errorCount > 0) {
|
|
91
|
+
p.log.warn(`Completed with ${errorCount} error(s)`);
|
|
92
|
+
}
|
|
93
|
+
logSuccess(`Generated ${successCount} server route(s) in ${serverRoutePath}`);
|
|
94
|
+
// Build next steps message
|
|
95
|
+
let nextSteps = '1. Configure API_BASE_URL and API_SECRET in your .env\n';
|
|
96
|
+
nextSteps += '2. Update nuxt.config.ts with runtimeConfig (add apiBaseUrl and apiSecret)';
|
|
97
|
+
if (enableBff) {
|
|
98
|
+
nextSteps += '\n3. Implement authentication in server/auth/context.ts';
|
|
99
|
+
nextSteps += '\n4. Add business logic to transformers in server/bff/transformers/';
|
|
100
|
+
nextSteps += '\n5. See server/bff/README.md for BFF documentation';
|
|
101
|
+
nextSteps += '\n6. Start your Nuxt dev server and test the routes';
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
nextSteps += '\n3. Start your Nuxt dev server and test the routes';
|
|
105
|
+
}
|
|
106
|
+
logNote(nextSteps, 'Next steps');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Calculate relative import path from server routes to APIs
|
|
110
|
+
*/
|
|
111
|
+
function calculateRelativeImportPath(serverRoutePath, inputDir) {
|
|
112
|
+
// Use Nuxt's ~ alias (project root) so the path is stable regardless of serverRoutePath depth
|
|
113
|
+
const projectRoot = process.cwd();
|
|
114
|
+
const relativeInputDir = path.relative(projectRoot, path.resolve(inputDir));
|
|
115
|
+
// Convert Windows paths to Unix-style
|
|
116
|
+
return '~/' + relativeInputDir.replace(/\\/g, '/');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Format code with Prettier
|
|
120
|
+
*/
|
|
121
|
+
async function formatCode(code) {
|
|
122
|
+
try {
|
|
123
|
+
return await format(code, {
|
|
124
|
+
parser: 'typescript',
|
|
125
|
+
semi: false,
|
|
126
|
+
singleQuote: true,
|
|
127
|
+
trailingComma: 'es5',
|
|
128
|
+
printWidth: 100,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
p.log.warn('Prettier formatting failed, using unformatted code');
|
|
133
|
+
return code;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generate BFF structure (auth + transformers)
|
|
138
|
+
*/
|
|
139
|
+
async function generateBffStructure(allMethods, serverRoutePath, inputDir) {
|
|
140
|
+
const bffSpinner = p.spinner();
|
|
141
|
+
bffSpinner.start('Generating BFF structure (auth + transformers)');
|
|
142
|
+
const serverRoot = path.dirname(serverRoutePath);
|
|
143
|
+
// 1. Generate auth files (only if they don't exist)
|
|
144
|
+
const authDir = path.join(serverRoot, 'auth');
|
|
145
|
+
await fs.ensureDir(authDir);
|
|
146
|
+
const authContextPath = path.join(authDir, 'context.ts');
|
|
147
|
+
if (!fs.existsSync(authContextPath)) {
|
|
148
|
+
const authContextCode = generateAuthContextStub();
|
|
149
|
+
const formattedAuthContext = await formatCode(authContextCode);
|
|
150
|
+
await fs.writeFile(authContextPath, formattedAuthContext, 'utf-8');
|
|
151
|
+
}
|
|
152
|
+
const authTypesPath = path.join(authDir, 'types.ts');
|
|
153
|
+
if (!fs.existsSync(authTypesPath)) {
|
|
154
|
+
const authTypesCode = generateAuthTypesStub();
|
|
155
|
+
const formattedAuthTypes = await formatCode(authTypesCode);
|
|
156
|
+
await fs.writeFile(authTypesPath, formattedAuthTypes, 'utf-8');
|
|
157
|
+
}
|
|
158
|
+
// 2. Generate transformer stubs (only if they don't exist)
|
|
159
|
+
const bffDir = path.join(serverRoot, 'bff');
|
|
160
|
+
const transformersDir = path.join(bffDir, 'transformers');
|
|
161
|
+
await fs.ensureDir(transformersDir);
|
|
162
|
+
// Group methods by resource
|
|
163
|
+
const methodsByResource = new Map();
|
|
164
|
+
for (const method of allMethods) {
|
|
165
|
+
const resource = extractResourceFromPath(method.path);
|
|
166
|
+
if (!methodsByResource.has(resource)) {
|
|
167
|
+
methodsByResource.set(resource, []);
|
|
168
|
+
}
|
|
169
|
+
methodsByResource.get(resource).push(method);
|
|
170
|
+
}
|
|
171
|
+
// Generate transformer for each resource
|
|
172
|
+
for (const [resource, methods] of methodsByResource.entries()) {
|
|
173
|
+
const transformerPath = path.join(transformersDir, `${resource}.ts`);
|
|
174
|
+
if (!fs.existsSync(transformerPath)) {
|
|
175
|
+
const transformerCode = generateTransformerStub(resource, methods, inputDir);
|
|
176
|
+
const formattedTransformer = await formatCode(transformerCode);
|
|
177
|
+
await fs.writeFile(transformerPath, formattedTransformer, 'utf-8');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 3. Generate examples file (always regenerated)
|
|
181
|
+
const examplesPath = path.join(bffDir, '_transformers.example.ts');
|
|
182
|
+
const examplesCode = generateTransformerExamples();
|
|
183
|
+
const formattedExamples = await formatCode(examplesCode);
|
|
184
|
+
await fs.writeFile(examplesPath, formattedExamples, 'utf-8');
|
|
185
|
+
// 4. Generate BFF README (always regenerated)
|
|
186
|
+
const bffReadmePath = path.join(bffDir, 'README.md');
|
|
187
|
+
const bffReadmeCode = generateBffReadme();
|
|
188
|
+
await fs.writeFile(bffReadmePath, bffReadmeCode, 'utf-8');
|
|
189
|
+
bffSpinner.stop('BFF structure generated');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Extract resource name from API path
|
|
193
|
+
* Examples:
|
|
194
|
+
* /pet -> pet
|
|
195
|
+
* /pet/{id} -> pet
|
|
196
|
+
* /store/inventory -> store
|
|
197
|
+
* /user/login -> user
|
|
198
|
+
*/
|
|
199
|
+
function extractResourceFromPath(path) {
|
|
200
|
+
// Remove leading slash
|
|
201
|
+
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
202
|
+
// Get first segment
|
|
203
|
+
const firstSegment = cleanPath.split('/')[0];
|
|
204
|
+
// Remove any path params
|
|
205
|
+
return firstSegment.replace(/\{[^}]+\}/g, '').toLowerCase();
|
|
206
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MethodInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a complete server route file
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateServerRouteFile(method: MethodInfo, apiImportPath: string, options?: {
|
|
6
|
+
enableBff?: boolean;
|
|
7
|
+
resource?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
/**
|
|
10
|
+
* Calculate the file path for a server route
|
|
11
|
+
* Examples:
|
|
12
|
+
* /pet + GET -> pet/index.get.ts
|
|
13
|
+
* /pet + POST -> pet/index.post.ts
|
|
14
|
+
* /pet/{petId} + GET -> pet/[id].get.ts
|
|
15
|
+
* /pet/{petId}/uploadImage + POST -> pet/[id]/uploadImage.post.ts
|
|
16
|
+
* /store/inventory + GET -> store/inventory.get.ts
|
|
17
|
+
* /user/{username} + GET -> user/[username].get.ts
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateRouteFilePath(method: MethodInfo): string;
|
|
20
|
+
/**
|
|
21
|
+
* Generate nuxt.config.example.ts
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateConfigFile(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Generate .env.example
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateEnvFile(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Generate README.md
|
|
30
|
+
*/
|
|
31
|
+
export declare function generateReadme(serverPath: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Generate index file that exports all route paths (optional, for documentation)
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateRoutesIndexFile(methods: MethodInfo[]): string;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { camelCase, pascalCase } from 'change-case';
|
|
2
|
+
/**
|
|
3
|
+
* Generate file header with auto-generation warning
|
|
4
|
+
*/
|
|
5
|
+
function generateFileHeader() {
|
|
6
|
+
return `/**
|
|
7
|
+
* ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
|
8
|
+
*
|
|
9
|
+
* This file was automatically generated by nuxt-openapi-generator.
|
|
10
|
+
* Any manual changes will be overwritten on the next generation.
|
|
11
|
+
*
|
|
12
|
+
* @generated by nuxt-openapi-generator
|
|
13
|
+
* @see https://github.com/dmartindiaz/nuxt-openapi-hyperfetch
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* eslint-disable */
|
|
17
|
+
// @ts-nocheck
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a complete server route file
|
|
22
|
+
*/
|
|
23
|
+
export function generateServerRouteFile(method, apiImportPath, options) {
|
|
24
|
+
const header = generateFileHeader();
|
|
25
|
+
const imports = generateImports(method, apiImportPath);
|
|
26
|
+
const handlerBody = generateHandlerBody(method, options);
|
|
27
|
+
return `${header}${imports}\n\n${handlerBody}\n`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Calculate the file path for a server route
|
|
31
|
+
* Examples:
|
|
32
|
+
* /pet + GET -> pet/index.get.ts
|
|
33
|
+
* /pet + POST -> pet/index.post.ts
|
|
34
|
+
* /pet/{petId} + GET -> pet/[id].get.ts
|
|
35
|
+
* /pet/{petId}/uploadImage + POST -> pet/[id]/uploadImage.post.ts
|
|
36
|
+
* /store/inventory + GET -> store/inventory.get.ts
|
|
37
|
+
* /user/{username} + GET -> user/[username].get.ts
|
|
38
|
+
*/
|
|
39
|
+
export function generateRouteFilePath(method) {
|
|
40
|
+
let routePath = method.path;
|
|
41
|
+
// Remove leading slash
|
|
42
|
+
if (routePath.startsWith('/')) {
|
|
43
|
+
routePath = routePath.substring(1);
|
|
44
|
+
}
|
|
45
|
+
// Replace path params: {petId} -> [id], {username} -> [username]
|
|
46
|
+
routePath = routePath.replace(/\{(\w+)\}/g, (match, paramName) => {
|
|
47
|
+
// Simplify common params
|
|
48
|
+
const simplified = paramName.toLowerCase().replace(/id$/, '');
|
|
49
|
+
return `[${simplified || 'id'}]`;
|
|
50
|
+
});
|
|
51
|
+
// Split path into segments
|
|
52
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
53
|
+
// If empty or root, use 'index'
|
|
54
|
+
if (segments.length === 0) {
|
|
55
|
+
return `index.${method.httpMethod.toLowerCase()}.ts`;
|
|
56
|
+
}
|
|
57
|
+
// Check if last segment is a dynamic param [xxx]
|
|
58
|
+
const lastSegment = segments[segments.length - 1];
|
|
59
|
+
const isDynamicParam = lastSegment.startsWith('[') && lastSegment.endsWith(']');
|
|
60
|
+
// If last segment is dynamic, add index
|
|
61
|
+
if (isDynamicParam) {
|
|
62
|
+
return `${segments.join('/')}/index.${method.httpMethod.toLowerCase()}.ts`;
|
|
63
|
+
}
|
|
64
|
+
// Otherwise, use the last segment as filename
|
|
65
|
+
const fileName = segments.pop();
|
|
66
|
+
const dir = segments.length > 0 ? segments.join('/') + '/' : '';
|
|
67
|
+
return `${dir}${fileName}.${method.httpMethod.toLowerCase()}.ts`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extract base type names from a type string (same as use-fetch)
|
|
71
|
+
*/
|
|
72
|
+
function extractBaseTypes(type) {
|
|
73
|
+
if (!type) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
// Handle array syntax: Pet[]
|
|
77
|
+
const arrayMatch = type.match(/^(\w+)\[\]$/);
|
|
78
|
+
if (arrayMatch) {
|
|
79
|
+
return [arrayMatch[1]];
|
|
80
|
+
}
|
|
81
|
+
// Handle Array generic: Array<Pet>
|
|
82
|
+
const arrayGenericMatch = type.match(/^Array<(\w+)>$/);
|
|
83
|
+
if (arrayGenericMatch) {
|
|
84
|
+
return [arrayGenericMatch[1]];
|
|
85
|
+
}
|
|
86
|
+
// If it's a simple named type (single word, PascalCase), include it
|
|
87
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(type)) {
|
|
88
|
+
return [type];
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generate import statements
|
|
94
|
+
*/
|
|
95
|
+
function generateImports(method, apiImportPath) {
|
|
96
|
+
const h3Imports = ['defineEventHandler', 'createError'];
|
|
97
|
+
// Add h3 imports based on method needs
|
|
98
|
+
if (method.pathParams.length > 0) {
|
|
99
|
+
h3Imports.push('getRouterParam');
|
|
100
|
+
}
|
|
101
|
+
if (method.hasQueryParams) {
|
|
102
|
+
h3Imports.push('getQuery');
|
|
103
|
+
}
|
|
104
|
+
if (method.hasBody) {
|
|
105
|
+
h3Imports.push('readBody');
|
|
106
|
+
}
|
|
107
|
+
let imports = `import { ${h3Imports.join(', ')} } from 'h3'\n`;
|
|
108
|
+
// Import types
|
|
109
|
+
const typeNames = new Set();
|
|
110
|
+
// Extract base types from request type
|
|
111
|
+
if (method.requestType) {
|
|
112
|
+
const extracted = extractBaseTypes(method.requestType);
|
|
113
|
+
extracted.forEach((t) => typeNames.add(t));
|
|
114
|
+
}
|
|
115
|
+
// Extract base types from response type
|
|
116
|
+
if (method.responseType && method.responseType !== 'void') {
|
|
117
|
+
const extracted = extractBaseTypes(method.responseType);
|
|
118
|
+
extracted.forEach((t) => typeNames.add(t));
|
|
119
|
+
}
|
|
120
|
+
// Import types from API (only if we have named types to import)
|
|
121
|
+
if (typeNames.size > 0) {
|
|
122
|
+
imports += `import type { ${Array.from(typeNames).join(', ')} } from '${apiImportPath}'\n`;
|
|
123
|
+
}
|
|
124
|
+
return imports;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Generate the handler body
|
|
128
|
+
*/
|
|
129
|
+
function generateHandlerBody(method, options) {
|
|
130
|
+
const description = method.description ? `/**\n * ${method.description}\n */\n` : '';
|
|
131
|
+
const pathParamCapture = generatePathParamCapture(method);
|
|
132
|
+
const queryCapture = generateQueryCapture(method);
|
|
133
|
+
const bodyCapture = generateBodyCapture(method);
|
|
134
|
+
const backendUrl = generateBackendUrl(method);
|
|
135
|
+
const fetchOptions = generateFetchOptions(method);
|
|
136
|
+
// BFF: Auth context loading
|
|
137
|
+
const authContextCode = options?.enableBff
|
|
138
|
+
? ` // Try to load auth context (optional)
|
|
139
|
+
let auth = null
|
|
140
|
+
try {
|
|
141
|
+
const { getAuthContext } = await import('~/server/auth/context')
|
|
142
|
+
auth = await getAuthContext(event)
|
|
143
|
+
} catch {
|
|
144
|
+
// Auth not configured - continue without it
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
`
|
|
148
|
+
: '';
|
|
149
|
+
// BFF: Transformer call
|
|
150
|
+
const transformerCode = options?.enableBff && options?.resource
|
|
151
|
+
? `
|
|
152
|
+
// Try to transform data (optional)
|
|
153
|
+
try {
|
|
154
|
+
const { transform${pascalCase(options.resource)} } = await import('~/server/bff/transformers/${options.resource}')
|
|
155
|
+
return await transform${pascalCase(options.resource)}(data, event, auth)
|
|
156
|
+
} catch {
|
|
157
|
+
// Transformer not found - return raw data
|
|
158
|
+
return data
|
|
159
|
+
}`
|
|
160
|
+
: `
|
|
161
|
+
return data`;
|
|
162
|
+
return `${description}export default defineEventHandler(async (event): Promise<${method.responseType}> => {
|
|
163
|
+
${pathParamCapture}${queryCapture}${bodyCapture}${authContextCode}const config = useRuntimeConfig()
|
|
164
|
+
const baseUrl = config.apiBaseUrl
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const data = await $fetch<${method.responseType}>(${backendUrl}, {
|
|
168
|
+
${fetchOptions}
|
|
169
|
+
})${transformerCode}
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
throw createError({
|
|
172
|
+
statusCode: error.statusCode || 500,
|
|
173
|
+
statusMessage: error.message || 'Request failed'
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
})`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Generate path param capture code
|
|
180
|
+
*/
|
|
181
|
+
function generatePathParamCapture(method) {
|
|
182
|
+
if (method.pathParams.length === 0) {
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
const captures = method.pathParams.map((param) => {
|
|
186
|
+
const paramName = camelCase(param);
|
|
187
|
+
const paramKey = param.toLowerCase().replace(/id$/, '') || 'id';
|
|
188
|
+
return `const ${paramName} = getRouterParam(event, '${paramKey}')
|
|
189
|
+
if (!${paramName}) {
|
|
190
|
+
throw createError({
|
|
191
|
+
statusCode: 400,
|
|
192
|
+
statusMessage: '${param} is required'
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
});
|
|
197
|
+
return captures.join('') + '\n ';
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Generate query param capture code
|
|
201
|
+
*/
|
|
202
|
+
function generateQueryCapture(method) {
|
|
203
|
+
if (!method.hasQueryParams) {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
return 'const query = getQuery(event)\n ';
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Generate body capture code
|
|
210
|
+
*/
|
|
211
|
+
function generateBodyCapture(method) {
|
|
212
|
+
if (!method.hasBody) {
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
const typeAnnotation = method.requestType ? `<${method.requestType}>` : '';
|
|
216
|
+
return `const body = await readBody${typeAnnotation}(event)\n `;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Generate backend URL with path params replaced
|
|
220
|
+
*/
|
|
221
|
+
function generateBackendUrl(method) {
|
|
222
|
+
let url = method.path;
|
|
223
|
+
// Replace {param} with ${paramName}
|
|
224
|
+
if (method.pathParams.length > 0) {
|
|
225
|
+
for (const param of method.pathParams) {
|
|
226
|
+
const paramName = camelCase(param);
|
|
227
|
+
url = url.replace(`{${param}}`, `\${${paramName}}`);
|
|
228
|
+
}
|
|
229
|
+
return `\`\${baseUrl}${url}\``;
|
|
230
|
+
}
|
|
231
|
+
// Static URL
|
|
232
|
+
return `\`\${baseUrl}${url}\``;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Generate $fetch options object
|
|
236
|
+
*/
|
|
237
|
+
function generateFetchOptions(method) {
|
|
238
|
+
const options = [];
|
|
239
|
+
// Method (if not GET)
|
|
240
|
+
if (method.httpMethod !== 'GET') {
|
|
241
|
+
options.push(`method: '${method.httpMethod}'`);
|
|
242
|
+
}
|
|
243
|
+
// Query params
|
|
244
|
+
if (method.hasQueryParams) {
|
|
245
|
+
options.push('query: query');
|
|
246
|
+
}
|
|
247
|
+
// Body
|
|
248
|
+
if (method.hasBody) {
|
|
249
|
+
options.push('body: body');
|
|
250
|
+
}
|
|
251
|
+
// Headers
|
|
252
|
+
const headerLines = [];
|
|
253
|
+
if (method.hasBody) {
|
|
254
|
+
headerLines.push(`'Content-Type': 'application/json'`);
|
|
255
|
+
}
|
|
256
|
+
headerLines.push(`...(config.apiSecret ? { 'Authorization': \`Bearer \${config.apiSecret}\` } : {})`);
|
|
257
|
+
options.push(`headers: {\n ${headerLines.join(',\n ')}\n }`);
|
|
258
|
+
return options.join(',\n ');
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Generate nuxt.config.example.ts
|
|
262
|
+
*/
|
|
263
|
+
export function generateConfigFile() {
|
|
264
|
+
return `/**
|
|
265
|
+
* ⚠️ AUTO-GENERATED EXAMPLE FILE
|
|
266
|
+
*
|
|
267
|
+
* Copy this configuration to your nuxt.config.ts
|
|
268
|
+
* @generated by nuxt-openapi-generator
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
// nuxt.config.ts
|
|
272
|
+
export default defineNuxtConfig({
|
|
273
|
+
runtimeConfig: {
|
|
274
|
+
// Private keys (server-only, never exposed to client)
|
|
275
|
+
apiSecret: process.env.API_SECRET || '',
|
|
276
|
+
apiBaseUrl: process.env.API_BASE_URL || 'https://petstore3.swagger.io/api/v3'
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Generate .env.example
|
|
283
|
+
*/
|
|
284
|
+
export function generateEnvFile() {
|
|
285
|
+
return `# ⚠️ AUTO-GENERATED EXAMPLE FILE
|
|
286
|
+
# Copy this file to .env and configure your values
|
|
287
|
+
# @generated by nuxt-openapi-generator
|
|
288
|
+
|
|
289
|
+
# Backend API Configuration
|
|
290
|
+
API_BASE_URL=https://petstore3.swagger.io/api/v3
|
|
291
|
+
API_SECRET=your-api-secret-token-here
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Generate README.md
|
|
296
|
+
*/
|
|
297
|
+
export function generateReadme(serverPath) {
|
|
298
|
+
return `<!--
|
|
299
|
+
⚠️ AUTO-GENERATED DOCUMENTATION
|
|
300
|
+
This file was automatically generated by nuxt-openapi-generator.
|
|
301
|
+
@generated by nuxt-openapi-generator
|
|
302
|
+
-->
|
|
303
|
+
|
|
304
|
+
# Nuxt Server Routes
|
|
305
|
+
|
|
306
|
+
Auto-generated server routes that proxy requests to your backend API.
|
|
307
|
+
|
|
308
|
+
## 🔧 Configuration
|
|
309
|
+
|
|
310
|
+
1. **Copy \`.env.example\` to \`.env\`**:
|
|
311
|
+
\`\`\`bash
|
|
312
|
+
cp .env.example .env
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
2. **Update \`.env\` with your backend URL**:
|
|
316
|
+
\`\`\`env
|
|
317
|
+
API_BASE_URL=https://your-backend-api.com/api/v3
|
|
318
|
+
API_SECRET=your-secret-token
|
|
319
|
+
\`\`\`
|
|
320
|
+
|
|
321
|
+
3. **Update \`nuxt.config.ts\`** (see \`nuxt.config.example.ts\`):
|
|
322
|
+
\`\`\`typescript
|
|
323
|
+
export default defineNuxtConfig({
|
|
324
|
+
runtimeConfig: {
|
|
325
|
+
// All these variables are server-only
|
|
326
|
+
apiSecret: process.env.API_SECRET || '',
|
|
327
|
+
apiBaseUrl: process.env.API_BASE_URL || ''
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
\`\`\`
|
|
331
|
+
|
|
332
|
+
## 🚀 Usage
|
|
333
|
+
|
|
334
|
+
All routes are available at \`/api/*\`:
|
|
335
|
+
|
|
336
|
+
\`\`\`typescript
|
|
337
|
+
// In your Vue components
|
|
338
|
+
const { data: pet } = await useFetch('/api/pet/123')
|
|
339
|
+
|
|
340
|
+
// With query params
|
|
341
|
+
const { data: pets } = await useFetch('/api/pet', {
|
|
342
|
+
query: { status: 'available' }
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// POST request
|
|
346
|
+
const { data: newPet } = await useFetch('/api/pet', {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
body: { name: 'Fluffy', status: 'available' }
|
|
349
|
+
})
|
|
350
|
+
\`\`\`
|
|
351
|
+
|
|
352
|
+
## 📁 Generated Routes
|
|
353
|
+
|
|
354
|
+
- **${serverPath}/** - All server routes
|
|
355
|
+
- Each file corresponds to an OpenAPI endpoint
|
|
356
|
+
- Path params use dynamic routes: \`[id]\`, \`[username]\`
|
|
357
|
+
- HTTP methods: \`.get.ts\`, \`.post.ts\`, \`.put.ts\`, \`.delete.ts\`
|
|
358
|
+
|
|
359
|
+
## 🔒 Security
|
|
360
|
+
|
|
361
|
+
- \`API_SECRET\` is only available server-side (never exposed to client)
|
|
362
|
+
- All requests go through your Nuxt server (CORS handled automatically)
|
|
363
|
+
- You can add custom authentication/validation logic in each route
|
|
364
|
+
|
|
365
|
+
## 🛠️ Customization
|
|
366
|
+
|
|
367
|
+
Each generated route can be customized:
|
|
368
|
+
|
|
369
|
+
\`\`\`typescript
|
|
370
|
+
// server/api/pet/[id].get.ts
|
|
371
|
+
export default defineEventHandler(async (event) => {
|
|
372
|
+
const petId = getRouterParam(event, 'id')
|
|
373
|
+
|
|
374
|
+
// Add custom logic here
|
|
375
|
+
// - Rate limiting
|
|
376
|
+
// - Caching
|
|
377
|
+
// - Request validation
|
|
378
|
+
// - Response transformation
|
|
379
|
+
|
|
380
|
+
const config = useRuntimeConfig()
|
|
381
|
+
const data = await \$fetch(\`\${config.apiBaseUrl}/pet/\${petId}\`)
|
|
382
|
+
|
|
383
|
+
return data
|
|
384
|
+
})
|
|
385
|
+
\`\`\`
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Generate index file that exports all route paths (optional, for documentation)
|
|
390
|
+
*/
|
|
391
|
+
export function generateRoutesIndexFile(methods) {
|
|
392
|
+
const header = generateFileHeader();
|
|
393
|
+
const routes = methods.map((method) => {
|
|
394
|
+
const filePath = generateRouteFilePath(method);
|
|
395
|
+
const routePath = '/' +
|
|
396
|
+
filePath
|
|
397
|
+
.replace(/\\/g, '/')
|
|
398
|
+
.replace(/index\.(get|post|put|delete|patch)\.ts$/, '')
|
|
399
|
+
.replace(/\.(get|post|put|delete|patch)\.ts$/, '')
|
|
400
|
+
.replace(/\[(\w+)\]/g, ':$1');
|
|
401
|
+
return ` // ${method.httpMethod} ${routePath} -> ${filePath}`;
|
|
402
|
+
});
|
|
403
|
+
return `${header}/**
|
|
404
|
+
* Generated Server Routes
|
|
405
|
+
*
|
|
406
|
+
* Available routes:
|
|
407
|
+
${routes.join('\n')}
|
|
408
|
+
*/
|
|
409
|
+
|
|
410
|
+
export default {}
|
|
411
|
+
`;
|
|
412
|
+
}
|