> | S, BasePath>;
39 |
40 | all = any>(
41 | path: P,
42 | endpoint: typeof OpenAPIRoute | H,
43 | ): HonoOpenAPIRouterType, I, MergeTypedResponse>, BasePath>;
44 |
45 | delete = any>(
46 | path: P,
47 | endpoint: typeof OpenAPIRoute | H,
48 | ): HonoOpenAPIRouterType, I, MergeTypedResponse>, BasePath>;
49 | delete(path: string, router: Hono): Hono["delete"];
50 | get = any>(
51 | path: P,
52 | endpoint: typeof OpenAPIRoute | H,
53 | ): HonoOpenAPIRouterType, I, MergeTypedResponse>, BasePath>;
54 | get(path: string, router: Hono): Hono["get"];
55 | patch = any>(
56 | path: P,
57 | endpoint: typeof OpenAPIRoute | H,
58 | ): HonoOpenAPIRouterType, I, MergeTypedResponse>, BasePath>;
59 | patch(path: string, router: Hono): Hono["patch"];
60 | post = any>(
61 | path: P,
62 | endpoint: typeof OpenAPIRoute | H,
63 | ): HonoOpenAPIRouterType, I, MergeTypedResponse>, BasePath>;
64 | post(path: string, router: Hono): Hono["post"];
65 | put = any>(
66 | path: P,
67 | endpoint: typeof OpenAPIRoute | H,
68 | ): HonoOpenAPIRouterType, I, MergeTypedResponse>, BasePath>;
69 | put(path: string, router: Hono): Hono["put"];
70 | // Hono must be defined last, for the overwrite method to have priority!
71 | } & Hono;
72 |
73 | export class HonoOpenAPIHandler extends OpenAPIHandler {
74 | getRequest(args: any[]) {
75 | return args[0].req.raw;
76 | }
77 |
78 | getUrlParams(args: any[]): Record {
79 | return args[0].req.param();
80 | }
81 |
82 | getBindings(args: any[]): Record {
83 | return args[0].env;
84 | }
85 | }
86 |
87 | export function fromHono<
88 | M extends Hono,
89 | E extends Env = M extends Hono ? E : never,
90 | S extends Schema = M extends Hono ? S : never,
91 | BasePath extends string = M extends Hono ? BP : never,
92 | >(router: M, options?: RouterOptions): HonoOpenAPIRouterType {
93 | const openapiRouter = new HonoOpenAPIHandler(router, options);
94 |
95 | const proxy = new Proxy(router, {
96 | get: (target: any, prop: string, ...args: any[]) => {
97 | const _result = openapiRouter.handleCommonProxy(target, prop, ...args);
98 | if (_result !== undefined) {
99 | return _result;
100 | }
101 |
102 | if (typeof target[prop] !== "function") {
103 | return target[prop];
104 | }
105 |
106 | return (route: string, ...handlers: any[]) => {
107 | if (prop !== "fetch") {
108 | if (prop === "route" && handlers.length === 1 && handlers[0].isChanfana === true) {
109 | openapiRouter.registerNestedRouter({
110 | method: "",
111 | nestedRouter: handlers[0],
112 | path: route,
113 | });
114 |
115 | // Hacky clone
116 | const subApp = handlers[0].original.basePath("");
117 |
118 | const excludePath = new Set(["/openapi.json", "/openapi.yaml", "/docs", "/redocs"]);
119 | subApp.routes = subApp.routes.filter((obj: any) => {
120 | return !excludePath.has(obj.path);
121 | });
122 |
123 | router.route(route, subApp);
124 | return proxy;
125 | }
126 |
127 | if (prop === "all" && handlers.length === 1 && handlers[0].isRoute) {
128 | handlers = openapiRouter.registerRoute({
129 | method: prop,
130 | path: route,
131 | handlers: handlers,
132 | doRegister: false,
133 | });
134 | } else if (openapiRouter.allowedMethods.includes(prop)) {
135 | handlers = openapiRouter.registerRoute({
136 | method: prop,
137 | path: route,
138 | handlers: handlers,
139 | });
140 | } else if (prop === "on") {
141 | const methods: string | string[] = route;
142 | const paths: string | string[] = handlers.shift();
143 |
144 | if (Array.isArray(methods) || Array.isArray(paths)) {
145 | throw new Error("chanfana only supports single method+path on hono.on('method', 'path', EndpointClass)");
146 | }
147 |
148 | handlers = openapiRouter.registerRoute({
149 | method: methods.toLowerCase(),
150 | path: paths,
151 | handlers: handlers,
152 | });
153 |
154 | handlers = [paths, ...handlers];
155 | }
156 | }
157 |
158 | const resp = Reflect.get(target, prop, ...args)(route, ...handlers);
159 |
160 | if (HIJACKED_METHODS.has(prop)) {
161 | return proxy;
162 | }
163 |
164 | return resp;
165 | };
166 | },
167 | });
168 |
169 | return proxy as HonoOpenAPIRouterType;
170 | }
171 |
--------------------------------------------------------------------------------
/src/adapters/ittyRouter.ts:
--------------------------------------------------------------------------------
1 | import { OpenAPIHandler, type OpenAPIRouterType } from "../openapi";
2 | import type { OpenAPIRoute } from "../route";
3 | import type { RouterOptions } from "../types";
4 |
5 | export type IttyRouterOpenAPIRouterType = OpenAPIRouterType & {
6 | all(path: string, endpoint: typeof OpenAPIRoute): (M & any)["all"];
7 | all(path: string, router: M): (M & any)["all"];
8 | delete(path: string, endpoint: typeof OpenAPIRoute): (M & any)["delete"];
9 | delete(path: string, router: M): (M & any)["delete"];
10 | get(path: string, endpoint: typeof OpenAPIRoute): (M & any)["get"];
11 | get(path: string, router: M): (M & any)["get"];
12 | head(path: string, endpoint: typeof OpenAPIRoute): (M & any)["head"];
13 | head(path: string, router: M): (M & any)["head"];
14 | patch(path: string, endpoint: typeof OpenAPIRoute): (M & any)["patch"];
15 | patch(path: string, router: M): (M & any)["patch"];
16 | post(path: string, endpoint: typeof OpenAPIRoute): (M & any)["post"];
17 | post(path: string, router: M): (M & any)["post"];
18 | put(path: string, endpoint: typeof OpenAPIRoute): (M & any)["put"];
19 | put(path: string, router: M): (M & any)["put"];
20 | };
21 |
22 | export class IttyRouterOpenAPIHandler extends OpenAPIHandler {
23 | getRequest(args: any[]) {
24 | return args[0];
25 | }
26 |
27 | getUrlParams(args: any[]): Record {
28 | return args[0].params;
29 | }
30 |
31 | getBindings(args: any[]): Record {
32 | return args[1];
33 | }
34 | }
35 |
36 | export function fromIttyRouter(router: M, options?: RouterOptions): M & IttyRouterOpenAPIRouterType {
37 | const openapiRouter = new IttyRouterOpenAPIHandler(router, options);
38 |
39 | return new Proxy(router, {
40 | get: (target: any, prop: string, ...args: any[]) => {
41 | const _result = openapiRouter.handleCommonProxy(target, prop, ...args);
42 | if (_result !== undefined) {
43 | return _result;
44 | }
45 |
46 | return (route: string, ...handlers: any[]) => {
47 | if (prop !== "fetch") {
48 | if (handlers.length === 1 && handlers[0].isChanfana === true) {
49 | handlers = openapiRouter.registerNestedRouter({
50 | method: prop,
51 | nestedRouter: handlers[0],
52 | path: undefined,
53 | });
54 | } else if (openapiRouter.allowedMethods.includes(prop)) {
55 | handlers = openapiRouter.registerRoute({
56 | method: prop,
57 | path: route,
58 | handlers: handlers,
59 | });
60 | }
61 | }
62 |
63 | return Reflect.get(target, prop, ...args)(route, ...handlers);
64 | };
65 | },
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { spawn } from "node:child_process";
3 | import { mkdir, writeFile } from "node:fs/promises";
4 | import { dirname, join } from "node:path";
5 |
6 | const READY_KEYWORD = "ready on";
7 | const URL_REGEX = /ready on\s+(https?:\/\/[\w\.-]+:\d+)/i;
8 |
9 | // Parse command-line arguments
10 | let outputFile = "schema.json";
11 | const wranglerArgs: string[] = ["wrangler", "dev"];
12 | const args = process.argv.slice(2);
13 |
14 | if (args.includes("--help") || args.includes("-h")) {
15 | console.log(`
16 | Usage: npx chanfana [options]
17 |
18 | Options:
19 | -o, --output Specify output file path (including optional directory)
20 | -h, --help Display this help message
21 |
22 | Examples:
23 | npx chanfana -o output/schemas/public-api.json
24 | npx chanfana --output schema.json
25 | npx chanfana --help
26 | `);
27 | process.exit(0);
28 | }
29 |
30 | for (let i = 0; i < args.length; i++) {
31 | if (args[i] === "-o" || args[i] === "--output") {
32 | if (i + 1 >= args.length) {
33 | console.error("Error: -o/--output requires a file path");
34 | process.exit(1);
35 | }
36 | const filePath = args[i + 1];
37 | if (!filePath) {
38 | console.error("Error: -o/--output file path cannot be empty");
39 | process.exit(1);
40 | }
41 | outputFile = filePath;
42 | i++;
43 | } else if (typeof args[i] === "string") {
44 | // Ensure args[i] is a defined string
45 | wranglerArgs.push(args[i] as string);
46 | }
47 | }
48 |
49 | // Resolve output file path and ensure directory exists
50 | const resolvedOutputFile: string = join(process.cwd(), outputFile);
51 | const outputDir: string = dirname(resolvedOutputFile);
52 |
53 | // Spawn the 'npx wrangler dev' command with custom arguments
54 | const childProcess = spawn("npx", wranglerArgs, {
55 | cwd: process.cwd(),
56 | stdio: ["inherit", "pipe", "pipe"],
57 | shell: true,
58 | });
59 |
60 | // Buffer stdout and stderr lines in memory
61 | const outputBuffer: string[] = [];
62 |
63 | // Read stdout line by line
64 | childProcess.stdout.on("data", (data: Buffer) => {
65 | const line = data.toString().trim();
66 | outputBuffer.push(line);
67 | });
68 |
69 | // Read stderr line by line
70 | childProcess.stderr.on("data", (data: Buffer) => {
71 | const line = data.toString().trim();
72 | outputBuffer.push(`Error: ${line}`);
73 | });
74 |
75 | // Process stdout for "ready on" and fetch schema
76 | childProcess.stdout.on("data", async (data: Buffer) => {
77 | const line = data.toString().trim();
78 |
79 | if (line.toLowerCase().includes(READY_KEYWORD)) {
80 | const match = line.match(URL_REGEX);
81 | if (match?.[1]) {
82 | const url = match[1];
83 | const request = new Request(`${url}/openapi.json`, {
84 | method: "GET",
85 | headers: {
86 | "Content-Type": "application/json",
87 | },
88 | });
89 |
90 | try {
91 | const response = await fetch(request);
92 |
93 | if (!response.ok) {
94 | console.error(`Error fetching schema: ${response.status} ${response.statusText}`);
95 | const body = await response.text();
96 | console.error("Response body:", body);
97 | console.error("Buffered output:", outputBuffer.join("\n"));
98 | childProcess.kill("SIGTERM");
99 | process.exit(1);
100 | }
101 |
102 | const schema: { paths?: Record> } = await response.json();
103 |
104 | // Remove paths with x-ignore: true
105 | if (schema.paths && Object.keys(schema.paths).length > 0) {
106 | for (const path in schema.paths) {
107 | const pathObj = schema.paths[path];
108 | for (const method in pathObj) {
109 | // @ts-ignore
110 | if (pathObj[method]["x-ignore"] === true) {
111 | delete schema.paths[path];
112 | break;
113 | }
114 | }
115 | }
116 | }
117 |
118 | const schemaString = JSON.stringify(schema, null, 2);
119 |
120 | try {
121 | // Create output directory if it doesn't exist
122 | await mkdir(outputDir, { recursive: true });
123 | await writeFile(resolvedOutputFile, schemaString);
124 | console.log(`Schema written to ${resolvedOutputFile}`);
125 | } catch (err: unknown) {
126 | const error = err as Error;
127 | console.error(`Error writing schema to ${resolvedOutputFile}: ${error.message}`);
128 | console.error("Buffered output:", outputBuffer.join("\n"));
129 | childProcess.kill("SIGTERM");
130 | process.exit(1);
131 | }
132 |
133 | console.log("Successfully extracted schema");
134 | childProcess.kill("SIGTERM");
135 | process.exit(0);
136 | } catch (err: unknown) {
137 | const error = err as Error;
138 | console.error(`Fetch error: ${error.message}`);
139 | console.error("Buffered output:", outputBuffer.join("\n"));
140 | childProcess.kill("SIGTERM");
141 | process.exit(1);
142 | }
143 | } else {
144 | console.error(`No URL found in "ready on" line: ${line}`);
145 | console.error("Buffered output:", outputBuffer.join("\n"));
146 | }
147 | }
148 | });
149 |
150 | // Terminate after 60 seconds if not ready
151 | const timeoutId = setTimeout(() => {
152 | childProcess.kill("SIGTERM");
153 | console.error(`Command "npx wrangler dev" was never ready, exiting...`);
154 | console.error("Buffered output:", outputBuffer.join("\n"));
155 | process.exit(1);
156 | }, 60000);
157 |
158 | // Handle parent process exit scenarios
159 | const cleanup = () => {
160 | clearTimeout(timeoutId);
161 | if (!childProcess.killed) {
162 | childProcess.kill("SIGTERM");
163 | console.log("Cleaning up child process on exit");
164 | }
165 | };
166 |
167 | process.on("exit", cleanup);
168 | process.on("SIGINT", () => {
169 | console.log("Received SIGINT (Ctrl+C), exiting...");
170 | cleanup();
171 | process.exit(0);
172 | });
173 | process.on("SIGTERM", () => {
174 | console.log("Received SIGTERM, exiting...");
175 | cleanup();
176 | process.exit(0);
177 | });
178 | process.on("uncaughtException", (err: unknown) => {
179 | const error = err as Error;
180 | console.error("Uncaught Exception:", error.message);
181 | console.error("Buffered output:", outputBuffer.join("\n"));
182 | cleanup();
183 | process.exit(1);
184 | });
185 |
--------------------------------------------------------------------------------
/src/contentTypes.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { legacyTypeIntoZod } from "./zod/utils";
3 |
4 | type JsonContent = {
5 | content: {
6 | "application/json": {
7 | schema: z.ZodType;
8 | };
9 | };
10 | };
11 |
12 | type InferSchemaType = T extends z.ZodType ? z.infer : T;
13 |
14 | export const contentJson = (schema: T): JsonContent> => ({
15 | content: {
16 | "application/json": {
17 | schema: schema instanceof z.ZodType ? schema : legacyTypeIntoZod(schema),
18 | },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/endpoints/create.ts:
--------------------------------------------------------------------------------
1 | import { contentJson } from "../contentTypes";
2 | import { InputValidationException } from "../exceptions";
3 | import { OpenAPIRoute } from "../route";
4 | import { MetaGenerator, type MetaInput, type O } from "./types";
5 |
6 | export class CreateEndpoint = Array