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.
- package/dist/core/rpc/index.js +20 -22
- package/dist/core/type/index.js +6 -2
- package/dist/utils/index.js +6 -6
- package/dist/vite-plugin/index.d.ts +0 -41
- package/dist/vite-plugin/index.js +207 -141
- package/package.json +1 -1
package/dist/core/rpc/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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()} ${
|
|
213
|
-
...(summary ? [' *',
|
|
214
|
-
...(description ? [' *',
|
|
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}}`;
|
package/dist/core/type/index.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
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
|
}
|
package/dist/utils/index.js
CHANGED
|
@@ -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(/[
|
|
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(/[
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
const outDir = toAbs(
|
|
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,
|
|
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(
|
|
210
|
-
const schemaResult = await schemas(openAPI.components?.schemas, out, false,
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const outDir = toAbs(
|
|
220
|
-
if (
|
|
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,
|
|
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${
|
|
225
|
+
? `✓ parameters${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
225
226
|
: `✗ parameters: ${parameterResult.error}`;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const outDir = toAbs(
|
|
234
|
-
if (
|
|
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,
|
|
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${
|
|
239
|
+
? `✓ headers${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
239
240
|
: `✗ headers: ${headersResult.error}`;
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const outDir = toAbs(
|
|
248
|
-
if (
|
|
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,
|
|
251
|
+
const examplesResult = await examples(openAPI.components?.examples, outDir, cfg.split === true);
|
|
251
252
|
return examplesResult.ok
|
|
252
|
-
? `✓ examples${
|
|
253
|
+
? `✓ examples${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
253
254
|
: `✗ examples: ${examplesResult.error}`;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const outDir = toAbs(
|
|
262
|
-
if (
|
|
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,
|
|
265
|
+
const linksResult = await links(openAPI.components?.links, outDir, cfg.split === true);
|
|
265
266
|
return linksResult.ok
|
|
266
|
-
? `✓ links${
|
|
267
|
+
? `✓ links${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
267
268
|
: `✗ links: ${linksResult.error}`;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const outDir = toAbs(
|
|
276
|
-
if (
|
|
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,
|
|
279
|
+
const callbacksResult = await callbacks(openAPI.components?.callbacks, outDir, cfg.split === true);
|
|
279
280
|
return callbacksResult.ok
|
|
280
|
-
? `✓ callbacks${
|
|
281
|
+
? `✓ callbacks${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
281
282
|
: `✗ callbacks: ${callbacksResult.error}`;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const outDir = toAbs(
|
|
290
|
-
if (
|
|
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,
|
|
293
|
+
const securitySchemesResult = await securitySchemes(openAPI.components?.securitySchemes, outDir, cfg.split === true);
|
|
293
294
|
return securitySchemesResult.ok
|
|
294
|
-
? `✓ securitySchemes${
|
|
295
|
+
? `✓ securitySchemes${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
295
296
|
: `✗ securitySchemes: ${securitySchemesResult.error}`;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const outDir = toAbs(
|
|
304
|
-
if (
|
|
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,
|
|
307
|
+
const requestBodiesResult = await requestBodies(openAPI.components?.requestBodies, outDir, cfg.split === true, config['zod-openapi']?.components);
|
|
307
308
|
return requestBodiesResult.ok
|
|
308
|
-
? `✓ requestBodies${
|
|
309
|
+
? `✓ requestBodies${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
309
310
|
: `✗ requestBodies: ${requestBodiesResult.error}`;
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const outDir = toAbs(
|
|
318
|
-
if (
|
|
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,
|
|
321
|
+
const responsesResult = await responses(openAPI.components?.responses, outDir, cfg.split === true, config['zod-openapi']?.components);
|
|
321
322
|
return responsesResult.ok
|
|
322
|
-
? `✓ responses${
|
|
323
|
+
? `✓ responses${cfg.split === true ? '(split)' : ''} -> ${outDir}`
|
|
323
324
|
: `✗ responses: ${responsesResult.error}`;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const out = toAbs(
|
|
332
|
-
if (
|
|
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:
|
|
335
|
+
const routeResult = await route(openAPI, { output: out, split: cfg.split ?? false }, config['zod-openapi']?.components);
|
|
335
336
|
return routeResult.ok
|
|
336
|
-
? `✓ routes${
|
|
337
|
+
? `✓ routes${cfg.split === true ? '(split)' : ''} -> ${out}`
|
|
337
338
|
: `✗ routes: ${routeResult.error}`;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
const outDir = toAbs(
|
|
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,
|
|
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(
|
|
368
|
-
const rpcResult = await rpc(openAPI, out,
|
|
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
|
-
|
|
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);
|