hono-takibi 0.9.52 → 0.9.61
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/dist/cli/index.js +6 -1
- package/dist/config/index.d.ts +3 -0
- package/dist/core/route.js +6 -6
- package/dist/core/rpc.js +61 -79
- package/dist/core/takibi.d.ts +3 -3
- package/dist/core/takibi.js +41 -36
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +96 -0
- package/dist/generator/zod-openapi-hono/openapi/route/params/request-parameter.js +4 -7
- package/dist/generator/zod-openapi-hono/openapi/route/request/body/index.js +7 -16
- package/dist/generator/zod-openapi-hono/openapi/route/response/index.js +15 -51
- package/dist/generator/zod-to-openapi/z/enum.js +14 -12
- package/dist/generator/zod-to-openapi/z/integer.js +47 -69
- package/dist/generator/zod-to-openapi/z/number.js +43 -72
- package/dist/generator/zod-to-openapi/z/object.js +1 -1
- package/dist/generator/zod-to-openapi/z/string.d.ts +0 -20
- package/dist/generator/zod-to-openapi/z/string.js +14 -40
- package/dist/helper/docs.js +1 -1
- package/dist/helper/properties-schema.js +2 -2
- package/dist/helper/wrap.js +27 -23
- package/dist/openapi/index.d.ts +2 -2
- package/dist/typespec/index.js +3 -2
- package/dist/utils/index.d.ts +24 -121
- package/dist/utils/index.js +31 -122
- package/dist/vite-plugin/index.js +28 -31
- package/package.json +16 -17
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { route } from '../core/route.js';
|
|
|
5
5
|
import { rpc } from '../core/rpc.js';
|
|
6
6
|
import { schema } from '../core/schema.js';
|
|
7
7
|
import { takibi } from '../core/takibi.js';
|
|
8
|
+
import { type } from '../core/type.js';
|
|
8
9
|
// import { honoRpcWithSWR } from '../generator/swr/index.js'
|
|
9
10
|
import { parseCli } from '../utils/index.js';
|
|
10
11
|
/**
|
|
@@ -104,13 +105,17 @@ export async function honoTakibi() {
|
|
|
104
105
|
: undefined;
|
|
105
106
|
if (routeResult && !routeResult.ok)
|
|
106
107
|
return { ok: false, error: routeResult.error };
|
|
108
|
+
/** type */
|
|
109
|
+
const typeResult = c.type ? await type(c.input, c.type.output) : undefined;
|
|
110
|
+
if (typeResult && !typeResult.ok)
|
|
111
|
+
return { ok: false, error: typeResult.error };
|
|
107
112
|
/** rpc */
|
|
108
113
|
const rpcResult = c.rpc
|
|
109
114
|
? await rpc(c.input, c.rpc.output, c.rpc.import, c.rpc.split ?? false)
|
|
110
115
|
: undefined;
|
|
111
116
|
if (rpcResult && !rpcResult.ok)
|
|
112
117
|
return { ok: false, error: rpcResult.error };
|
|
113
|
-
const results = [takibiResult?.value, rpcResult?.value].filter((v) =>
|
|
118
|
+
const results = [takibiResult?.value, typeResult?.value, rpcResult?.value].filter((v) => v !== undefined);
|
|
114
119
|
return {
|
|
115
120
|
ok: true,
|
|
116
121
|
value: results.join('\n'),
|
package/dist/config/index.d.ts
CHANGED
package/dist/core/route.js
CHANGED
|
@@ -41,9 +41,9 @@ export async function route(input, output, importPath, split) {
|
|
|
41
41
|
if (!mkdirResult.ok)
|
|
42
42
|
return { ok: false, error: mkdirResult.error };
|
|
43
43
|
const writeResult = await writeFile(output, fmtResult.value);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
if (!writeResult.ok)
|
|
45
|
+
return { ok: false, error: writeResult.error };
|
|
46
|
+
return { ok: true, value: `Generated route code written to ${output}` };
|
|
47
47
|
}
|
|
48
48
|
const outDir = output.replace(/\.ts$/, '');
|
|
49
49
|
const blocks = extractRouteBlocks(routesSrc);
|
|
@@ -60,9 +60,9 @@ export async function route(input, output, importPath, split) {
|
|
|
60
60
|
if (!mkdirResult.ok)
|
|
61
61
|
return { ok: false, error: mkdirResult.error };
|
|
62
62
|
const writeResult = await writeFile(output, fmtResult.value);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if (!writeResult.ok)
|
|
64
|
+
return { ok: false, error: writeResult.error };
|
|
65
|
+
return { ok: true, value: `Generated route code written to ${output}` };
|
|
66
66
|
}
|
|
67
67
|
for (const { name, block } of blocks) {
|
|
68
68
|
const includeZ = block.includes('z.');
|
package/dist/core/rpc.js
CHANGED
|
@@ -139,14 +139,7 @@ const createResolveParameter = (componentsParameters) => (p) => {
|
|
|
139
139
|
const cand = name ? componentsParameters[name] : undefined;
|
|
140
140
|
return isParameterObject(cand) ? cand : undefined;
|
|
141
141
|
};
|
|
142
|
-
const createToParameterLikes = (resolveParam) => (arr) => Array.isArray(arr)
|
|
143
|
-
? arr.reduce((acc, x) => {
|
|
144
|
-
const r = resolveParam(x);
|
|
145
|
-
if (r)
|
|
146
|
-
acc.push(r);
|
|
147
|
-
return acc;
|
|
148
|
-
}, [])
|
|
149
|
-
: [];
|
|
142
|
+
const createToParameterLikes = (resolveParam) => (arr) => Array.isArray(arr) ? arr.map((x) => resolveParam(x)).filter((param) => param !== undefined) : [];
|
|
150
143
|
const isOperationLike = (v) => isRecord(v) && 'responses' in v;
|
|
151
144
|
const HTTP_METHODS = [
|
|
152
145
|
'get',
|
|
@@ -183,16 +176,14 @@ const pickBodySchema = (op) => {
|
|
|
183
176
|
};
|
|
184
177
|
/* ─────────────────────────────── Args builders ─────────────────────────────── */
|
|
185
178
|
const createBuildParamsType = (tsTypeFromSchema) => (pathParams, queryParams) => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
return parts.length ? `{${parts.join(',')}}` : '';
|
|
179
|
+
const pathPart = pathParams.length === 0
|
|
180
|
+
? []
|
|
181
|
+
: [`path:{${pathParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',')}}`];
|
|
182
|
+
const queryPart = queryParams.length === 0
|
|
183
|
+
? []
|
|
184
|
+
: [`query:{${queryParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',')}}`];
|
|
185
|
+
const parts = [...pathPart, ...queryPart];
|
|
186
|
+
return parts.length === 0 ? '' : `{${parts.join(',')}}`;
|
|
196
187
|
};
|
|
197
188
|
const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
|
|
198
189
|
? `params:${paramsType},body:${bodyType}`
|
|
@@ -203,14 +194,11 @@ const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
|
|
|
203
194
|
: '';
|
|
204
195
|
/** pass path/query as-is (keep numbers/arrays) */
|
|
205
196
|
const buildClientArgs = (pathParams, queryParams, hasBody) => {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (hasBody)
|
|
212
|
-
pieces.push('json:body');
|
|
213
|
-
return pieces.length ? `{${pieces.join(',')}}` : '';
|
|
197
|
+
const paramPiece = pathParams.length ? ['param:params.path'] : [];
|
|
198
|
+
const queryPiece = queryParams.length ? ['query:params.query'] : [];
|
|
199
|
+
const bodyPiece = hasBody ? ['json:body'] : [];
|
|
200
|
+
const pieces = [...paramPiece, ...queryPiece, ...bodyPiece];
|
|
201
|
+
return pieces.length === 0 ? '' : `{${pieces.join(',')}}`;
|
|
214
202
|
};
|
|
215
203
|
/* ─────────────────────────────── Single-operation generator ─────────────────────────────── */
|
|
216
204
|
const generateOperationCode = (pathStr, method, item, deps) => {
|
|
@@ -246,6 +234,25 @@ const generateOperationCode = (pathStr, method, item, deps) => {
|
|
|
246
234
|
const func = `export async function ${funcName}(${argSig}){return await ${call}}`;
|
|
247
235
|
return `${docs}\n${func}`;
|
|
248
236
|
};
|
|
237
|
+
const buildOperationCodes = (paths, deps) => Object.entries(paths)
|
|
238
|
+
.filter((entry) => isRecord(entry[1]))
|
|
239
|
+
.flatMap(([p, rawItem]) => {
|
|
240
|
+
const pathItem = {
|
|
241
|
+
parameters: rawItem.parameters,
|
|
242
|
+
get: isOperationLike(rawItem.get) ? rawItem.get : undefined,
|
|
243
|
+
put: isOperationLike(rawItem.put) ? rawItem.put : undefined,
|
|
244
|
+
post: isOperationLike(rawItem.post) ? rawItem.post : undefined,
|
|
245
|
+
delete: isOperationLike(rawItem.delete) ? rawItem.delete : undefined,
|
|
246
|
+
options: isOperationLike(rawItem.options) ? rawItem.options : undefined,
|
|
247
|
+
head: isOperationLike(rawItem.head) ? rawItem.head : undefined,
|
|
248
|
+
patch: isOperationLike(rawItem.patch) ? rawItem.patch : undefined,
|
|
249
|
+
trace: isOperationLike(rawItem.trace) ? rawItem.trace : undefined,
|
|
250
|
+
};
|
|
251
|
+
return HTTP_METHODS.map((method) => {
|
|
252
|
+
const code = generateOperationCode(p, method, pathItem, deps);
|
|
253
|
+
return code ? { funcName: methodPath(method, p), code } : null;
|
|
254
|
+
}).filter((item) => item !== null);
|
|
255
|
+
});
|
|
249
256
|
/* ─────────────────────────────── Split ─────────────────────────────── */
|
|
250
257
|
const resolveSplitOutDir = (output) => {
|
|
251
258
|
const looksLikeFile = output.endsWith('.ts');
|
|
@@ -268,7 +275,6 @@ export async function rpc(input, output, importPath, split) {
|
|
|
268
275
|
const client = 'client';
|
|
269
276
|
const s = `import { client } from '${importPath}'`;
|
|
270
277
|
const header = s.length ? `${s}\n\n` : '';
|
|
271
|
-
const combinedOut = [];
|
|
272
278
|
const pathsMaybe = openAPI.paths;
|
|
273
279
|
if (!isOpenAPIPaths(pathsMaybe)) {
|
|
274
280
|
return { ok: false, error: 'Invalid OpenAPI paths' };
|
|
@@ -279,60 +285,19 @@ export async function rpc(input, output, importPath, split) {
|
|
|
279
285
|
const componentsParameters = openAPI.components?.parameters ?? {};
|
|
280
286
|
const resolveParameter = createResolveParameter(componentsParameters);
|
|
281
287
|
const toParameterLikes = createToParameterLikes(resolveParameter);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
continue;
|
|
289
|
-
const pathItem = {
|
|
290
|
-
parameters: rawItem.parameters,
|
|
291
|
-
get: isOperationLike(rawItem.get) ? rawItem.get : undefined,
|
|
292
|
-
put: isOperationLike(rawItem.put) ? rawItem.put : undefined,
|
|
293
|
-
post: isOperationLike(rawItem.post) ? rawItem.post : undefined,
|
|
294
|
-
delete: isOperationLike(rawItem.delete) ? rawItem.delete : undefined,
|
|
295
|
-
options: isOperationLike(rawItem.options) ? rawItem.options : undefined,
|
|
296
|
-
head: isOperationLike(rawItem.head) ? rawItem.head : undefined,
|
|
297
|
-
patch: isOperationLike(rawItem.patch) ? rawItem.patch : undefined,
|
|
298
|
-
trace: isOperationLike(rawItem.trace) ? rawItem.trace : undefined,
|
|
299
|
-
};
|
|
300
|
-
for (const method of HTTP_METHODS) {
|
|
301
|
-
const code = generateOperationCode(p, method, pathItem, {
|
|
302
|
-
client,
|
|
303
|
-
tsTypeFromSchema,
|
|
304
|
-
toParameterLikes,
|
|
305
|
-
});
|
|
306
|
-
if (!code)
|
|
307
|
-
continue;
|
|
308
|
-
if (split) {
|
|
309
|
-
// One file per RPC function: <funcName>.ts
|
|
310
|
-
const funcName = methodPath(method, p);
|
|
311
|
-
const fileSrc = `${header}${code}\n`;
|
|
312
|
-
const fmtResult = await fmt(fileSrc);
|
|
313
|
-
if (!fmtResult.ok)
|
|
314
|
-
return { ok: false, error: fmtResult.error };
|
|
315
|
-
const { outDir } = resolveSplitOutDir(output);
|
|
316
|
-
const filePath = path.join(outDir, `${funcName}.ts`);
|
|
317
|
-
const mkdirResult = await mkdir(path.dirname(filePath));
|
|
318
|
-
if (!mkdirResult.ok)
|
|
319
|
-
return { ok: false, error: mkdirResult.error };
|
|
320
|
-
const writeResult = await writeFile(filePath, fmtResult.value);
|
|
321
|
-
if (!writeResult.ok)
|
|
322
|
-
return { ok: false, error: writeResult.error };
|
|
323
|
-
splitExports.add(`export * from './${funcName}'`);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
combinedOut.push(code);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
288
|
+
const deps = {
|
|
289
|
+
client,
|
|
290
|
+
tsTypeFromSchema,
|
|
291
|
+
toParameterLikes,
|
|
292
|
+
};
|
|
293
|
+
const operationCodes = buildOperationCodes(pathsMaybe, deps);
|
|
330
294
|
// Non-split: write single file
|
|
331
295
|
if (!split) {
|
|
332
296
|
const mkdirResult = await mkdir(path.dirname(output));
|
|
333
297
|
if (!mkdirResult.ok)
|
|
334
298
|
return { ok: false, error: mkdirResult.error };
|
|
335
|
-
const
|
|
299
|
+
const body = operationCodes.map(({ code }) => code).join('\n\n');
|
|
300
|
+
const code = `${header}${body}${operationCodes.length ? '\n' : ''}`;
|
|
336
301
|
const fmtResult = await fmt(code);
|
|
337
302
|
if (!fmtResult.ok)
|
|
338
303
|
return { ok: false, error: fmtResult.error };
|
|
@@ -341,9 +306,23 @@ export async function rpc(input, output, importPath, split) {
|
|
|
341
306
|
return { ok: false, error: writeResult.error };
|
|
342
307
|
return { ok: true, value: `Generated rpc code written to ${output}` };
|
|
343
308
|
}
|
|
344
|
-
// Split: write index.ts (barrel)
|
|
309
|
+
// Split: write each file + index.ts (barrel)
|
|
345
310
|
const { outDir, indexPath } = resolveSplitOutDir(output);
|
|
346
|
-
const
|
|
311
|
+
for (const { funcName, code } of operationCodes) {
|
|
312
|
+
const fileSrc = `${header}${code}\n`;
|
|
313
|
+
const fmtResult = await fmt(fileSrc);
|
|
314
|
+
if (!fmtResult.ok)
|
|
315
|
+
return { ok: false, error: fmtResult.error };
|
|
316
|
+
const filePath = path.join(outDir, `${funcName}.ts`);
|
|
317
|
+
const mkdirResult = await mkdir(path.dirname(filePath));
|
|
318
|
+
if (!mkdirResult.ok)
|
|
319
|
+
return { ok: false, error: mkdirResult.error };
|
|
320
|
+
const writeResult = await writeFile(filePath, fmtResult.value);
|
|
321
|
+
if (!writeResult.ok)
|
|
322
|
+
return { ok: false, error: writeResult.error };
|
|
323
|
+
}
|
|
324
|
+
const exportLines = Array.from(new Set(operationCodes.map(({ funcName }) => `export * from './${funcName}'`))).sort();
|
|
325
|
+
const index = `${exportLines.join('\n')}\n`;
|
|
347
326
|
const fmtResult = await fmt(index);
|
|
348
327
|
if (!fmtResult.ok)
|
|
349
328
|
return { ok: false, error: fmtResult.error };
|
|
@@ -353,5 +332,8 @@ export async function rpc(input, output, importPath, split) {
|
|
|
353
332
|
const writeResult = await writeFile(indexPath, fmtResult.value);
|
|
354
333
|
if (!writeResult.ok)
|
|
355
334
|
return { ok: false, error: writeResult.error };
|
|
356
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
value: `Generated rpc code written to ${outDir}/*.ts (index.ts included)`,
|
|
338
|
+
};
|
|
357
339
|
}
|
package/dist/core/takibi.d.ts
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
* Y --> Z["writeResult2 = writeFile(target, appResult.value)"]
|
|
31
31
|
* Z --> ZA{"writeResult2.ok ?"}
|
|
32
32
|
* ZA -->|No| ZB["return { ok:false, error: writeResult2.error }"]
|
|
33
|
-
* ZA -->|Yes| ZC["
|
|
34
|
-
* ZC --> ZD{"
|
|
35
|
-
* ZD -->|No| ZE["return { ok:false, error:
|
|
33
|
+
* ZA -->|Yes| ZC["zodOpenAPIHonoHandlerResult = zodOpenAPIHonoHandler(openAPI, output, test)"]
|
|
34
|
+
* ZC --> ZD{"zodOpenAPIHonoHandlerResult.ok ?"}
|
|
35
|
+
* ZD -->|No| ZE["return { ok:false, error: zodOpenAPIHonoHandlerResult.error }"]
|
|
36
36
|
* ZD -->|Yes| ZF["return { ok:true, value: 'Generated code and template files written' }"]
|
|
37
37
|
* ```
|
|
38
38
|
*
|
package/dist/core/takibi.js
CHANGED
|
@@ -37,9 +37,9 @@ import { isHttpMethod, methodPath } from '../utils/index.js';
|
|
|
37
37
|
* Y --> Z["writeResult2 = writeFile(target, appResult.value)"]
|
|
38
38
|
* Z --> ZA{"writeResult2.ok ?"}
|
|
39
39
|
* ZA -->|No| ZB["return { ok:false, error: writeResult2.error }"]
|
|
40
|
-
* ZA -->|Yes| ZC["
|
|
41
|
-
* ZC --> ZD{"
|
|
42
|
-
* ZD -->|No| ZE["return { ok:false, error:
|
|
40
|
+
* ZA -->|Yes| ZC["zodOpenAPIHonoHandlerResult = zodOpenAPIHonoHandler(openAPI, output, test)"]
|
|
41
|
+
* ZC --> ZD{"zodOpenAPIHonoHandlerResult.ok ?"}
|
|
42
|
+
* ZD -->|No| ZE["return { ok:false, error: zodOpenAPIHonoHandlerResult.error }"]
|
|
43
43
|
* ZD -->|Yes| ZF["return { ok:true, value: 'Generated code and template files written' }"]
|
|
44
44
|
* ```
|
|
45
45
|
*
|
|
@@ -53,41 +53,46 @@ import { isHttpMethod, methodPath } from '../utils/index.js';
|
|
|
53
53
|
* @returns A `Result` containing a success message or an error string.
|
|
54
54
|
*/
|
|
55
55
|
export async function takibi(input, output, exportSchema, exportType, template, test, basePath) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return { ok: false, error: writeResult.error };
|
|
69
|
-
/** template */
|
|
70
|
-
if (template && output.includes('/')) {
|
|
71
|
-
const appResult = await fmt(app(openAPI, output, basePath));
|
|
72
|
-
if (!appResult.ok)
|
|
73
|
-
return { ok: false, error: appResult.error };
|
|
74
|
-
const dir = path.dirname(output);
|
|
75
|
-
const readdirResult = await readdir(dir);
|
|
76
|
-
if (!readdirResult.ok)
|
|
77
|
-
return { ok: false, error: readdirResult.error };
|
|
78
|
-
const target = path.join(dir, 'index.ts');
|
|
79
|
-
const writeResult = await writeFile(target, appResult.value);
|
|
56
|
+
try {
|
|
57
|
+
const openAPIResult = await parseOpenAPI(input);
|
|
58
|
+
if (!openAPIResult.ok)
|
|
59
|
+
return { ok: false, error: openAPIResult.error };
|
|
60
|
+
const openAPI = openAPIResult.value;
|
|
61
|
+
const honoResult = await fmt(zodOpenAPIHono(openAPI, exportSchema, exportType));
|
|
62
|
+
if (!honoResult.ok)
|
|
63
|
+
return { ok: false, error: honoResult.error };
|
|
64
|
+
const mkdirResult = await mkdir(path.dirname(output));
|
|
65
|
+
if (!mkdirResult.ok)
|
|
66
|
+
return { ok: false, error: mkdirResult.error };
|
|
67
|
+
const writeResult = await writeFile(output, honoResult.value);
|
|
80
68
|
if (!writeResult.ok)
|
|
81
69
|
return { ok: false, error: writeResult.error };
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
70
|
+
/** template */
|
|
71
|
+
if (template && output.includes('/')) {
|
|
72
|
+
const appResult = await fmt(app(openAPI, output, basePath));
|
|
73
|
+
if (!appResult.ok)
|
|
74
|
+
return { ok: false, error: appResult.error };
|
|
75
|
+
const dir = path.dirname(output);
|
|
76
|
+
const readdirResult = await readdir(dir);
|
|
77
|
+
if (!readdirResult.ok)
|
|
78
|
+
return { ok: false, error: readdirResult.error };
|
|
79
|
+
const target = path.join(dir, 'index.ts');
|
|
80
|
+
const writeResult = await writeFile(target, appResult.value);
|
|
81
|
+
if (!writeResult.ok)
|
|
82
|
+
return { ok: false, error: writeResult.error };
|
|
83
|
+
const zodOpenAPIHonoHandlerResult = await zodOpenAPIHonoHandler(openAPI, output, test);
|
|
84
|
+
if (!zodOpenAPIHonoHandlerResult.ok)
|
|
85
|
+
return { ok: false, error: zodOpenAPIHonoHandlerResult.error };
|
|
86
|
+
return { ok: true, value: 'Generated code and template files written' };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
value: `Generated code written to ${output}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
86
95
|
}
|
|
87
|
-
return {
|
|
88
|
-
ok: true,
|
|
89
|
-
value: `Generated code written to ${output}`,
|
|
90
|
-
};
|
|
91
96
|
}
|
|
92
97
|
/**
|
|
93
98
|
* Generates route handler files for a Hono app using Zod and OpenAPI.
|
|
@@ -97,7 +102,7 @@ export async function takibi(input, output, exportSchema, exportType, template,
|
|
|
97
102
|
* @param test - Whether to generate corresponding empty test files.
|
|
98
103
|
* @returns A `Result` indicating success or error with message.
|
|
99
104
|
*/
|
|
100
|
-
async function
|
|
105
|
+
async function zodOpenAPIHonoHandler(openapi, output, test) {
|
|
101
106
|
const paths = openapi.paths;
|
|
102
107
|
const handlers = Object.entries(paths).flatMap(([p, pathItem]) => Object.entries(pathItem)
|
|
103
108
|
.filter(([m]) => isHttpMethod(m))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { fmt } from '../format/index.js';
|
|
4
|
+
import { mkdir, writeFile } from '../fsp/index.js';
|
|
5
|
+
import { zodOpenAPIHono } from '../generator/zod-openapi-hono/openapi/index.js';
|
|
6
|
+
import { parseOpenAPI } from '../openapi/index.js';
|
|
7
|
+
import { isHttpMethod, methodPath } from '../utils/index.js';
|
|
8
|
+
export async function type(input, output) {
|
|
9
|
+
try {
|
|
10
|
+
const openAPIResult = await parseOpenAPI(input);
|
|
11
|
+
if (!openAPIResult.ok)
|
|
12
|
+
return { ok: false, error: openAPIResult.error };
|
|
13
|
+
const openAPI = openAPIResult.value;
|
|
14
|
+
const hono = zodOpenAPIHono(openAPI, false, false);
|
|
15
|
+
const paths = openAPI.paths;
|
|
16
|
+
const routes = Object.entries(paths).flatMap(([p, pathItem]) => Object.entries(pathItem)
|
|
17
|
+
.filter(([m]) => isHttpMethod(m))
|
|
18
|
+
.map(([method]) => {
|
|
19
|
+
const routeId = methodPath(method, p);
|
|
20
|
+
return `export const ${routeId}RouteHandler:RouteHandler<typeof ${routeId}Route>=async(c)=>{}`;
|
|
21
|
+
}));
|
|
22
|
+
const getRouteMaps = (openapi) => {
|
|
23
|
+
const openapiPaths = openapi.paths;
|
|
24
|
+
const routeMappings = Object.entries(openapiPaths).flatMap(([path, pathItem]) => {
|
|
25
|
+
return Object.entries(pathItem).flatMap(([method]) => {
|
|
26
|
+
if (!isHttpMethod(method)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const base = methodPath(method, path);
|
|
30
|
+
return {
|
|
31
|
+
routeName: `${base}Route`,
|
|
32
|
+
handlerName: `${base}RouteHandler`,
|
|
33
|
+
path,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return routeMappings;
|
|
38
|
+
};
|
|
39
|
+
const routeMappings = getRouteMaps(openAPI);
|
|
40
|
+
const appInit = 'export const api = app' +
|
|
41
|
+
routeMappings
|
|
42
|
+
.sort((a, b) => (a.routeName < b.routeName ? -1 : a.routeName > b.routeName ? 1 : 0))
|
|
43
|
+
.map(({ routeName, handlerName }) => `.openapi(${routeName},${handlerName})`)
|
|
44
|
+
.join('');
|
|
45
|
+
const code = `import { OpenAPIHono, type RouteHandler } from '@hono/zod-openapi'\n${hono}\nconst app = new OpenAPIHono()\n${routes.join('\n')}\n${appInit}\nexport type AddType = typeof api`;
|
|
46
|
+
const honoType = apiType(code);
|
|
47
|
+
if (honoType === undefined)
|
|
48
|
+
return { ok: false, error: 'not generated type' };
|
|
49
|
+
const mkdirResult = await mkdir(path.dirname(output));
|
|
50
|
+
if (!mkdirResult.ok)
|
|
51
|
+
return { ok: false, error: mkdirResult.error };
|
|
52
|
+
const type = `declare const routes:\n${honoType}\nexport default routes`;
|
|
53
|
+
const fmtResult = await fmt(type);
|
|
54
|
+
if (!fmtResult.ok)
|
|
55
|
+
return { ok: false, error: fmtResult.error };
|
|
56
|
+
const writeResult = await writeFile(output, fmtResult.value);
|
|
57
|
+
if (!writeResult.ok)
|
|
58
|
+
return { ok: false, error: writeResult.error };
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
value: `Generated type code written to ${output}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function apiType(code) {
|
|
69
|
+
const VIRTUAL_FILE_NAME = 'virtual.ts';
|
|
70
|
+
const compilerOptions = {
|
|
71
|
+
target: ts.ScriptTarget.ESNext,
|
|
72
|
+
module: ts.ModuleKind.NodeNext,
|
|
73
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
74
|
+
strict: true,
|
|
75
|
+
};
|
|
76
|
+
const sourceFile = ts.createSourceFile(VIRTUAL_FILE_NAME, code, compilerOptions.target ?? ts.ScriptTarget.ESNext);
|
|
77
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
78
|
+
const originalGetSourceFile = host.getSourceFile.bind(host);
|
|
79
|
+
host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
|
80
|
+
if (fileName === VIRTUAL_FILE_NAME)
|
|
81
|
+
return sourceFile;
|
|
82
|
+
return originalGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
83
|
+
};
|
|
84
|
+
const program = ts.createProgram([VIRTUAL_FILE_NAME], compilerOptions, host);
|
|
85
|
+
const checker = program.getTypeChecker();
|
|
86
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
87
|
+
if (!moduleSymbol)
|
|
88
|
+
return undefined;
|
|
89
|
+
const apiSymbol = checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === 'api');
|
|
90
|
+
if (!apiSymbol)
|
|
91
|
+
return undefined;
|
|
92
|
+
const type = checker.getTypeOfSymbolAtLocation(apiSymbol, sourceFile);
|
|
93
|
+
return checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation |
|
|
94
|
+
ts.TypeFormatFlags.UseFullyQualifiedType |
|
|
95
|
+
ts.TypeFormatFlags.WriteTypeArgumentsOfSignature);
|
|
96
|
+
}
|
|
@@ -16,18 +16,16 @@ import { paramsObject } from './index.js';
|
|
|
16
16
|
*/
|
|
17
17
|
export function requestParameter(parameters, body) {
|
|
18
18
|
// Early return if no parameters or content
|
|
19
|
-
if (!(parameters || body?.content))
|
|
19
|
+
if (!(parameters || body?.content))
|
|
20
20
|
return '';
|
|
21
|
-
}
|
|
22
21
|
const requestBodyContentTypes = Object.keys(body?.content || {});
|
|
23
22
|
const params = parameters
|
|
24
23
|
? (() => {
|
|
25
24
|
const paramsObj = paramsObject(parameters);
|
|
26
25
|
const requestParamsArr = requestParamsArray(paramsObj);
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// params
|
|
27
|
+
if (requestParamsArr.length)
|
|
29
28
|
return `request:{${requestParamsArr.join(',')}},`;
|
|
30
|
-
}
|
|
31
29
|
return '';
|
|
32
30
|
})()
|
|
33
31
|
: '';
|
|
@@ -42,9 +40,8 @@ export function requestParameter(parameters, body) {
|
|
|
42
40
|
const required = body.required ?? false;
|
|
43
41
|
const [firstSchema] = uniqueSchemas.values();
|
|
44
42
|
const requestBodyCode = requestBody(required, body.content, firstSchema);
|
|
45
|
-
if (params)
|
|
43
|
+
if (params)
|
|
46
44
|
return params.replace('request:{', `request:{${requestBodyCode}`);
|
|
47
|
-
}
|
|
48
45
|
return `request:{${requestBodyCode}},`;
|
|
49
46
|
}
|
|
50
47
|
return params;
|
|
@@ -11,21 +11,12 @@ export function requestBody(required, content, schema) {
|
|
|
11
11
|
const contentTypes = Object.keys(content);
|
|
12
12
|
if (contentTypes.length === 0)
|
|
13
13
|
return '';
|
|
14
|
-
// check duplication
|
|
15
14
|
const isUniqueSchema = isUniqueContentSchema(contentTypes, content);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
contentParts.push(`'${contentType}':{schema:${schema}}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return `body:{required:${required},content:{${contentParts.join(',')}}},`;
|
|
29
|
-
}
|
|
30
|
-
return '';
|
|
15
|
+
if (!isUniqueSchema)
|
|
16
|
+
return '';
|
|
17
|
+
// if z.date() → z.coerce.date()
|
|
18
|
+
const contentParts = contentTypes
|
|
19
|
+
.map((contentType) => `'${contentType}':{schema:${schema.includes('z.date()') ? `z.coerce.${schema.replace('z.', '')}` : schema}}`)
|
|
20
|
+
.join(',');
|
|
21
|
+
return `body:{required:${required},content:{${contentParts}}},`;
|
|
31
22
|
}
|
|
@@ -12,53 +12,14 @@ import { zodToOpenAPI } from '../../../../zod-to-openapi/index.js';
|
|
|
12
12
|
* - Deduplicates content types if all share the same schema.
|
|
13
13
|
* - Escapes all descriptions safely for inline code.
|
|
14
14
|
*/
|
|
15
|
-
// export function response(responses: Responses): string {
|
|
16
|
-
// // 1. get response codes (200, 404, etc.)
|
|
17
|
-
// const responseCodes = Object.keys(responses)
|
|
18
|
-
// // 2. processing for each response code
|
|
19
|
-
// const responseEntries = responseCodes.map((code) => {
|
|
20
|
-
// const response = responses[code]
|
|
21
|
-
// // 2.1 no content (description only response)
|
|
22
|
-
// if (!response.content)
|
|
23
|
-
// return `${code}:{description:'${escapeStringLiteral(response.description ?? '')}',},`
|
|
24
|
-
// // check duplication
|
|
25
|
-
// const contentTypes = Object.keys(response.content)
|
|
26
|
-
// const isUniqueSchema = isUniqueContentSchema(contentTypes, response.content)
|
|
27
|
-
// // all duplication same schema
|
|
28
|
-
// if (isUniqueSchema) {
|
|
29
|
-
// const contentParts: string[] = []
|
|
30
|
-
// for (const contentType of contentTypes) {
|
|
31
|
-
// const content = response.content[contentType]
|
|
32
|
-
// const z = zodToOpenAPI(content.schema)
|
|
33
|
-
// const examples = content.examples
|
|
34
|
-
// const exampleString =
|
|
35
|
-
// examples && Object.keys(examples).length > 0
|
|
36
|
-
// ? `,examples:{${Object.entries(examples)
|
|
37
|
-
// .map(([key, example]) => {
|
|
38
|
-
// const parts = []
|
|
39
|
-
// if (example.summary) parts.push(`summary:${JSON.stringify(example.summary)}`)
|
|
40
|
-
// if (example.value !== undefined)
|
|
41
|
-
// parts.push(`value:${JSON.stringify(example.value)}`)
|
|
42
|
-
// return `${JSON.stringify(key)}:{${parts.join(',')}}`
|
|
43
|
-
// })
|
|
44
|
-
// .join(',')}}`
|
|
45
|
-
// : ''
|
|
46
|
-
// contentParts.push(`'${contentType}':{schema:${z}${exampleString}}`)
|
|
47
|
-
// }
|
|
48
|
-
// return `${code}:{description:'${escapeStringLiteral(response.description ?? '')}',content:{${contentParts.join(',')}}},`
|
|
49
|
-
// }
|
|
50
|
-
// })
|
|
51
|
-
// // 3.combine all response definitions
|
|
52
|
-
// return responseEntries.join('')
|
|
53
|
-
// }
|
|
54
15
|
export function response(responses) {
|
|
55
16
|
// 1. get response codes (200, 404, etc.)
|
|
56
|
-
|
|
17
|
+
return Object.keys(responses)
|
|
18
|
+
.map((code) => {
|
|
57
19
|
const res = responses[code];
|
|
58
20
|
const content = res.content;
|
|
59
|
-
if (!content)
|
|
21
|
+
if (!content)
|
|
60
22
|
return `${code}:{description:'${escapeStringLiteral(res.description ?? '')}',},`;
|
|
61
|
-
}
|
|
62
23
|
const contentTypes = Object.keys(content);
|
|
63
24
|
const isUnique = isUniqueContentSchema(contentTypes, content);
|
|
64
25
|
const sharedZ = isUnique ? zodToOpenAPI(content[contentTypes[0]].schema) : undefined;
|
|
@@ -68,19 +29,22 @@ export function response(responses) {
|
|
|
68
29
|
const examples = media.examples;
|
|
69
30
|
const exampleString = examples && Object.keys(examples).length > 0
|
|
70
31
|
? `,examples:{${Object.entries(examples)
|
|
71
|
-
.map(([
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
32
|
+
.map(([exampleKey, example]) => {
|
|
33
|
+
const fields = [
|
|
34
|
+
example.summary !== undefined
|
|
35
|
+
? `summary:${JSON.stringify(example.summary)}`
|
|
36
|
+
: undefined,
|
|
37
|
+
example.value !== undefined
|
|
38
|
+
? `value:${JSON.stringify(example.value)}`
|
|
39
|
+
: undefined,
|
|
40
|
+
].filter((field) => field !== undefined);
|
|
41
|
+
return `${JSON.stringify(exampleKey)}:{${fields.join(',')}}`;
|
|
78
42
|
})
|
|
79
43
|
.join(',')}}`
|
|
80
44
|
: '';
|
|
81
45
|
return `'${ct}':{schema:${z}${exampleString}}`;
|
|
82
46
|
});
|
|
83
47
|
return `${code}:{description:'${escapeStringLiteral(res.description ?? '')}',content:{${contentParts.join(',')}}},`;
|
|
84
|
-
})
|
|
85
|
-
|
|
48
|
+
})
|
|
49
|
+
.join('');
|
|
86
50
|
}
|