hono-takibi 0.9.21 → 0.9.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -5,7 +5,7 @@ import core from '../core/core.js';
5
5
  import { takibi } from '../core/takibi.js';
6
6
  import rpc from '../generator/rpc/index.js';
7
7
  // import { honoRpcWithSWR } from '../generator/swr/index.js'
8
- import { parseCli } from '../utils/index.js';
8
+ import { parseCli, parseIO } from '../utils/index.js';
9
9
  const HELP_TEXT = `Usage: hono-takibi <input.{yaml,json,tsp}> -o <routes.ts> [options]
10
10
 
11
11
  Options:
@@ -81,13 +81,21 @@ export async function honoTakibi() {
81
81
  return { ok: false, error: configResult.error };
82
82
  }
83
83
  const c = configResult.value;
84
- const takibiResult = c['hono-takibi']
85
- ? await takibi(c['hono-takibi']?.input, c['hono-takibi']?.output, c['hono-takibi']?.exportSchema ?? false, c['hono-takibi']?.exportType ?? false, false, // template
84
+ const zodOpenAPIConfigResult = parseIO(c['zod-openapi']);
85
+ if (!zodOpenAPIConfigResult.ok) {
86
+ return { ok: false, error: zodOpenAPIConfigResult.error };
87
+ }
88
+ const takibiResult = c['zod-openapi']
89
+ ? await takibi(c['zod-openapi']?.input, c['zod-openapi']?.output, c['zod-openapi']?.exportSchema ?? false, c['zod-openapi']?.exportType ?? false, false, // template
86
90
  false)
87
91
  : undefined;
88
92
  if (takibiResult && !takibiResult.ok) {
89
93
  return { ok: false, error: takibiResult.error };
90
94
  }
95
+ const rpcConfigResult = parseIO(c.rpc);
96
+ if (!rpcConfigResult.ok) {
97
+ return { ok: false, error: rpcConfigResult.error };
98
+ }
91
99
  const rpcResult = c.rpc
92
100
  ? await core(c.rpc.input, c.rpc.output, c.rpc.import, 'Generated RPC code written to', rpc)
93
101
  : undefined;
@@ -1,5 +1,5 @@
1
1
  type Config = {
2
- 'hono-takibi'?: {
2
+ 'zod-openapi'?: {
3
3
  input: `${string}.yaml` | `${string}.json` | `${string}.tsp`;
4
4
  output: `${string}.ts`;
5
5
  exportType?: boolean;
@@ -10,11 +10,6 @@ type Config = {
10
10
  output: `${string}.ts`;
11
11
  import: string;
12
12
  };
13
- swr?: {
14
- input: `${string}.yaml` | `${string}.json` | `${string}.tsp`;
15
- output: `${string}.ts`;
16
- import: string;
17
- };
18
13
  };
19
14
  export declare function config(): Promise<{
20
15
  ok: true;
@@ -9,9 +9,9 @@ export async function config() {
9
9
  register();
10
10
  try {
11
11
  const mod = await import(pathToFileURL(abs).href);
12
- if (!('default' in mod)) {
13
- return { ok: false, error: 'Config must export default object' };
14
- }
12
+ // if (!('default' in mod)) {
13
+ // return { ok: false, error: 'Config must export default object' }
14
+ // }
15
15
  return { ok: true, value: mod.default };
16
16
  }
17
17
  catch (e) {
package/dist/core/core.js CHANGED
@@ -8,15 +8,15 @@ export default async function core(input, output, importCode, value, fn) {
8
8
  return { ok: false, error: openAPIResult.error };
9
9
  }
10
10
  const openAPI = openAPIResult.value;
11
- const honoRpcResult = await fmt(fn(openAPI, importCode));
12
- if (!honoRpcResult.ok) {
13
- return { ok: false, error: honoRpcResult.error };
11
+ const fnResult = await fmt(fn(openAPI, importCode));
12
+ if (!fnResult.ok) {
13
+ return { ok: false, error: fnResult.error };
14
14
  }
15
15
  const mkdirResult = await mkdir(path.dirname(output));
16
16
  if (!mkdirResult.ok) {
17
17
  return { ok: false, error: mkdirResult.error };
18
18
  }
19
- const writeResult = await writeFile(output, honoRpcResult.value);
19
+ const writeResult = await writeFile(output, fnResult.value);
20
20
  if (!writeResult.ok) {
21
21
  return { ok: false, error: writeResult.error };
22
22
  }
@@ -4,7 +4,7 @@ import { mkdir, readdir, writeFile } from '../fsp/index.js';
4
4
  import { app } from '../generator/zod-openapi-hono/app/index.js';
5
5
  import zodOpenAPIHono from '../generator/zod-openapi-hono/openapi/index.js';
6
6
  import { parseOpenAPI } from '../openapi/index.js';
7
- import { groupHandlersByFileName, methodPath } from '../utils/index.js';
7
+ import { groupHandlersByFileName, isHttpMethod, methodPath } from '../utils/index.js';
8
8
  /**
9
9
  * Generates TypeScript code from an OpenAPI spec and optional templates.
10
10
  *
@@ -109,13 +109,6 @@ export async function takibi(input, output, exportSchema, exportType, template,
109
109
  async function zodOpenapiHonoHandler(openapi, output, test) {
110
110
  const paths = openapi.paths;
111
111
  const handlers = [];
112
- const isHttpMethod = (v) => {
113
- for (const m of ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace']) {
114
- if (m === v)
115
- return true;
116
- }
117
- return false;
118
- };
119
112
  for (const [path, pathItem] of Object.entries(paths)) {
120
113
  for (const [method] of Object.entries(pathItem)) {
121
114
  if (!isHttpMethod(method))
@@ -1,8 +1,6 @@
1
1
  import { methodPath } from '../../utils/index.js';
2
2
  /* ─────────────────────────────── Guards ─────────────────────────────── */
3
- /** Narrow to generic object records */
4
3
  const isRecord = (v) => typeof v === 'object' && v !== null;
5
- /** Narrow to OpenAPI paths object (shallow structural check) */
6
4
  const isOpenAPIPaths = (v) => {
7
5
  if (!isRecord(v))
8
6
  return false;
@@ -11,20 +9,11 @@ const isOpenAPIPaths = (v) => {
11
9
  return false;
12
10
  return true;
13
11
  };
14
- /** Treat any object as Schema (we rely on downstream field checks) */
15
12
  const isSchema = (v) => isRecord(v);
16
13
  /* ─────────────────────────────── Formatters ─────────────────────────────── */
17
- /** JS identifier check */
18
14
  const isValidIdent = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
19
- /** Escape single quotes and backslashes for single-quoted strings */
20
15
  const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
21
- /**
22
- * Convert an OpenAPI path to a client access chain.
23
- * examples:
24
- * '/' -> ".index"
25
- * '/hono-x' -> "['hono-x']"
26
- * '/posts/hono/{id}' -> ".posts.hono[':id']"
27
- */
16
+ /** '/'->'.index' | '/hono-x'->"['hono-x']" | '/posts/hono/{id}'->".posts.hono[':id']" */
28
17
  const formatPath = (path) => {
29
18
  const segs = (path === '/' ? ['index'] : path.replace(/^\/+/, '').split('/')).filter(Boolean);
30
19
  return segs
@@ -35,7 +24,6 @@ const formatPath = (path) => {
35
24
  : `['${esc(seg)}']`)
36
25
  .join('');
37
26
  };
38
- /** 'type' to normalized list for uniform checks */
39
27
  const isJSONTypeName = (s) => typeof s === 'string' &&
40
28
  (s === 'object' ||
41
29
  s === 'array' ||
@@ -51,7 +39,6 @@ const toTypeArray = (t) => {
51
39
  return t.filter(isJSONTypeName);
52
40
  return [];
53
41
  };
54
- /** Build literal union from enum values (kept compact) */
55
42
  const literalFromEnum = (vals) => {
56
43
  const toLit = (v) => typeof v === 'string'
57
44
  ? `'${v.replace(/'/g, "\\'")}'`
