hono-takibi 0.9.72 → 0.9.73

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.
@@ -60,8 +60,8 @@ const formatPath = (p) => {
60
60
  };
61
61
  }
62
62
  const segs = p.replace(/^\/+/, '').split('/').filter(Boolean);
63
- // Convert {param} to :param
64
- const honoSegs = segs.map((seg) => seg.startsWith('{') && seg.endsWith('}') ? `:${seg.slice(1, -1)}` : seg);
63
+ // Convert {param} to :param (handles both full segments like {id} and partial like {Sid}.json)
64
+ const honoSegs = segs.map((seg) => seg.replace(/\{([^}]+)\}/g, ':$1'));
65
65
  // Find the first segment that needs bracket notation
66
66
  const firstBracketIdx = honoSegs.findIndex((seg) => !isValidIdent(seg));
67
67
  const hasBracket = firstBracketIdx !== -1;
@@ -130,24 +130,15 @@ const refRequestBodyName = (refLike) => {
130
130
  const pickAllBodyInfoFromContent = (content) => {
131
131
  if (!isRecord(content))
132
132
  return undefined;
133
- const formInfos = [];
134
- const jsonInfos = [];
135
133
  const formContentTypes = ['multipart/form-data', 'application/x-www-form-urlencoded'];
136
- for (const [ct, mediaObj] of Object.entries(content)) {
137
- if (!(isRecord(mediaObj) && hasSchemaProp(mediaObj) && isRecord(mediaObj.schema)))
138
- continue;
139
- const info = { contentType: ct };
140
- // Extract base content type (before semicolon) for matching
141
- // e.g., "multipart/form-data; boundary=..." -> "multipart/form-data"
142
- const baseContentType = ct.split(';')[0].trim();
143
- if (formContentTypes.includes(baseContentType)) {
144
- formInfos.push(info);
145
- }
146
- else {
147
- // All other content types go to json
148
- jsonInfos.push(info);
149
- }
150
- }
134
+ const isFormContentType = (ct) => formContentTypes.includes(ct.split(';')[0].trim());
135
+ const validEntries = Object.entries(content).filter(([_, mediaObj]) => isRecord(mediaObj) && hasSchemaProp(mediaObj) && isRecord(mediaObj.schema));
136
+ const formInfos = validEntries
137
+ .filter(([ct]) => isFormContentType(ct))
138
+ .map(([ct]) => ({ contentType: ct }));
139
+ const jsonInfos = validEntries
140
+ .filter(([ct]) => !isFormContentType(ct))
141
+ .map(([ct]) => ({ contentType: ct }));
151
142
  if (formInfos.length === 0 && jsonInfos.length === 0)
152
143
  return undefined;
153
144
  return { form: formInfos, json: jsonInfos };
@@ -207,11 +198,18 @@ const generateOperationCode = (pathStr, method, item, deps) => {
207
198
  : `${deps.client}${runtimePath}${methodAccess}(undefined,options)`;
208
199
  const summary = typeof op.summary === 'string' ? op.summary : '';
209
200
  const description = typeof op.description === 'string' ? op.description : '';
201
+ // Format multiline description with JSDoc prefix on each line
202
+ const formatJsDocLines = (text) => text
203
+ .trimEnd()
204
+ .split('\n')
205
+ .map((line) => ` * ${line}`);
206
+ // Escape /* in path to avoid oxfmt regex parsing issue (/* looks like /regex/)
207
+ const safePathStr = pathStr.replace(/\/\*/g, '/[*]');
210
208
  const docs = [
211
209
  '/**',
212
- ` * ${method.toUpperCase()} ${pathStr}`,
213
- ...(summary ? [' *', ` * ${summary.trimEnd()}`] : []),
214
- ...(description ? [' *', ` * ${description.trimEnd()}`] : []),
210
+ ` * ${method.toUpperCase()} ${safePathStr}`,
211
+ ...(summary ? [' *', ...formatJsDocLines(summary)] : []),
212
+ ...(description ? [' *', ...formatJsDocLines(description)] : []),
215
213
  ' */',
216
214
  ].join('\n');
217
215
  const func = `export async function ${funcName}(${argSig}){return await ${call}}`;
@@ -136,13 +136,17 @@ function makePathParams(openApiPath) {
136
136
  return Array.from(openApiPath.matchAll(/\{([^}]+)\}/g)).map((m) => m[1]);
137
137
  }
138
138
  function makeParamPart(params, components) {
139
- const props = params.map((p) => `${p.name}:${makeSchemaTypeString(makeParameterSchema(p), components, new Set())}`);
139
+ const props = params.map((p) => {
140
+ const safeKey = makeSafeKey(p.name);
141
+ return `${safeKey}:${makeSchemaTypeString(makeParameterSchema(p), components, new Set())}`;
142
+ });
140
143
  return props.length > 0 ? `{param:{${props.join(';')}}}` : undefined;
141
144
  }
142
145
  function makeQueryPart(params, components) {
143
146
  const props = params.map((p) => {
144
147
  const typeStr = makeSchemaTypeString(makeParameterSchema(p), components, new Set());
145
- return p.required ? `${p.name}:${typeStr}` : `${p.name}?:${typeStr}|undefined`;
148
+ const safeKey = makeSafeKey(p.name);
149
+ return p.required ? `${safeKey}:${typeStr}` : `${safeKey}?:${typeStr}|undefined`;
146
150
  });
147
151
  return props.length > 0 ? `{query:{${props.join(';')}}}` : undefined;
148
152
  }
@@ -80,14 +80,14 @@ export function isHttpMethod(method) {
80
80
  * methodPath('get', '/users/{id}/posts') // 'getUsersIdPosts'
81
81
  */
82
82
  export function methodPath(method, path) {
83
- // 1. api_path: `/user/createWithList`
84
- // 2. replace(/[\/{}-]/g, ' ') -> ` user createWithList`
85
- // 3. trim() -> `user createWithList`
86
- // 4. split(/\s+/) -> `['user', 'createWithList']`
83
+ // 1. api_path: `/user/createWithList` or `/applications/@me`
84
+ // 2. replace(/[^A-Za-z0-9]/g, ' ') -> ` user createWithList` or ` applications me`
85
+ // 3. trim() -> `user createWithList` or `applications me`
86
+ // 4. split(/\s+/) -> `['user', 'createWithList']` or `['applications', 'me']`
87
87
  // 5. map((str) => `${str.charAt(0).toUpperCase()}${str.slice(1)}`) -> `['User', 'CreateWithList']`
88
- // 6. join('') -> `UserCreateWithList`
88
+ // 6. join('') -> `UserCreateWithList` or `ApplicationsMe`
89
89
  const apiPath = path
90
- .replace(/[/{}._-]/g, ' ')
90
+ .replace(/[^A-Za-z0-9]/g, ' ')
91
91
  .trim()
92
92
  .split(/\s+/)
93
93
  .map((str) => `${str.charAt(0).toUpperCase()}${str.slice(1)}`)
@@ -1,42 +1 @@
1
- /**
2
- * Creates a Vite plugin for hono-takibi code generation.
3
- *
4
- * This plugin automatically regenerates TypeScript code from OpenAPI
5
- * specifications during development. It watches for changes in:
6
- * - The OpenAPI spec file (yaml/json/tsp)
7
- * - The hono-takibi.config.ts configuration file
8
- *
9
- * ```mermaid
10
- * sequenceDiagram
11
- * participant V as Vite
12
- * participant P as Plugin
13
- * participant C as Config
14
- * participant G as Generator
15
- *
16
- * V->>P: configureServer()
17
- * P->>C: loadConfigHot()
18
- * C-->>P: config
19
- * P->>G: runAllWithConf()
20
- * G-->>P: logs
21
- * P->>V: hot reload
22
- *
23
- * Note over V,G: On file change
24
- * V->>P: handleHotUpdate()
25
- * P->>G: runAllWithConf()
26
- * G-->>P: logs
27
- * P->>V: full-reload
28
- * ```
29
- *
30
- * @returns Vite plugin object
31
- *
32
- * @example
33
- * ```ts
34
- * // vite.config.ts
35
- * import { honoTakibiVite } from 'hono-takibi/vite-plugin'
36
- *
37
- * export default defineConfig({
38
- * plugins: [honoTakibiVite()]
39
- * })
40
- * ```
41
- */
42
1
  export declare function honoTakibiVite(): any;
@@ -165,13 +165,14 @@ const runAllWithConf = async (config) => {
165
165
  if (!openAPIResult.ok)
166
166
  return { logs: [`✗ parseOpenAPI: ${openAPIResult.error}`] };
167
167
  const openAPI = openAPIResult.value;
168
- const jobs = [];
169
- // zod-openapi top-level output (non-split)
170
- if (config['zod-openapi'] &&
171
- !(config['zod-openapi'].components?.schemas || config['zod-openapi'].routes) &&
172
- config['zod-openapi'].output) {
168
+ // Job makers - each returns undefined if not applicable
169
+ const makeZodOpenAPIJob = () => {
170
+ if (!(config['zod-openapi'] &&
171
+ !(config['zod-openapi'].components?.schemas || config['zod-openapi'].routes) &&
172
+ config['zod-openapi'].output))
173
+ return undefined;
173
174
  const out = toAbs(config['zod-openapi'].output);
174
- const runZodOpenAPI = async () => {
175
+ return (async () => {
175
176
  if (!isTsFile(out))
176
177
  return `✗ zod-openapi: Invalid output format: ${out}`;
177
178
  const result = await takibi(openAPI, out, false, false, '/', {
@@ -189,187 +190,202 @@ const runAllWithConf = async (config) => {
189
190
  exportCallbacks: config['zod-openapi']?.exportCallbacks ?? false,
190
191
  });
191
192
  return result.ok ? `✓ zod-openapi -> ${out}` : `✗ zod-openapi: ${result.error}`;
192
- };
193
- jobs.push(runZodOpenAPI());
194
- }
195
- // components.schemas
196
- if (config['zod-openapi']?.components?.schemas) {
197
- const schemasConfig = config['zod-openapi'].components.schemas;
198
- const runSchema = async () => {
199
- if (schemasConfig.split === true) {
200
- const outDir = toAbs(schemasConfig.output);
193
+ })();
194
+ };
195
+ const makeSchemaJob = () => {
196
+ const cfg = config['zod-openapi']?.components?.schemas;
197
+ if (!cfg)
198
+ return undefined;
199
+ return (async () => {
200
+ if (cfg.split === true) {
201
+ const outDir = toAbs(cfg.output);
201
202
  const removed = await deleteAllTsShallow(outDir);
202
- const schemaResult = await schemas(openAPI.components?.schemas, outDir, true, schemasConfig.exportTypes === true);
203
+ const schemaResult = await schemas(openAPI.components?.schemas, outDir, true, cfg.exportTypes === true);
203
204
  if (!schemaResult.ok)
204
205
  return `✗ schemas(split): ${schemaResult.error}`;
205
206
  return removed.length > 0
206
207
  ? `✓ schemas(split) -> ${outDir}/*.ts (cleaned ${removed.length})`
207
208
  : `✓ schemas(split) -> ${outDir}/*.ts`;
208
209
  }
209
- const out = toAbs(schemasConfig.output);
210
- const schemaResult = await schemas(openAPI.components?.schemas, out, false, schemasConfig.exportTypes === true);
210
+ const out = toAbs(cfg.output);
211
+ const schemaResult = await schemas(openAPI.components?.schemas, out, false, cfg.exportTypes === true);
211
212
  return schemaResult.ok ? `✓ schemas -> ${out}` : `✗ schemas: ${schemaResult.error}`;
212
- };
213
- jobs.push(runSchema());
214
- }
215
- // components.parameters
216
- if (config['zod-openapi']?.components?.parameters) {
217
- const parametersConfig = config['zod-openapi'].components.parameters;
218
- const runParameters = async () => {
219
- const outDir = toAbs(parametersConfig.output);
220
- if (parametersConfig.split === true)
213
+ })();
214
+ };
215
+ const makeParametersJob = () => {
216
+ const cfg = config['zod-openapi']?.components?.parameters;
217
+ if (!cfg)
218
+ return undefined;
219
+ return (async () => {
220
+ const outDir = toAbs(cfg.output);
221
+ if (cfg.split === true)
221
222
  await deleteAllTsShallow(outDir);
222
- const parameterResult = await parameters(openAPI.components?.parameters, outDir, parametersConfig.split === true, parametersConfig.exportTypes === true, config['zod-openapi']?.components);
223
+ const parameterResult = await parameters(openAPI.components?.parameters, outDir, cfg.split === true, cfg.exportTypes === true, config['zod-openapi']?.components);
223
224
  return parameterResult.ok
224
- ? `✓ parameters${parametersConfig.split === true ? '(split)' : ''} -> ${outDir}`
225
+ ? `✓ parameters${cfg.split === true ? '(split)' : ''} -> ${outDir}`
225
226
  : `✗ parameters: ${parameterResult.error}`;
226
- };
227
- jobs.push(runParameters());
228
- }
229
- // components.headers
230
- if (config['zod-openapi']?.components?.headers) {
231
- const headersConfig = config['zod-openapi'].components.headers;
232
- const runHeaders = async () => {
233
- const outDir = toAbs(headersConfig.output);
234
- if (headersConfig.split === true)
227
+ })();
228
+ };
229
+ const makeHeadersJob = () => {
230
+ const cfg = config['zod-openapi']?.components?.headers;
231
+ if (!cfg)
232
+ return undefined;
233
+ return (async () => {
234
+ const outDir = toAbs(cfg.output);
235
+ if (cfg.split === true)
235
236
  await deleteAllTsShallow(outDir);
236
- const headersResult = await headers(openAPI.components?.headers, outDir, headersConfig.split === true, headersConfig.exportTypes === true, config['zod-openapi']?.components);
237
+ const headersResult = await headers(openAPI.components?.headers, outDir, cfg.split === true, cfg.exportTypes === true, config['zod-openapi']?.components);
237
238
  return headersResult.ok
238
- ? `✓ headers${headersConfig.split === true ? '(split)' : ''} -> ${outDir}`
239
+ ? `✓ headers${cfg.split === true ? '(split)' : ''} -> ${outDir}`
239
240
  : `✗ headers: ${headersResult.error}`;
240
- };
241
- jobs.push(runHeaders());
242
- }
243
- // components.examples
244
- if (config['zod-openapi']?.components?.examples) {
245
- const examplesConfig = config['zod-openapi'].components.examples;
246
- const runExamples = async () => {
247
- const outDir = toAbs(examplesConfig.output);
248
- if (examplesConfig.split === true)
241
+ })();
242
+ };
243
+ const makeExamplesJob = () => {
244
+ const cfg = config['zod-openapi']?.components?.examples;
245
+ if (!cfg)
246
+ return undefined;
247
+ return (async () => {
248
+ const outDir = toAbs(cfg.output);
249
+ if (cfg.split === true)
249
250
  await deleteAllTsShallow(outDir);
250
- const examplesResult = await examples(openAPI.components?.examples, outDir, examplesConfig.split === true);
251
+ const examplesResult = await examples(openAPI.components?.examples, outDir, cfg.split === true);
251
252
  return examplesResult.ok
252
- ? `✓ examples${examplesConfig.split === true ? '(split)' : ''} -> ${outDir}`
253
+ ? `✓ examples${cfg.split === true ? '(split)' : ''} -> ${outDir}`
253
254
  : `✗ examples: ${examplesResult.error}`;
254
- };
255
- jobs.push(runExamples());
256
- }
257
- // components.links
258
- if (config['zod-openapi']?.components?.links) {
259
- const linksConfig = config['zod-openapi'].components.links;
260
- const runLinks = async () => {
261
- const outDir = toAbs(linksConfig.output);
262
- if (linksConfig.split === true)
255
+ })();
256
+ };
257
+ const makeLinksJob = () => {
258
+ const cfg = config['zod-openapi']?.components?.links;
259
+ if (!cfg)
260
+ return undefined;
261
+ return (async () => {
262
+ const outDir = toAbs(cfg.output);
263
+ if (cfg.split === true)
263
264
  await deleteAllTsShallow(outDir);
264
- const linksResult = await links(openAPI.components?.links, outDir, linksConfig.split === true);
265
+ const linksResult = await links(openAPI.components?.links, outDir, cfg.split === true);
265
266
  return linksResult.ok
266
- ? `✓ links${linksConfig.split === true ? '(split)' : ''} -> ${outDir}`
267
+ ? `✓ links${cfg.split === true ? '(split)' : ''} -> ${outDir}`
267
268
  : `✗ links: ${linksResult.error}`;
268
- };
269
- jobs.push(runLinks());
270
- }
271
- // components.callbacks
272
- if (config['zod-openapi']?.components?.callbacks) {
273
- const callbacksConfig = config['zod-openapi'].components.callbacks;
274
- const runCallbacks = async () => {
275
- const outDir = toAbs(callbacksConfig.output);
276
- if (callbacksConfig.split === true)
269
+ })();
270
+ };
271
+ const makeCallbacksJob = () => {
272
+ const cfg = config['zod-openapi']?.components?.callbacks;
273
+ if (!cfg)
274
+ return undefined;
275
+ return (async () => {
276
+ const outDir = toAbs(cfg.output);
277
+ if (cfg.split === true)
277
278
  await deleteAllTsShallow(outDir);
278
- const callbacksResult = await callbacks(openAPI.components?.callbacks, outDir, callbacksConfig.split === true);
279
+ const callbacksResult = await callbacks(openAPI.components?.callbacks, outDir, cfg.split === true);
279
280
  return callbacksResult.ok
280
- ? `✓ callbacks${callbacksConfig.split === true ? '(split)' : ''} -> ${outDir}`
281
+ ? `✓ callbacks${cfg.split === true ? '(split)' : ''} -> ${outDir}`
281
282
  : `✗ callbacks: ${callbacksResult.error}`;
282
- };
283
- jobs.push(runCallbacks());
284
- }
285
- // components.securitySchemes
286
- if (config['zod-openapi']?.components?.securitySchemes) {
287
- const securitySchemesConfig = config['zod-openapi'].components.securitySchemes;
288
- const runSecuritySchemes = async () => {
289
- const outDir = toAbs(securitySchemesConfig.output);
290
- if (securitySchemesConfig.split === true)
283
+ })();
284
+ };
285
+ const makeSecuritySchemesJob = () => {
286
+ const cfg = config['zod-openapi']?.components?.securitySchemes;
287
+ if (!cfg)
288
+ return undefined;
289
+ return (async () => {
290
+ const outDir = toAbs(cfg.output);
291
+ if (cfg.split === true)
291
292
  await deleteAllTsShallow(outDir);
292
- const securitySchemesResult = await securitySchemes(openAPI.components?.securitySchemes, outDir, securitySchemesConfig.split === true);
293
+ const securitySchemesResult = await securitySchemes(openAPI.components?.securitySchemes, outDir, cfg.split === true);
293
294
  return securitySchemesResult.ok
294
- ? `✓ securitySchemes${securitySchemesConfig.split === true ? '(split)' : ''} -> ${outDir}`
295
+ ? `✓ securitySchemes${cfg.split === true ? '(split)' : ''} -> ${outDir}`
295
296
  : `✗ securitySchemes: ${securitySchemesResult.error}`;
296
- };
297
- jobs.push(runSecuritySchemes());
298
- }
299
- // components.requestBodies
300
- if (config['zod-openapi']?.components?.requestBodies) {
301
- const requestBodiesConfig = config['zod-openapi'].components.requestBodies;
302
- const runRequestBodies = async () => {
303
- const outDir = toAbs(requestBodiesConfig.output);
304
- if (requestBodiesConfig.split === true)
297
+ })();
298
+ };
299
+ const makeRequestBodiesJob = () => {
300
+ const cfg = config['zod-openapi']?.components?.requestBodies;
301
+ if (!cfg)
302
+ return undefined;
303
+ return (async () => {
304
+ const outDir = toAbs(cfg.output);
305
+ if (cfg.split === true)
305
306
  await deleteAllTsShallow(outDir);
306
- const requestBodiesResult = await requestBodies(openAPI.components?.requestBodies, outDir, requestBodiesConfig.split === true, config['zod-openapi']?.components);
307
+ const requestBodiesResult = await requestBodies(openAPI.components?.requestBodies, outDir, cfg.split === true, config['zod-openapi']?.components);
307
308
  return requestBodiesResult.ok
308
- ? `✓ requestBodies${requestBodiesConfig.split === true ? '(split)' : ''} -> ${outDir}`
309
+ ? `✓ requestBodies${cfg.split === true ? '(split)' : ''} -> ${outDir}`
309
310
  : `✗ requestBodies: ${requestBodiesResult.error}`;
310
- };
311
- jobs.push(runRequestBodies());
312
- }
313
- // components.responses
314
- if (config['zod-openapi']?.components?.responses) {
315
- const responsesConfig = config['zod-openapi'].components.responses;
316
- const runResponses = async () => {
317
- const outDir = toAbs(responsesConfig.output);
318
- if (responsesConfig.split === true)
311
+ })();
312
+ };
313
+ const makeResponsesJob = () => {
314
+ const cfg = config['zod-openapi']?.components?.responses;
315
+ if (!cfg)
316
+ return undefined;
317
+ return (async () => {
318
+ const outDir = toAbs(cfg.output);
319
+ if (cfg.split === true)
319
320
  await deleteAllTsShallow(outDir);
320
- const responsesResult = await responses(openAPI.components?.responses, outDir, responsesConfig.split === true, config['zod-openapi']?.components);
321
+ const responsesResult = await responses(openAPI.components?.responses, outDir, cfg.split === true, config['zod-openapi']?.components);
321
322
  return responsesResult.ok
322
- ? `✓ responses${responsesConfig.split === true ? '(split)' : ''} -> ${outDir}`
323
+ ? `✓ responses${cfg.split === true ? '(split)' : ''} -> ${outDir}`
323
324
  : `✗ responses: ${responsesResult.error}`;
324
- };
325
- jobs.push(runResponses());
326
- }
327
- // zod-openapi.routes
328
- if (config['zod-openapi']?.routes) {
329
- const routesConfig = config['zod-openapi'].routes;
330
- const runRoutes = async () => {
331
- const out = toAbs(routesConfig.output);
332
- if (routesConfig.split === true)
325
+ })();
326
+ };
327
+ const makeRoutesJob = () => {
328
+ const cfg = config['zod-openapi']?.routes;
329
+ if (!cfg)
330
+ return undefined;
331
+ return (async () => {
332
+ const out = toAbs(cfg.output);
333
+ if (cfg.split === true)
333
334
  await deleteAllTsShallow(out);
334
- const routeResult = await route(openAPI, { output: out, split: routesConfig.split ?? false }, config['zod-openapi']?.components);
335
+ const routeResult = await route(openAPI, { output: out, split: cfg.split ?? false }, config['zod-openapi']?.components);
335
336
  return routeResult.ok
336
- ? `✓ routes${routesConfig.split === true ? '(split)' : ''} -> ${out}`
337
+ ? `✓ routes${cfg.split === true ? '(split)' : ''} -> ${out}`
337
338
  : `✗ routes: ${routeResult.error}`;
338
- };
339
- jobs.push(runRoutes());
340
- }
341
- // type
342
- if (config.type) {
343
- const typeConfig = config.type;
344
- const out = toAbs(typeConfig.output);
345
- const runType = async () => {
339
+ })();
340
+ };
341
+ const makeTypeJob = () => {
342
+ const cfg = config.type;
343
+ if (!cfg)
344
+ return undefined;
345
+ return (async () => {
346
+ const out = toAbs(cfg.output);
346
347
  if (!isTsFile(out))
347
348
  return `✗ type: Invalid output format: ${out}`;
348
349
  const typeResult = await type(openAPI, out);
349
350
  return typeResult.ok ? `✓ type -> ${out}` : `✗ type: ${typeResult.error}`;
350
- };
351
- jobs.push(runType());
352
- }
353
- // rpc
354
- if (config.rpc) {
355
- const rpcConfig = config.rpc;
356
- const runRpc = async () => {
357
- if (rpcConfig.split === true) {
358
- const outDir = toAbs(rpcConfig.output);
351
+ })();
352
+ };
353
+ const makeRpcJob = () => {
354
+ const cfg = config.rpc;
355
+ if (!cfg)
356
+ return undefined;
357
+ return (async () => {
358
+ if (cfg.split === true) {
359
+ const outDir = toAbs(cfg.output);
359
360
  const removed = await deleteAllTsShallow(outDir);
360
- const rpcResult = await rpc(openAPI, outDir, rpcConfig.import, true);
361
+ const rpcResult = await rpc(openAPI, outDir, cfg.import, true);
361
362
  if (!rpcResult.ok)
362
363
  return `✗ rpc(split): ${rpcResult.error}`;
363
364
  return removed.length > 0
364
365
  ? `✓ rpc(split) -> ${outDir}/*.ts (cleaned ${removed.length})`
365
366
  : `✓ rpc(split) -> ${outDir}/*.ts`;
366
367
  }
367
- const out = toAbs(rpcConfig.output);
368
- const rpcResult = await rpc(openAPI, out, rpcConfig.import, false);
368
+ const out = toAbs(cfg.output);
369
+ const rpcResult = await rpc(openAPI, out, cfg.import, false);
369
370
  return rpcResult.ok ? `✓ rpc -> ${out}` : `✗ rpc: ${rpcResult.error}`;
370
- };
371
- jobs.push(runRpc());
372
- }
371
+ })();
372
+ };
373
+ // Build jobs array immutably - filter out undefined
374
+ const jobs = [
375
+ makeZodOpenAPIJob(),
376
+ makeSchemaJob(),
377
+ makeParametersJob(),
378
+ makeHeadersJob(),
379
+ makeExamplesJob(),
380
+ makeLinksJob(),
381
+ makeCallbacksJob(),
382
+ makeSecuritySchemesJob(),
383
+ makeRequestBodiesJob(),
384
+ makeResponsesJob(),
385
+ makeRoutesJob(),
386
+ makeTypeJob(),
387
+ makeRpcJob(),
388
+ ].filter((job) => job !== undefined);
373
389
  return Promise.all(jobs).then((logs) => ({ logs }));
374
390
  };
375
391
  /* ──────────────────────────────────────────────────────────────
@@ -438,9 +454,52 @@ const addInputGlobs = (server, absInput) => {
438
454
  * })
439
455
  * ```
440
456
  */
457
+ /**
458
+ * Extracts all output paths from a configuration.
459
+ */
460
+ const extractOutputPaths = (conf) => [
461
+ conf['zod-openapi']?.output,
462
+ conf['zod-openapi']?.components?.schemas?.output,
463
+ conf['zod-openapi']?.components?.parameters?.output,
464
+ conf['zod-openapi']?.components?.headers?.output,
465
+ conf['zod-openapi']?.components?.examples?.output,
466
+ conf['zod-openapi']?.components?.links?.output,
467
+ conf['zod-openapi']?.components?.callbacks?.output,
468
+ conf['zod-openapi']?.components?.securitySchemes?.output,
469
+ conf['zod-openapi']?.components?.requestBodies?.output,
470
+ conf['zod-openapi']?.components?.responses?.output,
471
+ conf['zod-openapi']?.routes?.output,
472
+ conf.type?.output,
473
+ conf.rpc?.output,
474
+ ]
475
+ .filter((p) => p !== undefined)
476
+ .map(toAbs);
477
+ /**
478
+ * Cleans up output paths that exist in previous config but not in current config.
479
+ */
480
+ const cleanupStaleOutputs = async (prev, curr) => {
481
+ const prevPaths = new Set(extractOutputPaths(prev));
482
+ const currPaths = new Set(extractOutputPaths(curr));
483
+ const stalePaths = [...prevPaths].filter((p) => !currPaths.has(p));
484
+ const results = await Promise.all(stalePaths.map(async (p) => {
485
+ const stat = await fsp.stat(p).catch(() => null);
486
+ if (!stat)
487
+ return null;
488
+ if (stat.isDirectory()) {
489
+ const removed = await deleteAllTsShallow(p);
490
+ return removed.length > 0 ? `${p}/*.ts (${removed.length} files)` : null;
491
+ }
492
+ if (stat.isFile() && p.endsWith('.ts')) {
493
+ await fsp.unlink(p).catch(() => { });
494
+ return p;
495
+ }
496
+ return null;
497
+ }));
498
+ return results.filter((r) => r !== null);
499
+ };
441
500
  // biome-ignore lint: plugin returns any for Vite compatibility
442
501
  export function honoTakibiVite() {
443
- const state = { current: null };
502
+ const state = { current: null, previous: null };
444
503
  const absConfig = path.resolve(process.cwd(), 'hono-takibi.config.ts');
445
504
  const run = async () => {
446
505
  if (!state.current)
@@ -460,6 +519,13 @@ export function honoTakibiVite() {
460
519
  console.error(`[hono-takibi] ✗ config: ${next.error}`);
461
520
  return;
462
521
  }
522
+ // Cleanup stale outputs from previous config
523
+ if (state.current) {
524
+ const cleaned = await cleanupStaleOutputs(state.current, next.value);
525
+ for (const p of cleaned)
526
+ console.log(`[hono-takibi] ✓ cleanup: ${p}`);
527
+ }
528
+ state.previous = state.current;
463
529
  state.current = next.value;
464
530
  addInputGlobs(server, toAbs(state.current.input));
465
531
  await runAndReload(server);
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.72",
4
+ "version": "0.9.73",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [