hono-takibi 0.9.99994 → 0.9.99995

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
@@ -200,7 +200,6 @@ export default app
200
200
  ```ts
201
201
  export default defineConfig({
202
202
  input: 'openapi.yaml',
203
- output: './src/index.ts',
204
203
  template: { define: true },
205
204
  })
206
205
  ```
@@ -221,7 +220,7 @@ export const getUsersIdRoute = defineOpenAPIRoute({
221
220
  })
222
221
  ```
223
222
 
224
- Component schemas go to `components/index.ts` (override with `components.output`). Set `template.output` to change the route directory (default `./src/routes`).
223
+ The app entry defaults to `src/index.ts` (override with `output`, which must be an `index.ts` path such as `./server/index.ts`). Route files always go to `routes/` next to the app entry, and component schemas to `components/index.ts` (override with `components.output`). When `output` is omitted, `components.output` also anchors the layout: `components: { output: './server/components/index.ts' }` puts the app entry at `server/index.ts` and routes at `server/routes/`.
225
224
 
226
225
  ## Client Library Integrations
227
226
 
@@ -305,7 +304,7 @@ export default defineConfig({
305
304
 
306
305
  ## Full Config Reference
307
306
 
308
- Some options are mutually exclusive: `output` ↔ `routes`, `components.output` ↔ per-type components, `template.define` ↔ `routeHandler`.
307
+ Some options are mutually exclusive: `output` ↔ `routes`, `template.define` ↔ `routes`, `components.output` ↔ per-type components, `template.define` ↔ `routeHandler`.
309
308
 
310
309
  ```ts
311
310
  import { defineConfig } from 'hono-takibi/config'