@@ -62,7 +49,6 @@ const literalFromEnum = (vals) => {
62
49
  : 'unknown';
63
50
  return vals.map(toLit).join('|');
64
51
  };
65
- /** Create a $ref resolver for #/components/schemas/... */
66
52
  const createResolveRef = (schemas) => (ref) => {
67
53
  if (!ref)
68
54
  return undefined;
@@ -72,60 +58,52 @@ const createResolveRef = (schemas) => (ref) => {
72
58
  const target = schemas[m[1]];
73
59
  return isSchema(target) ? target : undefined;
74
60
  };
75
- /** Create a Schema->TypeScript type printer (single instance, recursive-safe) */
61
+ /** TS type printer (handles $ref / enums / combinators / additionalProperties / nullable) */
76
62
  const createTsTypeFromSchema = (resolveRef) => {
77
63
  const tt = (schema, seen = new Set()) => {
78
64
  if (!schema)
79
65
  return 'unknown';
80
- // $ref resolution
81
66
  if (schema.$ref) {
82
67
  const tgt = resolveRef(schema.$ref);
83
68
  return tt(tgt, seen);
84
69
  }
85
- // recursion guard
86
70
  if (seen.has(schema))
87
71
  return 'unknown';
88
- const nextSeen = new Set(seen);
89
- nextSeen.add(schema);
90
- // combinators
72
+ const next = new Set(seen);
73
+ next.add(schema);
91
74
  if (Array.isArray(schema.oneOf) && schema.oneOf.length)
92
- return schema.oneOf.map((s) => tt(s, nextSeen)).join('|') || 'unknown';
75
+ return schema.oneOf.map((s) => tt(s, next)).join('|') || 'unknown';
93
76
  if (Array.isArray(schema.anyOf) && schema.anyOf.length)
94
- return schema.anyOf.map((s) => tt(s, nextSeen)).join('|') || 'unknown';
77
+ return schema.anyOf.map((s) => tt(s, next)).join('|') || 'unknown';
95
78
  if (Array.isArray(schema.allOf) && schema.allOf.length)
96
- return schema.allOf.map((s) => tt(s, nextSeen)).join('&') || 'unknown';
97
- // enum
79
+ return schema.allOf.map((s) => tt(s, next)).join('&') || 'unknown';
98
80
  if (Array.isArray(schema.enum) && schema.enum.length) {
99
81
  const base = literalFromEnum(schema.enum);
100
82
  return schema.nullable ? `${base}|null` : base;
101
83
  }
102
84
  const types = toTypeArray(schema.type);
103
- // array
85
+ // array (parentheses when inner contains union/intersection)
104
86
  if (types.includes('array')) {
105
- const inner = tt(isSchema(schema.items) ? schema.items : undefined, nextSeen);
106
- const core = `${inner}[]`;
87
+ const item = isSchema(schema.items) ? schema.items : undefined;
88
+ const inner = tt(item, next);
89
+ const needParens = /[|&]/.test(inner) && !/^\(.*\)$/.test(inner);
90
+ const core = `${needParens ? `(${inner})` : inner}[]`;
107
91
  return schema.nullable ? `${core}|null` : core;
108
92
  }
109
- // object
110
93
  if (types.includes('object')) {
111
94
  const req = new Set(Array.isArray(schema.required) ? schema.required : []);
112
95
  const props = schema.properties ?? {};
113
96
  const fields = Object.entries(props).map(([k, v]) => {
114
97
  const opt = req.has(k) ? '' : '?';
115
98
  const child = isSchema(v) ? v : undefined;
116
- return `${k}${opt}:${tt(child, nextSeen)}`;
99
+ return `${k}${opt}:${tt(child, next)}`;
117
100
  });
118
101
  const ap = schema.additionalProperties;
119
- const addl = ap === true
120
- ? '[key:string]:unknown'
121
- : isSchema(ap)
122
- ? `[key:string]:${tt(ap, nextSeen)}`
123
- : '';
102
+ const addl = ap === true ? '[key:string]:unknown' : isSchema(ap) ? `[key:string]:${tt(ap, next)}` : '';
124
103
  const members = [...fields, addl].filter(Boolean).join(',');
125
104
  const core = `{${members}}`;
126
105
  return schema.nullable ? `${core}|null` : core;
127
106
  }
128
- // primitives
129
107
  if (types.length === 0)
130
108
  return schema.nullable ? 'unknown|null' : 'unknown';
131
109
  const prim = types
@@ -144,13 +122,11 @@ const isParameterObject = (v) => {
144
122
  const pos = v.in;
145
123
  return pos === 'path' || pos === 'query' || pos === 'header' || pos === 'cookie';
146
124
  };
147
- /** Extract components/parameters name from a ref-like value */
148
125
  const refParamName = (refLike) => {
149
126
  const ref = typeof refLike === 'string' ? refLike : isRefObject(refLike) ? refLike.$ref : undefined;
150
127
  const m = ref?.match(/^#\/components\/parameters\/(.+)$/);
151
128
  return m ? m[1] : undefined;
152
129
  };
153
- /** Build a resolver that returns normalized ParameterLike (resolving $ref) */
154
130
  const createResolveParameter = (componentsParameters) => (p) => {
155
131
  if (isParameterObject(p))
156
132
  return p;
@@ -158,7 +134,6 @@ const createResolveParameter = (componentsParameters) => (p) => {
158
134
  const cand = name ? componentsParameters[name] : undefined;
159
135
  return isParameterObject(cand) ? cand : undefined;
160
136
  };
161
- /** Convert raw parameters array into ParameterLike[] */
162
137
  const createToParameterLikes = (resolveParam) => (arr) => Array.isArray(arr)
163
138
  ? arr.reduce((acc, x) => {
164
139
  const r = resolveParam(x);
@@ -178,7 +153,7 @@ const HTTP_METHODS = [
178
153
  'patch',
179
154
  'trace',
180
155
  ];
181
- /** Extract the first suitable schema from requestBody.content by priority order */
156
+ const hasSchemaProp = (v) => isRecord(v) && 'schema' in v;
182
157
  const pickBodySchema = (op) => {
183
158
  const rb = op.requestBody;
184
159
  if (!isRecord(rb))
@@ -196,14 +171,12 @@ const pickBodySchema = (op) => {
196
171
  ];
197
172
  for (const k of order) {
198
173
  const media = isRecord(content[k]) ? content[k] : undefined;
199
- if (isRecord(media) && 'schema' in media && isSchema(media.schema)) {
174
+ if (hasSchemaProp(media) && isSchema(media.schema))
200
175
  return media.schema;
201
- }
202
176
  }
203
177
  return undefined;
204
178
  };
205
179
  /* ─────────────────────────────── Args builders ─────────────────────────────── */
206
- /** Build TS type for params arg (compact formatting) */
207
180
  const createBuildParamsType = (tsTypeFromSchema) => (pathParams, queryParams) => {
208
181
  const parts = [];
209
182
  if (pathParams.length) {
@@ -216,7 +189,6 @@ const createBuildParamsType = (tsTypeFromSchema) => (pathParams, queryParams) =>
216
189
  }
217
190
  return parts.length ? `{${parts.join(',')}}` : '';
218
191
  };
219
- /** Build function argument signature */
220
192
  const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
221
193
  ? `params:${paramsType}, body:${bodyType}`
222
194
  : paramsType
@@ -224,20 +196,7 @@ const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
224
196
  : bodyType
225
197
  ? `body:${bodyType}`
226
198
  : '';
227
- /** Build one query key:value piece with integer-to-string rules */
228
- const buildQueryPiece = (p) => {
229
- const types = toTypeArray(p.schema?.type);
230
- const isArr = types.includes('array');
231
- const itemsInt = isArr && isSchema(p.schema?.items) && toTypeArray(p.schema?.items?.type).includes('integer');
232
- const isInt = types.includes('integer');
233
- const rhs = itemsInt
234
- ? `(params.query.${p.name}??[]).map((v:unknown)=>String(v))`
235
- : isInt
236
- ? `String(params.query.${p.name})`
237
- : `params.query.${p.name}`;
238
- return `${p.name}:${rhs}`;
239
- };
240
- /** Build client call argument object (compact formatting) */
199
+ /** pass query as-is (keep numbers/arrays) */
241
200
  const buildClientArgs = (pathParams, queryParams, hasBody) => {
242
201
  const pieces = [];
243
202
  if (pathParams.length) {
@@ -245,7 +204,7 @@ const buildClientArgs = (pathParams, queryParams, hasBody) => {
245
204
  pieces.push(`param:{${inner}}`);
246
205
  }
247
206
  if (queryParams.length) {
248
- const inner = queryParams.map(buildQueryPiece).join(',');
207
+ const inner = queryParams.map((p) => `${p.name}:params.query.${p.name}`).join(',');
249
208
  pieces.push(`query:{${inner}}`);
250
209
  }
251
210
  if (hasBody)
@@ -276,36 +235,25 @@ const generateOperationCode = (path, method, item, deps) => {
276
235
  : `${deps.client}${clientAccess}.$${method}()`;
277
236
  const summary = typeof op.summary === 'string' ? op.summary : '';
278
237
  const description = typeof op.description === 'string' ? op.description : '';
279
- return ('/**\n' +
280
- (summary ? ` * ${summary}\n *\n` : '') +
281
- (description ? ` * ${description}\n *\n` : '') +
282
- ` * ${method.toUpperCase()} ${path}\n` +
283
- ' */\n' +
284
- `export async function ${funcName}(${argSig}) {\n` +
285
- ` return await ${call}\n` +
286
- '}');
238
+ const summaryBlock = summary ? ` * ${summary}\n *\n` : '';
239
+ const descriptionBlock = description ? ` * ${description}\n *\n` : '';
240
+ return `/**\n${summaryBlock}${descriptionBlock} * ${method.toUpperCase()} ${path}\n */\nexport async function ${funcName}(${argSig}){return await ${call}}`;
287
241
  };
288
242
  /* ─────────────────────────────── Entry ─────────────────────────────── */
289
243
  export default function rpc(openapi, importCode) {
290
244
  const client = 'client';
291
245
  const out = [];
292
- // import header (kept as-is, then a blank line if present)
293
- const header = (() => {
294
- const s = (importCode ?? '').trim();
295
- return s.length ? `${s}\n\n` : '';
296
- })();
297
- // paths guard
246
+ const s = (importCode ?? '').trim();
247
+ const header = s.length ? `${s}\n\n` : '';
298
248
  const pathsMaybe = openapi.paths;
299
249
  if (!isOpenAPIPaths(pathsMaybe))
300
250
  return header;
301
- // schema & parameter resolvers
302
251
  const schemas = openapi.components?.schemas ?? {};
303
252
  const resolveRef = createResolveRef(schemas);
304
253
  const tsTypeFromSchema = createTsTypeFromSchema(resolveRef);
305
254
  const componentsParameters = openapi.components?.parameters ?? {};
306
255
  const resolveParameter = createResolveParameter(componentsParameters);
307
256
  const toParameterLikes = createToParameterLikes(resolveParameter);
308
- // iterate path items & operations
309
257
  for (const path in pathsMaybe) {
310
258
  const rawItem = pathsMaybe[path];
311
259
  if (!isRecord(rawItem))
@@ -331,6 +279,5 @@ export default function rpc(openapi, importCode) {
331
279
  out.push(code);
332
280
  }
333
281
  }
334
- // final string (compact; Prettier will handle formatting as configured)
335
- return header + out.join('\n\n') + (out.length ? '\n' : '');
282
+ return `${header}${out.join('\n\n')}${out.length ? '\n' : ''}`;
336
283
  }
@@ -1,4 +1,4 @@
1
- import { methodPath } from '../utils/index.js';
1
+ import { isHttpMethod, methodPath } from '../utils/index.js';
2
2
  /**
3
3
  * Extracts route mappings from an OpenAPI specification.
4
4
  *
@@ -9,6 +9,8 @@ export function getRouteMaps(openapi) {
9
9
  const paths = openapi.paths;
10
10
  const routeMappings = Object.entries(paths).flatMap(([path, pathItem]) => {
11
11
  return Object.entries(pathItem).flatMap(([method]) => {
12
+ if (!isHttpMethod(method))
13
+ return [];
12
14
  return {
13
15
  routeName: `${methodPath(method, path)}Route`,
14
16
  handlerName: `${methodPath(method, path)}RouteHandler`,
@@ -34,6 +34,13 @@ export declare function parseCli(args: readonly string[]): {
34
34
  ok: false;
35
35
  error: string;
36
36
  };
37
+ export declare function parseIO<T>(section: T): {
38
+ ok: true;
39
+ value: T;
40
+ } | {
41
+ ok: false;
42
+ error: string;
43
+ };
37
44
  /**
38
45
  * Normalize a JSON Schema `type` value into an array of type strings.
39
46
  *
@@ -241,6 +248,13 @@ export declare function isRefObject(value: unknown): value is {
241
248
  $ref?: string;
242
249
  [key: string]: unknown;
243
250
  };
251
+ /**
252
+ * Checks if a string is a valid HTTP method.
253
+ *
254
+ * @param method - The HTTP method to check.
255
+ * @returns `true` if the method is a valid HTTP method; otherwise `false`.
256
+ */
257
+ export declare function isHttpMethod(method: string): method is 'get' | 'put' | 'post' | 'delete' | 'patch' | 'options' | 'head' | 'trace';
244
258
  /**
245
259
  * Checks if all given content types share the same schema definition.
246
260
  *
@@ -51,6 +51,29 @@ export function parseCli(args) {
51
51
  },
52
52
  };
53
53
  }
54
+ export function parseIO(section) {
55
+ const inputExts = ['yaml', 'json', 'tsp'];
56
+ const outputExts = ['ts'];
57
+ const isString = (x) => typeof x === 'string';
58
+ const hasExt = (path, exts) => exts.some((e) => path.endsWith(`.${e}`));
59
+ if (section == null)
60
+ return { ok: true, value: section };
61
+ if (typeof section !== 'object')
62
+ return { ok: true, value: section };
63
+ if (Reflect.has(section, 'input')) {
64
+ const input = Reflect.get(section, 'input');
65
+ if (isString(input) && !hasExt(input, inputExts)) {
66
+ return { ok: false, error: 'input must be a .yaml, .json, or .tsp file' };
67
+ }
68
+ }
69
+ if (Reflect.has(section, 'output')) {
70
+ const output = Reflect.get(section, 'output');
71
+ if (isString(output) && !hasExt(output, outputExts)) {
72
+ return { ok: false, error: 'output must be a .ts file' };
73
+ }
74
+ }
75
+ return { ok: true, value: section };
76
+ }
54
77
  /**
55
78
  * Normalize a JSON Schema `type` value into an array of type strings.
56
79
  *
@@ -302,6 +325,22 @@ export function getHandlerImports(handlerMaps) {
302
325
  export function isRefObject(value) {
303
326
  return typeof value === 'object' && value !== null;
304
327
  }
328
+ /**
329
+ * Checks if a string is a valid HTTP method.
330
+ *
331
+ * @param method - The HTTP method to check.
332
+ * @returns `true` if the method is a valid HTTP method; otherwise `false`.
333
+ */
334
+ export function isHttpMethod(method) {
335
+ return (method === 'get' ||
336
+ method === 'put' ||
337
+ method === 'post' ||
338
+ method === 'delete' ||
339
+ method === 'patch' ||
340
+ method === 'options' ||
341
+ method === 'head' ||
342
+ method === 'trace');
343
+ }
305
344
  /**
306
345
  * Checks if all given content types share the same schema definition.
307
346
  *
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.9.21",
4
+ "version": "0.9.23",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [