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,307 @@
|
|
|
1
|
+
import { Project, SyntaxKind, type SourceFile, type TypeAliasDeclaration } from 'ts-morph';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import type { ApiClassInfo, MethodInfo } from '../types.js';
|
|
5
|
+
import { pascalCase } from 'change-case';
|
|
6
|
+
import { p } from '../../../cli/logger.js';
|
|
7
|
+
|
|
8
|
+
interface SdkOpInfo {
|
|
9
|
+
httpMethod: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get Hey API generated files from the output directory.
|
|
16
|
+
* Returns the sdk.gen.ts path — the single entry point for all operations.
|
|
17
|
+
*/
|
|
18
|
+
export function getApiFiles(inputDir: string): string[] {
|
|
19
|
+
const sdkFile = path.join(inputDir, 'sdk.gen.ts');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(sdkFile)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Hey API output not found: ${sdkFile}\nMake sure to run @hey-api/openapi-ts before generating composables.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [sdkFile];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse Hey API generated files and return all operations as ApiClassInfo.
|
|
32
|
+
* Reads both sdk.gen.ts (for HTTP method + description) and types.gen.ts (for param/response types).
|
|
33
|
+
*/
|
|
34
|
+
export function parseApiFile(sdkFilePath: string): ApiClassInfo {
|
|
35
|
+
const inputDir = path.dirname(sdkFilePath);
|
|
36
|
+
const typesFilePath = path.join(inputDir, 'types.gen.ts');
|
|
37
|
+
|
|
38
|
+
if (!existsSync(typesFilePath)) {
|
|
39
|
+
throw new Error(`types.gen.ts not found in ${inputDir}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse types.gen.ts with ts-morph to read type declarations
|
|
43
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
44
|
+
const typesFile = project.addSourceFileAtPath(typesFilePath);
|
|
45
|
+
|
|
46
|
+
// Build operation info map from sdk.gen.ts using text parsing
|
|
47
|
+
const sdkText = readFileSync(sdkFilePath, 'utf-8');
|
|
48
|
+
const opMap = buildOperationMap(sdkText);
|
|
49
|
+
|
|
50
|
+
const methodInfos: MethodInfo[] = [];
|
|
51
|
+
|
|
52
|
+
// Iterate over all *Data type aliases — each represents one API operation
|
|
53
|
+
for (const typeAlias of typesFile.getTypeAliases()) {
|
|
54
|
+
const name = typeAlias.getName();
|
|
55
|
+
|
|
56
|
+
// Skip non-operation types (ClientOptions, etc.)
|
|
57
|
+
if (!name.endsWith('Data') || name === 'ClientOptions') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const baseName = name.slice(0, -4); // 'AddPetData' → 'AddPet'
|
|
62
|
+
const opName = baseName.charAt(0).toLowerCase() + baseName.slice(1); // 'addPet'
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const methodInfo = extractMethodInfo(typeAlias, typesFile, baseName, opName, opMap);
|
|
66
|
+
if (methodInfo) {
|
|
67
|
+
methodInfos.push(methodInfo);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
p.log.warn(`Could not parse Hey API operation "${opName}": ${String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { className: 'Api', methods: methodInfos };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a map of operation name → SdkOpInfo by text-parsing sdk.gen.ts.
|
|
79
|
+
* Extracts HTTP method, JSDoc description, and Content-Type header.
|
|
80
|
+
*/
|
|
81
|
+
function buildOperationMap(sdkText: string): Map<string, SdkOpInfo> {
|
|
82
|
+
const map = new Map<string, SdkOpInfo>();
|
|
83
|
+
const lines = sdkText.split('\n');
|
|
84
|
+
|
|
85
|
+
let jsDocLines: string[] = [];
|
|
86
|
+
let inJsDoc = false;
|
|
87
|
+
let pendingDescription: string | undefined;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i];
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
|
|
93
|
+
if (trimmed === '/**') {
|
|
94
|
+
inJsDoc = true;
|
|
95
|
+
jsDocLines = [line];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (inJsDoc) {
|
|
100
|
+
jsDocLines.push(line);
|
|
101
|
+
if (trimmed === '*/') {
|
|
102
|
+
inJsDoc = false;
|
|
103
|
+
pendingDescription = parseJsDocDescription(jsDocLines.join('\n'));
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Match: export const funcName =
|
|
109
|
+
const funcMatch = line.match(/^export const (\w+) =/);
|
|
110
|
+
if (funcMatch) {
|
|
111
|
+
const funcName = funcMatch[1];
|
|
112
|
+
|
|
113
|
+
// Scan ahead a few lines to find the HTTP method call
|
|
114
|
+
const snippet = lines.slice(i, Math.min(i + 8, lines.length)).join('\n');
|
|
115
|
+
const methodMatch = snippet.match(/\.(post|get|put|delete|patch)</i);
|
|
116
|
+
const httpMethod = methodMatch ? methodMatch[1].toUpperCase() : 'GET';
|
|
117
|
+
|
|
118
|
+
// Detect Content-Type header
|
|
119
|
+
const headers: Record<string, string> = {};
|
|
120
|
+
if (snippet.includes("'Content-Type': 'application/json'")) {
|
|
121
|
+
headers['Content-Type'] = 'application/json';
|
|
122
|
+
} else if (snippet.includes("'Content-Type': 'application/octet-stream'")) {
|
|
123
|
+
headers['Content-Type'] = 'application/octet-stream';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
map.set(funcName, { httpMethod, description: pendingDescription, headers });
|
|
127
|
+
pendingDescription = undefined;
|
|
128
|
+
jsDocLines = [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return map;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract the first meaningful line from a JSDoc comment block.
|
|
137
|
+
*/
|
|
138
|
+
function parseJsDocDescription(jsDoc: string): string | undefined {
|
|
139
|
+
for (const line of jsDoc.split('\n')) {
|
|
140
|
+
const cleaned = line.replace(/^\s*[/*]+\s?/, '').trim();
|
|
141
|
+
if (cleaned && !cleaned.startsWith('@')) {
|
|
142
|
+
return cleaned;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert a *Data TypeAliasDeclaration into a MethodInfo object.
|
|
150
|
+
*/
|
|
151
|
+
function extractMethodInfo(
|
|
152
|
+
typeAlias: TypeAliasDeclaration,
|
|
153
|
+
typesFile: SourceFile,
|
|
154
|
+
baseName: string,
|
|
155
|
+
opName: string,
|
|
156
|
+
opMap: Map<string, SdkOpInfo>
|
|
157
|
+
): MethodInfo | null {
|
|
158
|
+
const dataTypeName = typeAlias.getName(); // e.g. 'AddPetData'
|
|
159
|
+
|
|
160
|
+
const opInfo = opMap.get(opName);
|
|
161
|
+
if (!opInfo) {
|
|
162
|
+
p.log.warn(`Operation "${opName}" not found in sdk.gen.ts — skipping`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const typeNode = typeAlias.getTypeNode();
|
|
167
|
+
if (!typeNode || typeNode.getKind() !== SyntaxKind.TypeLiteral) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const typeLiteral = typeNode.asKind(SyntaxKind.TypeLiteral);
|
|
172
|
+
if (!typeLiteral) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let urlPath = '';
|
|
177
|
+
let hasBody = false;
|
|
178
|
+
let pathParams: string[] = [];
|
|
179
|
+
let queryParams: string[] = [];
|
|
180
|
+
let hasQueryParams = false;
|
|
181
|
+
|
|
182
|
+
for (const member of typeLiteral.getMembers()) {
|
|
183
|
+
if (member.getKind() !== SyntaxKind.PropertySignature) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const propSig = member.asKind(SyntaxKind.PropertySignature);
|
|
188
|
+
if (!propSig) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const propName = propSig.getName();
|
|
193
|
+
const typeNodeText = propSig.getTypeNode()?.getText() ?? '';
|
|
194
|
+
|
|
195
|
+
switch (propName) {
|
|
196
|
+
case 'url':
|
|
197
|
+
// String literal type: "'/pet'" → strip quotes → '/pet'
|
|
198
|
+
urlPath = typeNodeText.replace(/^['"`]|['"`]$/g, '');
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'body':
|
|
202
|
+
if (typeNodeText !== 'never') {
|
|
203
|
+
hasBody = true;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 'path': {
|
|
208
|
+
if (typeNodeText !== 'never') {
|
|
209
|
+
const pathTypeNode = propSig.getTypeNode();
|
|
210
|
+
if (pathTypeNode && pathTypeNode.getKind() === SyntaxKind.TypeLiteral) {
|
|
211
|
+
const innerLiteral = pathTypeNode.asKind(SyntaxKind.TypeLiteral);
|
|
212
|
+
if (innerLiteral) {
|
|
213
|
+
pathParams = innerLiteral
|
|
214
|
+
.getMembers()
|
|
215
|
+
.filter((m) => m.getKind() === SyntaxKind.PropertySignature)
|
|
216
|
+
.map((m) => m.asKind(SyntaxKind.PropertySignature)!.getName());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case 'query': {
|
|
224
|
+
if (typeNodeText !== 'never') {
|
|
225
|
+
const queryTypeNode = propSig.getTypeNode();
|
|
226
|
+
if (queryTypeNode && queryTypeNode.getKind() === SyntaxKind.TypeLiteral) {
|
|
227
|
+
const innerLiteral = queryTypeNode.asKind(SyntaxKind.TypeLiteral);
|
|
228
|
+
if (innerLiteral) {
|
|
229
|
+
queryParams = innerLiteral
|
|
230
|
+
.getMembers()
|
|
231
|
+
.filter((m) => m.getKind() === SyntaxKind.PropertySignature)
|
|
232
|
+
.map((m) => m.asKind(SyntaxKind.PropertySignature)!.getName());
|
|
233
|
+
hasQueryParams = queryParams.length > 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!urlPath) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Extract response type from the matching *Responses type alias
|
|
247
|
+
const responsesAlias = typesFile.getTypeAlias(`${baseName}Responses`);
|
|
248
|
+
const responseType = responsesAlias ? extractResponseType(responsesAlias) : 'void';
|
|
249
|
+
|
|
250
|
+
// Only set requestType if the operation actually requires input params
|
|
251
|
+
const hasParams = hasBody || pathParams.length > 0 || hasQueryParams;
|
|
252
|
+
const requestType = hasParams ? dataTypeName : undefined;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
name: opName,
|
|
256
|
+
composableName: `useFetch${pascalCase(opName)}`,
|
|
257
|
+
requestType,
|
|
258
|
+
responseType,
|
|
259
|
+
httpMethod: opInfo.httpMethod,
|
|
260
|
+
path: urlPath,
|
|
261
|
+
hasBody,
|
|
262
|
+
bodyField: hasBody ? 'body' : undefined,
|
|
263
|
+
hasQueryParams,
|
|
264
|
+
queryParams,
|
|
265
|
+
pathParams,
|
|
266
|
+
headers: opInfo.headers,
|
|
267
|
+
description: opInfo.description,
|
|
268
|
+
hasRawMethod: false,
|
|
269
|
+
rawMethodName: undefined,
|
|
270
|
+
paramsShape: 'nested',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extract the success (2xx) response type from a *Responses type alias.
|
|
276
|
+
* Maps `unknown` to `void` for operations with no meaningful response body.
|
|
277
|
+
*/
|
|
278
|
+
function extractResponseType(responsesAlias: TypeAliasDeclaration): string {
|
|
279
|
+
const typeNode = responsesAlias.getTypeNode();
|
|
280
|
+
if (!typeNode || typeNode.getKind() !== SyntaxKind.TypeLiteral) {
|
|
281
|
+
return 'void';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const typeLiteral = typeNode.asKind(SyntaxKind.TypeLiteral);
|
|
285
|
+
if (!typeLiteral) {
|
|
286
|
+
return 'void';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const member of typeLiteral.getMembers()) {
|
|
290
|
+
if (member.getKind() !== SyntaxKind.PropertySignature) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const propSig = member.asKind(SyntaxKind.PropertySignature);
|
|
295
|
+
if (!propSig) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const memberName = propSig.getName();
|
|
300
|
+
if (memberName === '200' || memberName === '201' || memberName === '202') {
|
|
301
|
+
const typeText = propSig.getTypeNode()?.getText() ?? '';
|
|
302
|
+
return typeText === 'unknown' || typeText === '' ? 'void' : typeText;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return 'void';
|
|
307
|
+
}
|