From 46afaf9ddf53864e655d9f16e6e42e325b36b6dd Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Fri, 10 Oct 2025 01:47:57 +0900 Subject: [PATCH 01/10] feat: introduce `serve` command Co-authored-by: Taku Amano --- README.md | 17 +++++ bun.lock | 10 ++- package.json | 6 +- src/cli.ts | 2 + src/commands/serve/builtin-map.ts | 44 ++++++++++++ src/commands/serve/index.ts | 112 ++++++++++++++++++++++++++++++ src/commands/serve/server.ts | 5 ++ tsconfig.json | 9 ++- tsup.config.ts | 2 +- 9 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 src/commands/serve/builtin-map.ts create mode 100644 src/commands/serve/index.ts create mode 100644 src/commands/serve/server.ts diff --git a/README.md b/README.md index e687973..20cb367 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,28 @@ hono hello # Say hello to someone hono hello yusuke + +# Start development server +hono serve + +# Start server with specific file +hono serve src/app.ts + +# Start server with middleware +hono serve --use 'logger()' --use 'cors()' + +# Start server and show routes +hono serve --show-routes ``` ## Commands - `hello [name]` - Say hello (default: Hono) +- `serve [entry]` - Start development server + - `[entry]` - Entry file (default: `./src/index.ts`) + - `-p, --port ` - Port number (default: 3000) + - `--show-routes` - Show registered routes + - `--use ` - Use built-in middleware (can be used multiple times) ## Authors diff --git a/bun.lock b/bun.lock index 6bf6717..9b3a255 100644 --- a/bun.lock +++ b/bun.lock @@ -4,13 +4,15 @@ "": { "name": "@hono/cli", "dependencies": { + "@hono/node-server": "^1.19.5", "commander": "^14.0.1", + "esbuild": "^0.25.10", + "hono": "^4.9.10", }, "devDependencies": { "@hono/eslint-config": "^1.1.1", "@types/node": "^24.7.0", "eslint": "^9.37.0", - "hono": "4.4.13", "np": "^10.2.0", "prettier": "^3.6.2", "tsup": "^8.5.0", @@ -101,6 +103,8 @@ "@hono/eslint-config": ["@hono/eslint-config@1.1.1", "", { "dependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.10.0", "eslint-config-prettier": "^10.1.1", "eslint-import-resolver-typescript": "^4.2.2", "eslint-plugin-import-x": "^4.1.1", "eslint-plugin-n": "^17.10.2", "typescript-eslint": "^8.27.0" }, "peerDependencies": { "eslint": "^9.0.0", "typescript": "^5.0.0" } }, "sha512-BMnaj+0zliH+OA4mxpY58zi1J5+zXMkpkZQlqwTSw5ZkBIPIgpOk+yRYm9poKL5PBrua3x7okTficFR1NwjLxg=="], + "@hono/node-server": ["@hono/node-server@1.19.5", "", { "peerDependencies": { "hono": "^4" } }, "sha512-iBuhh+uaaggeAuf+TftcjZyWh2GEgZcVGXkNtskLVoWaXhnJtC5HLHrU8W1KHDoucqO1MswwglmkWLFyiDn4WQ=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -493,7 +497,7 @@ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "hono": ["hono@4.4.13", "", {}, "sha512-c6qqenclmQ6wpXzqiElMa2jt423PVCmgBreDfC5s2lPPpGk7d0lOymd8QTzFZyYC5mSSs6imiTMPip+gLwuW/g=="], + "hono": ["hono@4.9.10", "", {}, "sha512-AlI15ijFyKTXR7eHo7QK7OR4RoKIedZvBuRjO8iy4zrxvlY5oFCdiRG/V/lFJHCNXJ0k72ATgnyzx8Yqa5arug=="], "hosted-git-info": ["hosted-git-info@8.1.0", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw=="], @@ -849,6 +853,8 @@ "tsup": ["tsup@8.5.0", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.25.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ=="], + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], diff --git a/package.json b/package.json index 2225713..4f2c52e 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,15 @@ }, "homepage": "https://hono.dev", "dependencies": { - "commander": "^14.0.1" + "@hono/node-server": "^1.19.5", + "commander": "^14.0.1", + "esbuild": "^0.25.10", + "hono": "^4.9.10" }, "devDependencies": { "@hono/eslint-config": "^1.1.1", "@types/node": "^24.7.0", "eslint": "^9.37.0", - "hono": "4.4.13", "np": "^10.2.0", "prettier": "^3.6.2", "tsup": "^8.5.0", diff --git a/src/cli.ts b/src/cli.ts index 0a0d7bf..42fdf4c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { helloCommand } from './commands/hello/index.js' +import { serveCommand } from './commands/serve/index.js' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -19,5 +20,6 @@ program // Register commands helloCommand(program) +serveCommand(program) program.parse() diff --git a/src/commands/serve/builtin-map.ts b/src/commands/serve/builtin-map.ts new file mode 100644 index 0000000..75f3ba4 --- /dev/null +++ b/src/commands/serve/builtin-map.ts @@ -0,0 +1,44 @@ +// Built-in imports mapping based on official documentation +export const builtinMap: Record = { + // Authentication + basicAuth: 'hono/basic-auth', + bearerAuth: 'hono/bearer-auth', + + // Security & Validation + csrf: 'hono/csrf', + secureHeaders: 'hono/secure-headers', + jwt: 'hono/jwt', + jwk: 'hono/jwk', + + // Request/Response Processing + cors: 'hono/cors', + etag: 'hono/etag', + compress: 'hono/compress', + prettyJSON: 'hono/pretty-json', + bodyLimit: 'hono/body-limit', + combine: 'hono/combine', + contextStorage: 'hono/context-storage', + methodOverride: 'hono/method-override', + trailingSlash: 'hono/trailing-slash', + + // Utilities & Performance + cache: 'hono/cache', + timeout: 'hono/timeout', + + // Logging & Monitoring + logger: 'hono/logger', + timing: 'hono/timing', + requestId: 'hono/request-id', + + // Internationalization + language: 'hono/language', + + // Access Control + ipRestriction: 'hono/ip-restriction', + + // Rendering + jsxRenderer: 'hono/jsx-renderer', + + // Static Files (Node.js specific) + serveStatic: '@hono/node-server/serve-static', +} diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts new file mode 100644 index 0000000..b58c24b --- /dev/null +++ b/src/commands/serve/index.ts @@ -0,0 +1,112 @@ +import { serveStatic } from '@hono/node-server/serve-static' +import type { Command } from 'commander' +import * as esbuild from 'esbuild' +import { Hono } from 'hono' +import { showRoutes } from 'hono/dev' +import { existsSync, realpathSync } from 'node:fs' +import { resolve, extname } from 'node:path' +import { pathToFileURL } from 'node:url' +import { builtinMap } from './builtin-map.js' + +// Keep serveStatic to prevent bundler removal +; +import { serve } from './server.js' +[serveStatic].forEach((f) => { + if (typeof f === 'function') { + // useless process to avoid being deleted by bundler + } +}) + +export function serveCommand(program: Command) { + program + .command('serve') + .description('Start development server') + .argument('[entry]', 'entry file', './src/index.ts') + .option('-p, --port ', 'port number') + .option('--show-routes', 'show registered routes') + .option( + '--use ', + 'use middleware', + (value, previous: string[]) => { + return previous ? [...previous, value] : [value] + }, + [] + ) + .action( + async (entry: string, options: { port?: string; showRoutes?: boolean; use?: string[] }) => { + const appPath = resolve(process.cwd(), entry) + + let app: Hono + + if (!existsSync(appPath)) { + // Create a default Hono app if entry file doesn't exist + app = new Hono() + } else { + const appFilePath = realpathSync(appPath) + const ext = extname(appFilePath) + + // TypeScript/JSX files need transformation and bundling + if (['.ts', '.tsx', '.jsx'].includes(ext)) { + // Use build API to resolve imports and bundle + const result = await esbuild.build({ + entryPoints: [appFilePath], + bundle: true, + write: false, + format: 'esm', + target: 'node20', + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + platform: 'node', + external: ['@hono/node-server'], // Keep server external + sourcemap: 'inline', + }) + + // Execute the bundled code using data URL + const code = result.outputFiles[0].text + const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` + const module = await import(dataUrl) + app = module.default + } else { + // Regular JS files can be imported directly + const module = await import(pathToFileURL(appFilePath).href) + app = module.default + } + } + + const baseApp = new Hono() + // Apply middleware from --use options + for (const use of options.use || []) { + baseApp.use(async (c, next) => { + // Check if it's a built-in function + const functionName = use.match(/^(\w+)\(/)?.[1] + if (functionName && builtinMap[functionName]) { + const module = await import(builtinMap[functionName]) + const fn = module[functionName] + const evalRes = eval(use.replace(functionName, 'fn')) + return typeof evalRes === 'function' ? evalRes(c, next) : evalRes + } else { + // Fallback to regular eval + const evalRes = eval(use) + return typeof evalRes === 'function' ? evalRes(c, next) : evalRes + } + }) + } + + baseApp.route('/', app) + + if (options.showRoutes) { + showRoutes(baseApp) + } + + serve( + { + fetch: baseApp.fetch, + port: options.port ? Number.parseInt(options.port) : 3000, + }, + (info) => { + console.log(`Listening on http://localhost:${info.port}`) + } + ) + } + ) +} diff --git a/src/commands/serve/server.ts b/src/commands/serve/server.ts new file mode 100644 index 0000000..31ee38b --- /dev/null +++ b/src/commands/serve/server.ts @@ -0,0 +1,5 @@ +import { serve as nodeServe } from '@hono/node-server' + +export function serve(options: any, callback?: (info: { port: number }) => void) { + return nodeServe(options, callback) +} diff --git a/tsconfig.json b/tsconfig.json index 3f11d28..84e48e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,11 +7,10 @@ "strict": true, "declaration": true, "skipLibCheck": true, - "types": ["node"], + "types": [ + "node" + ], "jsx": "react-jsx", "jsxImportSource": "hono/jsx", - }, - "include": [ - "src", - ] + } } \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts index d3ed47a..c537341 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -11,4 +11,4 @@ export default defineConfig({ banner: { js: '#!/usr/bin/env node', }, -}) \ No newline at end of file +}) From 732d7daf82c4bf54388557a4aeb3360cb8eee09a Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Fri, 10 Oct 2025 02:07:05 +0900 Subject: [PATCH 02/10] formart --- src/commands/serve/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index b58c24b..0e9c8b8 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -9,9 +9,8 @@ import { pathToFileURL } from 'node:url' import { builtinMap } from './builtin-map.js' // Keep serveStatic to prevent bundler removal -; import { serve } from './server.js' -[serveStatic].forEach((f) => { +;[serveStatic].forEach((f) => { if (typeof f === 'function') { // useless process to avoid being deleted by bundler } From 39246a05e0f739cd2611760013e62fa89f62a289 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 04:37:03 +0900 Subject: [PATCH 03/10] fix the defulat port as 7070 --- README.md | 2 +- src/commands/serve/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 685f9c7..dee3e0d 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ hono serve [entry] [options] **Options:** -- `-p, --port ` - Port number (default: 3000) +- `-p, --port ` - Port number (default: 7070) - `--show-routes` - Show registered routes - `--use ` - Use middleware (can be used multiple times) diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index cd50bdc..0f90ce7 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -100,7 +100,7 @@ export function serveCommand(program: Command) { serve( { fetch: baseApp.fetch, - port: options.port ? Number.parseInt(options.port) : 3000, + port: options.port ? Number.parseInt(options.port) : 7070, }, (info) => { console.log(`Listening on http://localhost:${info.port}`) From 8d8b1ab22639fcfe7bd8a217d852e68ee96de8b2 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 05:50:04 +0900 Subject: [PATCH 04/10] add test --- src/commands/serve/builtin-map.ts | 41 +++++ src/commands/serve/index.test.ts | 241 ++++++++++++++++++++++++++++++ src/commands/serve/index.ts | 39 +++-- src/commands/serve/server.ts | 5 - 4 files changed, 308 insertions(+), 18 deletions(-) create mode 100644 src/commands/serve/index.test.ts delete mode 100644 src/commands/serve/server.ts diff --git a/src/commands/serve/builtin-map.ts b/src/commands/serve/builtin-map.ts index 75f3ba4..92043f5 100644 --- a/src/commands/serve/builtin-map.ts +++ b/src/commands/serve/builtin-map.ts @@ -41,4 +41,45 @@ export const builtinMap: Record = { // Static Files (Node.js specific) serveStatic: '@hono/node-server/serve-static', + + // Helpers - Accepts + accepts: 'hono/accepts', + + // Helpers - Adapter + env: 'hono/adapter', + getRuntimeKey: 'hono/adapter', + + // Helpers - Connection Info + getConnInfo: 'hono/conninfo', + + // Helpers - Cookie + getCookie: 'hono/cookie', + getSignedCookie: 'hono/cookie', + setCookie: 'hono/cookie', + setSignedCookie: 'hono/cookie', + deleteCookie: 'hono/cookie', + + // Helpers - CSS + css: 'hono/css', + rawCssString: 'hono/css', + viewTransition: 'hono/css', + createCssContext: 'hono/css', + createCssMiddleware: 'hono/css', + + // Helpers - HTML + html: 'hono/html', + raw: 'hono/html', + + // Helpers - JWT (different from middleware) + sign: 'hono/jwt', + verify: 'hono/jwt', + decode: 'hono/jwt', + + // Helpers - Proxy + proxy: 'hono/proxy', + + // Helpers - Streaming + stream: 'hono/streaming', + streamText: 'hono/streaming', + streamSSE: 'hono/streaming', } diff --git a/src/commands/serve/index.test.ts b/src/commands/serve/index.test.ts new file mode 100644 index 0000000..5a37e0a --- /dev/null +++ b/src/commands/serve/index.test.ts @@ -0,0 +1,241 @@ +import { Command } from 'commander' +import { Hono } from 'hono' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock dependencies +vi.mock('node:fs', () => ({ + existsSync: vi.fn(), + realpathSync: vi.fn(), +})) + +vi.mock('node:path', () => ({ + extname: vi.fn(), + resolve: vi.fn(), +})) + +vi.mock('node:url', () => ({ + pathToFileURL: vi.fn(), +})) + +vi.mock('esbuild', () => ({ + build: vi.fn(), +})) + +vi.mock('@hono/node-server', () => ({ + serve: vi.fn(), +})) + +vi.mock('hono/dev', () => ({ + showRoutes: vi.fn(), +})) + +vi.mock('./builtin-map.js', () => ({ + builtinMap: { + cors: 'hono/cors', + logger: 'hono/logger', + proxy: 'hono/proxy', + html: 'hono/html', + getCookie: 'hono/cookie', + basicAuth: 'hono/basic-auth', + }, +})) + +import { serveCommand } from './index.js' + +describe('serveCommand', () => { + let program: Command + let mockModules: any + let mockServe: any + let mockShowRoutes: any + let capturedFetchFunction: any + + beforeEach(async () => { + program = new Command() + serveCommand(program) + + // Get mocked modules + mockModules = { + existsSync: vi.mocked((await import('node:fs')).existsSync), + realpathSync: vi.mocked((await import('node:fs')).realpathSync), + extname: vi.mocked((await import('node:path')).extname), + resolve: vi.mocked((await import('node:path')).resolve), + pathToFileURL: vi.mocked((await import('node:url')).pathToFileURL), + } + + mockServe = vi.mocked((await import('@hono/node-server')).serve) + mockShowRoutes = vi.mocked((await import('hono/dev')).showRoutes) + + // Capture the fetch function passed to serve + mockServe.mockImplementation((options: any, callback?: any) => { + capturedFetchFunction = options.fetch + if (callback) { + callback({ port: options.port }) + } + }) + + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should start server with default port', async () => { + mockModules.existsSync.mockReturnValue(false) + mockModules.resolve.mockImplementation((cwd: string, path: string) => { + return `${cwd}/${path}` + }) + + await program.parseAsync(['node', 'test', 'serve']) + + // Verify serve was called with default port 7070 + expect(mockServe).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: expect.any(Function), + port: 7070, + }), + expect.any(Function) + ) + }) + + it('should start server with custom port', async () => { + mockModules.existsSync.mockReturnValue(false) + mockModules.resolve.mockImplementation((cwd: string, path: string) => { + return `${cwd}/${path}` + }) + + await program.parseAsync(['node', 'test', 'serve', '-p', '8080']) + + // Verify serve was called with custom port + expect(mockServe).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: expect.any(Function), + port: 8080, + }), + expect.any(Function) + ) + }) + + it('should serve app that responds correctly to requests', async () => { + const mockApp = new Hono() + mockApp.get('/', (c) => c.text('Hello World')) + mockApp.get('/api', (c) => c.json({ message: 'API response' })) + + const expectedPath = 'app.js' + const absolutePath = `${process.cwd()}/${expectedPath}` + + mockModules.existsSync.mockReturnValue(true) + mockModules.realpathSync.mockReturnValue(absolutePath) + mockModules.extname.mockReturnValue('.js') + mockModules.resolve.mockImplementation((cwd: string, path: string) => { + return `${cwd}/${path}` + }) + mockModules.pathToFileURL.mockReturnValue(new URL(`file://${absolutePath}`)) + + // Mock the import of JS file + vi.doMock(absolutePath, () => ({ default: mockApp })) + + await program.parseAsync(['node', 'test', 'serve', 'app.js']) + + // Test the captured fetch function + const rootRequest = new Request('http://localhost:7070/') + const rootResponse = await capturedFetchFunction(rootRequest) + expect(await rootResponse.text()).toBe('Hello World') + + const apiRequest = new Request('http://localhost:7070/api') + const apiResponse = await capturedFetchFunction(apiRequest) + const apiData = await apiResponse.json() + expect(apiData).toEqual({ message: 'API response' }) + }) + + it('should return 404 for non-existent routes when no app file exists', async () => { + mockModules.existsSync.mockReturnValue(false) + mockModules.resolve.mockImplementation((cwd: string, path: string) => { + return `${cwd}/${path}` + }) + + await program.parseAsync(['node', 'test', 'serve']) + + // Test 404 behavior with default empty app + const request = new Request('http://localhost:7070/non-existent') + const response = await capturedFetchFunction(request) + expect(response.status).toBe(404) + }) + + it('should handle typical use case: basicAuth + proxy to ramen-api.dev', async () => { + mockModules.existsSync.mockReturnValue(false) + mockModules.resolve.mockImplementation((cwd: string, path: string) => { + return `${cwd}/${path}` + }) + + // Mock basicAuth middleware + const mockBasicAuth = vi.fn().mockImplementation(() => { + return async (c: any, next: any) => { + const auth = c.req.header('Authorization') + if (!auth || auth !== 'Basic aG9ubzpob25v') { + // hono:hono in base64 + return c.text('Unauthorized', 401) + } + await next() + } + }) + + // Mock proxy helper + const mockProxy = vi.fn().mockImplementation((url) => { + if (url.includes('/shops')) { + return Promise.resolve( + new Response( + JSON.stringify({ + shops: [ + { id: 1, name: 'Ramen Taro', location: 'Tokyo' }, + { id: 2, name: 'Noodle House', location: 'Osaka' }, + ], + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ) + ) + } + return Promise.resolve(new Response('API endpoint not found', { status: 404 })) + }) + + vi.doMock('hono/basic-auth', () => ({ basicAuth: mockBasicAuth })) + vi.doMock('hono/proxy', () => ({ proxy: mockProxy })) + + await program.parseAsync([ + 'node', + 'test', + 'serve', + '--use', + 'basicAuth({username: "hono", password: "hono"})', + '--use', + '(c) => proxy(`https://ramen-api.dev${new URL(c.req.url).pathname}`)', + ]) + + // Test without auth - should get 401 + const unauthorizedRequest = new Request('http://localhost:7070/shops') + const unauthorizedResponse = await capturedFetchFunction(unauthorizedRequest) + expect(unauthorizedResponse.status).toBe(401) + expect(await unauthorizedResponse.text()).toBe('Unauthorized') + + // Test with valid auth - shops endpoint + const shopsRequest = new Request('http://localhost:7070/shops', { + headers: { Authorization: 'Basic aG9ubzpob25v' }, + }) + const shopsResponse = await capturedFetchFunction(shopsRequest) + expect(shopsResponse.status).toBe(200) + const shopsData = await shopsResponse.json() + expect(shopsData.shops).toHaveLength(2) + expect(shopsData.shops[0].name).toBe('Ramen Taro') + + // Test with valid auth - unknown endpoint + const unknownRequest = new Request('http://localhost:7070/unknown', { + headers: { Authorization: 'Basic aG9ubzpob25v' }, + }) + const unknownResponse = await capturedFetchFunction(unknownRequest) + expect(unknownResponse.status).toBe(404) + expect(await unknownResponse.text()).toBe('API endpoint not found') + }) +}) diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index 0f90ce7..e7c309d 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -1,3 +1,4 @@ +import { serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import type { Command } from 'commander' import * as esbuild from 'esbuild' @@ -9,7 +10,6 @@ import { pathToFileURL } from 'node:url' import { builtinMap } from './builtin-map.js' // Keep serveStatic to prevent bundler removal -import { serve } from './server.js' ;[serveStatic].forEach((f) => { if (typeof f === 'function') { // useless process to avoid being deleted by bundler @@ -72,22 +72,35 @@ export function serveCommand(program: Command) { } } + // Import all builtin functions from the builtin map + const allFunctions: Record = {} + const uniqueModules = [...new Set(Object.values(builtinMap))] + + for (const modulePath of uniqueModules) { + try { + const module = await import(modulePath) + // Add all exported functions from this module + for (const [funcName, modulePathInMap] of Object.entries(builtinMap)) { + if (modulePathInMap === modulePath && module[funcName]) { + allFunctions[funcName] = module[funcName] + } + } + } catch (error) { + // Skip modules that can't be imported (optional dependencies) + } + } + const baseApp = new Hono() // Apply middleware from --use options for (const use of options.use || []) { baseApp.use(async (c, next) => { - // Check if it's a built-in function - const functionName = use.match(/^(\w+)\(/)?.[1] - if (functionName && builtinMap[functionName]) { - const module = await import(builtinMap[functionName]) - const fn = module[functionName] - const evalRes = eval(use.replace(functionName, 'fn')) - return typeof evalRes === 'function' ? evalRes(c, next) : evalRes - } else { - // Fallback to regular eval - const evalRes = eval(use) - return typeof evalRes === 'function' ? evalRes(c, next) : evalRes - } + // Create function with all available functions in scope + const functionNames = Object.keys(allFunctions) + const functionValues = Object.values(allFunctions) + + const func = new Function('c', 'next', ...functionNames, `return (${use})`) + const middleware = func(c, next, ...functionValues) + return typeof middleware === 'function' ? middleware(c, next) : middleware }) } diff --git a/src/commands/serve/server.ts b/src/commands/serve/server.ts deleted file mode 100644 index 31ee38b..0000000 --- a/src/commands/serve/server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { serve as nodeServe } from '@hono/node-server' - -export function serve(options: any, callback?: (info: { port: number }) => void) { - return nodeServe(options, callback) -} From c9c921d0a0c5aefc7774240e04a8a380151cb464 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 06:43:54 +0900 Subject: [PATCH 05/10] add `poweredBy` and update CLAUDE.md --- CLAUDE.md | 5 +++++ src/commands/serve/builtin-map.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9aa6b0a..21a6a35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,6 +89,11 @@ For efficient documentation access from web URLs, convert paths using the follow Simply remove the `https://hono.dev` portion and append the path to the `hono docs` command to access documentation directly in the terminal. +**Important**: When you need to fetch information from Hono documentation (e.g., `https://hono.dev/docs/middleware/builtin/basic-auth`), always use the `hono docs` command instead of WebFetch. For example: + +- Instead of: `WebFetch(https://hono.dev/docs/middleware/builtin/basic-auth)` +- Use: `hono docs /docs/middleware/builtin/basic-auth` + ### Recommended Workflow 1. **Search first**: Use `hono search ` to find relevant documentation diff --git a/src/commands/serve/builtin-map.ts b/src/commands/serve/builtin-map.ts index 92043f5..478acea 100644 --- a/src/commands/serve/builtin-map.ts +++ b/src/commands/serve/builtin-map.ts @@ -24,6 +24,7 @@ export const builtinMap: Record = { // Utilities & Performance cache: 'hono/cache', timeout: 'hono/timeout', + poweredBy: 'hono/powered-by', // Logging & Monitoring logger: 'hono/logger', From 2272fcd1fcfa96d6f7a2d1d40f5b2cb7d361c4e8 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 06:56:26 +0900 Subject: [PATCH 06/10] don't use default file name --- src/commands/serve/index.test.ts | 18 ++++++++ src/commands/serve/index.ts | 72 ++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/commands/serve/index.test.ts b/src/commands/serve/index.test.ts index 5a37e0a..1848b89 100644 --- a/src/commands/serve/index.test.ts +++ b/src/commands/serve/index.test.ts @@ -162,6 +162,24 @@ describe('serveCommand', () => { expect(response.status).toBe(404) }) + it('should create default empty app when no entry argument provided', async () => { + await program.parseAsync(['node', 'test', 'serve']) + + // Verify serve was called + expect(mockServe).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: expect.any(Function), + port: 7070, + }), + expect.any(Function) + ) + + // Test that the default app returns 404 for any route + const rootRequest = new Request('http://localhost:7070/') + const rootResponse = await capturedFetchFunction(rootRequest) + expect(rootResponse.status).toBe(404) + }) + it('should handle typical use case: basicAuth + proxy to ramen-api.dev', async () => { mockModules.existsSync.mockReturnValue(false) mockModules.resolve.mockImplementation((cwd: string, path: string) => { diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index e7c309d..7e74e93 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -20,7 +20,7 @@ export function serveCommand(program: Command) { program .command('serve') .description('Start server') - .argument('[entry]', 'entry file', './src/index.ts') + .argument('[entry]', 'entry file') .option('-p, --port ', 'port number') .option('--show-routes', 'show registered routes') .option( @@ -32,43 +32,51 @@ export function serveCommand(program: Command) { [] ) .action( - async (entry: string, options: { port?: string; showRoutes?: boolean; use?: string[] }) => { - const appPath = resolve(process.cwd(), entry) - + async ( + entry: string | undefined, + options: { port?: string; showRoutes?: boolean; use?: string[] } + ) => { let app: Hono - if (!existsSync(appPath)) { - // Create a default Hono app if entry file doesn't exist + if (!entry) { + // Create a default Hono app if no entry is provided app = new Hono() } else { - const appFilePath = realpathSync(appPath) - const ext = extname(appFilePath) - - // TypeScript/JSX files need transformation and bundling - if (['.ts', '.tsx', '.jsx'].includes(ext)) { - // Use build API to resolve imports and bundle - const result = await esbuild.build({ - entryPoints: [appFilePath], - bundle: true, - write: false, - format: 'esm', - target: 'node20', - jsx: 'automatic', - jsxImportSource: 'hono/jsx', - platform: 'node', - external: ['@hono/node-server'], // Keep server external - sourcemap: 'inline', - }) + const appPath = resolve(process.cwd(), entry) - // Execute the bundled code using data URL - const code = result.outputFiles[0].text - const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` - const module = await import(dataUrl) - app = module.default + if (!existsSync(appPath)) { + // Create a default Hono app if entry file doesn't exist + app = new Hono() } else { - // Regular JS files can be imported directly - const module = await import(pathToFileURL(appFilePath).href) - app = module.default + const appFilePath = realpathSync(appPath) + const ext = extname(appFilePath) + + // TypeScript/JSX files need transformation and bundling + if (['.ts', '.tsx', '.jsx'].includes(ext)) { + // Use build API to resolve imports and bundle + const result = await esbuild.build({ + entryPoints: [appFilePath], + bundle: true, + write: false, + format: 'esm', + target: 'node20', + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + platform: 'node', + external: ['@hono/node-server'], // Keep server external + sourcemap: 'inline', + }) + + // Execute the bundled code using data URL + const code = result.outputFiles[0].text + const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` + const module = await import(dataUrl) + app = module.default + } else { + // Regular JS files can be imported directly + const module = await import(pathToFileURL(appFilePath).href) + app = module.default + } } } From 06bf24d3d210ba58d939e3f55fe6ab665f49ed55 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 07:02:10 +0900 Subject: [PATCH 07/10] update the readme --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ad734b5..691e5f9 100644 --- a/README.md +++ b/README.md @@ -200,26 +200,29 @@ hono serve [entry] [options] **Examples:** ```bash -# Start server with default settings (uses ./src/index.ts) +# Start server with default empty app (no entry file needed) hono serve # Start server on specific port -hono serve -p 8080 +hono serve -p 8080 src/app.ts # Start server with specific entry file hono serve src/app.ts # Start server and show routes -hono serve --show-routes +hono serve --show-routes src/app.ts # Start server with middleware -hono serve --use 'cors()' +hono serve --use 'cors()' src/app.ts # Start server with multiple middleware -hono serve --use 'cors()' --use 'logger()' +hono serve --use 'cors()' --use 'logger()' src/app.ts + +# Start server with authentication and static file serving +hono serve --use 'basicAuth({username:"foo", password:"bar"})' --use "serveStatic({ root: './' })" # Combine all options -hono serve src/app.ts -p 8080 --show-routes --use 'cors()' --use 'logger()' +hono serve -p 8080 --show-routes --use 'cors()' --use 'logger()' src/app.ts ``` ## Authors From f1d79e0b0dfd868083310ee7e4ed0d1444063e3c Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 07:42:56 +0900 Subject: [PATCH 08/10] make `utils/build.ts` --- src/commands/request/index.test.ts | 24 ++-- src/commands/request/index.ts | 36 +----- src/commands/serve/index.ts | 36 +----- src/utils/build.test.ts | 195 +++++++++++++++++++++++++++++ src/utils/build.ts | 39 ++++++ 5 files changed, 251 insertions(+), 79 deletions(-) create mode 100644 src/utils/build.test.ts create mode 100644 src/utils/build.ts diff --git a/src/commands/request/index.test.ts b/src/commands/request/index.test.ts index 4b8efab..4555d77 100644 --- a/src/commands/request/index.test.ts +++ b/src/commands/request/index.test.ts @@ -9,16 +9,11 @@ vi.mock('node:fs', () => ({ })) vi.mock('node:path', () => ({ - extname: vi.fn(), resolve: vi.fn(), })) -vi.mock('node:url', () => ({ - pathToFileURL: vi.fn(), -})) - -vi.mock('esbuild', () => ({ - build: vi.fn(), +vi.mock('../../utils/build.js', () => ({ + buildAndImportApp: vi.fn(), })) import { requestCommand } from './index.js' @@ -27,17 +22,15 @@ describe('requestCommand', () => { let program: Command let consoleLogSpy: ReturnType let mockModules: any + let mockBuildAndImportApp: any const setupBasicMocks = (appPath: string, mockApp: Hono) => { mockModules.existsSync.mockReturnValue(true) mockModules.realpathSync.mockReturnValue(appPath) - mockModules.extname.mockReturnValue('.js') mockModules.resolve.mockImplementation((cwd: string, path: string) => { return `${cwd}/${path}` }) - const absolutePath = appPath.startsWith('/') ? appPath : `${process.cwd()}/${appPath}` - mockModules.pathToFileURL.mockReturnValue(new URL(`file://${absolutePath}`)) - vi.doMock(absolutePath, () => ({ default: mockApp })) + mockBuildAndImportApp.mockResolvedValue(mockApp) } beforeEach(async () => { @@ -49,11 +42,11 @@ describe('requestCommand', () => { mockModules = { existsSync: vi.mocked((await import('node:fs')).existsSync), realpathSync: vi.mocked((await import('node:fs')).realpathSync), - extname: vi.mocked((await import('node:path')).extname), resolve: vi.mocked((await import('node:path')).resolve), - pathToFileURL: vi.mocked((await import('node:url')).pathToFileURL), } + mockBuildAndImportApp = vi.mocked((await import('../../utils/build.js')).buildAndImportApp) + vi.clearAllMocks() }) @@ -138,13 +131,10 @@ describe('requestCommand', () => { return path === resolvedPath }) mockModules.realpathSync.mockReturnValue(expectedPath) - mockModules.extname.mockReturnValue('.js') mockModules.resolve.mockImplementation((cwd: string, path: string) => { return `${cwd}/${path}` }) - const absolutePath = `${process.cwd()}/${expectedPath}` - mockModules.pathToFileURL.mockReturnValue(new URL(`file://${absolutePath}`)) - vi.doMock(absolutePath, () => ({ default: mockApp })) + mockBuildAndImportApp.mockResolvedValue(mockApp) await program.parseAsync(['node', 'test', 'request']) diff --git a/src/commands/request/index.ts b/src/commands/request/index.ts index 45d145a..124c49e 100644 --- a/src/commands/request/index.ts +++ b/src/commands/request/index.ts @@ -1,9 +1,8 @@ import type { Command } from 'commander' -import * as esbuild from 'esbuild' import type { Hono } from 'hono' import { existsSync, realpathSync } from 'node:fs' -import { extname, resolve } from 'node:path' -import { pathToFileURL } from 'node:url' +import { resolve } from 'node:path' +import { buildAndImportApp } from '../../utils/build.js' const DEFAULT_ENTRY_CANDIDATES = ['src/index.ts', 'src/index.tsx', 'src/index.js', 'src/index.jsx'] @@ -51,39 +50,14 @@ export async function executeRequest( resolvedAppPath = resolve(process.cwd(), entry) } - const ext = extname(entry) - if (!existsSync(resolvedAppPath)) { throw new Error(`Entry file ${entry} does not exist`) } - let app: Hono const appFilePath = realpathSync(resolvedAppPath) - - if (['.ts', '.tsx', '.jsx'].includes(ext)) { - // Use esbuild to bundle TypeScript/JSX files - const result = await esbuild.build({ - entryPoints: [appFilePath], - bundle: true, - write: false, - format: 'esm', - target: 'node20', - jsx: 'automatic', - jsxImportSource: 'hono/jsx', - platform: 'node', - external: ['@hono/node-server'], - }) - - // Execute the bundled code using data URL - const code = result.outputFiles[0].text - const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` - const module = await import(dataUrl) - app = module.default - } else { - // Regular JS files can be imported directly - const module = await import(pathToFileURL(appFilePath).href) - app = module.default - } + const app: Hono = await buildAndImportApp(appFilePath, { + external: ['@hono/node-server'], + }) if (!app || typeof app.request !== 'function') { throw new Error('No valid Hono app exported from the file') diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index 7e74e93..286ccdb 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -1,12 +1,11 @@ import { serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import type { Command } from 'commander' -import * as esbuild from 'esbuild' import { Hono } from 'hono' import { showRoutes } from 'hono/dev' import { existsSync, realpathSync } from 'node:fs' -import { resolve, extname } from 'node:path' -import { pathToFileURL } from 'node:url' +import { resolve } from 'node:path' +import { buildAndImportApp } from '../../utils/build.js' import { builtinMap } from './builtin-map.js' // Keep serveStatic to prevent bundler removal @@ -49,34 +48,9 @@ export function serveCommand(program: Command) { app = new Hono() } else { const appFilePath = realpathSync(appPath) - const ext = extname(appFilePath) - - // TypeScript/JSX files need transformation and bundling - if (['.ts', '.tsx', '.jsx'].includes(ext)) { - // Use build API to resolve imports and bundle - const result = await esbuild.build({ - entryPoints: [appFilePath], - bundle: true, - write: false, - format: 'esm', - target: 'node20', - jsx: 'automatic', - jsxImportSource: 'hono/jsx', - platform: 'node', - external: ['@hono/node-server'], // Keep server external - sourcemap: 'inline', - }) - - // Execute the bundled code using data URL - const code = result.outputFiles[0].text - const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` - const module = await import(dataUrl) - app = module.default - } else { - // Regular JS files can be imported directly - const module = await import(pathToFileURL(appFilePath).href) - app = module.default - } + app = await buildAndImportApp(appFilePath, { + external: ['@hono/node-server'], + }) } } diff --git a/src/utils/build.test.ts b/src/utils/build.test.ts new file mode 100644 index 0000000..7fd4e31 --- /dev/null +++ b/src/utils/build.test.ts @@ -0,0 +1,195 @@ +import { Hono } from 'hono' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock dependencies +vi.mock('esbuild', () => ({ + build: vi.fn(), +})) + +vi.mock('node:url', () => ({ + pathToFileURL: vi.fn(), +})) + +vi.mock('node:path', () => ({ + extname: vi.fn(), +})) + +import { buildAndImportApp } from './build.js' + +describe('buildAndImportApp', () => { + let mockEsbuild: any + let mockPathToFileURL: any + let mockExtname: any + + beforeEach(async () => { + mockEsbuild = vi.mocked((await import('esbuild')).build) + mockPathToFileURL = vi.mocked((await import('node:url')).pathToFileURL) + mockExtname = vi.mocked((await import('node:path')).extname) + + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should build and import TypeScript file', async () => { + const mockApp = new Hono() + const filePath = '/path/to/app.ts' + const bundledCode = 'export default app;' + + mockExtname.mockReturnValue('.ts') + mockEsbuild.mockResolvedValue({ + outputFiles: [{ text: bundledCode }], + }) + + // Mock dynamic import + const dataUrl = `data:text/javascript;base64,${Buffer.from(bundledCode).toString('base64')}` + vi.doMock(dataUrl, () => ({ default: mockApp })) + + const result = await buildAndImportApp(filePath) + + expect(mockExtname).toHaveBeenCalledWith(filePath) + expect(mockEsbuild).toHaveBeenCalledWith({ + entryPoints: [filePath], + bundle: true, + write: false, + format: 'esm', + target: 'node20', + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + platform: 'node', + external: [], + }) + expect(result).toBe(mockApp) + }) + + it('should build and import JSX file with custom options', async () => { + const mockApp = new Hono() + const filePath = '/path/to/app.jsx' + const bundledCode = 'export default app;' + const options = { + external: ['@hono/node-server'], + } + + mockExtname.mockReturnValue('.jsx') + mockEsbuild.mockResolvedValue({ + outputFiles: [{ text: bundledCode }], + }) + + // Mock dynamic import + const dataUrl = `data:text/javascript;base64,${Buffer.from(bundledCode).toString('base64')}` + vi.doMock(dataUrl, () => ({ default: mockApp })) + + const result = await buildAndImportApp(filePath, options) + + expect(mockEsbuild).toHaveBeenCalledWith({ + entryPoints: [filePath], + bundle: true, + write: false, + format: 'esm', + target: 'node20', + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + platform: 'node', + external: ['@hono/node-server'], + }) + expect(result).toBe(mockApp) + }) + + it('should build and import TSX file', async () => { + const mockApp = new Hono() + const filePath = '/path/to/app.tsx' + const bundledCode = 'export default app;' + + mockExtname.mockReturnValue('.tsx') + mockEsbuild.mockResolvedValue({ + outputFiles: [{ text: bundledCode }], + }) + + // Mock dynamic import + const dataUrl = `data:text/javascript;base64,${Buffer.from(bundledCode).toString('base64')}` + vi.doMock(dataUrl, () => ({ default: mockApp })) + + const result = await buildAndImportApp(filePath) + + expect(mockEsbuild).toHaveBeenCalledWith( + expect.objectContaining({ + entryPoints: [filePath], + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + }) + ) + expect(result).toBe(mockApp) + }) + + it('should directly import JavaScript file without building', async () => { + const mockApp = new Hono() + const filePath = '/path/to/app.js' + const fileUrl = new URL(`file://${filePath}`) + + mockExtname.mockReturnValue('.js') + mockPathToFileURL.mockReturnValue(fileUrl) + + // Mock dynamic import for JS file + vi.doMock(fileUrl.href, () => ({ default: mockApp })) + + const result = await buildAndImportApp(filePath) + + expect(mockExtname).toHaveBeenCalledWith(filePath) + expect(mockPathToFileURL).toHaveBeenCalledWith(filePath) + expect(mockEsbuild).not.toHaveBeenCalled() + expect(result).toBe(mockApp) + }) + + it('should handle esbuild errors', async () => { + const filePath = '/path/to/app.ts' + const buildError = new Error('Build failed') + + mockExtname.mockReturnValue('.ts') + mockEsbuild.mockRejectedValue(buildError) + + await expect(buildAndImportApp(filePath)).rejects.toThrow('Build failed') + }) + + it('should handle import errors for JS files', async () => { + const filePath = '/path/to/nonexistent.js' + const fileUrl = new URL(`file://${filePath}`) + + mockExtname.mockReturnValue('.js') + mockPathToFileURL.mockReturnValue(fileUrl) + + // Since we can't easily mock dynamic import, just test with a non-existent module + // The import will naturally fail + await expect(buildAndImportApp(filePath)).rejects.toThrow() + }) + + it('should use default empty options when none provided', async () => { + const mockApp = new Hono() + const filePath = '/path/to/app.ts' + const bundledCode = 'export default app;' + + mockExtname.mockReturnValue('.ts') + mockEsbuild.mockResolvedValue({ + outputFiles: [{ text: bundledCode }], + }) + + // Mock dynamic import + const dataUrl = `data:text/javascript;base64,${Buffer.from(bundledCode).toString('base64')}` + vi.doMock(dataUrl, () => ({ default: mockApp })) + + await buildAndImportApp(filePath) + + expect(mockEsbuild).toHaveBeenCalledWith({ + entryPoints: [filePath], + bundle: true, + write: false, + format: 'esm', + target: 'node20', + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + platform: 'node', + external: [], + }) + }) +}) diff --git a/src/utils/build.ts b/src/utils/build.ts new file mode 100644 index 0000000..ce3848e --- /dev/null +++ b/src/utils/build.ts @@ -0,0 +1,39 @@ +import * as esbuild from 'esbuild' +import { extname } from 'node:path' +import { pathToFileURL } from 'node:url' + +export interface BuildOptions { + external?: string[] +} + +/** + * Build and import a TypeScript/JSX/JS file as an app + */ +export async function buildAndImportApp(filePath: string, options: BuildOptions = {}) { + const ext = extname(filePath) + + // TypeScript/JSX files need transformation and bundling + if (['.ts', '.tsx', '.jsx'].includes(ext)) { + const result = await esbuild.build({ + entryPoints: [filePath], + bundle: true, + write: false, + format: 'esm', + target: 'node20', + jsx: 'automatic', + jsxImportSource: 'hono/jsx', + platform: 'node', + external: options.external || [], + }) + + // Execute the bundled code using data URL + const code = result.outputFiles[0].text + const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` + const module = await import(dataUrl) + return module.default + } else { + // Regular JS files can be imported directly + const module = await import(pathToFileURL(filePath).href) + return module.default + } +} From 7db887fe99d4877e703573c1fd0f24bfe2bfeac5 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 16:26:36 +0900 Subject: [PATCH 09/10] define func and functions* outside the handler --- src/commands/serve/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index 286ccdb..af432e0 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -75,12 +75,11 @@ export function serveCommand(program: Command) { const baseApp = new Hono() // Apply middleware from --use options for (const use of options.use || []) { + // Create function with all available functions in scope + const functionNames = Object.keys(allFunctions) + const functionValues = Object.values(allFunctions) + const func = new Function('c', 'next', ...functionNames, `return (${use})`) baseApp.use(async (c, next) => { - // Create function with all available functions in scope - const functionNames = Object.keys(allFunctions) - const functionValues = Object.values(allFunctions) - - const func = new Function('c', 'next', ...functionNames, `return (${use})`) const middleware = func(c, next, ...functionValues) return typeof middleware === 'function' ? middleware(c, next) : middleware }) From 3a02408d32e9ec33cb797a30600f95da942fd32b Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Wed, 15 Oct 2025 16:28:43 +0900 Subject: [PATCH 10/10] fix the import path of `getConnInfo` --- src/commands/serve/builtin-map.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/serve/builtin-map.ts b/src/commands/serve/builtin-map.ts index 478acea..7a2eb4e 100644 --- a/src/commands/serve/builtin-map.ts +++ b/src/commands/serve/builtin-map.ts @@ -50,8 +50,8 @@ export const builtinMap: Record = { env: 'hono/adapter', getRuntimeKey: 'hono/adapter', - // Helpers - Connection Info - getConnInfo: 'hono/conninfo', + // Helpers - Connection Info (Node.js specific) + getConnInfo: '@hono/node-server/conninfo', // Helpers - Cookie getCookie: 'hono/cookie',