hono-takibi 0.9.995 → 0.9.996
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/README.md +5 -5
- package/dist/config/index.d.ts +8 -11
- package/dist/config/index.js +2 -5
- package/dist/core/docs/index.js +1 -1
- package/dist/core/rpc/index.js +4 -4
- package/dist/core/svelte-query/index.js +1 -1
- package/dist/core/swr/index.js +1 -1
- package/dist/core/tanstack-query/index.js +1 -1
- package/dist/core/type/index.js +3 -3
- package/dist/core/vue-query/index.js +1 -1
- package/dist/{core-BlUaJE2n.js → core-CaXcQRYE.js} +0 -1
- package/dist/{docs-Da_MOdAe.js → docs-BnLvHynR.js} +1 -1
- package/dist/generator/zod-openapi-hono/openapi/index.js +1 -1
- package/dist/{hono-rpc-BOLdCVD_.js → hono-rpc-CvPU8S0M.js} +11 -12
- package/dist/index.js +5 -5
- package/dist/{openapi-BZ9b1y3X.js → openapi-Bq_Z1BrA.js} +240 -249
- package/dist/{openapi-D2QO8Njt.js → openapi-CSP6nmmf.js} +26 -16
- package/dist/{query-CArUJxE4.js → query-BVXAozkk.js} +4 -4
- package/dist/vite-plugin/index.js +34 -72
- package/package.json +4 -4
- /package/dist/{guard-BabA4f3q.js → guard-BzaK5_qz.js} +0 -0
- /package/dist/{utils-B9bUHU9P.js → utils-BqOCY-3W.js} +0 -0
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ export default defineConfig({
|
|
|
196
196
|
template: {
|
|
197
197
|
test: true,
|
|
198
198
|
pathAlias: '@/',
|
|
199
|
-
|
|
199
|
+
testFramework: 'bun', // "vitest" (default) | "vite-plus" | "bun"
|
|
200
200
|
},
|
|
201
201
|
},
|
|
202
202
|
})
|
|
@@ -206,7 +206,7 @@ This generates:
|
|
|
206
206
|
|
|
207
207
|
- `src/index.ts` - App entry point with route registrations
|
|
208
208
|
- `src/handlers/*.ts` - Handler stubs for each resource
|
|
209
|
-
- `src/handlers/*.test.ts` - Test files with `@faker-js/faker` mock data (imports from `vitest` or `bun:test`)
|
|
209
|
+
- `src/handlers/*.test.ts` - Test files with `@faker-js/faker` mock data (imports from `vitest`, `vite-plus/test`, or `bun:test`)
|
|
210
210
|
|
|
211
211
|
Re-running after updating your OpenAPI spec is safe — your hand-written handler logic and test customizations are preserved. Only new routes are added as stubs.
|
|
212
212
|
|
|
@@ -300,7 +300,7 @@ export default defineConfig({
|
|
|
300
300
|
test: {
|
|
301
301
|
output: './src/test.ts',
|
|
302
302
|
import: '../index',
|
|
303
|
-
|
|
303
|
+
testFramework: 'bun', // "vitest" (default) | "vite-plus" | "bun"
|
|
304
304
|
},
|
|
305
305
|
})
|
|
306
306
|
```
|
|
@@ -387,7 +387,7 @@ export default defineConfig({
|
|
|
387
387
|
test: true, // Generate test files
|
|
388
388
|
routeHandler: false, // false: inline .openapi() (default), true: RouteHandler exports
|
|
389
389
|
pathAlias: '@/', // TypeScript path alias for imports
|
|
390
|
-
|
|
390
|
+
testFramework: 'vitest', // "vitest" (default) | "vite-plus" | "bun" — test import source
|
|
391
391
|
},
|
|
392
392
|
|
|
393
393
|
// Export options (OpenAPI Components Object)
|
|
@@ -528,7 +528,7 @@ export default defineConfig({
|
|
|
528
528
|
test: {
|
|
529
529
|
output: './src/test.ts',
|
|
530
530
|
import: '../index', // Import path for the app instance
|
|
531
|
-
|
|
531
|
+
testFramework: 'vitest', // "vitest" (default) | "vite-plus" | "bun" — test import source
|
|
532
532
|
},
|
|
533
533
|
|
|
534
534
|
// Mock server generation
|
package/dist/config/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodPipe<z.ZodObject<{
|
|
|
13
13
|
test: z.ZodDefault<z.ZodBoolean>;
|
|
14
14
|
routeHandler: z.ZodDefault<z.ZodBoolean>;
|
|
15
15
|
pathAlias: z.ZodExactOptional<z.ZodString>;
|
|
16
|
-
|
|
16
|
+
testFramework: z.ZodExactOptional<z.ZodDefault<z.ZodEnum<{
|
|
17
17
|
vitest: "vitest";
|
|
18
18
|
"vite-plus": "vite-plus";
|
|
19
19
|
bun: "bun";
|
|
@@ -144,7 +144,7 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodPipe<z.ZodObject<{
|
|
|
144
144
|
test: z.ZodExactOptional<z.ZodObject<{
|
|
145
145
|
output: z.ZodString;
|
|
146
146
|
import: z.ZodString;
|
|
147
|
-
|
|
147
|
+
testFramework: z.ZodExactOptional<z.ZodDefault<z.ZodEnum<{
|
|
148
148
|
vitest: "vitest";
|
|
149
149
|
"vite-plus": "vite-plus";
|
|
150
150
|
bun: "bun";
|
|
@@ -167,7 +167,7 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodPipe<z.ZodObject<{
|
|
|
167
167
|
test: boolean;
|
|
168
168
|
routeHandler: boolean;
|
|
169
169
|
pathAlias?: string;
|
|
170
|
-
|
|
170
|
+
testFramework?: "vitest" | "vite-plus" | "bun";
|
|
171
171
|
};
|
|
172
172
|
exportSchemas?: boolean;
|
|
173
173
|
exportSchemasTypes?: boolean;
|
|
@@ -297,7 +297,7 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodPipe<z.ZodObject<{
|
|
|
297
297
|
test?: {
|
|
298
298
|
output: string;
|
|
299
299
|
import: string;
|
|
300
|
-
|
|
300
|
+
testFramework?: "vitest" | "vite-plus" | "bun";
|
|
301
301
|
};
|
|
302
302
|
mock?: {
|
|
303
303
|
output: string;
|
|
@@ -319,7 +319,7 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodPipe<z.ZodObject<{
|
|
|
319
319
|
test: boolean;
|
|
320
320
|
routeHandler: boolean;
|
|
321
321
|
pathAlias?: string;
|
|
322
|
-
|
|
322
|
+
testFramework?: "vitest" | "vite-plus" | "bun";
|
|
323
323
|
};
|
|
324
324
|
exportSchemas?: boolean;
|
|
325
325
|
exportSchemasTypes?: boolean;
|
|
@@ -446,7 +446,7 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodPipe<z.ZodObject<{
|
|
|
446
446
|
test?: {
|
|
447
447
|
output: string;
|
|
448
448
|
import: string;
|
|
449
|
-
|
|
449
|
+
testFramework?: "vitest" | "vite-plus" | "bun";
|
|
450
450
|
};
|
|
451
451
|
mock?: {
|
|
452
452
|
output: string;
|
|
@@ -480,9 +480,6 @@ declare function readConfig(): Promise<{
|
|
|
480
480
|
readonly ok: false;
|
|
481
481
|
readonly error: string;
|
|
482
482
|
}>;
|
|
483
|
-
/**
|
|
484
|
-
* Helper to define a config with full type completion.
|
|
485
|
-
*/
|
|
486
483
|
declare function defineConfig(config: ConfigInput): Readonly<{
|
|
487
484
|
input: `${string}.yaml` | `${string}.json` | `${string}.tsp`;
|
|
488
485
|
basePath?: string;
|
|
@@ -494,7 +491,7 @@ declare function defineConfig(config: ConfigInput): Readonly<{
|
|
|
494
491
|
test?: boolean | undefined;
|
|
495
492
|
routeHandler?: boolean | undefined;
|
|
496
493
|
pathAlias?: string;
|
|
497
|
-
|
|
494
|
+
testFramework?: "vitest" | "vite-plus" | "bun" | undefined;
|
|
498
495
|
};
|
|
499
496
|
exportSchemas?: boolean;
|
|
500
497
|
exportSchemasTypes?: boolean;
|
|
@@ -621,7 +618,7 @@ declare function defineConfig(config: ConfigInput): Readonly<{
|
|
|
621
618
|
test?: {
|
|
622
619
|
output: string;
|
|
623
620
|
import: string;
|
|
624
|
-
|
|
621
|
+
testFramework?: "vitest" | "vite-plus" | "bun" | undefined;
|
|
625
622
|
};
|
|
626
623
|
mock?: {
|
|
627
624
|
output: string;
|
package/dist/config/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const ConfigSchema = z.object({
|
|
|
16
16
|
test: z.boolean().default(false),
|
|
17
17
|
routeHandler: z.boolean().default(false),
|
|
18
18
|
pathAlias: z.string().exactOptional(),
|
|
19
|
-
|
|
19
|
+
testFramework: z.enum([
|
|
20
20
|
"vitest",
|
|
21
21
|
"vite-plus",
|
|
22
22
|
"bun"
|
|
@@ -147,7 +147,7 @@ const ConfigSchema = z.object({
|
|
|
147
147
|
test: z.object({
|
|
148
148
|
output: z.string(),
|
|
149
149
|
import: z.string(),
|
|
150
|
-
|
|
150
|
+
testFramework: z.enum([
|
|
151
151
|
"vitest",
|
|
152
152
|
"vite-plus",
|
|
153
153
|
"bun"
|
|
@@ -250,9 +250,6 @@ async function readConfig() {
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
-
/**
|
|
254
|
-
* Helper to define a config with full type completion.
|
|
255
|
-
*/
|
|
256
253
|
function defineConfig(config) {
|
|
257
254
|
return config;
|
|
258
255
|
}
|
package/dist/core/docs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as makeDocs, t as docs } from "../../docs-
|
|
1
|
+
import { n as makeDocs, t as docs } from "../../docs-BnLvHynR.js";
|
|
2
2
|
export { docs, makeDocs };
|
package/dist/core/rpc/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as core } from "../../core-
|
|
4
|
-
import { a as parsePathItem, i as operationHasArgs, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "../../hono-rpc-
|
|
1
|
+
import { a as makeInferRequestType, s as methodPath } from "../../utils-BqOCY-3W.js";
|
|
2
|
+
import { c as isOperationLike, f as isRecord, o as isOpenAPIPaths } from "../../guard-BzaK5_qz.js";
|
|
3
|
+
import { t as core } from "../../core-CaXcQRYE.js";
|
|
4
|
+
import { a as parsePathItem, i as operationHasArgs, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "../../hono-rpc-CvPU8S0M.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
//#region src/core/rpc/index.ts
|
|
7
7
|
const makeOperationCode = (pathStr, method, item, deps, useParseResponse, hasBasePath) => {
|
package/dist/core/swr/index.js
CHANGED
package/dist/core/type/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as core } from "../../core-
|
|
1
|
+
import { o as makeSafeKey } from "../../utils-BqOCY-3W.js";
|
|
2
|
+
import { b as isStringRef, g as isSchemaArray, i as isMediaWithSchema, l as isParameter, m as isRequestBody, n as isHttpMethod, s as isOperation, u as isParameterArray } from "../../guard-BzaK5_qz.js";
|
|
3
|
+
import { t as core } from "../../core-CaXcQRYE.js";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
//#region src/core/type/index.ts
|
|
6
6
|
/**
|
|
@@ -61,7 +61,6 @@ async function fmt(input) {
|
|
|
61
61
|
}
|
|
62
62
|
//#endregion
|
|
63
63
|
//#region src/helper/core.ts
|
|
64
|
-
/** Formats code, creates directory, and writes the file. */
|
|
65
64
|
async function core(code, dir, output) {
|
|
66
65
|
const [fmtResult, mkdirResult] = await Promise.all([fmt(code), mkdir(dir)]);
|
|
67
66
|
if (!fmtResult.ok) return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isOAuthFlowValue, f as isRecord, h as isResponses, m as isRequestBody, p as isRefObject, r as isMedia, v as isSecurityArray, y as isSecurityScheme } from "./guard-
|
|
1
|
+
import { a as isOAuthFlowValue, f as isRecord, h as isResponses, m as isRequestBody, p as isRefObject, r as isMedia, v as isSecurityArray, y as isSecurityScheme } from "./guard-BzaK5_qz.js";
|
|
2
2
|
import { a as writeFile, t as mkdir } from "./fsp-Bv1yR6UV.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { STATUS_CODES } from "node:http";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as zodOpenAPIHono } from "../../../openapi-
|
|
1
|
+
import { t as zodOpenAPIHono } from "../../../openapi-Bq_Z1BrA.js";
|
|
2
2
|
export { zodOpenAPIHono };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as isSchemaProperty, c as isOperationLike, d as isParameterObject, f as isRecord, p as isRefObject, x as isValidIdent } from "./guard-
|
|
1
|
+
import { _ as isSchemaProperty, c as isOperationLike, d as isParameterObject, f as isRecord, p as isRefObject, x as isValidIdent } from "./guard-BzaK5_qz.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/helper/hono-rpc.ts
|
|
4
4
|
/**
|
|
@@ -30,9 +30,8 @@ function formatPath(p, hasBasePath) {
|
|
|
30
30
|
hasBracket: false
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
const hasTrailingSlash = p !== "/" && p.endsWith("/");
|
|
34
33
|
const segs = p.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
35
|
-
if (
|
|
34
|
+
if (p !== "/" && p.endsWith("/")) segs.push("index");
|
|
36
35
|
const honoSegs = segs.map((seg) => seg.replace(/\{([^}]+)\}/g, ":$1"));
|
|
37
36
|
const firstBracketIdx = honoSegs.findIndex((seg) => !isValidIdent(seg));
|
|
38
37
|
const hasBracket = firstBracketIdx !== -1;
|
|
@@ -68,7 +67,6 @@ function makeResolveParameter(componentsParameters) {
|
|
|
68
67
|
function makeToParameterLikes(resolveParam) {
|
|
69
68
|
return (arr) => Array.isArray(arr) ? arr.map((x) => resolveParam(x)).filter((param) => param !== void 0) : [];
|
|
70
69
|
}
|
|
71
|
-
const NO_CONTENT_STATUS_CODES = [204, 205];
|
|
72
70
|
/**
|
|
73
71
|
* Check if operation has No Content response (204 or 205).
|
|
74
72
|
*/
|
|
@@ -77,10 +75,13 @@ function hasNoContentResponse(op) {
|
|
|
77
75
|
if (!isRecord(responses)) return false;
|
|
78
76
|
return Object.keys(responses).some((status) => {
|
|
79
77
|
const code = Number.parseInt(status, 10);
|
|
80
|
-
return !Number.isNaN(code) &&
|
|
78
|
+
return !Number.isNaN(code) && [204, 205].includes(code);
|
|
81
79
|
});
|
|
82
80
|
}
|
|
83
81
|
/**
|
|
82
|
+
* All body info grouped by type.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
84
85
|
* Extract requestBody name from $ref.
|
|
85
86
|
*/
|
|
86
87
|
function refRequestBodyName(refLike) {
|
|
@@ -108,9 +109,9 @@ function pickAllBodyInfoFromContent(content) {
|
|
|
108
109
|
*/
|
|
109
110
|
function makePickAllBodyInfo(componentsRequestBodies) {
|
|
110
111
|
return (op) => {
|
|
111
|
-
const
|
|
112
|
-
if (!isRecord(
|
|
113
|
-
const refName = refRequestBodyName(
|
|
112
|
+
const requestBody = op.requestBody;
|
|
113
|
+
if (!isRecord(requestBody)) return void 0;
|
|
114
|
+
const refName = refRequestBodyName(requestBody);
|
|
114
115
|
if (refName) {
|
|
115
116
|
const resolved = componentsRequestBodies[refName];
|
|
116
117
|
if (isRecord(resolved) && isRecord(resolved.content)) return pickAllBodyInfoFromContent(resolved.content);
|
|
@@ -120,7 +121,7 @@ function makePickAllBodyInfo(componentsRequestBodies) {
|
|
|
120
121
|
};
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
|
-
return pickAllBodyInfoFromContent(
|
|
124
|
+
return pickAllBodyInfoFromContent(requestBody.content);
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
@@ -163,9 +164,7 @@ function makeOperationDeps(clientName, componentsParameters, componentsRequestBo
|
|
|
163
164
|
* Check if operation has arguments (parameters or body).
|
|
164
165
|
*/
|
|
165
166
|
function operationHasArgs(item, op, deps) {
|
|
166
|
-
const
|
|
167
|
-
const opParams = deps.toParameterLikes(op.parameters);
|
|
168
|
-
const allParams = [...pathLevelParams, ...opParams];
|
|
167
|
+
const allParams = [...deps.toParameterLikes(item.parameters), ...deps.toParameterLikes(op.parameters)];
|
|
169
168
|
const hasParams = allParams.filter((p) => p.in === "path").length > 0 || allParams.filter((p) => p.in === "query").length > 0 || allParams.filter((p) => p.in === "header").length > 0 || allParams.filter((p) => p.in === "cookie").length > 0;
|
|
170
169
|
const allBodyInfo = deps.pickAllBodyInfo(op);
|
|
171
170
|
const hasBody = allBodyInfo !== void 0 && (allBodyInfo.form.length > 0 || allBodyInfo.json.length > 0);
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readConfig } from "./config/index.js";
|
|
3
|
-
import { r as setFormatOptions } from "./core-
|
|
4
|
-
import { _ as examples, a as takibi, c as securitySchemes, d as requestBodies, f as pathItems, g as headers, h as links, i as template, l as schemas, m as mediaTypes, n as webhooks, o as route, p as parameters, r as test, s as mock, t as parseOpenAPI, u as responses, v as callbacks } from "./openapi-
|
|
5
|
-
import { t as docs } from "./docs-
|
|
3
|
+
import { r as setFormatOptions } from "./core-CaXcQRYE.js";
|
|
4
|
+
import { _ as examples, a as takibi, c as securitySchemes, d as requestBodies, f as pathItems, g as headers, h as links, i as template, l as schemas, m as mediaTypes, n as webhooks, o as route, p as parameters, r as test, s as mock, t as parseOpenAPI, u as responses, v as callbacks } from "./openapi-CSP6nmmf.js";
|
|
5
|
+
import { t as docs } from "./docs-BnLvHynR.js";
|
|
6
6
|
import { rpc } from "./core/rpc/index.js";
|
|
7
7
|
import { svelteQuery } from "./core/svelte-query/index.js";
|
|
8
8
|
import { swr } from "./core/swr/index.js";
|
|
@@ -133,13 +133,13 @@ async function honoTakibi() {
|
|
|
133
133
|
config["tanstack-query"] ? tanstackQuery(openAPI, config["tanstack-query"].output, config["tanstack-query"].import, config["tanstack-query"].split ?? false, config["tanstack-query"].client ?? "client") : Promise.resolve(void 0),
|
|
134
134
|
config["svelte-query"] ? svelteQuery(openAPI, config["svelte-query"].output, config["svelte-query"].import, config["svelte-query"].split ?? false, config["svelte-query"].client ?? "client") : Promise.resolve(void 0),
|
|
135
135
|
config["vue-query"] ? vueQuery(openAPI, config["vue-query"].output, config["vue-query"].import, config["vue-query"].split ?? false, config["vue-query"].client ?? "client") : Promise.resolve(void 0),
|
|
136
|
-
config.test ? test(openAPI, config.test.output, config.test.import, config.basePath ?? "/", config.test.
|
|
136
|
+
config.test ? test(openAPI, config.test.output, config.test.import, config.basePath ?? "/", config.test.testFramework) : Promise.resolve(void 0),
|
|
137
137
|
config.mock ? mock(openAPI, config.mock.output, config.basePath ?? "/", config["zod-openapi"]?.readonly) : Promise.resolve(void 0),
|
|
138
138
|
config.docs ? docs(openAPI, config.docs.output, config.docs.entry, config.basePath ?? "/", config.docs.curl, config.docs.baseUrl) : Promise.resolve(void 0),
|
|
139
139
|
(() => {
|
|
140
140
|
if (!config["zod-openapi"]?.template) return Promise.resolve(void 0);
|
|
141
141
|
if (!(config["zod-openapi"]?.output ?? config["zod-openapi"]?.routes?.output)) return Promise.resolve(void 0);
|
|
142
|
-
return template(openAPI, config["zod-openapi"]?.output ?? config["zod-openapi"]?.routes?.output ?? "src/routes/index.ts", config["zod-openapi"]?.template.test, config.basePath ?? "/", config["zod-openapi"]?.template.pathAlias, config["zod-openapi"]?.routes?.import, config["zod-openapi"]?.template.routeHandler, config["zod-openapi"]?.template.
|
|
142
|
+
return template(openAPI, config["zod-openapi"]?.output ?? config["zod-openapi"]?.routes?.output ?? "src/routes/index.ts", config["zod-openapi"]?.template.test, config.basePath ?? "/", config["zod-openapi"]?.template.pathAlias, config["zod-openapi"]?.routes?.import, config["zod-openapi"]?.template.routeHandler, config["zod-openapi"]?.template.testFramework);
|
|
143
143
|
})()
|
|
144
144
|
]);
|
|
145
145
|
const values = [];
|
|
@@ -1,28 +1,219 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { c as normalizeTypes, d as toIdentifierPascalCase, f as uncapitalize, l as renderNamedImport, n as ensureSuffix, o as makeSafeKey, p as zodToOpenAPISchema, r as error, s as methodPath, u as requestParamsArray } from "./utils-BqOCY-3W.js";
|
|
2
|
+
import { f as isRecord, g as isSchemaArray, l as isParameter, p as isRefObject, r as isMedia, s as isOperation } from "./guard-BzaK5_qz.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import ts from "typescript";
|
|
5
|
-
//#region src/helper/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
//#region src/helper/code.ts
|
|
6
|
+
/**
|
|
7
|
+
* Builds a relative module specifier from `fromFile` to a configured output.
|
|
8
|
+
*
|
|
9
|
+
* Computes the relative path from the directory of `fromFile` to the `target.output`,
|
|
10
|
+
* stripping `.ts` extension and `/index` suffix for clean ES module import paths.
|
|
11
|
+
*
|
|
12
|
+
* @param fromFile - The absolute path of the source file that will import the target.
|
|
13
|
+
* @param target - Configuration for the target module.
|
|
14
|
+
* @param target.output - The absolute path to the output file or directory.
|
|
15
|
+
* @param target.split - When true, treats output as a directory path rather than a file.
|
|
16
|
+
* @returns A relative module specifier (e.g., `'../schemas'`, `'./user'`, `'.'`).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* // File to directory (strips /index)
|
|
21
|
+
* makeModuleSpec('/src/routes/index.ts', { output: '/src/schemas/index.ts' })
|
|
22
|
+
* // → '../schemas'
|
|
23
|
+
*
|
|
24
|
+
* // File to file (strips .ts extension)
|
|
25
|
+
* makeModuleSpec('/src/routes/user.ts', { output: '/src/schemas/user.ts' })
|
|
26
|
+
* // → '../schemas/user'
|
|
27
|
+
*
|
|
28
|
+
* // Split mode: same directory
|
|
29
|
+
* makeModuleSpec('/src/routes/index.ts', { output: '/src/routes', split: true })
|
|
30
|
+
* // → '.'
|
|
31
|
+
*
|
|
32
|
+
* // Ensures dot-relative prefix
|
|
33
|
+
* makeModuleSpec('/src/index.ts', { output: '/src/schemas.ts' })
|
|
34
|
+
* // → './schemas'
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function makeModuleSpec(fromFile, target) {
|
|
38
|
+
const stripped = path.relative(path.dirname(fromFile), target.output).replace(/\\/g, "/").replace(/\.ts$/, "").replace(/\/index$/, "");
|
|
39
|
+
return stripped === "" ? "." : stripped.startsWith(".") ? stripped : `./${stripped}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generates a const declaration prefix with optional export.
|
|
43
|
+
*
|
|
44
|
+
* Combines the text and suffix, converts to PascalCase, and prepends
|
|
45
|
+
* `export const ` or `const ` based on the `exportVariable` flag.
|
|
46
|
+
*
|
|
47
|
+
* @param exportVariable - Whether to add the `export` keyword.
|
|
48
|
+
* @param text - The base name for the constant (will be converted to PascalCase).
|
|
49
|
+
* @param suffix - The suffix to append to the name (added before PascalCase conversion).
|
|
50
|
+
* @returns A string like `'export const UserSchema='` or `'const UserSchema='`.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* makeConst(true, 'User', 'Schema')
|
|
55
|
+
* // → 'export const UserSchema='
|
|
56
|
+
*
|
|
57
|
+
* makeConst(false, 'post', 'Response')
|
|
58
|
+
* // → 'const PostResponse='
|
|
59
|
+
*
|
|
60
|
+
* // Handles kebab-case input
|
|
61
|
+
* makeConst(true, 'user-profile', 'Schema')
|
|
62
|
+
* // → 'export const UserProfileSchema='
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
function makeConst(exportVariable, text, suffix) {
|
|
66
|
+
return `${exportVariable ? "export const " : "const "}${toIdentifierPascalCase(ensureSuffix(text, suffix))}=`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Generates a string of export const statements for the given value.
|
|
70
|
+
*
|
|
71
|
+
* Iterates over the keys of `value`, converts each key to PascalCase with the suffix,
|
|
72
|
+
* and serializes the value as JSON. Multiple entries are separated by double newlines.
|
|
73
|
+
*
|
|
74
|
+
* @param value - Object containing values to export (keys become constant names).
|
|
75
|
+
* @param suffix - Suffix to append to each export name (before PascalCase conversion).
|
|
76
|
+
* @param readonly - Whether to add `as const` assertion for TypeScript readonly inference.
|
|
77
|
+
* @returns A string of TypeScript export const statements separated by `\n\n`.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* // Single export
|
|
82
|
+
* makeExportConst({ user: { id: 1 } }, 'Example')
|
|
83
|
+
* // → 'export const UserExample={"id":1}'
|
|
84
|
+
*
|
|
85
|
+
* // With as const assertion
|
|
86
|
+
* makeExportConst({ user: { id: 1 } }, 'Example', true)
|
|
87
|
+
* // → 'export const UserExample={"id":1} as const'
|
|
88
|
+
*
|
|
89
|
+
* // Multiple exports
|
|
90
|
+
* makeExportConst({ user: { id: 1 }, post: { title: 'Hello' } }, 'Data')
|
|
91
|
+
* // → 'export const UserData={"id":1}\n\nexport const PostData={"title":"Hello"}'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
function makeExportConst(value, suffix, readonly) {
|
|
95
|
+
const asConst = readonly ? " as const" : "";
|
|
96
|
+
return Object.keys(value).map((key) => `export const ${toIdentifierPascalCase(ensureSuffix(key, suffix))}=${JSON.stringify(value[key])}${asConst}`).join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Universal import generator for OpenAPI-based Hono route files.
|
|
100
|
+
*
|
|
101
|
+
* Analyzes the provided code to auto-detect required imports and generates
|
|
102
|
+
* import statements for `@hono/zod-openapi` and OpenAPI component references.
|
|
103
|
+
*
|
|
104
|
+
* **Import Detection Logic:**
|
|
105
|
+
* - Detects `z.` usage to import `z` from `@hono/zod-openapi`
|
|
106
|
+
* - Detects `createRoute(` usage to import `createRoute`
|
|
107
|
+
* - Scans for OpenAPI component references by suffix pattern (e.g., `*Schema`, `*Response`)
|
|
108
|
+
* - Excludes locally defined exports from import generation
|
|
109
|
+
*
|
|
110
|
+
* **OpenAPI Component Patterns** (ordered per OpenAPI 3.0 spec):
|
|
111
|
+
* | Suffix | Component Type | Example |
|
|
112
|
+
* |--------|---------------|---------|
|
|
113
|
+
* | `*Schema` | schemas | `UserSchema` |
|
|
114
|
+
* | `*ParamsSchema` | parameters | `IdParamsSchema` |
|
|
115
|
+
* | `*SecurityScheme` | securitySchemes | `BearerSecurityScheme` |
|
|
116
|
+
* | `*RequestBody` | requestBodies | `CreateUserRequestBody` |
|
|
117
|
+
* | `*Response` | responses | `UserResponse` |
|
|
118
|
+
* | `*HeaderSchema` | headers | `AuthHeaderSchema` |
|
|
119
|
+
* | `*Example` | examples | `UserExample` |
|
|
120
|
+
* | `*Link` | links | `GetUserLink` |
|
|
121
|
+
* | `*Callback` | callbacks | `WebhookCallback` |
|
|
122
|
+
*
|
|
123
|
+
* @param code - The TypeScript code to analyze and prepend imports to.
|
|
124
|
+
* @param fromFile - The absolute path of the file where code will be written.
|
|
125
|
+
* @param components - Configuration mapping OpenAPI component types to output locations.
|
|
126
|
+
* @param split - When true, uses `'..'` as fallback prefix; otherwise `'.'`.
|
|
127
|
+
* @returns The code with generated import statements prepended.
|
|
128
|
+
*
|
|
129
|
+
* @see {@link https://swagger.io/docs/specification/v3_0/components/|OpenAPI Components}
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* // Basic usage with schema reference
|
|
134
|
+
* const code = 'const route = createRoute({ request: { body: UserSchema } })'
|
|
135
|
+
* makeImports(code, '/src/routes/user.ts', { schemas: { output: '/src/schemas.ts' } })
|
|
136
|
+
* // → "import{createRoute}from'@hono/zod-openapi'\nimport{UserSchema}from'../schemas'\n\n..."
|
|
137
|
+
*
|
|
138
|
+
* // With z usage
|
|
139
|
+
* const code2 = 'z.string()'
|
|
140
|
+
* makeImports(code2, '/src/routes/user.ts', undefined)
|
|
141
|
+
* // → "import{z}from'@hono/zod-openapi'\n\n..."
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
/** Valid JavaScript identifier pattern (e.g., `UserSchema`, `_private`, `$special`) */
|
|
145
|
+
const JS_IDENT = "[A-Za-z_$][A-Za-z0-9_$]*";
|
|
146
|
+
/**
|
|
147
|
+
* Regex patterns for OpenAPI component types.
|
|
148
|
+
* Ordered per OpenAPI 3.0 specification.
|
|
149
|
+
* Note: (?<!Params)(?<!Header) excludes ParamsSchema/HeaderSchema from generic Schema matches
|
|
150
|
+
*
|
|
151
|
+
* Hoisted to module scope to avoid re-creating RegExp objects on every call.
|
|
152
|
+
*/
|
|
153
|
+
const IMPORT_PATTERNS = [
|
|
154
|
+
{
|
|
155
|
+
pattern: new RegExp(`\\b(${JS_IDENT}(?<!Params)(?<!Header)(?<!MediaType)Schema)\\b`, "g"),
|
|
156
|
+
key: "schemas"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
pattern: new RegExp(`\\b(${JS_IDENT}ParamsSchema)\\b`, "g"),
|
|
160
|
+
key: "parameters"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
pattern: new RegExp(`\\b(${JS_IDENT}SecurityScheme)\\b`, "g"),
|
|
164
|
+
key: "securitySchemes"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
pattern: new RegExp(`\\b(${JS_IDENT}RequestBody)\\b`, "g"),
|
|
168
|
+
key: "requestBodies"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
pattern: new RegExp(`\\b(${JS_IDENT}Response)\\b`, "g"),
|
|
172
|
+
key: "responses"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
pattern: new RegExp(`\\b(${JS_IDENT}HeaderSchema)\\b`, "g"),
|
|
176
|
+
key: "headers"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
pattern: new RegExp(`\\b(${JS_IDENT}Example)\\b`, "g"),
|
|
180
|
+
key: "examples"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
pattern: new RegExp(`\\b(${JS_IDENT}Link)\\b`, "g"),
|
|
184
|
+
key: "links"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
pattern: new RegExp(`\\b(${JS_IDENT}Callback)\\b`, "g"),
|
|
188
|
+
key: "callbacks"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
pattern: new RegExp(`\\b(${JS_IDENT}MediaTypeSchema)\\b`, "g"),
|
|
192
|
+
key: "mediaTypes"
|
|
24
193
|
}
|
|
25
|
-
|
|
194
|
+
];
|
|
195
|
+
/** Pattern to find locally exported constants */
|
|
196
|
+
const EXPORT_CONST_PATTERN = new RegExp(`export\\s+const\\s+(${JS_IDENT})\\s*=`, "g");
|
|
197
|
+
function makeImports(code, fromFile, components, split = false) {
|
|
198
|
+
const fallbackPrefix = split ? ".." : ".";
|
|
199
|
+
const resolvePath = (key) => {
|
|
200
|
+
const target = components?.[key];
|
|
201
|
+
return target?.import ?? (target ? makeModuleSpec(fromFile, target) : `${fallbackPrefix}/${key}`);
|
|
202
|
+
};
|
|
203
|
+
const defined = new Set(Array.from(code.matchAll(EXPORT_CONST_PATTERN), (m) => m[1]).filter(Boolean));
|
|
204
|
+
const needsCreateRoute = code.includes("createRoute(");
|
|
205
|
+
const needsZ = code.includes("z.");
|
|
206
|
+
const honoImports = [needsCreateRoute && "createRoute", needsZ && "z"].filter(Boolean);
|
|
207
|
+
return [
|
|
208
|
+
honoImports.length > 0 ? `import{${honoImports.join(",")}}from'@hono/zod-openapi'` : "",
|
|
209
|
+
...IMPORT_PATTERNS.flatMap(({ pattern, key }) => {
|
|
210
|
+
const tokens = [...new Set(Array.from(code.matchAll(pattern), (m) => m[1]))].filter((t) => Boolean(t) && !defined.has(t)).sort();
|
|
211
|
+
return tokens.length > 0 ? [renderNamedImport(tokens, resolvePath(key))] : [];
|
|
212
|
+
}),
|
|
213
|
+
"\n",
|
|
214
|
+
code,
|
|
215
|
+
""
|
|
216
|
+
].filter(Boolean).join("\n");
|
|
26
217
|
}
|
|
27
218
|
//#endregion
|
|
28
219
|
//#region src/helper/openapi.ts
|
|
@@ -192,10 +383,9 @@ function makeCallbacks(callbacks, readonly) {
|
|
|
192
383
|
return Object.entries(callbacks).map(([callbackKey, pathItem]) => {
|
|
193
384
|
if (isRefObject(pathItem)) return `${JSON.stringify(callbackKey)}:${makeRef(pathItem.$ref)}`;
|
|
194
385
|
if (!isRecord(pathItem)) return void 0;
|
|
195
|
-
const
|
|
196
|
-
const pathItemCode = makeMethodsCode(pathItemRecord);
|
|
386
|
+
const pathItemCode = makeMethodsCode(pathItem);
|
|
197
387
|
if (pathItemCode) return `${JSON.stringify(callbackKey)}:{${pathItemCode}}`;
|
|
198
|
-
const nestedCode = Object.entries(
|
|
388
|
+
const nestedCode = Object.entries(pathItem).map(([pathExpr, inner]) => {
|
|
199
389
|
if (!isRecord(inner)) return void 0;
|
|
200
390
|
const code = makeMethodsCode(inner);
|
|
201
391
|
return code ? `${JSON.stringify(pathExpr)}:{${code}}` : void 0;
|
|
@@ -344,7 +534,7 @@ function makeParameters(parameters, readonly) {
|
|
|
344
534
|
return acc;
|
|
345
535
|
}
|
|
346
536
|
const baseSchema = zodToOpenAPI(schema, { parameters: param }, readonly);
|
|
347
|
-
const z = param.in === "query" && (schema.type === "number" || schema.type === "integer") ?
|
|
537
|
+
const z = param.in === "query" && (schema.type === "number" || schema.type === "integer") ? coerce(baseSchema, schema.type, schema.format) : param.in === "query" && schema.type === "boolean" ? baseSchema.replace("boolean", "stringbool").replace(/\.default\("true"\)/g, ".default(true)").replace(/\.default\("false"\)/g, ".default(false)") : param.in === "query" && schema.type === "date" ? `z.coerce.${baseSchema.replace("z.", "")}` : param.in === "query" && (schema.type === "object" || schema.type === "array") ? baseSchema.replace(/z\.(int\d*)\(\)((?:\.(?:min|max|gt|lt|positive|negative|nonnegative|nonpositive|multipleOf)\([^)]*\))*)/g, (_, type, constraints) => `z.coerce.number().pipe(z.${type}()${constraints})`).replace(/z\.bigint\(\)/g, "z.coerce.bigint()").replace(/z\.number\(\)/g, "z.coerce.number()").replace(/z\.boolean\(\)/g, "z.stringbool()").replace(/z\.date\(\)/g, "z.coerce.date()") : baseSchema;
|
|
348
538
|
acc[param.in][makeSafeKey(param.name)] = z;
|
|
349
539
|
return acc;
|
|
350
540
|
}, {});
|
|
@@ -412,7 +602,7 @@ function makeOperation(operation, readonly) {
|
|
|
412
602
|
].filter((v) => v !== void 0).join(",")}}`;
|
|
413
603
|
}
|
|
414
604
|
function makePathItem(pathItem) {
|
|
415
|
-
const
|
|
605
|
+
const additionalOperationsCode = pathItem.additionalOperations ? Object.entries(pathItem.additionalOperations).map(([opName, op]) => `${JSON.stringify(opName)}:${makeOperation(op)}`).join(",") : void 0;
|
|
416
606
|
return `{${[
|
|
417
607
|
pathItem.$ref ? `$ref:${makeRef(pathItem.$ref)}` : void 0,
|
|
418
608
|
pathItem.summary ? `summary:${JSON.stringify(pathItem.summary)}` : void 0,
|
|
@@ -426,7 +616,7 @@ function makePathItem(pathItem) {
|
|
|
426
616
|
pathItem.patch ? `patch:${makeOperation(pathItem.patch)}` : void 0,
|
|
427
617
|
pathItem.trace ? `trace:${makeOperation(pathItem.trace)}` : void 0,
|
|
428
618
|
pathItem.query ? `query:${makeOperation(pathItem.query)}` : void 0,
|
|
429
|
-
|
|
619
|
+
additionalOperationsCode ? `additionalOperations:{${additionalOperationsCode}}` : void 0,
|
|
430
620
|
pathItem.servers ? `servers:${JSON.stringify(pathItem.servers)}` : void 0,
|
|
431
621
|
pathItem.parameters ? `parameters:${makePathParameters(pathItem.parameters)}` : void 0
|
|
432
622
|
].filter((v) => v !== void 0).join(",")}}`;
|
|
@@ -1343,218 +1533,26 @@ function ast(code) {
|
|
|
1343
1533
|
return topoSort(decls).map((d) => d.fullText).join("\n\n");
|
|
1344
1534
|
}
|
|
1345
1535
|
//#endregion
|
|
1346
|
-
//#region src/helper/
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
* @param fromFile - The absolute path of the source file that will import the target.
|
|
1354
|
-
* @param target - Configuration for the target module.
|
|
1355
|
-
* @param target.output - The absolute path to the output file or directory.
|
|
1356
|
-
* @param target.split - When true, treats output as a directory path rather than a file.
|
|
1357
|
-
* @returns A relative module specifier (e.g., `'../schemas'`, `'./user'`, `'.'`).
|
|
1358
|
-
*
|
|
1359
|
-
* @example
|
|
1360
|
-
* ```ts
|
|
1361
|
-
* // File to directory (strips /index)
|
|
1362
|
-
* makeModuleSpec('/src/routes/index.ts', { output: '/src/schemas/index.ts' })
|
|
1363
|
-
* // → '../schemas'
|
|
1364
|
-
*
|
|
1365
|
-
* // File to file (strips .ts extension)
|
|
1366
|
-
* makeModuleSpec('/src/routes/user.ts', { output: '/src/schemas/user.ts' })
|
|
1367
|
-
* // → '../schemas/user'
|
|
1368
|
-
*
|
|
1369
|
-
* // Split mode: same directory
|
|
1370
|
-
* makeModuleSpec('/src/routes/index.ts', { output: '/src/routes', split: true })
|
|
1371
|
-
* // → '.'
|
|
1372
|
-
*
|
|
1373
|
-
* // Ensures dot-relative prefix
|
|
1374
|
-
* makeModuleSpec('/src/index.ts', { output: '/src/schemas.ts' })
|
|
1375
|
-
* // → './schemas'
|
|
1376
|
-
* ```
|
|
1377
|
-
*/
|
|
1378
|
-
function makeModuleSpec(fromFile, target) {
|
|
1379
|
-
const stripped = path.relative(path.dirname(fromFile), target.output).replace(/\\/g, "/").replace(/\.ts$/, "").replace(/\/index$/, "");
|
|
1380
|
-
return stripped === "" ? "." : stripped.startsWith(".") ? stripped : `./${stripped}`;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Generates a const declaration prefix with optional export.
|
|
1384
|
-
*
|
|
1385
|
-
* Combines the text and suffix, converts to PascalCase, and prepends
|
|
1386
|
-
* `export const ` or `const ` based on the `exportVariable` flag.
|
|
1387
|
-
*
|
|
1388
|
-
* @param exportVariable - Whether to add the `export` keyword.
|
|
1389
|
-
* @param text - The base name for the constant (will be converted to PascalCase).
|
|
1390
|
-
* @param suffix - The suffix to append to the name (added before PascalCase conversion).
|
|
1391
|
-
* @returns A string like `'export const UserSchema='` or `'const UserSchema='`.
|
|
1392
|
-
*
|
|
1393
|
-
* @example
|
|
1394
|
-
* ```ts
|
|
1395
|
-
* makeConst(true, 'User', 'Schema')
|
|
1396
|
-
* // → 'export const UserSchema='
|
|
1397
|
-
*
|
|
1398
|
-
* makeConst(false, 'post', 'Response')
|
|
1399
|
-
* // → 'const PostResponse='
|
|
1400
|
-
*
|
|
1401
|
-
* // Handles kebab-case input
|
|
1402
|
-
* makeConst(true, 'user-profile', 'Schema')
|
|
1403
|
-
* // → 'export const UserProfileSchema='
|
|
1404
|
-
* ```
|
|
1405
|
-
*/
|
|
1406
|
-
function makeConst(exportVariable, text, suffix) {
|
|
1407
|
-
return `${exportVariable ? "export const " : "const "}${toIdentifierPascalCase(ensureSuffix(text, suffix))}=`;
|
|
1408
|
-
}
|
|
1409
|
-
/**
|
|
1410
|
-
* Generates a string of export const statements for the given value.
|
|
1411
|
-
*
|
|
1412
|
-
* Iterates over the keys of `value`, converts each key to PascalCase with the suffix,
|
|
1413
|
-
* and serializes the value as JSON. Multiple entries are separated by double newlines.
|
|
1414
|
-
*
|
|
1415
|
-
* @param value - Object containing values to export (keys become constant names).
|
|
1416
|
-
* @param suffix - Suffix to append to each export name (before PascalCase conversion).
|
|
1417
|
-
* @param readonly - Whether to add `as const` assertion for TypeScript readonly inference.
|
|
1418
|
-
* @returns A string of TypeScript export const statements separated by `\n\n`.
|
|
1419
|
-
*
|
|
1420
|
-
* @example
|
|
1421
|
-
* ```ts
|
|
1422
|
-
* // Single export
|
|
1423
|
-
* makeExportConst({ user: { id: 1 } }, 'Example')
|
|
1424
|
-
* // → 'export const UserExample={"id":1}'
|
|
1425
|
-
*
|
|
1426
|
-
* // With as const assertion
|
|
1427
|
-
* makeExportConst({ user: { id: 1 } }, 'Example', true)
|
|
1428
|
-
* // → 'export const UserExample={"id":1} as const'
|
|
1429
|
-
*
|
|
1430
|
-
* // Multiple exports
|
|
1431
|
-
* makeExportConst({ user: { id: 1 }, post: { title: 'Hello' } }, 'Data')
|
|
1432
|
-
* // → 'export const UserData={"id":1}\n\nexport const PostData={"title":"Hello"}'
|
|
1433
|
-
* ```
|
|
1434
|
-
*/
|
|
1435
|
-
function makeExportConst(value, suffix, readonly) {
|
|
1436
|
-
const asConst = readonly ? " as const" : "";
|
|
1437
|
-
return Object.keys(value).map((key) => `export const ${toIdentifierPascalCase(ensureSuffix(key, suffix))}=${JSON.stringify(value[key])}${asConst}`).join("\n\n");
|
|
1438
|
-
}
|
|
1439
|
-
/**
|
|
1440
|
-
* Universal import generator for OpenAPI-based Hono route files.
|
|
1441
|
-
*
|
|
1442
|
-
* Analyzes the provided code to auto-detect required imports and generates
|
|
1443
|
-
* import statements for `@hono/zod-openapi` and OpenAPI component references.
|
|
1444
|
-
*
|
|
1445
|
-
* **Import Detection Logic:**
|
|
1446
|
-
* - Detects `z.` usage to import `z` from `@hono/zod-openapi`
|
|
1447
|
-
* - Detects `createRoute(` usage to import `createRoute`
|
|
1448
|
-
* - Scans for OpenAPI component references by suffix pattern (e.g., `*Schema`, `*Response`)
|
|
1449
|
-
* - Excludes locally defined exports from import generation
|
|
1450
|
-
*
|
|
1451
|
-
* **OpenAPI Component Patterns** (ordered per OpenAPI 3.0 spec):
|
|
1452
|
-
* | Suffix | Component Type | Example |
|
|
1453
|
-
* |--------|---------------|---------|
|
|
1454
|
-
* | `*Schema` | schemas | `UserSchema` |
|
|
1455
|
-
* | `*ParamsSchema` | parameters | `IdParamsSchema` |
|
|
1456
|
-
* | `*SecurityScheme` | securitySchemes | `BearerSecurityScheme` |
|
|
1457
|
-
* | `*RequestBody` | requestBodies | `CreateUserRequestBody` |
|
|
1458
|
-
* | `*Response` | responses | `UserResponse` |
|
|
1459
|
-
* | `*HeaderSchema` | headers | `AuthHeaderSchema` |
|
|
1460
|
-
* | `*Example` | examples | `UserExample` |
|
|
1461
|
-
* | `*Link` | links | `GetUserLink` |
|
|
1462
|
-
* | `*Callback` | callbacks | `WebhookCallback` |
|
|
1463
|
-
*
|
|
1464
|
-
* @param code - The TypeScript code to analyze and prepend imports to.
|
|
1465
|
-
* @param fromFile - The absolute path of the file where code will be written.
|
|
1466
|
-
* @param components - Configuration mapping OpenAPI component types to output locations.
|
|
1467
|
-
* @param split - When true, uses `'..'` as fallback prefix; otherwise `'.'`.
|
|
1468
|
-
* @returns The code with generated import statements prepended.
|
|
1469
|
-
*
|
|
1470
|
-
* @see {@link https://swagger.io/docs/specification/v3_0/components/|OpenAPI Components}
|
|
1471
|
-
*
|
|
1472
|
-
* @example
|
|
1473
|
-
* ```ts
|
|
1474
|
-
* // Basic usage with schema reference
|
|
1475
|
-
* const code = 'const route = createRoute({ request: { body: UserSchema } })'
|
|
1476
|
-
* makeImports(code, '/src/routes/user.ts', { schemas: { output: '/src/schemas.ts' } })
|
|
1477
|
-
* // → "import{createRoute}from'@hono/zod-openapi'\nimport{UserSchema}from'../schemas'\n\n..."
|
|
1478
|
-
*
|
|
1479
|
-
* // With z usage
|
|
1480
|
-
* const code2 = 'z.string()'
|
|
1481
|
-
* makeImports(code2, '/src/routes/user.ts', undefined)
|
|
1482
|
-
* // → "import{z}from'@hono/zod-openapi'\n\n..."
|
|
1483
|
-
* ```
|
|
1484
|
-
*/
|
|
1485
|
-
/** Valid JavaScript identifier pattern (e.g., `UserSchema`, `_private`, `$special`) */
|
|
1486
|
-
const JS_IDENT = "[A-Za-z_$][A-Za-z0-9_$]*";
|
|
1487
|
-
/**
|
|
1488
|
-
* Regex patterns for OpenAPI component types.
|
|
1489
|
-
* Ordered per OpenAPI 3.0 specification.
|
|
1490
|
-
* Note: (?<!Params)(?<!Header) excludes ParamsSchema/HeaderSchema from generic Schema matches
|
|
1491
|
-
*
|
|
1492
|
-
* Hoisted to module scope to avoid re-creating RegExp objects on every call.
|
|
1493
|
-
*/
|
|
1494
|
-
const IMPORT_PATTERNS = [
|
|
1495
|
-
{
|
|
1496
|
-
pattern: new RegExp(`\\b(${JS_IDENT}(?<!Params)(?<!Header)(?<!MediaType)Schema)\\b`, "g"),
|
|
1497
|
-
key: "schemas"
|
|
1498
|
-
},
|
|
1499
|
-
{
|
|
1500
|
-
pattern: new RegExp(`\\b(${JS_IDENT}ParamsSchema)\\b`, "g"),
|
|
1501
|
-
key: "parameters"
|
|
1502
|
-
},
|
|
1503
|
-
{
|
|
1504
|
-
pattern: new RegExp(`\\b(${JS_IDENT}SecurityScheme)\\b`, "g"),
|
|
1505
|
-
key: "securitySchemes"
|
|
1506
|
-
},
|
|
1507
|
-
{
|
|
1508
|
-
pattern: new RegExp(`\\b(${JS_IDENT}RequestBody)\\b`, "g"),
|
|
1509
|
-
key: "requestBodies"
|
|
1510
|
-
},
|
|
1511
|
-
{
|
|
1512
|
-
pattern: new RegExp(`\\b(${JS_IDENT}Response)\\b`, "g"),
|
|
1513
|
-
key: "responses"
|
|
1514
|
-
},
|
|
1515
|
-
{
|
|
1516
|
-
pattern: new RegExp(`\\b(${JS_IDENT}HeaderSchema)\\b`, "g"),
|
|
1517
|
-
key: "headers"
|
|
1518
|
-
},
|
|
1519
|
-
{
|
|
1520
|
-
pattern: new RegExp(`\\b(${JS_IDENT}Example)\\b`, "g"),
|
|
1521
|
-
key: "examples"
|
|
1522
|
-
},
|
|
1523
|
-
{
|
|
1524
|
-
pattern: new RegExp(`\\b(${JS_IDENT}Link)\\b`, "g"),
|
|
1525
|
-
key: "links"
|
|
1526
|
-
},
|
|
1527
|
-
{
|
|
1528
|
-
pattern: new RegExp(`\\b(${JS_IDENT}Callback)\\b`, "g"),
|
|
1529
|
-
key: "callbacks"
|
|
1530
|
-
},
|
|
1531
|
-
{
|
|
1532
|
-
pattern: new RegExp(`\\b(${JS_IDENT}MediaTypeSchema)\\b`, "g"),
|
|
1533
|
-
key: "mediaTypes"
|
|
1534
|
-
}
|
|
1535
|
-
];
|
|
1536
|
-
/** Pattern to find locally exported constants */
|
|
1537
|
-
const EXPORT_CONST_PATTERN = new RegExp(`export\\s+const\\s+(${JS_IDENT})\\s*=`, "g");
|
|
1538
|
-
function makeImports(code, fromFile, components, split = false) {
|
|
1539
|
-
const fallbackPrefix = split ? ".." : ".";
|
|
1540
|
-
const resolvePath = (key) => {
|
|
1541
|
-
const target = components?.[key];
|
|
1542
|
-
return target?.import ?? (target ? makeModuleSpec(fromFile, target) : `${fallbackPrefix}/${key}`);
|
|
1536
|
+
//#region src/helper/coerce.ts
|
|
1537
|
+
function coerce(baseSchema, schemaType, format) {
|
|
1538
|
+
const pipeCoerce = (coerceBase, typeBase, baseSchema) => {
|
|
1539
|
+
const afterType = baseSchema.slice(typeBase.length);
|
|
1540
|
+
const match = afterType.match(/^(?:\.(?:min|max|gt|lt|positive|negative|nonnegative|nonpositive|multipleOf)\([^)]*\))*/);
|
|
1541
|
+
const constraints = match ? match[0] : "";
|
|
1542
|
+
return `${coerceBase}.pipe(${typeBase}${constraints})${afterType.slice(constraints.length)}`;
|
|
1543
1543
|
};
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
""
|
|
1557
|
-
].filter(Boolean).join("\n");
|
|
1544
|
+
if (schemaType === "number") {
|
|
1545
|
+
if (format === "float" || format === "float32") return pipeCoerce("z.coerce.number()", "z.float32()", baseSchema);
|
|
1546
|
+
if (format === "float64") return pipeCoerce("z.coerce.number()", "z.float64()", baseSchema);
|
|
1547
|
+
return baseSchema.replace("z.number()", "z.coerce.number()");
|
|
1548
|
+
}
|
|
1549
|
+
if (schemaType === "integer") {
|
|
1550
|
+
if (format === "int32") return pipeCoerce("z.coerce.number()", "z.int32()", baseSchema);
|
|
1551
|
+
if (format === "int64") return pipeCoerce("z.coerce.bigint()", "z.int64()", baseSchema);
|
|
1552
|
+
if (format === "bigint") return baseSchema.replace("z.bigint()", "z.coerce.bigint()");
|
|
1553
|
+
return pipeCoerce("z.coerce.number()", "z.int()", baseSchema);
|
|
1554
|
+
}
|
|
1555
|
+
return baseSchema;
|
|
1558
1556
|
}
|
|
1559
1557
|
//#endregion
|
|
1560
1558
|
//#region src/generator/zod-to-openapi/type/index.ts
|
|
@@ -1852,15 +1850,8 @@ function makeSingleTypeString(schema, type, selfTypeName, cyclicGroup, readonly)
|
|
|
1852
1850
|
if (type === "object") return makeObjectTypeString(schema, selfTypeName, cyclicGroup, readonly);
|
|
1853
1851
|
return "unknown";
|
|
1854
1852
|
}
|
|
1855
|
-
/**
|
|
1856
|
-
* Wraps a type in parentheses if it needs grouping for array syntax.
|
|
1857
|
-
*
|
|
1858
|
-
* When an inner type starts with 'readonly ' (for readonly arrays),
|
|
1859
|
-
* we need to wrap it in parentheses to avoid invalid syntax like
|
|
1860
|
-
* `readonly readonly T[][]`. Instead, we get `readonly (readonly T[])[]`.
|
|
1861
|
-
*/
|
|
1862
|
-
const wrapForArrayElement = (type) => type.startsWith("readonly ") ? `(${type})` : type;
|
|
1863
1853
|
function makeArrayTypeString(schema, selfTypeName, cyclicGroup, readonly) {
|
|
1854
|
+
const wrapForArrayElement = (type) => type.startsWith("readonly ") ? `(${type})` : type;
|
|
1864
1855
|
const prefix = readonly ? "readonly " : "";
|
|
1865
1856
|
if (!schema.items) return `${prefix}unknown[]`;
|
|
1866
1857
|
const items = schema.items;
|
|
@@ -1964,7 +1955,7 @@ function parametersCode(components, exportParameters, exportParametersTypes, rea
|
|
|
1964
1955
|
const meta = { parameters: { ...parameter } };
|
|
1965
1956
|
const schema = parameter.schema ?? getSchemaFromContent(parameter.content);
|
|
1966
1957
|
const baseSchema = schema ? zodToOpenAPI(schema, meta) : "z.any()";
|
|
1967
|
-
const z = parameter.in === "query" && (schema?.type === "number" || schema?.type === "integer") ?
|
|
1958
|
+
const z = parameter.in === "query" && (schema?.type === "number" || schema?.type === "integer") ? coerce(baseSchema, schema.type, schema.format) : parameter.in === "query" && schema?.type === "boolean" ? baseSchema.replace("boolean", "stringbool") : parameter.in === "query" && schema?.type === "date" ? `z.coerce.${baseSchema.replace("z.", "")}` : parameter.in === "query" && (schema?.type === "object" || schema?.type === "array") ? baseSchema.replace(/z\.(int\d*)\(\)((?:\.(?:min|max|gt|lt|positive|negative|nonnegative|nonpositive|multipleOf)\([^)]*\))*)/g, (_, type, constraints) => `z.coerce.number().pipe(z.${type}()${constraints})`).replace(/z\.bigint\(\)/g, "z.coerce.bigint()").replace(/z\.number\(\)/g, "z.coerce.number()").replace(/z\.boolean\(\)/g, "z.stringbool()").replace(/z\.date\(\)/g, "z.coerce.date()") : baseSchema;
|
|
1968
1959
|
return zodToOpenAPISchema(toIdentifierPascalCase(ensureSuffix(k, "ParamsSchema")), z, exportParameters, exportParametersTypes, true, readonly);
|
|
1969
1960
|
}).join("\n\n");
|
|
1970
1961
|
}
|
|
@@ -2522,4 +2513,4 @@ function zodOpenAPIHono(openapi, options) {
|
|
|
2522
2513
|
].filter((s) => s.length > 0).join("\n\n")}`;
|
|
2523
2514
|
}
|
|
2524
2515
|
//#endregion
|
|
2525
|
-
export {
|
|
2516
|
+
export { makeConst as _, schemasCode as a, pathItemsCode as c, makeSplitSchemaFile as d, analyzeCircularSchemas as f, makeRef as g, makeCallback as h, componentsCode as i, parametersCode as l, zodToOpenAPI as m, webhookCode as n, responsesCode as o, ast as p, routeCode as r, requestBodiesCode as s, zodOpenAPIHono as t, headersCode as u, makeExportConst as v, makeImports as y };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { d as
|
|
3
|
-
import {
|
|
4
|
-
import { n as fmt, t as core } from "./core-
|
|
1
|
+
import { d as toIdentifierPascalCase, f as uncapitalize, i as makeBarrel, l as renderNamedImport, n as ensureSuffix, p as zodToOpenAPISchema, s as methodPath } from "./utils-BqOCY-3W.js";
|
|
2
|
+
import { _ as makeConst, a as schemasCode, c as pathItemsCode, d as makeSplitSchemaFile, f as analyzeCircularSchemas, g as makeRef, h as makeCallback, i as componentsCode, l as parametersCode, m as zodToOpenAPI, n as webhookCode, o as responsesCode, p as ast, r as routeCode, s as requestBodiesCode, t as zodOpenAPIHono, u as headersCode, v as makeExportConst, y as makeImports } from "./openapi-Bq_Z1BrA.js";
|
|
3
|
+
import { i as isMediaWithSchema, n as isHttpMethod, p as isRefObject, s as isOperation, t as isContentBody, v as isSecurityArray, y as isSecurityScheme } from "./guard-BzaK5_qz.js";
|
|
4
|
+
import { n as fmt, t as core } from "./core-CaXcQRYE.js";
|
|
5
5
|
import { a as writeFile, i as unlink, n as readFile, r as readdir, t as mkdir } from "./fsp-Bv1yR6UV.js";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Project } from "ts-morph";
|
|
@@ -1600,7 +1600,7 @@ const TEST_IMPORT_SOURCE = {
|
|
|
1600
1600
|
"vite-plus": "vite-plus/test",
|
|
1601
1601
|
bun: "bun:test"
|
|
1602
1602
|
};
|
|
1603
|
-
function makeTestFile(spec, appImportPath = "./app", basePath = "/",
|
|
1603
|
+
function makeTestFile(spec, appImportPath = "./app", basePath = "/", testFramework = "vitest") {
|
|
1604
1604
|
const testCases = extractTestCases(spec);
|
|
1605
1605
|
const apiTitle = spec.info?.title || "API";
|
|
1606
1606
|
const usedSchemaNames = new Set(testCases.flatMap((tc) => tc.usedSchemaRefs));
|
|
@@ -1616,7 +1616,7 @@ function makeTestFile(spec, appImportPath = "./app", basePath = "/", framework =
|
|
|
1616
1616
|
}).join("");
|
|
1617
1617
|
const body = `${mockFunctions ? `${mockFunctions}\n\n` : ""}describe('${escapeString(apiTitle)}',()=>{${tagDescribes}})\n`;
|
|
1618
1618
|
const fakerImport = body.includes("faker.") ? `\nimport{faker}from'@faker-js/faker'` : "";
|
|
1619
|
-
return `${`import{describe,it,expect}from'${TEST_IMPORT_SOURCE[
|
|
1619
|
+
return `${`import{describe,it,expect}from'${TEST_IMPORT_SOURCE[testFramework]}'${fakerImport}\nimport app from'${appImportPath}'\n`}\n${body}`;
|
|
1620
1620
|
}
|
|
1621
1621
|
/**
|
|
1622
1622
|
* Extract the first path segment from an API path.
|
|
@@ -1626,7 +1626,7 @@ function getPathFirstSegment(path) {
|
|
|
1626
1626
|
const sanitized = (path.replace(/^\/+/, "").split("/")[0] ?? "").replace(/\{([^}]+)\}/g, "$1").replace(/[^0-9A-Za-z._-]/g, "_").replace(/^[._-]+|[._-]+$/g, "").replace(/__+/g, "_").replace(/[-._](\w)/g, (_, c) => c.toUpperCase());
|
|
1627
1627
|
return sanitized === "" ? "__root" : sanitized;
|
|
1628
1628
|
}
|
|
1629
|
-
function makeHandlerTestCode(spec, handlerPath, _routeNames, importFrom, basePath = "/",
|
|
1629
|
+
function makeHandlerTestCode(spec, handlerPath, _routeNames, importFrom, basePath = "/", testFramework = "vitest") {
|
|
1630
1630
|
const handlerFileName = handlerPath.split("/").pop()?.replace(/\.ts$/, "") ?? "";
|
|
1631
1631
|
const relevantCases = extractTestCases(spec).filter((tc) => {
|
|
1632
1632
|
return getPathFirstSegment(tc.path) === handlerFileName;
|
|
@@ -1636,7 +1636,7 @@ function makeHandlerTestCode(spec, handlerPath, _routeNames, importFrom, basePat
|
|
|
1636
1636
|
const testCasesCode = relevantCases.map((tc) => makeTestCase(tc, basePath, spec.components?.schemas)).join("");
|
|
1637
1637
|
const body = `${mockFunctions ? `${mockFunctions}\n\n` : ""}describe('${handlerFileName.charAt(0).toUpperCase() + handlerFileName.slice(1)}',()=>{${testCasesCode}})\n`;
|
|
1638
1638
|
const fakerImport = body.includes("faker.") ? `\nimport{faker}from'@faker-js/faker'` : "";
|
|
1639
|
-
return `${`import{describe,it,expect}from'${TEST_IMPORT_SOURCE[
|
|
1639
|
+
return `${`import{describe,it,expect}from'${TEST_IMPORT_SOURCE[testFramework]}'${fakerImport}\nimport app from'${importFrom}'\n`}\n${body}`;
|
|
1640
1640
|
}
|
|
1641
1641
|
//#endregion
|
|
1642
1642
|
//#region src/merge/index.ts
|
|
@@ -1989,6 +1989,16 @@ function mergeTestFile(existingCode, generatedCode) {
|
|
|
1989
1989
|
const newBlocks = [...generatedBlocks.entries()].filter(([route]) => !existingRoutes.has(route)).map(([, block]) => block.text);
|
|
1990
1990
|
const { existingFile, generatedFile } = createSourcePair(existingCode, generatedCode);
|
|
1991
1991
|
const mergedImports = mergeImports(existingFile, generatedFile);
|
|
1992
|
+
const TEST_FRAMEWORK_MODULES = new Set([
|
|
1993
|
+
"vitest",
|
|
1994
|
+
"bun:test",
|
|
1995
|
+
"vite-plus/test"
|
|
1996
|
+
]);
|
|
1997
|
+
const generatedTestModule = generatedFile.getImportDeclarations().map((d) => d.getModuleSpecifierValue()).find((spec) => TEST_FRAMEWORK_MODULES.has(spec));
|
|
1998
|
+
const filteredImports = generatedTestModule ? mergedImports.filter((line) => {
|
|
1999
|
+
const spec = line.match(/from\s+'([^']+)'/)?.[1] ?? "";
|
|
2000
|
+
return !TEST_FRAMEWORK_MODULES.has(spec) || spec === generatedTestModule;
|
|
2001
|
+
}) : mergedImports;
|
|
1992
2002
|
const bodyStart = getBodyStart(existingFile);
|
|
1993
2003
|
const bodyWithStaleRemoved = applyRangeOps(existingCode, bodyStart, staleRanges.filter(([start]) => start >= bodyStart).map(([start, end]) => [
|
|
1994
2004
|
start,
|
|
@@ -1997,7 +2007,7 @@ function mergeTestFile(existingCode, generatedCode) {
|
|
|
1997
2007
|
])).replace(/\n{3,}/g, "\n\n");
|
|
1998
2008
|
const existingMocks = extractMockFunctions(existingCode);
|
|
1999
2009
|
const bodyWithRemovals = insertMissingMocks(bodyWithStaleRemoved, [...extractMockFunctions(generatedCode).entries()].filter(([name]) => !existingMocks.has(name)).map(([, block]) => block.text));
|
|
2000
|
-
if (newBlocks.length === 0) return `${[
|
|
2010
|
+
if (newBlocks.length === 0) return `${[filteredImports.length > 0 ? filteredImports.join("\n") : "", bodyWithRemovals.trim()].filter(Boolean).join("\n\n")}\n`;
|
|
2001
2011
|
const lines = bodyWithRemovals.split("\n");
|
|
2002
2012
|
const insertLineIndex = lines.findLastIndex((line) => /^\s*\}\s*\)\s*;?\s*$/.test(line));
|
|
2003
2013
|
const body = (insertLineIndex !== -1 ? [
|
|
@@ -2010,7 +2020,7 @@ function mergeTestFile(existingCode, generatedCode) {
|
|
|
2010
2020
|
"",
|
|
2011
2021
|
...newBlocks
|
|
2012
2022
|
]).join("\n").trim();
|
|
2013
|
-
return `${[
|
|
2023
|
+
return `${[filteredImports.length > 0 ? filteredImports.join("\n") : "", body].filter(Boolean).join("\n\n")}\n`;
|
|
2014
2024
|
}
|
|
2015
2025
|
/**
|
|
2016
2026
|
* Syncs barrel file (index.ts) with the generated version.
|
|
@@ -2153,7 +2163,7 @@ async function removeStaleFiles(handlerPath, generatedFileNames) {
|
|
|
2153
2163
|
* @param test - Whether to generate corresponding test files.
|
|
2154
2164
|
* @returns A `Result` indicating success or error with message.
|
|
2155
2165
|
*/
|
|
2156
|
-
async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias = void 0, routeImport = void 0, routeHandler = false, basePath = "/",
|
|
2166
|
+
async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias = void 0, routeImport = void 0, routeHandler = false, basePath = "/", testFramework = "vitest") {
|
|
2157
2167
|
const paths = openapi.paths;
|
|
2158
2168
|
const handlers = makeMergedHandlers(Object.entries(paths).flatMap(([path, pathItem]) => Object.entries(pathItem).filter((entry) => isHttpMethod(entry[0]) && isOperation(entry[1])).map(([method]) => routeHandler ? makeStubHandlerInfo(path, method) : makeInlineStubHandlerInfo(path, method))));
|
|
2159
2169
|
const { handlerPath, importFrom, testImportFrom } = makePaths(output, pathAlias, routeImport);
|
|
@@ -2182,7 +2192,7 @@ async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias =
|
|
|
2182
2192
|
error: writeResult.error
|
|
2183
2193
|
};
|
|
2184
2194
|
if (test) {
|
|
2185
|
-
const testContent = makeHandlerTestCode(openapi, `${handlerPath}/${handler.fileName}`, [...handler.routeNames], testImportFrom, basePath,
|
|
2195
|
+
const testContent = makeHandlerTestCode(openapi, `${handlerPath}/${handler.fileName}`, [...handler.routeNames], testImportFrom, basePath, testFramework);
|
|
2186
2196
|
if (testContent) {
|
|
2187
2197
|
const testFmtResult = await fmt(testContent);
|
|
2188
2198
|
const testCode = testFmtResult.ok ? testFmtResult.value : testContent;
|
|
@@ -2309,10 +2319,10 @@ function app(openapi, output, basePath, pathAlias, routeImport = void 0, routeHa
|
|
|
2309
2319
|
*
|
|
2310
2320
|
* Used by the CLI and vite-plugin when `template` is configured.
|
|
2311
2321
|
*/
|
|
2312
|
-
async function template(openAPI, output, test, basePath, pathAlias, routeImport, routeHandler,
|
|
2322
|
+
async function template(openAPI, output, test, basePath, pathAlias, routeImport, routeHandler, testFramework = "vitest") {
|
|
2313
2323
|
const dir = output.endsWith("/index.ts") ? path.dirname(path.dirname(output)) : path.dirname(output);
|
|
2314
2324
|
const target = path.join(dir, "index.ts");
|
|
2315
|
-
const [appFmtResult, stubHandlersResult] = await Promise.all([fmt(app(openAPI, output, basePath, pathAlias, routeImport, routeHandler)), zodOpenAPIHonoHandler(openAPI, output, test, pathAlias, routeImport, routeHandler, basePath,
|
|
2325
|
+
const [appFmtResult, stubHandlersResult] = await Promise.all([fmt(app(openAPI, output, basePath, pathAlias, routeImport, routeHandler)), zodOpenAPIHonoHandler(openAPI, output, test, pathAlias, routeImport, routeHandler, basePath, testFramework)]);
|
|
2316
2326
|
if (!appFmtResult.ok) return {
|
|
2317
2327
|
ok: false,
|
|
2318
2328
|
error: appFmtResult.error
|
|
@@ -2340,8 +2350,8 @@ async function template(openAPI, output, test, basePath, pathAlias, routeImport,
|
|
|
2340
2350
|
}
|
|
2341
2351
|
//#endregion
|
|
2342
2352
|
//#region src/core/test/index.ts
|
|
2343
|
-
async function test(openAPI, output, importPath, basePath = "/",
|
|
2344
|
-
const testCode = makeTestFile(openAPI, importPath, basePath,
|
|
2353
|
+
async function test(openAPI, output, importPath, basePath = "/", testFramework = "vitest") {
|
|
2354
|
+
const testCode = makeTestFile(openAPI, importPath, basePath, testFramework);
|
|
2345
2355
|
const [fmtResult, mkdirResult, existingResult] = await Promise.all([
|
|
2346
2356
|
fmt(testCode),
|
|
2347
2357
|
mkdir(path.dirname(output)),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as core } from "./core-
|
|
4
|
-
import { a as parsePathItem, i as operationHasArgs, n as hasNoContentResponse, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "./hono-rpc-
|
|
1
|
+
import { d as toIdentifierPascalCase, s as methodPath, t as capitalize } from "./utils-BqOCY-3W.js";
|
|
2
|
+
import { c as isOperationLike, f as isRecord, o as isOpenAPIPaths } from "./guard-BzaK5_qz.js";
|
|
3
|
+
import { t as core } from "./core-CaXcQRYE.js";
|
|
4
|
+
import { a as parsePathItem, i as operationHasArgs, n as hasNoContentResponse, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "./hono-rpc-CvPU8S0M.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
//#region src/helper/query.ts
|
|
7
7
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseConfig } from "../config/index.js";
|
|
2
|
-
import { f as isRecord } from "../guard-
|
|
3
|
-
import { r as setFormatOptions } from "../core-
|
|
4
|
-
import { _ as examples, a as takibi, c as securitySchemes, d as requestBodies, f as pathItems, g as headers, h as links, i as template, l as schemas, m as mediaTypes, n as webhooks, o as route, p as parameters, r as test, s as mock, t as parseOpenAPI, u as responses, v as callbacks } from "../openapi-
|
|
5
|
-
import { t as docs } from "../docs-
|
|
2
|
+
import { f as isRecord } from "../guard-BzaK5_qz.js";
|
|
3
|
+
import { r as setFormatOptions } from "../core-CaXcQRYE.js";
|
|
4
|
+
import { _ as examples, a as takibi, c as securitySchemes, d as requestBodies, f as pathItems, g as headers, h as links, i as template, l as schemas, m as mediaTypes, n as webhooks, o as route, p as parameters, r as test, s as mock, t as parseOpenAPI, u as responses, v as callbacks } from "../openapi-CSP6nmmf.js";
|
|
5
|
+
import { t as docs } from "../docs-BnLvHynR.js";
|
|
6
6
|
import { rpc } from "../core/rpc/index.js";
|
|
7
7
|
import { svelteQuery } from "../core/svelte-query/index.js";
|
|
8
8
|
import { swr } from "../core/swr/index.js";
|
|
@@ -13,13 +13,6 @@ import path from "node:path";
|
|
|
13
13
|
import fsp from "node:fs/promises";
|
|
14
14
|
//#region src/vite-plugin/index.ts
|
|
15
15
|
/**
|
|
16
|
-
* Type guard for configuration objects.
|
|
17
|
-
*
|
|
18
|
-
* @param value - Value to check
|
|
19
|
-
* @returns True if value is a valid configuration object
|
|
20
|
-
*/
|
|
21
|
-
const isConfiguration = (value) => typeof value === "object" && value !== null;
|
|
22
|
-
/**
|
|
23
16
|
* Converts a relative path to an absolute path.
|
|
24
17
|
*
|
|
25
18
|
* @param relativePath - Relative path
|
|
@@ -52,7 +45,7 @@ const readConfigurationWithHotReload = async (server) => {
|
|
|
52
45
|
} else server.moduleGraph.invalidateAll();
|
|
53
46
|
const loadedModule = await server.ssrLoadModule(`${absoluteConfigPath}?t=${String(Date.now())}`);
|
|
54
47
|
const defaultExport = isRecord(loadedModule) ? Reflect.get(loadedModule, "default") : void 0;
|
|
55
|
-
if (!
|
|
48
|
+
if (!(typeof defaultExport === "object" && defaultExport !== null)) return {
|
|
56
49
|
ok: false,
|
|
57
50
|
error: "Config must export default object"
|
|
58
51
|
};
|
|
@@ -72,20 +65,6 @@ const readConfigurationWithHotReload = async (server) => {
|
|
|
72
65
|
}
|
|
73
66
|
};
|
|
74
67
|
/**
|
|
75
|
-
* Lists TypeScript files in a directory (shallow, non-recursive).
|
|
76
|
-
*
|
|
77
|
-
* @param directoryPath - Directory path to scan
|
|
78
|
-
* @returns Promise resolving to array of absolute file paths
|
|
79
|
-
*/
|
|
80
|
-
const listTypeScriptFilesShallow = async (directoryPath) => fsp.stat(directoryPath).then((fileStats) => fileStats.isDirectory() ? fsp.readdir(directoryPath, { withFileTypes: true }).then((directoryEntries) => directoryEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => path.join(directoryPath, entry.name))) : []).catch(() => []);
|
|
81
|
-
/**
|
|
82
|
-
* Deletes specified TypeScript files.
|
|
83
|
-
*
|
|
84
|
-
* @param filePaths - Array of file paths to delete
|
|
85
|
-
* @returns Promise resolving to array of deleted file paths
|
|
86
|
-
*/
|
|
87
|
-
const deleteTypeScriptFiles = async (filePaths) => Promise.all(filePaths.map((filePath) => fsp.unlink(filePath).then(() => filePath).catch(() => null))).then((results) => results.filter((result) => result !== null));
|
|
88
|
-
/**
|
|
89
68
|
* Creates a debounced version of a function.
|
|
90
69
|
*
|
|
91
70
|
* Delays invocation until after the specified milliseconds have elapsed
|
|
@@ -120,7 +99,11 @@ const runAllGenerationTasks = async (config) => {
|
|
|
120
99
|
*/
|
|
121
100
|
const runSplitAwareJob = async (name, output, isSplit, generate) => {
|
|
122
101
|
const absOutput = toAbsolutePath(output);
|
|
123
|
-
if (isSplit)
|
|
102
|
+
if (isSplit) {
|
|
103
|
+
const listTypeScriptFilesShallow = async (directoryPath) => fsp.stat(directoryPath).then((fileStats) => fileStats.isDirectory() ? fsp.readdir(directoryPath, { withFileTypes: true }).then((directoryEntries) => directoryEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => path.join(directoryPath, entry.name))) : []).catch(() => []);
|
|
104
|
+
const deleteTypeScriptFiles = async (filePaths) => Promise.all(filePaths.map((filePath) => fsp.unlink(filePath).then(() => filePath).catch(() => null))).then((results) => results.filter((result) => result !== null));
|
|
105
|
+
await deleteTypeScriptFiles(await listTypeScriptFilesShallow(absOutput));
|
|
106
|
+
}
|
|
124
107
|
const result = await generate(absOutput);
|
|
125
108
|
if (!result.ok) return `❌ ${name}: ${result.error}`;
|
|
126
109
|
return `✅ ${name}${isSplit ? "(split)" : ""} -> ${absOutput}`;
|
|
@@ -233,7 +216,7 @@ const runAllGenerationTasks = async (config) => {
|
|
|
233
216
|
if (!config.test) return void 0;
|
|
234
217
|
return (async () => {
|
|
235
218
|
const outputPath = toAbsolutePath(config.test?.output ?? "");
|
|
236
|
-
const result = await test(openAPI, outputPath, config.test?.import ?? "", config.basePath ?? "/", config.test?.
|
|
219
|
+
const result = await test(openAPI, outputPath, config.test?.import ?? "", config.basePath ?? "/", config.test?.testFramework);
|
|
237
220
|
return result.ok ? `✅ test -> ${outputPath}` : `❌ test: ${result.error}`;
|
|
238
221
|
})();
|
|
239
222
|
};
|
|
@@ -261,7 +244,7 @@ const runAllGenerationTasks = async (config) => {
|
|
|
261
244
|
return (async () => {
|
|
262
245
|
const absPath = toAbsolutePath(routeOutputPath);
|
|
263
246
|
if (!isTypeScriptFile(absPath)) return `❌ template: Invalid output format: ${absPath}`;
|
|
264
|
-
const result = await template(openAPI, absPath, tmpl.test, config.basePath ?? "/", tmpl.pathAlias, config["zod-openapi"]?.routes?.import, tmpl.routeHandler, tmpl.
|
|
247
|
+
const result = await template(openAPI, absPath, tmpl.test, config.basePath ?? "/", tmpl.pathAlias, config["zod-openapi"]?.routes?.import, tmpl.routeHandler, tmpl.testFramework);
|
|
265
248
|
return result.ok ? `✅ template -> ${absPath}` : `❌ template: ${result.error}`;
|
|
266
249
|
})();
|
|
267
250
|
};
|
|
@@ -294,17 +277,6 @@ const runAllGenerationTasks = async (config) => {
|
|
|
294
277
|
return Promise.all(generationJobs).then((logs) => ({ logs }));
|
|
295
278
|
};
|
|
296
279
|
/**
|
|
297
|
-
* Checks if a file path matches input file patterns.
|
|
298
|
-
*
|
|
299
|
-
* @param filePath - Absolute path to check
|
|
300
|
-
* @param inputDirectory - Directory containing input files
|
|
301
|
-
* @returns True if the file is an input file (yaml/json/tsp)
|
|
302
|
-
*/
|
|
303
|
-
const isInputFile = (filePath, inputDirectory) => {
|
|
304
|
-
if (!filePath.startsWith(inputDirectory)) return false;
|
|
305
|
-
return filePath.endsWith(".yaml") || filePath.endsWith(".json") || filePath.endsWith(".tsp");
|
|
306
|
-
};
|
|
307
|
-
/**
|
|
308
280
|
* Adds glob patterns to the Vite file watcher.
|
|
309
281
|
*
|
|
310
282
|
* Watches the input file and related files (.yaml, .json, .tsp) in the
|
|
@@ -376,37 +348,6 @@ const extractOutputPaths = (config) => {
|
|
|
376
348
|
config.docs?.output
|
|
377
349
|
].filter((outputPath) => outputPath !== void 0).map(toAbsolutePath);
|
|
378
350
|
};
|
|
379
|
-
/**
|
|
380
|
-
* Cleans up output paths that exist in previous config but not in current config.
|
|
381
|
-
*
|
|
382
|
-
* When an output path is removed from the configuration, this function
|
|
383
|
-
* removes the corresponding file or directory completely.
|
|
384
|
-
*
|
|
385
|
-
* @param previousConfiguration - Previous configuration
|
|
386
|
-
* @param currentConfiguration - Current configuration
|
|
387
|
-
* @returns Array of cleaned up paths
|
|
388
|
-
*/
|
|
389
|
-
const cleanupStaleOutputs = async (previousConfiguration, currentConfiguration) => {
|
|
390
|
-
const previousPaths = new Set(extractOutputPaths(previousConfiguration));
|
|
391
|
-
const currentPaths = new Set(extractOutputPaths(currentConfiguration));
|
|
392
|
-
const stalePaths = [...previousPaths].filter((stalePath) => !currentPaths.has(stalePath));
|
|
393
|
-
return (await Promise.all(stalePaths.map(async (stalePath) => {
|
|
394
|
-
const fileStats = await fsp.stat(stalePath).catch(() => null);
|
|
395
|
-
if (!fileStats) return null;
|
|
396
|
-
if (fileStats.isDirectory()) {
|
|
397
|
-
await fsp.rm(stalePath, {
|
|
398
|
-
recursive: true,
|
|
399
|
-
force: true
|
|
400
|
-
}).catch(() => {});
|
|
401
|
-
return stalePath;
|
|
402
|
-
}
|
|
403
|
-
if (fileStats.isFile() && (stalePath.endsWith(".ts") || stalePath.endsWith(".md"))) {
|
|
404
|
-
await fsp.unlink(stalePath).catch(() => {});
|
|
405
|
-
return stalePath;
|
|
406
|
-
}
|
|
407
|
-
return null;
|
|
408
|
-
}))).filter((result) => result !== null);
|
|
409
|
-
};
|
|
410
351
|
function honoTakibiVite() {
|
|
411
352
|
const pluginState = {
|
|
412
353
|
current: null,
|
|
@@ -431,6 +372,27 @@ function honoTakibiVite() {
|
|
|
431
372
|
return;
|
|
432
373
|
}
|
|
433
374
|
if (pluginState.current) {
|
|
375
|
+
const cleanupStaleOutputs = async (previousConfiguration, currentConfiguration) => {
|
|
376
|
+
const previousPaths = new Set(extractOutputPaths(previousConfiguration));
|
|
377
|
+
const currentPaths = new Set(extractOutputPaths(currentConfiguration));
|
|
378
|
+
const stalePaths = [...previousPaths].filter((stalePath) => !currentPaths.has(stalePath));
|
|
379
|
+
return (await Promise.all(stalePaths.map(async (stalePath) => {
|
|
380
|
+
const fileStats = await fsp.stat(stalePath).catch(() => null);
|
|
381
|
+
if (!fileStats) return null;
|
|
382
|
+
if (fileStats.isDirectory()) {
|
|
383
|
+
await fsp.rm(stalePath, {
|
|
384
|
+
recursive: true,
|
|
385
|
+
force: true
|
|
386
|
+
}).catch(() => {});
|
|
387
|
+
return stalePath;
|
|
388
|
+
}
|
|
389
|
+
if (fileStats.isFile() && (stalePath.endsWith(".ts") || stalePath.endsWith(".md"))) {
|
|
390
|
+
await fsp.unlink(stalePath).catch(() => {});
|
|
391
|
+
return stalePath;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}))).filter((result) => result !== null);
|
|
395
|
+
};
|
|
434
396
|
const cleanedPaths = await cleanupStaleOutputs(pluginState.current, nextConfiguration.value);
|
|
435
397
|
for (const cleanedPath of cleanedPaths) console.log(`✅ cleanup: ${cleanedPath}`);
|
|
436
398
|
}
|
|
@@ -467,7 +429,7 @@ function honoTakibiVite() {
|
|
|
467
429
|
await handleConfigurationChange(server);
|
|
468
430
|
return;
|
|
469
431
|
}
|
|
470
|
-
if (pluginState.inputDirectory &&
|
|
432
|
+
if (pluginState.inputDirectory && absoluteChangedPath.startsWith(pluginState.inputDirectory) && (absoluteChangedPath.endsWith(".yaml") || absoluteChangedPath.endsWith(".json") || absoluteChangedPath.endsWith(".tsp"))) debouncedRunGeneration();
|
|
471
433
|
});
|
|
472
434
|
await runGenerationAndReload(server);
|
|
473
435
|
})().catch((error) => console.error("❌ watch error:", error));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-takibi",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.996",
|
|
4
4
|
"description": "Hono Takibi is a code generator from OpenAPI to @hono/zod-openapi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hono",
|
|
@@ -72,18 +72,18 @@
|
|
|
72
72
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
73
73
|
"@typespec/compiler": "^1.10.0",
|
|
74
74
|
"@typespec/openapi3": "^1.10.0",
|
|
75
|
-
"oxfmt": "^0.
|
|
75
|
+
"oxfmt": "^0.43.0",
|
|
76
76
|
"ts-morph": "^27.0.2",
|
|
77
77
|
"tsx": "^4.21.0",
|
|
78
78
|
"typescript": "^6.0.2",
|
|
79
79
|
"zod": "^4.3.6"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@types/node": "^25.5.
|
|
82
|
+
"@types/node": "^25.5.2",
|
|
83
83
|
"@typespec/http": "^1.10.0",
|
|
84
84
|
"@typespec/rest": "^0.80.0",
|
|
85
85
|
"@typespec/versioning": "^0.80.0",
|
|
86
|
-
"tsdown": "0.21.
|
|
86
|
+
"tsdown": "0.21.7"
|
|
87
87
|
},
|
|
88
88
|
"scripts": {
|
|
89
89
|
"dev": "vite --host",
|
|
File without changes
|
|
File without changes
|