hono-takibi 0.8.8 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,7 +146,7 @@ If you use a `.tsp` TypeSpec file, you must set up the TypeSpec environment and
146
146
 
147
147
  ```ts
148
148
  import { defineConfig } from 'vite'
149
- import HonoTakibiVite from './src/vite-plugin'
149
+ import HonoTakibiVite from 'hono-takibi/vite-plugin'
150
150
 
151
151
  export default defineConfig({
152
152
  plugins: [
package/dist/cli/index.js CHANGED
@@ -1,5 +1,9 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { config } from '../config/index.js';
4
+ import { rpc } from '../core/rpc.js';
5
+ import { takibi } from '../core/takibi.js';
1
6
  import { parseCli } from '../utils/index.js';
2
- import { takibi } from './takibi.js';
3
7
  const HELP_TEXT = `Usage: hono-takibi <input.{yaml,json,tsp}> -o <routes.ts> [options]
4
8
 
5
9
  Options:
@@ -47,23 +51,48 @@ export async function honoTakibi() {
47
51
  const isHelpRequested = (args) => {
48
52
  return args.length === 1 && (args[0] === '--help' || args[0] === '-h');
49
53
  };
54
+ /** help */
50
55
  if (isHelpRequested(args)) {
51
56
  return {
52
57
  ok: true,
53
58
  value: HELP_TEXT,
54
59
  };
55
60
  }
56
- const cliResult = parseCli(args);
57
- if (!cliResult.ok) {
58
- return { ok: false, error: cliResult.error };
61
+ const abs = resolve(process.cwd(), 'hono-takibi.config.ts');
62
+ if (!existsSync(abs)) {
63
+ const cliResult = parseCli(args);
64
+ if (!cliResult.ok) {
65
+ return { ok: false, error: cliResult.error };
66
+ }
67
+ const cli = cliResult.value;
68
+ const takibiResult = await takibi(cli.input, cli.output, cli.exportSchema ?? false, cli.exportType ?? false, cli.template ?? false, cli.test ?? false, cli.basePath);
69
+ if (!takibiResult.ok) {
70
+ return { ok: false, error: takibiResult.error };
71
+ }
72
+ return {
73
+ ok: true,
74
+ value: takibiResult.value,
75
+ };
59
76
  }
60
- const cli = cliResult.value;
61
- const takibiResult = await takibi(cli.input, cli.output, cli.exportSchema ?? false, cli.exportType ?? false, cli.template ?? false, cli.test ?? false, cli.basePath);
62
- if (!takibiResult.ok) {
77
+ const configResult = await config();
78
+ if (!configResult.ok) {
79
+ return { ok: false, error: configResult.error };
80
+ }
81
+ const c = configResult.value;
82
+ const takibiResult = c['hono-takibi']
83
+ ? await takibi(c['hono-takibi']?.input, c['hono-takibi']?.output, c['hono-takibi']?.exportSchema ?? false, c['hono-takibi']?.exportType ?? false, false, // template
84
+ false)
85
+ : undefined;
86
+ if (takibiResult && !takibiResult.ok) {
63
87
  return { ok: false, error: takibiResult.error };
64
88
  }
89
+ const rpcResult = c.rpc ? await rpc(c.rpc.input, c.rpc.output, c.rpc.import) : undefined;
90
+ if (rpcResult && !rpcResult.ok) {
91
+ return { ok: false, error: rpcResult.error };
92
+ }
93
+ const results = [takibiResult?.value, rpcResult?.value].filter((v) => Boolean(v));
65
94
  return {
66
95
  ok: true,
67
- value: takibiResult.value,
96
+ value: results.join('\n'),
68
97
  };
69
98
  }
@@ -0,0 +1,22 @@
1
+ type Config = {
2
+ 'hono-takibi'?: {
3
+ input: `${string}.yaml` | `${string}.json` | `${string}.tsp`;
4
+ output: `${string}.ts`;
5
+ exportType?: boolean;
6
+ exportSchema?: boolean;
7
+ };
8
+ rpc?: {
9
+ input: `${string}.yaml` | `${string}.json` | `${string}.tsp`;
10
+ output: `${string}.ts`;
11
+ import: string;
12
+ };
13
+ };
14
+ export declare function config(): Promise<{
15
+ ok: true;
16
+ value: Config;
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ }>;
21
+ export declare function defineConfig(config: Config): Config;
22
+ export {};
@@ -0,0 +1,23 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { register } from 'tsx/esm/api';
4
+ export async function config() {
5
+ const abs = resolve(process.cwd(), 'hono-takibi.config.ts');
6
+ // if (!existsSync(abs)) {
7
+ // return { ok: false, error: `Config not found: ${abs}` }
8
+ // }
9
+ register();
10
+ try {
11
+ const mod = await import(pathToFileURL(abs).href);
12
+ if (!('default' in mod)) {
13
+ return { ok: false, error: 'Config must export default object' };
14
+ }
15
+ return { ok: true, value: mod.default };
16
+ }
17
+ catch (e) {
18
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
19
+ }
20
+ }
21
+ export function defineConfig(config) {
22
+ return config;
23
+ }
@@ -0,0 +1,7 @@
1
+ export declare function rpc(input: `${string}.yaml` | `${string}.json` | `${string}.tsp`, output: `${string}.ts`, importCode: string): Promise<{
2
+ ok: true;
3
+ value: string;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ }>;
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+ import { fmt } from '../format/index.js';
3
+ import { mkdir, writeFile } from '../fsp/index.js';
4
+ import { honoRpc } from '../generator/rpc/index.js';
5
+ import { parseOpenAPI } from '../openapi/index.js';
6
+ export async function rpc(input, output, importCode) {
7
+ const openAPIResult = await parseOpenAPI(input);
8
+ if (!openAPIResult.ok) {
9
+ return { ok: false, error: openAPIResult.error };
10
+ }
11
+ const openAPI = openAPIResult.value;
12
+ const honoRpcResult = await fmt(honoRpc(openAPI, importCode));
13
+ if (!honoRpcResult.ok) {
14
+ return { ok: false, error: honoRpcResult.error };
15
+ }
16
+ const mkdirResult = await mkdir(path.dirname(output));
17
+ if (!mkdirResult.ok) {
18
+ return { ok: false, error: mkdirResult.error };
19
+ }
20
+ const writeResult = await writeFile(output, honoRpcResult.value);
21
+ if (!writeResult.ok) {
22
+ return { ok: false, error: writeResult.error };
23
+ }
24
+ return {
25
+ ok: true,
26
+ value: `Generated RPC code written to ${output}`,
27
+ };
28
+ }
@@ -2,9 +2,9 @@ import path from 'node:path';
2
2
  import { fmt } from '../format/index.js';
3
3
  import { mkdir, readdir, writeFile } from '../fsp/index.js';
4
4
  import { app } from '../generator/zod-openapi-hono/app/index.js';
5
- import { zodOpenapiHonoHandler } from '../generator/zod-openapi-hono/handler/zod-openapi-hono-handler.js';
6
5
  import zodOpenAPIHono from '../generator/zod-openapi-hono/openapi/index.js';
7
6
  import { parseOpenAPI } from '../openapi/index.js';
7
+ import { groupHandlersByFileName, methodPath } from '../utils/index.js';
8
8
  /**
9
9
  * Generates TypeScript code from an OpenAPI spec and optional templates.
10
10
  *
@@ -70,6 +70,7 @@ export async function takibi(input, output, exportSchema, exportType, template,
70
70
  if (!writeResult.ok) {
71
71
  return { ok: false, error: writeResult.error };
72
72
  }
73
+ /** template */
73
74
  if (template && output.includes('/')) {
74
75
  const appResult = await fmt(app(openAPI, output, basePath));
75
76
  if (!appResult.ok) {
@@ -97,3 +98,65 @@ export async function takibi(input, output, exportSchema, exportType, template,
97
98
  value: `Generated code written to ${output}`,
98
99
  };
99
100
  }
101
+ /**
102
+ * Generates route handler files for a Hono app using Zod and OpenAPI.
103
+ *
104
+ * @param openapi - The OpenAPI specification object.
105
+ * @param output - The output directory or file path for generated handlers.
106
+ * @param test - Whether to generate corresponding empty test files.
107
+ * @returns A `Result` indicating success or error with message.
108
+ */
109
+ async function zodOpenapiHonoHandler(openapi, output, test) {
110
+ const paths = openapi.paths;
111
+ const handlers = [];
112
+ for (const [path, pathItem] of Object.entries(paths)) {
113
+ for (const [method] of Object.entries(pathItem)) {
114
+ const routeHandlerContent = `export const ${methodPath(method, path)}RouteHandler:RouteHandler<typeof ${methodPath(method, path)}>=async(c)=>{}`;
115
+ const rawSegment = path.replace(/^\/+/, '').split('/')[0] ?? '';
116
+ const pathName = (rawSegment === '' ? 'index' : rawSegment)
117
+ .replace(/\{([^}]+)\}/g, '$1')
118
+ .replace(/[^0-9A-Za-z._-]/g, '_')
119
+ .replace(/^[._-]+|[._-]+$/g, '')
120
+ .replace(/__+/g, '_')
121
+ .replace(/[-._](\w)/g, (_, c) => c.toUpperCase());
122
+ const fileName = pathName.length === 0 ? 'indexHandler.ts' : `${pathName}Handler.ts`;
123
+ const testFileName = pathName.length === 0 ? 'indexHandler.test.ts' : `${pathName}Handler.test.ts`;
124
+ handlers.push({
125
+ fileName,
126
+ testFileName,
127
+ routeHandlerContents: [routeHandlerContent],
128
+ routeNames: [`${methodPath(method, path)}Route`],
129
+ });
130
+ }
131
+ }
132
+ const mergedHandlers = groupHandlersByFileName(handlers);
133
+ for (const handler of mergedHandlers) {
134
+ const dirPath = output?.replace(/\/[^/]+\.ts$/, '');
135
+ const handlerPath = dirPath === 'index.ts' ? 'handlers' : `${dirPath}/handlers`;
136
+ const mkdirResult = await mkdir(handlerPath);
137
+ if (!mkdirResult.ok) {
138
+ return { ok: false, error: mkdirResult.error };
139
+ }
140
+ const routeTypes = handler.routeNames.map((routeName) => `${routeName}`).join(', ');
141
+ const match = output?.match(/[^/]+\.ts$/);
142
+ const matchPath = match ? match[0] : '';
143
+ const path = output === '.' || output === './' ? output : `../${matchPath}`;
144
+ const importRouteTypes = routeTypes ? `import type { ${routeTypes} } from '${path}';` : '';
145
+ const importStatements = `import type { RouteHandler } from '@hono/zod-openapi'\n${importRouteTypes}`;
146
+ const fileContent = `${importStatements}\n\n${handler.routeHandlerContents.join('\n\n')}`;
147
+ const formatCode = await fmt(fileContent);
148
+ if (!formatCode.ok) {
149
+ return { ok: false, error: formatCode.error };
150
+ }
151
+ const writeResult = await writeFile(`${handlerPath}/${handler.fileName}`, formatCode.value);
152
+ if (!writeResult.ok)
153
+ writeResult;
154
+ if (test) {
155
+ const writeResult = await writeFile(`${handlerPath}/${handler.testFileName}`, '');
156
+ if (!writeResult.ok) {
157
+ return { ok: false, error: writeResult.error };
158
+ }
159
+ }
160
+ }
161
+ return { ok: true, value: undefined };
162
+ }
@@ -7,13 +7,13 @@ import { format } from 'prettier';
7
7
  */
8
8
  export async function fmt(code) {
9
9
  try {
10
- const formatted = await format(code, {
10
+ const result = await format(code, {
11
11
  parser: 'typescript',
12
12
  printWidth: 100,
13
13
  singleQuote: true,
14
14
  semi: false,
15
15
  });
16
- return { ok: true, value: formatted };
16
+ return { ok: true, value: result };
17
17
  }
18
18
  catch (e) {
19
19
  return {
package/dist/fsp/index.js CHANGED
@@ -29,7 +29,6 @@ export async function mkdir(dir) {
29
29
  export async function readdir(dir) {
30
30
  try {
31
31
  const files = await fsp.readdir(dir);
32
- // return ok(files)
33
32
  return { ok: true, value: files };
34
33
  }
35
34
  catch (e) {
@@ -0,0 +1,2 @@
1
+ import type { OpenAPI } from '../../openapi/index.js';
2
+ export declare function honoRpc(openapi: OpenAPI, importCode: string): string;
@@ -0,0 +1,338 @@
1
+ import { methodPath } from '../../utils/index.js';
2
+ /* ─────────────────────────────── Guards ─────────────────────────────── */
3
+ /** Narrow to generic object records */
4
+ const isRecord = (v) => typeof v === 'object' && v !== null;
5
+ /** Narrow to OpenAPI paths object (shallow structural check) */
6
+ const isOpenAPIPaths = (v) => {
7
+ if (!isRecord(v))
8
+ return false;
9
+ for (const k in v)
10
+ if (!isRecord(v[k]))
11
+ return false;
12
+ return true;
13
+ };
14
+ /** Treat any object as Schema (we rely on downstream field checks) */
15
+ const isSchema = (v) => isRecord(v);
16
+ /* ─────────────────────────────── Formatters ─────────────────────────────── */
17
+ /** Uppercase the first character */
18
+ const upperHead = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
19
+ /** JS identifier check */
20
+ const isValidIdent = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
21
+ /** Escape single quotes and backslashes for single-quoted strings */
22
+ const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
23
+ /**
24
+ * Convert an OpenAPI path to a client access chain.
25
+ * examples:
26
+ * '/' -> ".index"
27
+ * '/hono-x' -> "['hono-x']"
28
+ * '/posts/hono/{id}' -> ".posts.hono[':id']"
29
+ */
30
+ const formatPath = (path) => {
31
+ const segs = (path === '/' ? ['index'] : path.replace(/^\/+/, '').split('/')).filter(Boolean);
32
+ return segs
33
+ .map((seg) => seg.startsWith('{') && seg.endsWith('}')
34
+ ? `[':${seg.slice(1, -1)}']`
35
+ : isValidIdent(seg)
36
+ ? `.${seg}`
37
+ : `['${esc(seg)}']`)
38
+ .join('');
39
+ };
40
+ /** 'type' to normalized list for uniform checks */
41
+ const isJSONTypeName = (s) => typeof s === 'string' &&
42
+ (s === 'object' ||
43
+ s === 'array' ||
44
+ s === 'string' ||
45
+ s === 'number' ||
46
+ s === 'integer' ||
47
+ s === 'boolean' ||
48
+ s === 'null');
49
+ const toTypeArray = (t) => {
50
+ if (isJSONTypeName(t))
51
+ return [t];
52
+ if (Array.isArray(t))
53
+ return t.filter(isJSONTypeName);
54
+ return [];
55
+ };
56
+ /** Build literal union from enum values (kept compact) */
57
+ const literalFromEnum = (vals) => {
58
+ const toLit = (v) => typeof v === 'string'
59
+ ? `'${v.replace(/'/g, "\\'")}'`
60
+ : typeof v === 'number' || typeof v === 'boolean'
61
+ ? String(v)
62
+ : v === null
63
+ ? 'null'
64
+ : 'unknown';
65
+ return vals.map(toLit).join('|');
66
+ };
67
+ /** Create a $ref resolver for #/components/schemas/... */
68
+ const createResolveRef = (schemas) => (ref) => {
69
+ if (!ref)
70
+ return undefined;
71
+ const m = ref.match(/^#\/components\/schemas\/(.+)$/);
72
+ if (!m)
73
+ return undefined;
74
+ const target = schemas[m[1]];
75
+ return isSchema(target) ? target : undefined;
76
+ };
77
+ /** Create a Schema->TypeScript type printer (single instance, recursive-safe) */
78
+ const createTsTypeFromSchema = (resolveRef) => {
79
+ const tt = (schema, seen = new Set()) => {
80
+ if (!schema)
81
+ return 'unknown';
82
+ // $ref resolution
83
+ if (schema.$ref) {
84
+ const tgt = resolveRef(schema.$ref);
85
+ return tt(tgt, seen);
86
+ }
87
+ // recursion guard
88
+ if (seen.has(schema))
89
+ return 'unknown';
90
+ const nextSeen = new Set(seen);
91
+ nextSeen.add(schema);
92
+ // combinators
93
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length)
94
+ return schema.oneOf.map((s) => tt(s, nextSeen)).join('|') || 'unknown';
95
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length)
96
+ return schema.anyOf.map((s) => tt(s, nextSeen)).join('|') || 'unknown';
97
+ if (Array.isArray(schema.allOf) && schema.allOf.length)
98
+ return schema.allOf.map((s) => tt(s, nextSeen)).join('&') || 'unknown';
99
+ // enum
100
+ if (Array.isArray(schema.enum) && schema.enum.length) {
101
+ const base = literalFromEnum(schema.enum);
102
+ return schema.nullable ? `${base}|null` : base;
103
+ }
104
+ const types = toTypeArray(schema.type);
105
+ // array
106
+ if (types.includes('array')) {
107
+ const inner = tt(isSchema(schema.items) ? schema.items : undefined, nextSeen);
108
+ const core = `${inner}[]`;
109
+ return schema.nullable ? `${core}|null` : core;
110
+ }
111
+ // object
112
+ if (types.includes('object')) {
113
+ const req = new Set(Array.isArray(schema.required) ? schema.required : []);
114
+ const props = schema.properties ?? {};
115
+ const fields = Object.entries(props).map(([k, v]) => {
116
+ const opt = req.has(k) ? '' : '?';
117
+ const child = isSchema(v) ? v : undefined;
118
+ return `${k}${opt}:${tt(child, nextSeen)}`;
119
+ });
120
+ const ap = schema.additionalProperties;
121
+ const addl = ap === true
122
+ ? '[key:string]:unknown'
123
+ : isSchema(ap)
124
+ ? `[key:string]:${tt(ap, nextSeen)}`
125
+ : '';
126
+ const members = [...fields, addl].filter(Boolean).join(',');
127
+ const core = `{${members}}`;
128
+ return schema.nullable ? `${core}|null` : core;
129
+ }
130
+ // primitives
131
+ if (types.length === 0)
132
+ return schema.nullable ? 'unknown|null' : 'unknown';
133
+ const prim = types
134
+ .map((t) => (t === 'integer' ? 'number' : t === 'null' ? 'null' : t))
135
+ .join('|');
136
+ return schema.nullable ? `${prim}|null` : prim;
137
+ };
138
+ return tt;
139
+ };
140
+ const isRefObject = (v) => isRecord(v) && typeof v.$ref === 'string';
141
+ const isParameterObject = (v) => {
142
+ if (!isRecord(v))
143
+ return false;
144
+ if (typeof v.name !== 'string')
145
+ return false;
146
+ const pos = v.in;
147
+ return pos === 'path' || pos === 'query' || pos === 'header' || pos === 'cookie';
148
+ };
149
+ /** Extract components/parameters name from a ref-like value */
150
+ const refParamName = (refLike) => {
151
+ const ref = typeof refLike === 'string' ? refLike : isRefObject(refLike) ? refLike.$ref : undefined;
152
+ const m = ref?.match(/^#\/components\/parameters\/(.+)$/);
153
+ return m ? m[1] : undefined;
154
+ };
155
+ /** Build a resolver that returns normalized ParameterLike (resolving $ref) */
156
+ const createResolveParameter = (componentsParameters) => (p) => {
157
+ if (isParameterObject(p))
158
+ return p;
159
+ const name = refParamName(p);
160
+ const cand = name ? componentsParameters[name] : undefined;
161
+ return isParameterObject(cand) ? cand : undefined;
162
+ };
163
+ /** Convert raw parameters array into ParameterLike[] */
164
+ const createToParameterLikes = (resolveParam) => (arr) => Array.isArray(arr)
165
+ ? arr.reduce((acc, x) => {
166
+ const r = resolveParam(x);
167
+ if (r)
168
+ acc.push(r);
169
+ return acc;
170
+ }, [])
171
+ : [];
172
+ const isOperationLike = (v) => isRecord(v) && 'responses' in v;
173
+ const HTTP_METHODS = [
174
+ 'get',
175
+ 'put',
176
+ 'post',
177
+ 'delete',
178
+ 'options',
179
+ 'head',
180
+ 'patch',
181
+ 'trace',
182
+ ];
183
+ /** Extract the first suitable schema from requestBody.content by priority order */
184
+ const pickBodySchema = (op) => {
185
+ const rb = op.requestBody;
186
+ if (!isRecord(rb))
187
+ return undefined;
188
+ const content = rb.content;
189
+ if (!isRecord(content))
190
+ return undefined;
191
+ const order = [
192
+ 'application/json',
193
+ 'application/*+json',
194
+ 'application/xml',
195
+ 'application/x-www-form-urlencoded',
196
+ 'multipart/form-data',
197
+ 'application/octet-stream',
198
+ ];
199
+ for (const k of order) {
200
+ const media = isRecord(content[k]) ? content[k] : undefined;
201
+ if (isRecord(media) && 'schema' in media && isSchema(media.schema)) {
202
+ return media.schema;
203
+ }
204
+ }
205
+ return undefined;
206
+ };
207
+ /* ─────────────────────────────── Args builders ─────────────────────────────── */
208
+ /** Build TS type for params arg (compact formatting) */
209
+ const createBuildParamsType = (tsTypeFromSchema) => (pathParams, queryParams) => {
210
+ const parts = [];
211
+ if (pathParams.length) {
212
+ const inner = pathParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',');
213
+ parts.push(`path:{${inner}}`);
214
+ }
215
+ if (queryParams.length) {
216
+ const inner = queryParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',');
217
+ parts.push(`query:{${inner}}`);
218
+ }
219
+ return parts.length ? `{${parts.join(',')}}` : '';
220
+ };
221
+ /** Build function argument signature */
222
+ const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
223
+ ? `params:${paramsType}, body:${bodyType}`
224
+ : paramsType
225
+ ? `params:${paramsType}`
226
+ : bodyType
227
+ ? `body:${bodyType}`
228
+ : '';
229
+ /** Build one query key:value piece with integer-to-string rules */
230
+ const buildQueryPiece = (p) => {
231
+ const types = toTypeArray(p.schema?.type);
232
+ const isArr = types.includes('array');
233
+ const itemsInt = isArr && isSchema(p.schema?.items) && toTypeArray(p.schema?.items?.type).includes('integer');
234
+ const isInt = types.includes('integer');
235
+ const rhs = itemsInt
236
+ ? `(params.query.${p.name}??[]).map((v:unknown)=>String(v))`
237
+ : isInt
238
+ ? `String(params.query.${p.name})`
239
+ : `params.query.${p.name}`;
240
+ return `${p.name}:${rhs}`;
241
+ };
242
+ /** Build client call argument object (compact formatting) */
243
+ const buildClientArgs = (pathParams, queryParams, hasBody) => {
244
+ const pieces = [];
245
+ if (pathParams.length) {
246
+ const inner = pathParams.map((p) => `${p.name}:params.path.${p.name}`).join(',');
247
+ pieces.push(`param:{${inner}}`);
248
+ }
249
+ if (queryParams.length) {
250
+ const inner = queryParams.map(buildQueryPiece).join(',');
251
+ pieces.push(`query:{${inner}}`);
252
+ }
253
+ if (hasBody)
254
+ pieces.push('json:body');
255
+ return pieces.length ? `{${pieces.join(',')}}` : '';
256
+ };
257
+ /* ─────────────────────────────── Single-operation generator ─────────────────────────────── */
258
+ const generateOperationCode = (path, method, item, deps) => {
259
+ const op = item[method];
260
+ if (!isOperationLike(op))
261
+ return '';
262
+ const funcName = methodPath(method, path);
263
+ const clientAccess = formatPath(path);
264
+ const pathLevelParams = deps.toParameterLikes(item.parameters);
265
+ const opParams = deps.toParameterLikes(op.parameters);
266
+ const allParams = [...pathLevelParams, ...opParams];
267
+ const pathParams = allParams.filter((p) => p.in === 'path');
268
+ const queryParams = allParams.filter((p) => p.in === 'query');
269
+ const bodySchema = pickBodySchema(op);
270
+ const hasBody = bodySchema !== undefined;
271
+ const bodyType = hasBody ? deps.tsTypeFromSchema(bodySchema) : null;
272
+ const buildParamsType = createBuildParamsType(deps.tsTypeFromSchema);
273
+ const paramsType = buildParamsType(pathParams, queryParams);
274
+ const argSig = buildArgSignature(paramsType, bodyType);
275
+ const clientArgs = buildClientArgs(pathParams, queryParams, hasBody);
276
+ const call = clientArgs
277
+ ? `${deps.client}${clientAccess}.$${method}(${clientArgs})`
278
+ : `${deps.client}${clientAccess}.$${method}()`;
279
+ const summary = typeof op.summary === 'string' ? op.summary : '';
280
+ const description = typeof op.description === 'string' ? op.description : '';
281
+ return ('/**\n' +
282
+ (summary ? ` * ${summary}\n *\n` : '') +
283
+ (description ? ` * ${description}\n *\n` : '') +
284
+ ` * ${method.toUpperCase()} ${path}\n` +
285
+ ' */\n' +
286
+ `export async function ${funcName}(${argSig}) {\n` +
287
+ ` return await ${call}\n` +
288
+ '}');
289
+ };
290
+ /* ─────────────────────────────── Entry ─────────────────────────────── */
291
+ export function honoRpc(openapi, importCode) {
292
+ const client = 'client';
293
+ const out = [];
294
+ // import header (kept as-is, then a blank line if present)
295
+ const header = (() => {
296
+ const s = (importCode ?? '').trim();
297
+ return s.length ? `${s}\n\n` : '';
298
+ })();
299
+ // paths guard
300
+ const pathsMaybe = openapi.paths;
301
+ if (!isOpenAPIPaths(pathsMaybe))
302
+ return header;
303
+ // schema & parameter resolvers
304
+ const schemas = openapi.components?.schemas ?? {};
305
+ const resolveRef = createResolveRef(schemas);
306
+ const tsTypeFromSchema = createTsTypeFromSchema(resolveRef);
307
+ const componentsParameters = openapi.components?.parameters ?? {};
308
+ const resolveParameter = createResolveParameter(componentsParameters);
309
+ const toParameterLikes = createToParameterLikes(resolveParameter);
310
+ // iterate path items & operations
311
+ for (const path in pathsMaybe) {
312
+ const rawItem = pathsMaybe[path];
313
+ if (!isRecord(rawItem))
314
+ continue;
315
+ const pathItem = {
316
+ parameters: rawItem.parameters,
317
+ get: isOperationLike(rawItem.get) ? rawItem.get : undefined,
318
+ put: isOperationLike(rawItem.put) ? rawItem.put : undefined,
319
+ post: isOperationLike(rawItem.post) ? rawItem.post : undefined,
320
+ delete: isOperationLike(rawItem.delete) ? rawItem.delete : undefined,
321
+ options: isOperationLike(rawItem.options) ? rawItem.options : undefined,
322
+ head: isOperationLike(rawItem.head) ? rawItem.head : undefined,
323
+ patch: isOperationLike(rawItem.patch) ? rawItem.patch : undefined,
324
+ trace: isOperationLike(rawItem.trace) ? rawItem.trace : undefined,
325
+ };
326
+ for (const method of HTTP_METHODS) {
327
+ const code = generateOperationCode(path, method, pathItem, {
328
+ client,
329
+ tsTypeFromSchema,
330
+ toParameterLikes,
331
+ });
332
+ if (code)
333
+ out.push(code);
334
+ }
335
+ }
336
+ // final string (compact; Prettier will handle formatting as configured)
337
+ return header + out.join('\n\n') + (out.length ? '\n' : '');
338
+ }
@@ -1,4 +1,4 @@
1
- import { createRoute, escapeStringLiteral, routeName } from '../../../../utils/index.js';
1
+ import { createRoute, escapeStringLiteral, methodPath } from '../../../../utils/index.js';
2
2
  import { requestParameter } from './params/index.js';
3
3
  import { response } from './response/index.js';
4
4
  /**
@@ -19,7 +19,7 @@ export function route(path, method, operation) {
19
19
  const tagList = tags ? JSON.stringify(tags) : '[]';
20
20
  const requestParams = requestParameter(parameters, requestBody);
21
21
  const create_args = {
22
- routeName: routeName(method, path),
22
+ routeName: `${methodPath(method, path)}Route`,
23
23
  tags: tags ? `tags:${tagList},` : '',
24
24
  method: `method:'${method}',`,
25
25
  path: `path:'${path}',`,
@@ -6,18 +6,16 @@ import { integer } from './z/integer.js';
6
6
  import { number } from './z/number.js';
7
7
  import { object } from './z/object.js';
8
8
  import { string } from './z/string.js';
9
- // Test run
10
- // pnpm vitest run ./src/generator/zod-to-openapi/index.test.ts
11
9
  export function zodToOpenAPI(schema, paramName, paramIn) {
12
10
  if (schema === undefined)
13
11
  throw new Error('hono-takibi: only #/components/schemas/* is supported');
14
- // ref
15
- if (schema.$ref) {
12
+ /** ref */
13
+ if (schema.$ref !== undefined) {
16
14
  return wrap(refSchema(schema.$ref), schema, paramName, paramIn);
17
15
  }
18
16
  /* combinators */
19
- // allOf
20
- if (schema.allOf) {
17
+ /** allOf */
18
+ if (schema.allOf !== undefined) {
21
19
  if (!schema.allOf || schema.allOf.length === 0) {
22
20
  return wrap('z.any()', schema, paramName, paramIn);
23
21
  }
@@ -49,8 +47,8 @@ export function zodToOpenAPI(schema, paramName, paramIn) {
49
47
  const z = `z.intersection(${schemas.join(',')})`;
50
48
  return wrap(z, schema, paramName, paramIn);
51
49
  }
52
- // anyOf
53
- if (schema.anyOf) {
50
+ /* anyOf */
51
+ if (schema.anyOf !== undefined) {
54
52
  if (!schema.anyOf || schema.anyOf.length === 0) {
55
53
  return wrap('z.any()', schema, paramName, paramIn);
56
54
  }
@@ -60,8 +58,8 @@ export function zodToOpenAPI(schema, paramName, paramIn) {
60
58
  const z = `z.union([${schemas.join(',')}])`;
61
59
  return wrap(z, schema, paramName, paramIn);
62
60
  }
63
- // oneOf
64
- if (schema.oneOf) {
61
+ /* oneOf */
62
+ if (schema.oneOf !== undefined) {
65
63
  if (!schema.oneOf || schema.oneOf.length === 0) {
66
64
  return wrap('z.any()', schema, paramName, paramIn);
67
65
  }
@@ -78,8 +76,8 @@ export function zodToOpenAPI(schema, paramName, paramIn) {
78
76
  const z = `z.union([${schemas.join(',')}])`;
79
77
  return wrap(z, schema, paramName, paramIn);
80
78
  }
81
- // not
82
- if (schema.not) {
79
+ /* not */
80
+ if (schema.not !== undefined) {
83
81
  if (typeof schema.not === 'object' && schema.not.type && typeof schema.not.type === 'string') {
84
82
  const predicate = `(v) => typeof v !== '${schema.not.type}'`;
85
83
  const z = `z.any().refine(${predicate})`;
@@ -93,16 +91,16 @@ export function zodToOpenAPI(schema, paramName, paramIn) {
93
91
  }
94
92
  return wrap('z.any()', schema, paramName, paramIn);
95
93
  }
96
- // const
97
- if (schema.const) {
94
+ /* const */
95
+ if (schema.const !== undefined) {
98
96
  const z = `z.literal(${JSON.stringify(schema.const)})`;
99
97
  return wrap(z, schema, paramName, paramIn);
100
98
  }
101
99
  /* enum */
102
- if (schema.enum)
100
+ if (schema.enum !== undefined)
103
101
  return wrap(_enum(schema), schema, paramName, paramIn);
104
102
  /* properties */
105
- if (schema.properties)
103
+ if (schema.properties !== undefined)
106
104
  return wrap(object(schema), schema, paramName, paramIn);
107
105
  const t = normalizeTypes(schema.type);
108
106
  /* string */
@@ -1,4 +1,4 @@
1
- import { routeName } from '../utils/index.js';
1
+ import { methodPath } from '../utils/index.js';
2
2
  /**
3
3
  * Extracts route mappings from an OpenAPI specification.
4
4
  *
@@ -10,8 +10,8 @@ export function getRouteMaps(openapi) {
10
10
  const routeMappings = Object.entries(paths).flatMap(([path, pathItem]) => {
11
11
  return Object.entries(pathItem).flatMap(([method]) => {
12
12
  return {
13
- routeName: routeName(method, path),
14
- handlerName: `${routeName(method, path)}Handler`,
13
+ routeName: `${methodPath(method, path)}Route`,
14
+ handlerName: `${methodPath(method, path)}RouteHandler`,
15
15
  path,
16
16
  };
17
17
  });
@@ -45,7 +45,7 @@ export async function typeSpecToOpenAPI(input) {
45
45
  catch (e) {
46
46
  return {
47
47
  ok: false,
48
- error: e instanceof Error ? String(e.message) : String(e),
48
+ error: e instanceof Error ? e.message : String(e),
49
49
  };
50
50
  }
51
51
  }
@@ -56,7 +56,7 @@ export declare function parseCli(args: readonly string[]): {
56
56
  export declare function normalizeTypes(t?: 'string' | 'number' | 'integer' | 'date' | 'boolean' | 'array' | 'object' | 'null' | [
57
57
  'string' | 'number' | 'integer' | 'date' | 'boolean' | 'array' | 'object' | 'null',
58
58
  ...('string' | 'number' | 'integer' | 'date' | 'boolean' | 'array' | 'object' | 'null')[]
59
- ]): ("string" | "number" | "boolean" | "object" | "integer" | "date" | "array" | "null")[];
59
+ ]): ("string" | "number" | "boolean" | "object" | "array" | "integer" | "null" | "date")[];
60
60
  /**
61
61
  * Generates import statements for route handler modules.
62
62
  *
@@ -312,9 +312,9 @@ export declare function refSchema($ref: `#/components/schemas/${string}`): strin
312
312
  * @returns A route name string (e.g., 'getUsersIdPostsRoute').
313
313
  *
314
314
  * @example
315
- * routeName('get', '/users/{id}/posts') // 'getUsersIdPostsRoute'
315
+ * methodPath('get', '/users/{id}/posts') // 'getUsersIdPosts'
316
316
  */
317
- export declare function routeName(method: string, path: string): string;
317
+ export declare function methodPath(method: string, path: string): string;
318
318
  /**
319
319
  * Generates a Hono route definition as a TypeScript export string.
320
320
  *
@@ -23,6 +23,7 @@ export function parseCli(args) {
23
23
  const input = args[0];
24
24
  const oIdx = args.indexOf('-o');
25
25
  const output = oIdx !== -1 ? args[oIdx + 1] : undefined;
26
+ /** yaml or json or tsp */
26
27
  const isYamlOrJsonOrTsp = (i) => i.endsWith('.yaml') || i.endsWith('.json') || i.endsWith('.tsp');
27
28
  const isTs = (o) => o.endsWith('.ts');
28
29
  const getFlagValue = (args, flag) => {
@@ -376,9 +377,9 @@ export function refSchema($ref) {
376
377
  * @returns A route name string (e.g., 'getUsersIdPostsRoute').
377
378
  *
378
379
  * @example
379
- * routeName('get', '/users/{id}/posts') // 'getUsersIdPostsRoute'
380
+ * methodPath('get', '/users/{id}/posts') // 'getUsersIdPosts'
380
381
  */
381
- export function routeName(method, path) {
382
+ export function methodPath(method, path) {
382
383
  // 1. api_path: `/user/createWithList`
383
384
  // 2. replace(/[\/{}-]/g, ' ') -> ` user createWithList`
384
385
  // 3. trim() -> `user createWithList`
@@ -391,7 +392,7 @@ export function routeName(method, path) {
391
392
  .split(/\s+/)
392
393
  .map((str) => `${str.charAt(0).toUpperCase()}${str.slice(1)}`)
393
394
  .join('');
394
- return `${method}${apiPath}Route`;
395
+ return apiPath ? `${method}${apiPath}` : `${method}Index`;
395
396
  }
396
397
  /**
397
398
  * Generates a Hono route definition as a TypeScript export string.
@@ -36,7 +36,6 @@ export default function HonoTakibiVite({ input, output, exportType, exportSchema
36
36
  exportSchema?: boolean;
37
37
  }): Promise<{
38
38
  name: string;
39
- apply: string;
40
39
  buildStart(): Promise<void>;
41
40
  configureServer(server: ViteDevServer): void;
42
41
  }>;
@@ -35,27 +35,29 @@ import { parseOpenAPI } from '../openapi/index.js';
35
35
  */
36
36
  export default async function HonoTakibiVite({ input, output, exportType = true, exportSchema = true, }) {
37
37
  const run = async () => {
38
- if (typeof input === 'string' &&
39
- ((i) => i.endsWith('.tsp'))(input)) {
40
- const spec = await parseOpenAPI(input);
41
- if (!spec.ok) {
42
- console.error(spec.error);
43
- return false;
44
- }
45
- try {
46
- const hono = zodOpenAPIHono(spec.value, exportSchema, exportType);
47
- const code = await fmt(hono);
48
- if (!code.ok) {
49
- console.error(`${code.error}`);
50
- return false;
51
- }
52
- await fsp.mkdir(path.dirname(output), { recursive: true });
53
- await fsp.writeFile(output, code.value, 'utf-8');
54
- }
55
- catch (e) {
56
- console.error(String(e));
57
- throw e;
38
+ const isYamlOrJsonOrTsp = (i) => i.endsWith('.yaml') || i.endsWith('.json') || i.endsWith('.tsp');
39
+ if (!isYamlOrJsonOrTsp(input)) {
40
+ console.error(`Invalid input file type: ${input}`);
41
+ return;
42
+ }
43
+ const spec = await parseOpenAPI(input);
44
+ if (!spec.ok) {
45
+ console.error(spec.error);
46
+ return;
47
+ }
48
+ try {
49
+ const hono = zodOpenAPIHono(spec.value, exportSchema, exportType);
50
+ const code = await fmt(hono);
51
+ if (!code.ok) {
52
+ console.error(`${code.error}`);
53
+ return;
58
54
  }
55
+ await fsp.mkdir(path.dirname(output), { recursive: true });
56
+ await fsp.writeFile(output, code.value, 'utf-8');
57
+ }
58
+ catch (e) {
59
+ console.error(e instanceof Error ? e.message : String(e));
60
+ throw e;
59
61
  }
60
62
  };
61
63
  const debounce = (ms, fn) => {
@@ -74,9 +76,6 @@ export default async function HonoTakibiVite({ input, output, exportType = true,
74
76
  const absInput = path.resolve(input);
75
77
  return {
76
78
  name: 'hono-takibi-vite',
77
- // https://vite.dev/guide/api-plugin.html#conditional-application
78
- // Only valid when the development server is running.
79
- apply: 'serve',
80
79
  async buildStart() {
81
80
  await run();
82
81
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-takibi",
3
3
  "description": "Hono Takibi is a CLI tool that generates Hono routes from OpenAPI specifications.",
4
- "version": "0.8.8",
4
+ "version": "0.9.01",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -32,16 +32,25 @@
32
32
  "types": "./dist/vite-plugin/index.d.ts",
33
33
  "import": "./dist/vite-plugin/index.js"
34
34
  },
35
+ "./config": {
36
+ "types": "./dist/generator/config/index.d.ts",
37
+ "import": "./dist/generator/config/index.js"
38
+ },
35
39
  "./zod-openapi-hono": {
36
40
  "types": "./dist/generator/zod-openapi-hono/openapi/index.d.ts",
37
41
  "import": "./dist/generator/zod-openapi-hono/openapi/index.js"
42
+ },
43
+ "./rpc-beta": {
44
+ "types": "./dist/core/rpc.d.ts",
45
+ "import": "./dist/core/rpc.js"
38
46
  }
39
47
  },
40
48
  "dependencies": {
41
49
  "@apidevtools/swagger-parser": "^12.0.0",
42
50
  "@typespec/compiler": "^1.3.0",
43
51
  "@typespec/openapi3": "^1.3.0",
44
- "prettier": "^3.6.2"
52
+ "prettier": "^3.6.2",
53
+ "tsx": "^4.20.4"
45
54
  },
46
55
  "devDependencies": {
47
56
  "@hono/zod-openapi": "1.1.0",
@@ -50,7 +59,6 @@
50
59
  "@typespec/rest": "^0.73.0",
51
60
  "@typespec/versioning": "^0.73.0",
52
61
  "@vitest/coverage-v8": "^3.2.4",
53
- "tsx": "^4.20.3",
54
62
  "typescript": "^5.8.3",
55
63
  "vite": "^7.0.6",
56
64
  "vitest": "^3.2.4",
@@ -1,16 +0,0 @@
1
- import type { OpenAPI } from '../../../openapi/index.js';
2
- /**
3
- * Generates route handler files for a Hono app using Zod and OpenAPI.
4
- *
5
- * @param openapi - The OpenAPI specification object.
6
- * @param output - The output directory or file path for generated handlers.
7
- * @param test - Whether to generate corresponding empty test files.
8
- * @returns A `Result` indicating success or error with message.
9
- */
10
- export declare function zodOpenapiHonoHandler(openapi: OpenAPI, output: string, test: boolean): Promise<{
11
- ok: true;
12
- value: undefined;
13
- } | {
14
- ok: false;
15
- error: string;
16
- }>;
@@ -1,65 +0,0 @@
1
- import { fmt } from '../../../format/index.js';
2
- import { mkdir, writeFile } from '../../../fsp/index.js';
3
- import { groupHandlersByFileName, routeName } from '../../../utils/index.js';
4
- /**
5
- * Generates route handler files for a Hono app using Zod and OpenAPI.
6
- *
7
- * @param openapi - The OpenAPI specification object.
8
- * @param output - The output directory or file path for generated handlers.
9
- * @param test - Whether to generate corresponding empty test files.
10
- * @returns A `Result` indicating success or error with message.
11
- */
12
- export async function zodOpenapiHonoHandler(openapi, output, test) {
13
- const paths = openapi.paths;
14
- const handlers = [];
15
- for (const [path, pathItem] of Object.entries(paths)) {
16
- for (const [method] of Object.entries(pathItem)) {
17
- const routeHandlerContent = `export const ${routeName(method, path)}Handler:RouteHandler<typeof ${routeName(method, path)}>=async(c)=>{}`;
18
- const rawSegment = path.replace(/^\/+/, '').split('/')[0] ?? '';
19
- const pathName = (rawSegment === '' ? 'index' : rawSegment)
20
- .replace(/\{([^}]+)\}/g, '$1')
21
- .replace(/[^0-9A-Za-z._-]/g, '_')
22
- .replace(/^[._-]+|[._-]+$/g, '')
23
- .replace(/__+/g, '_')
24
- .replace(/[-._](\w)/g, (_, c) => c.toUpperCase());
25
- const fileName = pathName.length === 0 ? 'indexHandler.ts' : `${pathName}Handler.ts`;
26
- const testFileName = pathName.length === 0 ? 'indexHandler.test.ts' : `${pathName}Handler.test.ts`;
27
- handlers.push({
28
- fileName,
29
- testFileName,
30
- routeHandlerContents: [routeHandlerContent],
31
- routeNames: [routeName(method, path)],
32
- });
33
- }
34
- }
35
- const mergedHandlers = groupHandlersByFileName(handlers);
36
- for (const handler of mergedHandlers) {
37
- const dirPath = output?.replace(/\/[^/]+\.ts$/, '');
38
- const handlerPath = dirPath === 'index.ts' ? 'handlers' : `${dirPath}/handlers`;
39
- const mkdirResult = await mkdir(handlerPath);
40
- if (!mkdirResult.ok) {
41
- return { ok: false, error: mkdirResult.error };
42
- }
43
- const routeTypes = handler.routeNames.map((routeName) => `${routeName}`).join(', ');
44
- const match = output?.match(/[^/]+\.ts$/);
45
- const matchPath = match ? match[0] : '';
46
- const path = output === '.' || output === './' ? output : `../${matchPath}`;
47
- const importRouteTypes = routeTypes ? `import type { ${routeTypes} } from '${path}';` : '';
48
- const importStatements = `import type { RouteHandler } from '@hono/zod-openapi'\n${importRouteTypes}`;
49
- const fileContent = `${importStatements}\n\n${handler.routeHandlerContents.join('\n\n')}`;
50
- const formatCode = await fmt(fileContent);
51
- if (!formatCode.ok) {
52
- return { ok: false, error: formatCode.error };
53
- }
54
- const writeResult = await writeFile(`${handlerPath}/${handler.fileName}`, formatCode.value);
55
- if (!writeResult.ok)
56
- writeResult;
57
- if (test) {
58
- const writeResult = await writeFile(`${handlerPath}/${handler.testFileName}`, '');
59
- if (!writeResult.ok) {
60
- return { ok: false, error: writeResult.error };
61
- }
62
- }
63
- }
64
- return { ok: true, value: undefined };
65
- }
File without changes