@@ -313,7 +312,7 @@ import { defineConfig } from 'hono-takibi/config'
313
312
  export default defineConfig({
314
313
  input: 'openapi.yaml',
315
314
 
316
- output: './src/routes.ts', // single-file mode; with template.define, the app entry (required)
315
+ output: './src/routes.ts', // single-file mode; with template.define, the app entry (an index.ts path, default ./src/index.ts)
317
316
  basePath: '/api',
318
317
  readonly: true,
319
318
  // format: {}, // oxfmt FormatConfig
@@ -322,7 +321,6 @@ export default defineConfig({
322
321
  test: true,
323
322
  routeHandler: false, // true: RouteHandler exports
324
323
  define: false, // true: defineOpenAPIRoute output
325
- // output: './src/routes', // define mode dir (default ./src/routes)
326
324
  pathAlias: '@/',
327
325
  testFramework: 'vitest', // "vitest" | "vite-plus" | "bun"
328
326
  },
@@ -10,7 +10,6 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodObject<{
10
10
  format: z.ZodExactOptional<z.ZodCustom<FormatConfig, FormatConfig>>;
11
11
  template: z.ZodExactOptional<z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
12
12
  define: z.ZodLiteral<true>;
13
- output: z.ZodDefault<z.ZodString>;
14
13
  test: z.ZodDefault<z.ZodBoolean>;
15
14
  pathAlias: z.ZodExactOptional<z.ZodString>;
16
15
  testFramework: z.ZodExactOptional<z.ZodDefault<z.ZodEnum<{
@@ -21,7 +20,6 @@ declare const ConfigSchema: z.ZodReadonly<z.ZodObject<{
21
20
  }, z.core.$strip>>, z.ZodReadonly<z.ZodObject<{
22
21
  define: z.ZodDefault<z.ZodOptional<z.ZodLiteral<false>>>;
23
22
  routeHandler: z.ZodDefault<z.ZodBoolean>;
24
- output: z.ZodExactOptional<z.ZodString>;
25
23
  test: z.ZodDefault<z.ZodBoolean>;
26
24
  pathAlias: z.ZodExactOptional<z.ZodString>;
27
25
  testFramework: z.ZodExactOptional<z.ZodDefault<z.ZodEnum<{
@@ -330,7 +328,6 @@ declare function parseConfig(config: unknown): {
330
328
  format?: FormatConfig;
331
329
  template?: Readonly<{
332
330
  define: true;
333
- output: string;
334
331
  test: boolean;
335
332
  pathAlias?: string;
336
333
  testFramework?: "vitest" | "vite-plus" | "bun";
@@ -338,7 +335,6 @@ declare function parseConfig(config: unknown): {
338
335
  define: false;
339
336
  routeHandler: boolean;
340
337
  test: boolean;
341
- output?: string;
342
338
  pathAlias?: string;
343
339
  testFramework?: "vitest" | "vite-plus" | "bun";
344
340
  }>;
@@ -625,7 +621,6 @@ declare function readConfig(): Promise<{
625
621
  format?: FormatConfig;
626
622
  template?: Readonly<{
627
623
  define: true;
628
- output: string;
629
624
  test: boolean;
630
625
  pathAlias?: string;
631
626
  testFramework?: "vitest" | "vite-plus" | "bun";
@@ -633,7 +628,6 @@ declare function readConfig(): Promise<{
633
628
  define: false;
634
629
  routeHandler: boolean;
635
630
  test: boolean;
636
- output?: string;
637
631
  pathAlias?: string;
638
632
  testFramework?: "vitest" | "vite-plus" | "bun";
639
633
  }>;
@@ -902,14 +896,12 @@ declare function defineConfig(config: z.input<typeof ConfigSchema>): Readonly<{
902
896
  format?: FormatConfig;
903
897
  template?: Readonly<{
904
898
  define: true;
905
- output?: string | undefined;
906
899
  test?: boolean | undefined;
907
900
  pathAlias?: string;
908
901
  testFramework?: "vitest" | "vite-plus" | "bun" | undefined;
909
902
  }> | Readonly<{
910
903
  define?: false | undefined;
911
904
  routeHandler?: boolean | undefined;
912
- output?: string;
913
905
  test?: boolean | undefined;
914
906
  pathAlias?: string;
915
907
  testFramework?: "vitest" | "vite-plus" | "bun" | undefined;
@@ -1,3 +1,4 @@
1
+ import { i as deriveAppEntry } from "../utils-B5PbMfWs.js";
1
2
  import { existsSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
@@ -64,7 +65,6 @@ const ConfigSchema = z.object({
64
65
  format: z.custom(() => true).exactOptional(),
65
66
  template: z.discriminatedUnion("define", [z.object({
66
67
  define: z.literal(true),
67
- output: z.string().regex(/^(?!.*\.ts$).+/, { error: "template.output must be a directory, not a .ts file" }).default("src/routes"),
68
68
  test: z.boolean().default(false),
69
69
  pathAlias: z.string().exactOptional(),
70
70
  testFramework: z.enum([
@@ -75,7 +75,6 @@ const ConfigSchema = z.object({
75
75
  }).readonly(), z.object({
76
76
  define: z.literal(false).optional().default(false),
77
77
  routeHandler: z.boolean().default(false),
78
- output: z.string().regex(/^(?!.*\.ts$).+/, { error: "template.output must be a directory, not a .ts file" }).exactOptional(),
79
78
  test: z.boolean().default(false),
80
79
  pathAlias: z.string().exactOptional(),
81
80
  testFramework: z.enum([
@@ -174,7 +173,28 @@ const ConfigSchema = z.object({
174
173
  entry: z.string().exactOptional(),
175
174
  baseUrl: z.string().exactOptional()
176
175
  }).readonly()]).exactOptional()
177
- }).readonly().refine((v) => !(v.output && v.routes), { message: "output and routes are mutually exclusive. Use output for single-file mode, or routes for separate route output." }).refine((v) => !(v.template?.define && !v.output), { message: "template.define requires output (the app entry file, e.g. ./src/index.ts)." });
176
+ }).readonly().refine((v) => !(v.output && v.routes), { message: "output and routes are mutually exclusive. Use output for single-file mode, or routes for separate route output." }).refine((v) => !(v.template?.define === true && v.routes), { message: "template.define and routes are mutually exclusive. define derives routes/ next to the app entry (output, default src/index.ts)." }).refine((v) => !(v.template?.define === true && v.output !== void 0 && !(v.output === "index.ts" || v.output.endsWith("/index.ts"))), { message: "with template.define, output is the app entry and must be an index.ts file (e.g. ./src/index.ts), or omitted to default to src/index.ts. Other names collide with the derived routes/ directory." }).refine((v) => !(v.template?.define === true && v.components !== void 0 && [
177
+ "schemas",
178
+ "responses",
179
+ "parameters",
180
+ "examples",
181
+ "requestBodies",
182
+ "headers",
183
+ "securitySchemes",
184
+ "links",
185
+ "callbacks",
186
+ "pathItems",
187
+ "mediaTypes"
188
+ ].some((k) => v.components?.[k] !== void 0)), { message: "with template.define, per-type component outputs (components.schemas, components.responses, ...) are not supported. Use components.output for a single components file." }).refine((v) => {
189
+ if (v.template?.define !== true || v.components?.output === void 0) return true;
190
+ const normalize = (p) => p.replace(/^\.\//, "");
191
+ const componentsOutput = normalize(v.components.output);
192
+ const appEntry = normalize(v.output ?? deriveAppEntry(componentsOutput));
193
+ if (!appEntry.endsWith("index.ts")) return true;
194
+ const baseDir = appEntry === "index.ts" ? "" : appEntry.slice(0, -9);
195
+ const routesDir = baseDir === "" ? "routes" : `${baseDir}/routes`;
196
+ return componentsOutput !== appEntry && !componentsOutput.startsWith(`${routesDir}/`);
197
+ }, { message: "with template.define, components.output must not point at the app entry or inside the derived routes/ directory (it would be overwritten). Choose another path, e.g. src/components/index.ts." });
178
198
  function parseConfig(config) {
179
199
  const result = ConfigSchema.safeParse(config);
180
200
  if (!result.success) {
@@ -1,2 +1,2 @@
1
- import { t as docs } from "../../docs-380Wcu6a.js";
1
+ import { t as docs } from "../../docs-DbknIWmx.js";
2
2
  export { docs };
@@ -1,2 +1,2 @@
1
- import { t as hooks } from "../../hooks-CkiatAe9.js";
1
+ import { t as hooks } from "../../hooks-CCw7jm8g.js";
2
2
  export { hooks };
@@ -1,6 +1,7 @@
1
+ import { f as methodPath, u as makeInferRequestType } from "../../utils-B5PbMfWs.js";
1
2
  import { t as emit } from "../../emit-CFR63U4L.js";
2
- import { A as makeInferRequestType, M as methodPath, c as isOperationLike, f as isRecord, o as isOpenAPIPaths } from "../../guard-zvkMUVYD.js";
3
- import { a as parsePathItem, i as operationHasArgs, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "../../rpc-CepeggWU.js";
3
+ import { c as isOperationLike, f as isRecord, o as isOpenAPIPaths } from "../../guard-BSZ8ezEv.js";
4
+ import { a as parsePathItem, i as operationHasArgs, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "../../rpc-BzPo2tZf.js";
4
5
  import path from "node:path";
5
6
  //#region src/core/rpc/index.ts
6
7
  /**
@@ -1,5 +1,6 @@
1
+ import { d as makeSafeKey, g as statusCodeToNumber } from "../../utils-B5PbMfWs.js";
1
2
  import { t as emit } from "../../emit-CFR63U4L.js";
2
- import { I as statusCodeToNumber, g as isSchemaArray, i as isMediaWithSchema, j as makeSafeKey, l as isParameter, m as isRequestBody, n as isHttpMethod, s as isOperation, u as isParameterArray, x as isStringRef } from "../../guard-zvkMUVYD.js";
3
+ import { g as isSchemaArray, i as isMediaWithSchema, l as isParameter, m as isRequestBody, n as isHttpMethod, s as isOperation, u as isParameterArray, x as isStringRef } from "../../guard-BSZ8ezEv.js";
3
4
  import path from "node:path";
4
5
  //#region src/core/type/index.ts
5
6
  /**
@@ -1,5 +1,6 @@
1
+ import { s as escapeHtml } from "./utils-B5PbMfWs.js";
1
2
  import { a as writeFile, t as mkdir } from "./fsp-BXry-Hx5.js";
2
- import { D as escapeHtml, a as isOAuthFlowValue, b as isSecurityScheme, f as isRecord, h as isResponses, m as isRequestBody, p as isRefObject, r as isMedia, y as isSecurityArray } from "./guard-zvkMUVYD.js";
3
+ import { a as isOAuthFlowValue, b as isSecurityScheme, f as isRecord, h as isResponses, m as isRequestBody, p as isRefObject, r as isMedia, y as isSecurityArray } from "./guard-BSZ8ezEv.js";
3
4
  import path from "node:path";
4
5
  import { STATUS_CODES } from "node:http";
5
6
  //#region src/generator/docs/index.ts
@@ -1,2 +1,2 @@
1
- import { t as zodOpenAPIHono } from "../../../openapi-8ZYAMj_e.js";
1
+ import { t as zodOpenAPIHono } from "../../../openapi-BlTDQFFF.js";
2
2
  export { zodOpenAPIHono };
@@ -0,0 +1,83 @@
1
+ //#region src/guard/index.ts
2
+ function isRecord(v) {
3
+ return typeof v === "object" && v !== null && !Array.isArray(v);
4
+ }
5
+ function isHttpMethod(method) {
6
+ return method === "get" || method === "put" || method === "post" || method === "delete" || method === "patch" || method === "options" || method === "head" || method === "trace";
7
+ }
8
+ function isValidIdent(str) {
9
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
10
+ }
11
+ function isOpenAPIPaths(v) {
12
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
13
+ return Object.values(v).every((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
14
+ }
15
+ function isRefObject(v) {
16
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "$ref" in v && typeof v.$ref === "string";
17
+ }
18
+ function isStringRef(v) {
19
+ return "$ref" in v && typeof v.$ref === "string";
20
+ }
21
+ function isParameterObject(v) {
22
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
23
+ if (!("name" in v) || typeof v.name !== "string") return false;
24
+ if (!("in" in v)) return false;
25
+ const pos = v.in;
26
+ return pos === "path" || pos === "query" || pos === "header" || pos === "cookie";
27
+ }
28
+ function isParameter(v) {
29
+ return typeof v === "object" && v !== null && "name" in v && "in" in v && ("schema" in v || "content" in v);
30
+ }
31
+ function isParameterArray(v) {
32
+ return Array.isArray(v);
33
+ }
34
+ function isOperationLike(v) {
35
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "responses" in v;
36
+ }
37
+ function isOperation(v) {
38
+ return typeof v === "object" && v !== null && "responses" in v;
39
+ }
40
+ function isSchemaProperty(v) {
41
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "schema" in v;
42
+ }
43
+ function isSchemaArray(v) {
44
+ return Array.isArray(v);
45
+ }
46
+ /**
47
+ * JSON Schema 2020-12 §4.3.2 / §10.3.1.2 type guard for schema-vs-boolean
48
+ * discrimination. `items`, `additionalProperties`, `unevaluatedItems`, and
49
+ * `unevaluatedProperties` may be either an object schema or a boolean schema
50
+ * (`true` = pass-through, `false` = reject). This narrows the union to the
51
+ * object form so callers can recurse without `as` casts.
52
+ */
53
+ function isSchemaObject(v) {
54
+ return typeof v === "object" && v !== null && !Array.isArray(v);
55
+ }
56
+ function isMediaWithSchema(v) {
57
+ return typeof v === "object" && v !== null && "schema" in v;
58
+ }
59
+ function isMedia(v) {
60
+ return typeof v === "object" && v !== null && "schema" in v;
61
+ }
62
+ function isRequestBody(v) {
63
+ return typeof v === "object" && v !== null && ("content" in v || "required" in v || "description" in v);
64
+ }
65
+ function isContentBody(v) {
66
+ return typeof v === "object" && v !== null && !("$ref" in v);
67
+ }
68
+ function isSecurityScheme(v) {
69
+ return typeof v === "object" && v !== null && !("$ref" in v);
70
+ }
71
+ function isSecurityArray(v) {
72
+ return Array.isArray(v);
73
+ }
74
+ function isResponses(v) {
75
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
76
+ return "$ref" in v || "description" in v || "content" in v || "headers" in v || "links" in v;
77
+ }
78
+ function isOAuthFlowValue(v) {
79
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
80
+ return "authorizationUrl" in v || "tokenUrl" in v || "scopes" in v;
81
+ }
82
+ //#endregion
83
+ export { isValidIdent as S, isSchemaObject as _, isOAuthFlowValue as a, isSecurityScheme as b, isOperationLike as c, isParameterObject as d, isRecord as f, isSchemaArray as g, isResponses as h, isMediaWithSchema as i, isParameter as l, isRequestBody as m, isHttpMethod as n, isOpenAPIPaths as o, isRefObject as p, isMedia as r, isOperation as s, isContentBody as t, isParameterArray as u, isSchemaProperty as v, isStringRef as x, isSecurityArray as y };
@@ -1,6 +1,7 @@
1
+ import { _ as toIdentifierPascalCase, f as methodPath, n as capitalize } from "./utils-B5PbMfWs.js";
1
2
  import { t as emit } from "./emit-CFR63U4L.js";
2
- import { L as toIdentifierPascalCase, M as methodPath, c as isOperationLike, f as isRecord, o as isOpenAPIPaths, w as capitalize } from "./guard-zvkMUVYD.js";
3
- import { a as parsePathItem, i as operationHasArgs, n as hasNoContentResponse, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "./rpc-CepeggWU.js";
3
+ import { c as isOperationLike, f as isRecord, o as isOpenAPIPaths } from "./guard-BSZ8ezEv.js";
4
+ import { a as parsePathItem, i as operationHasArgs, n as hasNoContentResponse, o as resolveSplitOutDir, r as makeOperationDeps, t as formatPath } from "./rpc-BzPo2tZf.js";
4
5
  import path from "node:path";
5
6
  //#region src/helper/query.ts
6
7
  function makeHookName(method, pathStr, prefix) {
@@ -313,7 +314,7 @@ function makeSWRHeader(importPath, clientName, hasQuery, hasMutation, hasAnyArgs
313
314
  lines.push("import useSWRMutation from'swr/mutation'");
314
315
  lines.push("import type{SWRMutationConfiguration}from'swr/mutation'");
315
316
  }
316
- const honoTypeImports = ["ClientRequestOptions", ...hasAnyArgs ? ["InferRequestType"] : []];
317
+ const honoTypeImports = ["ClientRequestOptions", ...hasAnyArgs || hasInfiniteQuery ? ["InferRequestType"] : []];
317
318
  lines.push(`import type{${honoTypeImports.join(",")}}from'hono/client'`);
318
319
  lines.push("import{parseResponse}from'hono/client'");
319
320
  lines.push(`import{${clientName}}from'${importPath}'`);
@@ -515,7 +516,7 @@ function makeHeader(importPath, clientName, hasQuery, hasMutation, hasAnyArgs, c
515
516
  ...hasMutation ? [config.useMutationOptionsType] : []
516
517
  ];
517
518
  const needsVueImports = config.isVueQuery && hasQueryWithArgs;
518
- const honoTypeImports = ["ClientRequestOptions", ...hasAnyArgs ? ["InferRequestType"] : []];
519
+ const honoTypeImports = ["ClientRequestOptions", ...hasAnyArgs || hasInfiniteQuery ? ["InferRequestType"] : []];
519
520
  return `${[
520
521
  ...queryImports.length > 0 ? [`import{${queryImports.join(",")}}from'${config.packageName}'`] : [],
521
522
  ...typeImports.length > 0 ? [`import type{${typeImports.join(",")}}from'${config.packageName}'`] : [],
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readConfig } from "./config/index.js";
3
3
  import { r as setFormatOptions } from "./emit-CFR63U4L.js";
4
- import { n as parseOpenAPI, r as takibi, t as makeJob } from "./shared-n3ZMLfl9.js";
4
+ import { n as parseOpenAPI, r as takibi, t as makeJob } from "./shared-B-TUm7Fa.js";
5
5
  import { existsSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  //#region src/cli/index.ts
@@ -1,4 +1,5 @@
1
- import { C as baseError, E as error, F as requestParamsArray, L as toIdentifierPascalCase, M as methodPath, N as normalizeTypes, O as escapeRegexLiteral, P as renderNamedImport, R as uncapitalize, T as ensureSuffix, _ as isSchemaObject, f as isRecord, g as isSchemaArray, j as makeSafeKey, l as isParameter, p as isRefObject, r as isMedia, s as isOperation, z as zodToOpenAPISchema } from "./guard-zvkMUVYD.js";
1
+ import { _ as toIdentifierPascalCase, a as ensureSuffix, c as escapeRegexLiteral, d as makeSafeKey, f as methodPath, h as requestParamsArray, m as renderNamedImport, o as error, p as normalizeTypes, r as cyclicNodes, t as baseError, v as uncapitalize, y as zodToOpenAPISchema } from "./utils-B5PbMfWs.js";
2
+ import { _ as isSchemaObject, f as isRecord, g as isSchemaArray, l as isParameter, p as isRefObject, r as isMedia, s as isOperation } from "./guard-BSZ8ezEv.js";
2
3
  import path from "node:path";
3
4
  import ts from "typescript";
4
5
  //#region src/helper/code.ts
@@ -1634,108 +1635,33 @@ function getChildren(node) {
1634
1635
  return semanticChildren;
1635
1636
  }
1636
1637
  function collectIdentifiers(node) {
1638
+ const identifiers = [];
1637
1639
  const visit = (n) => {
1638
- const current = ts.isIdentifier(n) ? [n.text] : [];
1639
- const children = getChildren(n).flatMap(visit);
1640
- return [...current, ...children];
1640
+ if (ts.isIdentifier(n)) identifiers.push(n.text);
1641
+ for (const child of getChildren(n)) visit(child);
1641
1642
  };
1642
- return visit(node);
1643
- }
1644
- function extractIdentifiers(code, varNames) {
1645
- const allIdentifiers = collectIdentifiers(makeSourceFile(code));
1646
- return [...new Set(allIdentifiers.filter((id) => varNames.has(id)))];
1647
- }
1648
- function createInitialState() {
1649
- return {
1650
- indices: /* @__PURE__ */ new Map(),
1651
- lowLinks: /* @__PURE__ */ new Map(),
1652
- onStack: /* @__PURE__ */ new Set(),
1653
- stack: [],
1654
- sccs: [],
1655
- index: 0
1656
- };
1657
- }
1658
- function popStackUntil(stack, onStack, name) {
1659
- const idx = stack.lastIndexOf(name);
1660
- if (idx === -1) return {
1661
- scc: [],
1662
- newStack: stack,
1663
- newOnStack: onStack
1664
- };
1665
- const scc = stack.slice(idx);
1666
- return {
1667
- scc,
1668
- newStack: stack.slice(0, idx),
1669
- newOnStack: new Set([...onStack].filter((n) => !scc.includes(n)))
1670
- };
1671
- }
1672
- function tarjanConnect(name, deps, var2name, state) {
1673
- const currentIndex = state.index;
1674
- const indices = new Map(state.indices).set(name, currentIndex);
1675
- const lowLinks = new Map(state.lowLinks).set(name, currentIndex);
1676
- const stack = [...state.stack, name];
1677
- const onStack = /* @__PURE__ */ new Set([...state.onStack, name]);
1678
- const initialState = {
1679
- ...state,
1680
- indices,
1681
- lowLinks,
1682
- stack,
1683
- onStack,
1684
- index: currentIndex + 1
1685
- };
1686
- const afterDeps = (deps.get(name) ?? []).reduce((s, depVar) => {
1687
- const depName = var2name.get(depVar);
1688
- if (depName === void 0) return s;
1689
- if (!s.indices.has(depName)) {
1690
- const afterConnect = tarjanConnect(depName, deps, var2name, s);
1691
- const newLowLink = Math.min(afterConnect.lowLinks.get(name) ?? 0, afterConnect.lowLinks.get(depName) ?? 0);
1692
- const updatedLowLinks = new Map(afterConnect.lowLinks).set(name, newLowLink);
1693
- return {
1694
- ...afterConnect,
1695
- lowLinks: updatedLowLinks
1696
- };
1697
- }
1698
- if (s.onStack.has(depName)) {
1699
- const newLowLink = Math.min(s.lowLinks.get(name) ?? 0, s.indices.get(depName) ?? 0);
1700
- const updatedLowLinks = new Map(s.lowLinks).set(name, newLowLink);
1701
- return {
1702
- ...s,
1703
- lowLinks: updatedLowLinks
1704
- };
1705
- }
1706
- return s;
1707
- }, initialState);
1708
- if (afterDeps.lowLinks.get(name) === afterDeps.indices.get(name)) {
1709
- const { scc, newStack, newOnStack } = popStackUntil(afterDeps.stack, afterDeps.onStack, name);
1710
- return {
1711
- ...afterDeps,
1712
- stack: newStack,
1713
- onStack: newOnStack,
1714
- sccs: [...afterDeps.sccs, scc]
1715
- };
1716
- }
1717
- return afterDeps;
1643
+ visit(node);
1644
+ return identifiers;
1718
1645
  }
1719
1646
  function findCyclicSchemas(names, deps) {
1720
- const name2var = new Map(names.map((n) => [n, toIdentifierPascalCase(ensureSuffix(n, "Schema"))]));
1721
1647
  const var2name = new Map(names.map((n) => [toIdentifierPascalCase(ensureSuffix(n, "Schema")), n]));
1722
- const finalState = names.reduce((state, n) => state.indices.has(n) ? state : tarjanConnect(n, deps, var2name, state), createInitialState());
1723
- return new Set(finalState.sccs.flatMap((scc) => {
1724
- if (scc.length > 1) return [...scc];
1725
- const single = scc[0];
1726
- if (!single) return [];
1727
- const selfVar = name2var.get(single);
1728
- return selfVar && (deps.get(single) ?? []).includes(selfVar) ? [single] : [];
1729
- }));
1648
+ return cyclicNodes(new Map(names.map((n) => [n, (deps.get(n) ?? []).map((v) => var2name.get(v)).filter((x) => x !== void 0)])));
1730
1649
  }
1731
1650
  function analyzeCircularSchemas(schemas, schemaNames, readonly) {
1732
1651
  const varNameSet = new Set(schemaNames.map((n) => toIdentifierPascalCase(ensureSuffix(n, "Schema"))));
1733
1652
  const varNameToName = new Map(schemaNames.map((n) => [toIdentifierPascalCase(ensureSuffix(n, "Schema")), n]));
1734
1653
  const zSchemaMap = new Map(schemaNames.map((n) => [n, zodToOpenAPI(schemas[n], void 0, readonly === true ? { readonly: true } : void 0)]));
1654
+ const batchedSource = makeSourceFile(schemaNames.map((n) => `const ${toIdentifierPascalCase(ensureSuffix(n, "Schema"))} = ${zSchemaMap.get(n)}`).join("\n"));
1655
+ const initializerIdentifiers = new Map(batchedSource.statements.flatMap((statement) => {
1656
+ if (!ts.isVariableStatement(statement)) return [];
1657
+ const declaration = statement.declarationList.declarations[0];
1658
+ if (!(declaration && ts.isIdentifier(declaration.name) && declaration.initializer)) return [];
1659
+ return [[declaration.name.text, collectIdentifiers(declaration.initializer)]];
1660
+ }));
1735
1661
  const depsMap = new Map(schemaNames.map((n) => {
1736
- const code = zSchemaMap.get(n) ?? "";
1737
1662
  const selfVar = toIdentifierPascalCase(ensureSuffix(n, "Schema"));
1738
- return [n, extractIdentifiers(code, varNameSet).filter((v) => v !== selfVar)];
1663
+ const identifiers = initializerIdentifiers.get(selfVar) ?? [];
1664
+ return [n, [...new Set(identifiers.filter((id) => varNameSet.has(id)))].filter((v) => v !== selfVar)];
1739
1665
  }));
1740
1666
  const cyclicSchemas = findCyclicSchemas(schemaNames, depsMap);
1741
1667
  const extendedCyclicSchemas = /* @__PURE__ */ new Set([...cyclicSchemas, ...[...cyclicSchemas].flatMap((n) => (depsMap.get(n) ?? []).map((v) => varNameToName.get(v)).filter((x) => x !== void 0))]);
@@ -1799,27 +1725,24 @@ function topoSort(decls) {
1799
1725
  const makeKey = (kind, name) => `${kind}:${name}`;
1800
1726
  const map = new Map(decls.map((d) => [makeKey(d.kind, d.name), d]));
1801
1727
  const findByName = (name) => map.get(makeKey("variable", name)) ?? map.get(makeKey("type", name)) ?? map.get(makeKey("interface", name));
1802
- const visit = (key, state) => {
1803
- if (state.perm.has(key) || state.temp.has(key)) return state;
1728
+ const sorted = [];
1729
+ const perm = /* @__PURE__ */ new Set();
1730
+ const temp = /* @__PURE__ */ new Set();
1731
+ const visit = (key) => {
1732
+ if (perm.has(key) || temp.has(key)) return;
1804
1733
  const decl = map.get(key);
1805
- if (!decl) return state;
1806
- const withTemp = {
1807
- ...state,
1808
- temp: /* @__PURE__ */ new Set([...state.temp, key])
1809
- };
1810
- const afterRefs = decl.refs.map((ref) => findByName(ref)).filter((d) => d !== void 0).reduce((s, d) => visit(makeKey(d.kind, d.name), s), withTemp);
1811
- return {
1812
- sorted: [...afterRefs.sorted, decl],
1813
- perm: /* @__PURE__ */ new Set([...afterRefs.perm, key]),
1814
- temp: new Set([...afterRefs.temp].filter((t) => t !== key))
1815
- };
1816
- };
1817
- const initial = {
1818
- sorted: [],
1819
- perm: /* @__PURE__ */ new Set(),
1820
- temp: /* @__PURE__ */ new Set()
1734
+ if (!decl) return;
1735
+ temp.add(key);
1736
+ for (const ref of decl.refs) {
1737
+ const found = findByName(ref);
1738
+ if (found) visit(makeKey(found.kind, found.name));
1739
+ }
1740
+ temp.delete(key);
1741
+ perm.add(key);
1742
+ sorted.push(decl);
1821
1743
  };
1822
- return decls.reduce((state, d) => visit(makeKey(d.kind, d.name), state), initial).sorted;
1744
+ for (const decl of decls) visit(makeKey(decl.kind, decl.name));
1745
+ return sorted;
1823
1746
  }
1824
1747
  function ast(code) {
1825
1748
  const decls = parseStatements(makeSourceFile(code));
@@ -2365,6 +2288,7 @@ function componentsCode(components, options) {
2365
2288
  function routeCode(openapi, readonly) {
2366
2289
  const routeEntries = (openapi, readonly) => {
2367
2290
  const makeEntry = (path, method, operation, readonly) => {
2291
+ const request = makeRequest(operation.parameters, operation.requestBody, readonly);
2368
2292
  const properties = [
2369
2293
  `method:${JSON.stringify(method)}`,
2370
2294
  `path:${JSON.stringify(path)}`,
@@ -2373,7 +2297,7 @@ function routeCode(openapi, readonly) {
2373
2297
  operation.description ? `description:${JSON.stringify(operation.description)}` : void 0,
2374
2298
  operation.externalDocs ? `externalDocs:${JSON.stringify(operation.externalDocs)}` : void 0,
2375
2299
  operation.operationId ? `operationId:${JSON.stringify(operation.operationId)}` : void 0,
2376
- makeRequest(operation.parameters, operation.requestBody, readonly) ? `request:${makeRequest(operation.parameters, operation.requestBody, readonly)}` : void 0,
2300
+ request ? `request:${request}` : void 0,
2377
2301
  operation.responses ? `responses:${makeOperationResponses(operation.responses, readonly)}` : void 0,
2378
2302
  operation.callbacks ? `callbacks:{${makeCallbacks(operation.callbacks, readonly)}}` : void 0,
2379
2303
  operation.deprecated ? `deprecated:${JSON.stringify(operation.deprecated)}` : void 0,
@@ -1,4 +1,4 @@
1
- import { S as isValidIdent, c as isOperationLike, d as isParameterObject, f as isRecord, p as isRefObject, v as isSchemaProperty } from "./guard-zvkMUVYD.js";
1
+ import { S as isValidIdent, c as isOperationLike, d as isParameterObject, f as isRecord, p as isRefObject, v as isSchemaProperty } from "./guard-BSZ8ezEv.js";
2
2
  import path from "node:path";
3
3
  //#region src/helper/rpc.ts
4
4
  function makeEscaped(s) {
@@ -1,9 +1,10 @@
1
+ import { _ as toIdentifierPascalCase, a as ensureSuffix, c as escapeRegexLiteral, f as methodPath, g as statusCodeToNumber, i as deriveAppEntry, l as makeBarrel, m as renderNamedImport, r as cyclicNodes, v as uncapitalize, y as zodToOpenAPISchema } from "./utils-B5PbMfWs.js";
1
2
  import { n as fmt, t as emit } from "./emit-CFR63U4L.js";
2
3
  import { a as writeFile, i as unlink, n as readFile, r as readdir, t as mkdir } from "./fsp-BXry-Hx5.js";
3
- import { I as statusCodeToNumber, L as toIdentifierPascalCase, M as methodPath, O as escapeRegexLiteral, P as renderNamedImport, R as uncapitalize, T as ensureSuffix, b as isSecurityScheme, i as isMediaWithSchema, k as makeBarrel, l as isParameter, n as isHttpMethod, p as isRefObject, s as isOperation, t as isContentBody, y as isSecurityArray, z as zodToOpenAPISchema } from "./guard-zvkMUVYD.js";
4
- import { S as makeModuleSpec, _ as makeRef, a as responsesCode, b as makeExportConst, c as headersCode, d as ast, f as zodToOpenAPI, g as makePathItem, h as makeOperationResponses, i as schemasCode, l as makeSplitSchemaFile, m as makeCallbacks, n as routeCode, o as requestBodiesCode, p as makeCallback, r as componentsCode, s as parametersCode, t as zodOpenAPIHono, u as analyzeCircularSchemas, v as makeRequest, x as makeImports, y as makeConst } from "./openapi-8ZYAMj_e.js";
5
- import { t as hooks } from "./hooks-CkiatAe9.js";
6
- import { t as docs } from "./docs-380Wcu6a.js";
4
+ import { S as makeModuleSpec, _ as makeRef, a as responsesCode, b as makeExportConst, c as headersCode, d as ast, f as zodToOpenAPI, g as makePathItem, h as makeOperationResponses, i as schemasCode, l as makeSplitSchemaFile, m as makeCallbacks, n as routeCode, o as requestBodiesCode, p as makeCallback, r as componentsCode, s as parametersCode, t as zodOpenAPIHono, u as analyzeCircularSchemas, v as makeRequest, x as makeImports, y as makeConst } from "./openapi-BlTDQFFF.js";
5
+ import { b as isSecurityScheme, i as isMediaWithSchema, l as isParameter, n as isHttpMethod, p as isRefObject, s as isOperation, t as isContentBody, y as isSecurityArray } from "./guard-BSZ8ezEv.js";
6
+ import { t as hooks } from "./hooks-CCw7jm8g.js";
7
+ import { t as docs } from "./docs-DbknIWmx.js";
7
8
  import { rpc } from "./core/rpc/index.js";
8
9
  import { type } from "./core/type/index.js";
9
10
  import path from "node:path";
@@ -652,7 +653,9 @@ function schemaToFaker(schema, propertyName, options = {}) {
652
653
  if (schema.format && FORMAT_TO_FAKER[schema.format]) return FORMAT_TO_FAKER[schema.format];
653
654
  if (propertyName && PROPERTY_NAME_TO_FAKER[propertyName]) {
654
655
  const hint = PROPERTY_NAME_TO_FAKER[propertyName];
655
- if (!(hint.includes("faker.number.") && schema.type === "string")) return hint;
656
+ const isNumberHint = hint.includes("faker.number.");
657
+ const hasStringConstraint = schema.pattern !== void 0 || schema.minLength !== void 0 || schema.maxLength !== void 0;
658
+ if (!(schema.type === "string" && (isNumberHint || hasStringConstraint))) return hint;
656
659
  }
657
660
  if (schema.type && typeof schema.type === "string" && TYPE_TO_FAKER[schema.type]) {
658
661
  if (schema.type === "string") {
@@ -708,34 +711,7 @@ function topologicalSort(refs, schemas) {
708
711
  return result;
709
712
  }
710
713
  function detectCircularSchemas$1(schemas) {
711
- const circular = /* @__PURE__ */ new Set();
712
- for (const name of Object.keys(schemas)) {
713
- const schema = schemas[name];
714
- if (!schema) continue;
715
- for (const dep of collectRefs(schema)) {
716
- if (dep === name) {
717
- circular.add(name);
718
- break;
719
- }
720
- const visited = /* @__PURE__ */ new Set();
721
- const stack = [dep];
722
- while (stack.length > 0) {
723
- const current = stack[stack.length - 1];
724
- stack.length -= 1;
725
- if (current === void 0) break;
726
- if (current === name) {
727
- circular.add(name);
728
- break;
729
- }
730
- if (visited.has(current)) continue;
731
- visited.add(current);
732
- const s = schemas[current];
733
- if (s) for (const r of collectRefs(s)) stack.push(r);
734
- }
735
- if (circular.has(name)) break;
736
- }
737
- }
738
- return circular;
714
+ return cyclicNodes(new Map(Object.entries(schemas).map(([name, schema]) => [name, [...collectRefs(schema)]])));
739
715
  }
740
716
  function collectSchemaRefs$1(node, refs) {
741
717
  if (Array.isArray(node)) {
@@ -1014,22 +990,24 @@ function makeMock(openapi, basePath, options = {}) {
1014
990
  const routes = routeCode(filteredOpenapi, readonlyOption);
1015
991
  const appSetup = routeMetas.map(({ routeId }) => `.openapi(${routeId}Route, ${routeId}RouteHandler)`).join("");
1016
992
  const handlersJoined = handlers.join("\n\n");
993
+ const mockFunctionsJoined = mockFunctions.join("\n\n");
994
+ const bigIntSerializer = `${mockFunctionsJoined}\n${handlersJoined}`.includes("faker.number.bigInt(") ? `if(typeof BigInt.prototype.toJSON!=='function'){Object.defineProperty(BigInt.prototype,'toJSON',{value(this:bigint){return this.toString()},writable:true,configurable:true})}` : "";
1017
995
  const needsCookieImport = handlersJoined.includes("getCookie(c,");
1018
996
  const imports = `import { OpenAPIHono, createRoute, z, type RouteHandler } from '@hono/zod-openapi'
1019
997
  ${locale ? `import { faker } from '@faker-js/faker/locale/${locale}'` : `import { faker } from '@faker-js/faker'`}${needsCookieImport ? `\nimport { getCookie } from 'hono/cookie'` : ""}`;
1020
998
  const delayMiddleware = delayMiddlewareCode(delay);
1021
- const appCode = `const app = new OpenAPIHono()${basePath !== "/" ? `.basePath('${basePath}')` : ""}${delayMiddleware}
1022
-
1023
- export const api = app${appSetup}
1024
-
1025
- export default app`;
1026
999
  return [
1027
1000
  imports,
1001
+ bigIntSerializer,
1028
1002
  components,
1029
1003
  routes,
1030
- mockFunctions.join("\n\n"),
1004
+ mockFunctionsJoined,
1031
1005
  handlersJoined,
1032
- appCode
1006
+ `const app = new OpenAPIHono()${basePath !== "/" ? `.basePath('${basePath}')` : ""}${delayMiddleware}
1007
+
1008
+ export const api = app${appSetup}
1009
+
1010
+ export default app`
1033
1011
  ].filter((s) => s.length > 0).join("\n\n");
1034
1012
  }
1035
1013
  //#endregion
@@ -1302,20 +1280,8 @@ function shallowRefs(schema) {
1302
1280
  ...compositeRefs
1303
1281
  ];
1304
1282
  }
1305
- function reachesSelf(start, target, schemas, visited = /* @__PURE__ */ new Set()) {
1306
- if (start === target) return true;
1307
- if (visited.has(start)) return false;
1308
- visited.add(start);
1309
- const schema = schemas[start];
1310
- if (!schema) return false;
1311
- return shallowRefs(schema).some((dep) => reachesSelf(dep, target, schemas, visited));
1312
- }
1313
1283
  function detectCircularSchemas(schemas) {
1314
- return new Set(Object.keys(schemas).filter((name) => {
1315
- const schema = schemas[name];
1316
- if (!schema) return false;
1317
- return shallowRefs(schema).some((dep) => reachesSelf(dep, name, schemas));
1318
- }));
1284
+ return cyclicNodes(new Map(Object.entries(schemas).map(([name, schema]) => [name, shallowRefs(schema)])));
1319
1285
  }
1320
1286
  function topologicalOrder(usedSchemaNames, schemas) {
1321
1287
  const visit = (name, visited, visiting) => {
@@ -1333,10 +1299,10 @@ function topologicalOrder(usedSchemaNames, schemas) {
1333
1299
  const visited = /* @__PURE__ */ new Set();
1334
1300
  return [...usedSchemaNames].flatMap((name) => visit(name, visited, /* @__PURE__ */ new Set()));
1335
1301
  }
1336
- function makeMockFunctions(spec, usedSchemaNames) {
1302
+ function makeMockFunctions(spec, usedSchemaNames, circularSchemas) {
1337
1303
  if (!spec.components?.schemas || usedSchemaNames.size === 0) return "";
1338
1304
  const schemas = spec.components.schemas;
1339
- const circular = detectCircularSchemas(schemas);
1305
+ const circular = circularSchemas ?? detectCircularSchemas(schemas);
1340
1306
  return topologicalOrder(usedSchemaNames, schemas).map((name) => {
1341
1307
  const returnType = circular.has(name) ? ": any" : "";
1342
1308
  return `function mock${name.replace(/\./g, "")}()${returnType} {\n return ${schemaToFaker(schemas[name])}\n}`;
@@ -1442,11 +1408,17 @@ function getPathFirstSegment(path) {
1442
1408
  const sanitized = (path.replace(/^\/+/, "").split("/")[0] ?? "").replace(/\{([^}]+)\}/g, "$1").replace(/[^0-9A-Za-z._-]/g, "_").replace(/^[._-]+|[._-]+$/g, "").replace(/__+/g, "_").replace(/[-._](\w)/g, (_, c) => c.toUpperCase());
1443
1409
  return sanitized === "" ? "__root" : sanitized;
1444
1410
  }
1445
- function makeHandlerTestCode(spec, handlerPath, _routeNames, importFrom, basePath = "/", testFramework = "vitest") {
1411
+ function makeHandlerTestContext(spec) {
1412
+ return {
1413
+ testCases: extractTestCases(spec),
1414
+ circularSchemas: detectCircularSchemas(spec.components?.schemas ?? {})
1415
+ };
1416
+ }
1417
+ function makeHandlerTestCode(spec, handlerPath, _routeNames, importFrom, basePath = "/", testFramework = "vitest", context = makeHandlerTestContext(spec)) {
1446
1418
  const handlerFileName = path.basename(handlerPath, ".ts");
1447
- const relevantCases = extractTestCases(spec).filter((testCase) => getPathFirstSegment(testCase.path) === handlerFileName);
1419
+ const relevantCases = context.testCases.filter((testCase) => getPathFirstSegment(testCase.path) === handlerFileName);
1448
1420
  if (relevantCases.length === 0) return "";
1449
- const mockFunctions = makeMockFunctions(spec, new Set(relevantCases.flatMap((testCase) => testCase.usedSchemaRefs)));
1421
+ const mockFunctions = makeMockFunctions(spec, new Set(relevantCases.flatMap((testCase) => testCase.usedSchemaRefs)), context.circularSchemas);
1450
1422
  const testCasesCode = relevantCases.map((testCase) => makeTestCase(testCase, basePath, spec.components?.schemas)).join("");
1451
1423
  const body = `${mockFunctions ? `${mockFunctions}\n\n` : ""}describe(${quoteSingle(handlerFileName.charAt(0).toUpperCase() + handlerFileName.slice(1))},()=>{${testCasesCode}})\n`;
1452
1424
  const fakerImport = body.includes("faker.") ? `\nimport{faker}from'@faker-js/faker'` : "";
@@ -2059,11 +2031,11 @@ function makeHandlerFileName(path) {
2059
2031
  function makeTestFileName(fileName) {
2060
2032
  return `${path.basename(fileName, ".ts")}.test.ts`;
2061
2033
  }
2062
- function makePaths(output, pathAlias, routeImport, handlerDir) {
2034
+ function makePaths(output, pathAlias, routeImport) {
2063
2035
  const isDot = output === "." || output === "./";
2064
2036
  const isIndexFile = !isDot && output.endsWith("/index.ts");
2065
2037
  const baseDir = isDot ? "." : isIndexFile ? output.match(/^(.*)\/[^/]+\/index\.ts$/)?.[1] ?? "." : output.match(/^(.*)\/[^/]+\.ts$/)?.[1] ?? ".";
2066
- const handlerPath = handlerDir ?? (baseDir === "." ? "handlers" : `${baseDir}/handlers`);
2038
+ const handlerPath = baseDir === "." ? "handlers" : `${baseDir}/handlers`;
2067
2039
  const routeModuleName = isIndexFile ? output.match(/([^/]+)\/index\.ts$/)?.[1] ?? "index" : output.endsWith(".ts") ? path.basename(output, ".ts") : "index";
2068
2040
  const aliasPrefix = pathAlias?.endsWith("/") ? pathAlias.slice(0, -1) : pathAlias;
2069
2041
  return {
@@ -2161,10 +2133,11 @@ async function removeStaleFiles(handlerPath, generatedFileNames) {
2161
2133
  * @param test - Whether to generate corresponding test files.
2162
2134
  * @returns A `Result` indicating success or error with message.
2163
2135
  */
2164
- async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias, routeImport, routeHandler = false, basePath = "/", testFramework = "vitest", handlerDir) {
2136
+ async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias, routeImport, routeHandler = false, basePath = "/", testFramework = "vitest") {
2165
2137
  const paths = openapi.paths;
2166
2138
  const handlers = makeMergedHandlers(Object.entries(paths).flatMap(([path, pathItem]) => Object.entries(pathItem).filter((entry) => isHttpMethod(entry[0]) && isOperation(entry[1])).map(([method]) => routeHandler ? makeStubHandlerInfo(path, method) : makeInlineStubHandlerInfo(path, method))));
2167
- const { handlerPath, importFrom, testImportFrom } = makePaths(output, pathAlias, routeImport, handlerDir);
2139
+ const { handlerPath, importFrom, testImportFrom } = makePaths(output, pathAlias, routeImport);
2140
+ const handlerTestContext = test ? makeHandlerTestContext(openapi) : void 0;
2168
2141
  const mkdirResult = await mkdir(handlerPath);
2169
2142
  if (!mkdirResult.ok) return {
2170
2143
  ok: false,
@@ -2189,8 +2162,8 @@ async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias, r
2189
2162
  ok: false,
2190
2163
  error: writeResult.error
2191
2164
  };
2192
- if (test) {
2193
- const testContent = makeHandlerTestCode(openapi, `${handlerPath}/${handler.fileName}`, [...handler.routeNames], testImportFrom, basePath, testFramework);
2165
+ if (handlerTestContext) {
2166
+ const testContent = makeHandlerTestCode(openapi, `${handlerPath}/${handler.fileName}`, [...handler.routeNames], testImportFrom, basePath, testFramework, handlerTestContext);
2194
2167
  if (testContent) {
2195
2168
  const testFmtResult = await fmt(testContent);
2196
2169
  const testCode = testFmtResult.ok ? testFmtResult.value : testContent;
@@ -2258,7 +2231,7 @@ async function zodOpenAPIHonoHandler(openapi, output, test = false, pathAlias, r
2258
2231
  * @param componentsOutput - The components module path schemas are imported from.
2259
2232
  * @returns A `Result` indicating success or error with message.
2260
2233
  */
2261
- async function defineOpenAPIRouteHandler(openapi, output, componentsOutput, test = false, pathAlias, basePath = "/", testFramework = "vitest", readonly, handlerDir) {
2234
+ async function defineOpenAPIRouteHandler(openapi, output, componentsOutput, test = false, pathAlias, basePath = "/", testFramework = "vitest", readonly) {
2262
2235
  const handlers = defineEntries(openapi, readonly).reduce((acc, entry) => {
2263
2236
  const fileName = makeHandlerFileName(entry.path);
2264
2237
  const prev = acc.get(fileName);
@@ -2270,7 +2243,7 @@ async function defineOpenAPIRouteHandler(openapi, output, componentsOutput, test
2270
2243
  });
2271
2244
  }, /* @__PURE__ */ new Map());
2272
2245
  const baseDir = path.dirname(output);
2273
- const handlerPath = handlerDir ?? (baseDir === "." ? "handlers" : `${baseDir}/handlers`);
2246
+ const handlerPath = baseDir === "." ? "routes" : `${baseDir}/routes`;
2274
2247
  const aliasPrefix = pathAlias?.endsWith("/") ? pathAlias.slice(0, -1) : pathAlias;
2275
2248
  const testImportFrom = aliasPrefix ?? makeModuleSpec(`${handlerPath}/handler.ts`, { output });
2276
2249
  const componentsModulePath = componentsOutput.endsWith("/index.ts") ? path.dirname(componentsOutput) : componentsOutput.replace(/\.ts$/, "");
@@ -2291,6 +2264,7 @@ async function defineOpenAPIRouteHandler(openapi, output, componentsOutput, test
2291
2264
  output: componentsOutput,
2292
2265
  ...componentsImport ? { import: componentsImport } : {}
2293
2266
  }]));
2267
+ const handlerTestContext = test ? makeHandlerTestContext(openapi) : void 0;
2294
2268
  const mkdirResult = await mkdir(handlerPath);
2295
2269
  if (!mkdirResult.ok) return {
2296
2270
  ok: false,
@@ -2316,8 +2290,8 @@ async function defineOpenAPIRouteHandler(openapi, output, componentsOutput, test
2316
2290
  ok: false,
2317
2291
  error: writeResult.error
2318
2292
  };
2319
- if (test) {
2320
- const testContent = makeHandlerTestCode(openapi, `${handlerPath}/${handler.fileName}`, [...handler.routeNames], testImportFrom, basePath, testFramework);
2293
+ if (handlerTestContext) {
2294
+ const testContent = makeHandlerTestCode(openapi, `${handlerPath}/${handler.fileName}`, [...handler.routeNames], testImportFrom, basePath, testFramework, handlerTestContext);
2321
2295
  if (testContent) {
2322
2296
  const testFmtResult = await fmt(testContent);
2323
2297
  const testCode = testFmtResult.ok ? testFmtResult.value : testContent;
@@ -2449,12 +2423,13 @@ function app(openapi, output, basePath, pathAlias, routeImport, routeHandler = f
2449
2423
  }
2450
2424
  //#endregion
2451
2425
  //#region src/core/template/define.ts
2452
- async function defineTemplate(openAPI, output, componentsOutput, test, basePath, pathAlias, routeImport, handlerDir, testFramework = "vitest", readonly) {
2426
+ async function defineTemplate(openAPI, output, componentsOutput, test, basePath, pathAlias, routeImport, testFramework = "vitest", readonly) {
2453
2427
  const target = output.endsWith(".ts") ? output : path.join(output, "index.ts");
2454
- const baseDir = path.dirname(output);
2428
+ const baseDir = path.dirname(target);
2429
+ const handlerDir = baseDir === "." ? "routes" : `${baseDir}/routes`;
2455
2430
  const aliasPrefix = pathAlias?.endsWith("/") ? pathAlias.slice(0, -1) : pathAlias;
2456
- const handlerImport = aliasPrefix ? `${aliasPrefix}/${path.relative(baseDir, handlerDir).replaceAll("\\", "/")}` : makeModuleSpec(output, { output: handlerDir });
2457
- const [appFmtResult, handlersResult] = await Promise.all([fmt(app(openAPI, output, basePath, pathAlias, routeImport, false, true, handlerImport)), defineOpenAPIRouteHandler(openAPI, output, componentsOutput, test, pathAlias, basePath, testFramework, readonly, handlerDir)]);
2431
+ const handlerImport = aliasPrefix ? `${aliasPrefix}/routes` : makeModuleSpec(target, { output: handlerDir });
2432
+ const [appFmtResult, handlersResult] = await Promise.all([fmt(app(openAPI, output, basePath, pathAlias, routeImport, false, true, handlerImport)), defineOpenAPIRouteHandler(openAPI, target, componentsOutput, test, pathAlias, basePath, testFramework, readonly)]);
2458
2433
  if (!appFmtResult.ok) return {
2459
2434
  ok: false,
2460
2435
  error: appFmtResult.error
@@ -2482,12 +2457,10 @@ async function defineTemplate(openAPI, output, componentsOutput, test, basePath,
2482
2457
  }
2483
2458
  //#endregion
2484
2459
  //#region src/core/template/index.ts
2485
- async function template(openAPI, output, test, basePath, pathAlias, routeImport, routeHandler, testFramework = "vitest", handlerDir) {
2460
+ async function template(openAPI, output, test, basePath, pathAlias, routeImport, routeHandler, testFramework = "vitest") {
2486
2461
  const dir = output.endsWith("/index.ts") ? path.dirname(path.dirname(output)) : path.dirname(output);
2487
2462
  const target = path.join(dir, "index.ts");
2488
- const aliasPrefix = pathAlias?.endsWith("/") ? pathAlias.slice(0, -1) : pathAlias;
2489
- const handlerImport = handlerDir ? aliasPrefix ? `${aliasPrefix}/${path.relative(dir, handlerDir).replaceAll("\\", "/")}` : makeModuleSpec(path.join(dir, "index.ts"), { output: handlerDir }) : void 0;
2490
- const [appFmtResult, stubHandlersResult] = await Promise.all([fmt(app(openAPI, output, basePath, pathAlias, routeImport, routeHandler, false, handlerImport)), zodOpenAPIHonoHandler(openAPI, output, test, pathAlias, routeImport, routeHandler, basePath, testFramework, handlerDir)]);
2463
+ const [appFmtResult, stubHandlersResult] = await Promise.all([fmt(app(openAPI, output, basePath, pathAlias, routeImport, routeHandler, false)), zodOpenAPIHonoHandler(openAPI, output, test, pathAlias, routeImport, routeHandler, basePath, testFramework)]);
2491
2464
  if (!appFmtResult.ok) return {
2492
2465
  ok: false,
2493
2466
  error: appFmtResult.error
@@ -2694,8 +2667,7 @@ async function parseOpenAPI(input) {
2694
2667
  //#region src/shared/index.ts
2695
2668
  function makeJob(openAPI, config) {
2696
2669
  const defineOn = config.template?.define === true;
2697
- const appOutput = config.output ?? config.routes?.output;
2698
- const defineHandlerDir = defineOn ? config.template?.output : void 0;
2670
+ const appOutput = config.output ?? (defineOn ? config.components?.output ? deriveAppEntry(config.components.output) : "src/index.ts" : config.routes?.output);
2699
2671
  const componentsOutput = config.components?.output ?? (defineOn && appOutput ? `${path.dirname(appOutput)}/components/index.ts` : void 0);
2700
2672
  const componentKinds = [
2701
2673
  "schemas",
@@ -2891,16 +2863,16 @@ function makeJob(openAPI, config) {
2891
2863
  split: false,
2892
2864
  run: (output) => docs(openAPI, output, config.docs?.entry, config.basePath, config.docs?.curl, config.docs?.baseUrl)
2893
2865
  } : void 0,
2894
- config.template && defineOn && appOutput && componentsOutput && defineHandlerDir ? {
2866
+ config.template && defineOn && appOutput && componentsOutput ? {
2895
2867
  name: "template",
2896
2868
  output: appOutput,
2897
2869
  split: false,
2898
- run: (output) => defineTemplate(openAPI, output, componentsOutput, config.template?.test ?? false, config.basePath, config.template?.pathAlias, config.routes?.import, defineHandlerDir, config.template?.testFramework, config.readonly)
2870
+ run: (output) => defineTemplate(openAPI, output, componentsOutput, config.template?.test ?? false, config.basePath, config.template?.pathAlias, config.routes?.import, config.template?.testFramework, config.readonly)
2899
2871
  } : config.template && !defineOn && appOutput ? {
2900
2872
  name: "template",
2901
2873
  output: appOutput,
2902
2874
  split: false,
2903
- run: (output) => template(openAPI, output, config.template?.test ?? false, config.basePath, config.template?.pathAlias, config.routes?.import, config.template?.define === false ? config.template.routeHandler : false, config.template?.testFramework, config.template?.output)
2875
+ run: (output) => template(openAPI, output, config.template?.test ?? false, config.basePath, config.template?.pathAlias, config.routes?.import, config.template?.define === false ? config.template.routeHandler : false, config.template?.testFramework)
2904
2876
  } : void 0
2905
2877
  ].filter((job) => job !== void 0);
2906
2878
  }
@@ -234,6 +234,27 @@ function makeBarrel(value) {
234
234
  return `${Object.keys(value).sort().map((k) => `export * from './${k.charAt(0).toLowerCase() + k.slice(1)}'`).join("\n")}\n`;
235
235
  }
236
236
  /**
237
+ * Derives the define-mode app entry from a components output path.
238
+ *
239
+ * The path is read as `<anchor>/<module>` where module is either a flat
240
+ * `.ts` file or a `<dir>/index.ts` pair; the app entry is `<anchor>/index.ts`.
241
+ *
242
+ * @param componentsOutput - The components output path (a `.ts` file).
243
+ * @returns The derived app entry path.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * deriveAppEntry('./server/components/index.ts') // → './server/index.ts'
248
+ * deriveAppEntry('server/components.ts') // → 'server/index.ts'
249
+ * deriveAppEntry('components/index.ts') // → 'index.ts'
250
+ * ```
251
+ */
252
+ function deriveAppEntry(componentsOutput) {
253
+ const container = componentsOutput.endsWith("/index.ts") ? componentsOutput.slice(0, -9) : componentsOutput;
254
+ const anchor = container.includes("/") ? container.slice(0, container.lastIndexOf("/")) : "";
255
+ return anchor === "" || anchor === "." ? "index.ts" : `${anchor}/index.ts`;
256
+ }
257
+ /**
237
258
  * Formats an error message argument using the Zod v4 unified `error` parameter.
238
259
  *
239
260
  * @param message - The error message string
@@ -272,87 +293,56 @@ function makeInferRequestType(clientName, pathResult, method) {
272
293
  const { runtimePath, typeofPrefix, bracketSuffix, hasBracket } = pathResult;
273
294
  return hasBracket ? `InferRequestType<typeof ${clientName}${typeofPrefix}${bracketSuffix}['$${method}']>` : `InferRequestType<typeof ${clientName}${runtimePath}.$${method}>`;
274
295
  }
275
- //#endregion
276
- //#region src/guard/index.ts
277
- function isRecord(v) {
278
- return typeof v === "object" && v !== null && !Array.isArray(v);
279
- }
280
- function isHttpMethod(method) {
281
- return method === "get" || method === "put" || method === "post" || method === "delete" || method === "patch" || method === "options" || method === "head" || method === "trace";
282
- }
283
- function isValidIdent(str) {
284
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
285
- }
286
- function isOpenAPIPaths(v) {
287
- if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
288
- return Object.values(v).every((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
289
- }
290
- function isRefObject(v) {
291
- return typeof v === "object" && v !== null && !Array.isArray(v) && "$ref" in v && typeof v.$ref === "string";
292
- }
293
- function isStringRef(v) {
294
- return "$ref" in v && typeof v.$ref === "string";
295
- }
296
- function isParameterObject(v) {
297
- if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
298
- if (!("name" in v) || typeof v.name !== "string") return false;
299
- if (!("in" in v)) return false;
300
- const pos = v.in;
301
- return pos === "path" || pos === "query" || pos === "header" || pos === "cookie";
302
- }
303
- function isParameter(v) {
304
- return typeof v === "object" && v !== null && "name" in v && "in" in v && ("schema" in v || "content" in v);
305
- }
306
- function isParameterArray(v) {
307
- return Array.isArray(v);
308
- }
309
- function isOperationLike(v) {
310
- return typeof v === "object" && v !== null && !Array.isArray(v) && "responses" in v;
311
- }
312
- function isOperation(v) {
313
- return typeof v === "object" && v !== null && "responses" in v;
314
- }
315
- function isSchemaProperty(v) {
316
- return typeof v === "object" && v !== null && !Array.isArray(v) && "schema" in v;
317
- }
318
- function isSchemaArray(v) {
319
- return Array.isArray(v);
320
- }
321
- /**
322
- * JSON Schema 2020-12 §4.3.2 / §10.3.1.2 type guard for schema-vs-boolean
323
- * discrimination. `items`, `additionalProperties`, `unevaluatedItems`, and
324
- * `unevaluatedProperties` may be either an object schema or a boolean schema
325
- * (`true` = pass-through, `false` = reject). This narrows the union to the
326
- * object form so callers can recurse without `as` casts.
327
- */
328
- function isSchemaObject(v) {
329
- return typeof v === "object" && v !== null && !Array.isArray(v);
330
- }
331
- function isMediaWithSchema(v) {
332
- return typeof v === "object" && v !== null && "schema" in v;
333
- }
334
- function isMedia(v) {
335
- return typeof v === "object" && v !== null && "schema" in v;
336
- }
337
- function isRequestBody(v) {
338
- return typeof v === "object" && v !== null && ("content" in v || "required" in v || "description" in v);
339
- }
340
- function isContentBody(v) {
341
- return typeof v === "object" && v !== null && !("$ref" in v);
342
- }
343
- function isSecurityScheme(v) {
344
- return typeof v === "object" && v !== null && !("$ref" in v);
345
- }
346
- function isSecurityArray(v) {
347
- return Array.isArray(v);
348
- }
349
- function isResponses(v) {
350
- if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
351
- return "$ref" in v || "description" in v || "content" in v || "headers" in v || "links" in v;
352
- }
353
- function isOAuthFlowValue(v) {
354
- if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
355
- return "authorizationUrl" in v || "tokenUrl" in v || "scopes" in v;
296
+ function cyclicNodes(deps) {
297
+ const indices = /* @__PURE__ */ new Map();
298
+ const lowLinks = /* @__PURE__ */ new Map();
299
+ const onStack = /* @__PURE__ */ new Set();
300
+ const stack = [];
301
+ const result = /* @__PURE__ */ new Set();
302
+ const open = (node) => {
303
+ const index = indices.size;
304
+ indices.set(node, index);
305
+ lowLinks.set(node, index);
306
+ stack.push(node);
307
+ onStack.add(node);
308
+ };
309
+ const close = (node) => {
310
+ if (lowLinks.get(node) !== indices.get(node)) return;
311
+ const sccStart = stack.lastIndexOf(node);
312
+ const scc = stack.slice(sccStart);
313
+ stack.length = sccStart;
314
+ for (const member of scc) onStack.delete(member);
315
+ if (scc.length > 1 || (deps.get(node) ?? []).includes(node)) for (const member of scc) result.add(member);
316
+ };
317
+ const connect = (root) => {
318
+ open(root);
319
+ const frames = [{
320
+ node: root,
321
+ depIndex: 0
322
+ }];
323
+ while (frames.length > 0) {
324
+ const frame = frames[frames.length - 1];
325
+ if (frame === void 0) return;
326
+ const dep = (deps.get(frame.node) ?? [])[frame.depIndex];
327
+ if (dep === void 0) {
328
+ close(frame.node);
329
+ frames.length -= 1;
330
+ const parent = frames[frames.length - 1];
331
+ if (parent) lowLinks.set(parent.node, Math.min(lowLinks.get(parent.node) ?? 0, lowLinks.get(frame.node) ?? 0));
332
+ continue;
333
+ }
334
+ frame.depIndex += 1;
335
+ if (!indices.has(dep)) {
336
+ open(dep);
337
+ frames.push({
338
+ node: dep,
339
+ depIndex: 0
340
+ });
341
+ } else if (onStack.has(dep)) lowLinks.set(frame.node, Math.min(lowLinks.get(frame.node) ?? 0, indices.get(dep) ?? 0));
342
+ }
343
+ };
344
+ for (const node of deps.keys()) if (!indices.has(node)) connect(node);
345
+ return result;
356
346
  }
357
347
  //#endregion
358
- export { makeInferRequestType as A, baseError as C, escapeHtml as D, error as E, requestParamsArray as F, statusCodeToNumber as I, toIdentifierPascalCase as L, methodPath as M, normalizeTypes as N, escapeRegexLiteral as O, renderNamedImport as P, uncapitalize as R, isValidIdent as S, ensureSuffix as T, isSchemaObject as _, isOAuthFlowValue as a, isSecurityScheme as b, isOperationLike as c, isParameterObject as d, isRecord as f, isSchemaArray as g, isResponses as h, isMediaWithSchema as i, makeSafeKey as j, makeBarrel as k, isParameter as l, isRequestBody as m, isHttpMethod as n, isOpenAPIPaths as o, isRefObject as p, isMedia as r, isOperation as s, isContentBody as t, isParameterArray as u, isSchemaProperty as v, capitalize as w, isStringRef as x, isSecurityArray as y, zodToOpenAPISchema as z };
348
+ export { toIdentifierPascalCase as _, ensureSuffix as a, escapeRegexLiteral as c, makeSafeKey as d, methodPath as f, statusCodeToNumber as g, requestParamsArray as h, deriveAppEntry as i, makeBarrel as l, renderNamedImport as m, capitalize as n, error as o, normalizeTypes as p, cyclicNodes as r, escapeHtml as s, baseError as t, makeInferRequestType as u, uncapitalize as v, zodToOpenAPISchema as y };
@@ -1,9 +1,10 @@
1
1
  import { parseConfig } from "../config/index.js";
2
2
  import { r as setFormatOptions } from "../emit-CFR63U4L.js";
3
- import { f as isRecord } from "../guard-zvkMUVYD.js";
4
- import { n as parseOpenAPI, t as makeJob } from "../shared-n3ZMLfl9.js";
3
+ import { f as isRecord } from "../guard-BSZ8ezEv.js";
4
+ import { n as parseOpenAPI, t as makeJob } from "../shared-B-TUm7Fa.js";
5
5
  import path from "node:path";
6
6
  import fsp from "node:fs/promises";
7
+ import crypto from "node:crypto";
7
8
  //#region src/vite-plugin/index.ts
8
9
  async function readConfigurationWithHotReload(server) {
9
10
  const absoluteConfigPath = path.resolve(process.cwd(), "hono-takibi.config.ts");
@@ -53,28 +54,97 @@ function debounce(delayMilliseconds, callback) {
53
54
  };
54
55
  return wrappedFunction;
55
56
  }
57
+ function isWatchedInputFile(filePath) {
58
+ return filePath.endsWith(".yaml") || filePath.endsWith(".json") || filePath.endsWith(".tsp");
59
+ }
60
+ async function listWatchedInputFiles(directory) {
61
+ const entries = await fsp.readdir(directory, { withFileTypes: true }).catch(() => []);
62
+ return (await Promise.all(entries.map((entry) => {
63
+ const entryPath = path.join(directory, entry.name);
64
+ if (entry.isDirectory()) return entry.name === "node_modules" || entry.name.startsWith(".") ? Promise.resolve([]) : listWatchedInputFiles(entryPath);
65
+ return Promise.resolve(entry.isFile() && isWatchedInputFile(entryPath) ? [entryPath] : []);
66
+ }))).flat();
67
+ }
68
+ /**
69
+ * Hashes the contents of all watched input files under the input directory.
70
+ *
71
+ * Returns null when the set cannot be read reliably, so callers treat
72
+ * "unknown" as "changed" and regenerate.
73
+ */
74
+ async function hashWatchedInputs(directory) {
75
+ const files = [...await listWatchedInputFiles(directory)].sort();
76
+ if (files.length === 0) return null;
77
+ const contents = await Promise.all(files.map(async (file) => ({
78
+ file,
79
+ content: await fsp.readFile(file, "utf-8").catch(() => null)
80
+ })));
81
+ if (contents.some((entry) => entry.content === null)) return null;
82
+ const hash = crypto.createHash("sha256");
83
+ for (const entry of contents) {
84
+ hash.update(entry.file);
85
+ hash.update("\0");
86
+ hash.update(entry.content ?? "");
87
+ hash.update("\0");
88
+ }
89
+ return hash.digest("hex");
90
+ }
91
+ async function listOutputFiles(target) {
92
+ const stats = await fsp.stat(target).catch(() => null);
93
+ if (!stats) return [];
94
+ if (stats.isFile()) return [{
95
+ file: target,
96
+ mtimeMs: stats.mtimeMs
97
+ }];
98
+ if (!stats.isDirectory()) return [];
99
+ const entries = await fsp.readdir(target, { withFileTypes: true }).catch(() => []);
100
+ return (await Promise.all(entries.map((entry) => listOutputFiles(path.join(target, entry.name))))).flat();
101
+ }
102
+ /**
103
+ * Snapshots mtimes of all files under the job output paths.
104
+ *
105
+ * `writeFile` skips identical content, so an unchanged mtime means the file
106
+ * was not rewritten. For `.ts` outputs the sibling split directory
107
+ * (`dir/name/` derived from `dir/name.ts`) is included as well.
108
+ */
109
+ async function snapshotOutputs(outputPaths) {
110
+ const targets = [...new Set(outputPaths.flatMap((outputPath) => [outputPath, ...outputPath.endsWith(".ts") ? [path.join(path.dirname(outputPath), path.basename(outputPath, ".ts"))] : []]))];
111
+ const collected = await Promise.all(targets.map((target) => listOutputFiles(target)));
112
+ return new Map(collected.flat().map((entry) => [entry.file, entry.mtimeMs]));
113
+ }
114
+ function sameOutputSnapshot(before, after) {
115
+ return before.size === after.size && [...before].every(([file, mtimeMs]) => after.get(file) === mtimeMs);
116
+ }
56
117
  /**
57
118
  * Runs all code generation tasks in parallel based on the provided configuration.
58
119
  *
59
120
  * @param config - Parsed configuration object
60
- * @returns Promise resolving to object containing log messages
121
+ * @returns Promise resolving to object containing log messages and whether any output file changed
61
122
  */
62
123
  async function runAllGenerationTasks(config) {
63
124
  if (config.format) setFormatOptions(config.format);
64
125
  const openAPIResult = await parseOpenAPI(config.input);
65
- if (!openAPIResult.ok) return { logs: [`❌ parseOpenAPI: ${openAPIResult.error}`] };
126
+ if (!openAPIResult.ok) return {
127
+ logs: [`❌ parseOpenAPI: ${openAPIResult.error}`],
128
+ changed: false
129
+ };
66
130
  const openAPI = openAPIResult.value;
67
131
  const cleanupSplitOutput = async (absOutput) => {
68
132
  if (!(await fsp.stat(absOutput).catch(() => null))?.isDirectory()) return;
69
133
  const entries = await fsp.readdir(absOutput, { withFileTypes: true }).catch(() => []);
70
134
  await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => fsp.unlink(path.join(absOutput, entry.name)).catch(() => void 0)));
71
135
  };
72
- return { logs: await Promise.all(makeJob(openAPI, config).map(async (job) => {
73
- const absOutput = path.resolve(process.cwd(), job.output);
74
- if (job.split) await cleanupSplitOutput(absOutput);
75
- const result = await job.run(absOutput);
76
- return result.ok ? `✅ ${job.name}${job.split ? "(split)" : ""} -> ${absOutput}` : `❌ ${job.name}: ${result.error}`;
77
- })) };
136
+ const jobs = makeJob(openAPI, config);
137
+ const outputPaths = jobs.map((job) => path.resolve(process.cwd(), job.output));
138
+ const beforeSnapshot = await snapshotOutputs(outputPaths);
139
+ return {
140
+ logs: await Promise.all(jobs.map(async (job) => {
141
+ const absOutput = path.resolve(process.cwd(), job.output);
142
+ if (job.split) await cleanupSplitOutput(absOutput);
143
+ const result = await job.run(absOutput);
144
+ return result.ok ? `✅ ${job.name}${job.split ? "(split)" : ""} -> ${absOutput}` : `❌ ${job.name}: ${result.error}`;
145
+ })),
146
+ changed: !sameOutputSnapshot(beforeSnapshot, await snapshotOutputs(outputPaths))
147
+ };
78
148
  }
79
149
  /**
80
150
  * Adds glob patterns to the Vite file watcher.
@@ -97,6 +167,9 @@ function addInputGlobsToWatcher(server, absoluteInputPath) {
97
167
  server.watcher.add(watchPatterns);
98
168
  return inputDirectory;
99
169
  }
170
+ async function allOutputsExist(config) {
171
+ return (await Promise.all(extractOutputPaths(config).map((outputPath) => fsp.stat(outputPath).catch(() => null)))).every((stat) => stat !== null);
172
+ }
100
173
  function extractOutputPaths(config) {
101
174
  return [
102
175
  config.output,
@@ -131,18 +204,36 @@ function honoTakibiVite() {
131
204
  const pluginState = {
132
205
  current: null,
133
206
  previous: null,
134
- inputDirectory: null
207
+ inputDirectory: null,
208
+ lastInputHash: null,
209
+ runQueue: Promise.resolve()
135
210
  };
136
211
  const absoluteConfigFilePath = path.resolve(process.cwd(), "hono-takibi.config.ts");
137
212
  const runGeneration = async () => {
138
- if (!pluginState.current) return;
213
+ if (!pluginState.current) return false;
139
214
  console.log("🔥 hono-takibi");
140
- const { logs } = await runAllGenerationTasks(pluginState.current);
215
+ const { logs, changed } = await runAllGenerationTasks(pluginState.current);
141
216
  for (const logMessage of logs) console.log(logMessage);
217
+ return changed;
142
218
  };
143
219
  const runGenerationAndReload = async (server) => {
144
- await runGeneration();
145
- if (server) server.ws.send({ type: "full-reload" });
220
+ const changed = await runGeneration();
221
+ if (server && changed) server.ws.send({ type: "full-reload" });
222
+ };
223
+ const runIfInputsChanged = async (server) => {
224
+ if (!pluginState.inputDirectory || !pluginState.current) return;
225
+ const inputHash = await hashWatchedInputs(pluginState.inputDirectory);
226
+ if (inputHash !== null && inputHash === pluginState.lastInputHash && await allOutputsExist(pluginState.current)) {
227
+ console.log("⏭️ input unchanged - skipped regeneration");
228
+ return;
229
+ }
230
+ pluginState.lastInputHash = inputHash;
231
+ await runGenerationAndReload(server);
232
+ };
233
+ const enqueueRun = (task) => {
234
+ const queued = pluginState.runQueue.then(task).catch((error) => console.error("❌ run error:", error));
235
+ pluginState.runQueue = queued;
236
+ return queued;
146
237
  };
147
238
  const handleConfigurationChange = async (server) => {
148
239
  const nextConfiguration = await readConfigurationWithHotReload(server);
@@ -177,7 +268,9 @@ function honoTakibiVite() {
177
268
  }
178
269
  pluginState.previous = pluginState.current;
179
270
  pluginState.current = nextConfiguration.value;
180
- pluginState.inputDirectory = addInputGlobsToWatcher(server, path.resolve(process.cwd(), pluginState.current.input));
271
+ const inputDirectory = addInputGlobsToWatcher(server, path.resolve(process.cwd(), pluginState.current.input));
272
+ pluginState.inputDirectory = inputDirectory;
273
+ pluginState.lastInputHash = await hashWatchedInputs(inputDirectory);
181
274
  await runGenerationAndReload(server);
182
275
  };
183
276
  return {
@@ -185,7 +278,7 @@ function honoTakibiVite() {
185
278
  handleHotUpdate(context) {
186
279
  if (path.resolve(context.file) === path.resolve(process.cwd(), "hono-takibi.config.ts")) {
187
280
  console.log("config changed (hot-update)");
188
- handleConfigurationChange(context.server).catch((error) => console.error("❌ hot-update error:", error));
281
+ enqueueRun(() => handleConfigurationChange(context.server));
189
282
  return [];
190
283
  }
191
284
  },
@@ -198,19 +291,21 @@ function honoTakibiVite() {
198
291
  return;
199
292
  }
200
293
  pluginState.current = initialConfiguration.value;
201
- pluginState.inputDirectory = addInputGlobsToWatcher(server, path.resolve(process.cwd(), pluginState.current.input));
294
+ const inputDirectory = addInputGlobsToWatcher(server, path.resolve(process.cwd(), pluginState.current.input));
295
+ pluginState.inputDirectory = inputDirectory;
296
+ pluginState.lastInputHash = await hashWatchedInputs(inputDirectory);
202
297
  server.watcher.add(absoluteConfigFilePath);
203
- const debouncedRunGeneration = debounce(200, () => void runGenerationAndReload(server));
298
+ const debouncedRunGeneration = debounce(200, () => void enqueueRun(() => runIfInputsChanged(server)));
204
299
  server.watcher.on("all", async (_eventType, filePath) => {
205
300
  const absoluteChangedPath = path.resolve(filePath);
206
301
  if (absoluteChangedPath === absoluteConfigFilePath) {
207
302
  console.log("config changed (watch)");
208
- await handleConfigurationChange(server);
303
+ await enqueueRun(() => handleConfigurationChange(server));
209
304
  return;
210
305
  }
211
- if (pluginState.inputDirectory && absoluteChangedPath.startsWith(pluginState.inputDirectory) && (absoluteChangedPath.endsWith(".yaml") || absoluteChangedPath.endsWith(".json") || absoluteChangedPath.endsWith(".tsp"))) debouncedRunGeneration();
306
+ if (pluginState.inputDirectory && absoluteChangedPath.startsWith(pluginState.inputDirectory) && isWatchedInputFile(absoluteChangedPath)) debouncedRunGeneration();
212
307
  });
213
- await runGenerationAndReload(server);
308
+ await enqueueRun(() => runGenerationAndReload(server));
214
309
  })().catch((e) => console.error("❌ watch error:", e));
215
310
  }
216
311
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-takibi",
3
- "version": "0.9.99994",
3
+ "version": "0.9.99995",
4
4
  "description": "Hono Takibi is a code generator from OpenAPI to @hono/zod-openapi",
5
5
  "keywords": [
6
6
  "hono",
@@ -57,17 +57,21 @@
57
57
  "@apidevtools/swagger-parser": "^12.1.0",
58
58
  "@typespec/compiler": "^1.13.0",
59
59
  "@typespec/openapi3": "^1.13.0",
60
- "oxfmt": "^0.56.0",
60
+ "oxfmt": "^0.57.0",
61
61
  "ts-morph": "^28.0.0",
62
62
  "typescript": "^6.0.3",
63
63
  "zod": "^4.4.3"
64
64
  },
65
65
  "devDependencies": {
66
+ "@faker-js/faker": "^10.5.0",
67
+ "@hono/zod-openapi": "^1.4.0",
66
68
  "@types/node": "^26.0.1",
67
69
  "@typespec/http": "^1.13.0",
68
70
  "@typespec/rest": "^0.83.0",
69
71
  "@typespec/versioning": "^0.83.0",
70
- "tsdown": "0.22.3"
72
+ "hono": "^4.12.27",
73
+ "tsdown": "0.22.3",
74
+ "zod": "^4.0.0"
71
75
  },
72
76
  "scripts": {
73
77
  "build": "tsdown"