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/README.md b/README.md index 2290b20..691e5f9 100644 --- a/README.md +++ b/README.md @@ -14,80 +14,25 @@ npm install -g @hono/cli # Show help hono --help -# Send request to Hono app -hono request - # Display documentation hono docs # Search documentation hono search middleware -``` - -## Commands - -- `request [file]` - Send request to Hono app using `app.request()` -- `docs [path]` - Display Hono documentation -- `search ` - Search Hono documentation - -### `request` - -Send HTTP requests to your Hono application using the built-in `app.request()` method. This is particularly useful for testing and development. - -```bash -hono request [file] [options] -``` - -**Arguments:** -- `file` - Path to the Hono app file (TypeScript/JSX supported, optional) - -**Options:** - -- `-P, --path ` - Request path (default: "/") -- `-X, --method ` - HTTP method (default: GET) -- `-d, --data ` - Request body data -- `-H, --header
` - Custom headers (can be used multiple times) - -**Examples:** - -```bash -# GET request to default app root (uses src/index.ts or src/index.tsx) +# Send request to Hono app hono request -# GET request to specific path -hono request -P /users/123 - -# POST request with data -hono request -P /api/users -X POST -d '{"name":"Alice"}' - -# Request to specific file -hono request -P /api src/your-app.ts - -# Request with custom headers -hono request -P /api/protected -H 'Authorization: Bearer token' -H 'User-Agent: MyApp' src/your-app.ts - -# PUT request with data and headers to specific file -hono request -P /api/users/1 -X PUT -d '{"name":"Bob"}' -H 'Content-Type: application/json' src/your-app.ts - -# Complex example with multiple options -hono request -P /webhook -X POST -d '{"event":"test"}' -H 'Content-Type: application/json' -H 'X-API-Key: secret' my-project/server.ts +# Start server +hono serve ``` -**Response Format:** - -The command returns a JSON object with the following structure: +## Commands -```json -{ - "status": 200, - "body": "{\"message\":\"Hello World\"}", - "headers": { - "content-type": "application/json", - "x-custom-header": "value" - } -} -``` +- `docs [path]` - Display Hono documentation +- `search [query]` - Search Hono documentation +- `request [file]` - Send request to Hono app using `app.request()` +- `serve [entry]` - Start server for Hono app ### `docs` @@ -175,6 +120,111 @@ Example output: Command: hono docs /docs/middleware/third-party ``` +### `request` + +Send HTTP requests to your Hono application using the built-in `app.request()` method. This is particularly useful for testing and development. + +```bash +hono request [file] [options] +``` + +**Arguments:** + +- `file` - Path to the Hono app file (TypeScript/JSX supported, optional) + +**Options:** + +- `-P, --path ` - Request path (default: "/") +- `-X, --method ` - HTTP method (default: GET) +- `-d, --data ` - Request body data +- `-H, --header
` - Custom headers (can be used multiple times) + +**Examples:** + +```bash +# GET request to default app root (uses src/index.ts or src/index.tsx) +hono request + +# GET request to specific path +hono request -P /users/123 + +# POST request with data +hono request -P /api/users -X POST -d '{"name":"Alice"}' + +# Request to specific file +hono request -P /api src/your-app.ts + +# Request with custom headers +hono request -P /api/protected -H 'Authorization: Bearer token' -H 'User-Agent: MyApp' src/your-app.ts + +# PUT request with data and headers to specific file +hono request -P /api/users/1 -X PUT -d '{"name":"Bob"}' -H 'Content-Type: application/json' src/your-app.ts + +# Complex example with multiple options +hono request -P /webhook -X POST -d '{"event":"test"}' -H 'Content-Type: application/json' -H 'X-API-Key: secret' my-project/server.ts +``` + +**Response Format:** + +The command returns a JSON object with the following structure: + +```json +{ + "status": 200, + "body": "{\"message\":\"Hello World\"}", + "headers": { + "content-type": "application/json", + "x-custom-header": "value" + } +} +``` + +### `serve` + +Start a server for your Hono application. This is a simple server specialized for Hono applications with built-in TypeScript and JSX support. + +```bash +hono serve [entry] [options] +``` + +**Arguments:** + +- `entry` - Entry file for your Hono app (TypeScript/JSX supported, optional) + +**Options:** + +- `-p, --port ` - Port number (default: 7070) +- `--show-routes` - Show registered routes +- `--use ` - Use middleware (can be used multiple times) + +**Examples:** + +```bash +# Start server with default empty app (no entry file needed) +hono serve + +# Start server on specific port +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 src/app.ts + +# Start server with middleware +hono serve --use 'cors()' src/app.ts + +# Start server with multiple middleware +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 -p 8080 --show-routes --use 'cors()' --use 'logger()' src/app.ts +``` + ## Authors - Yusuke Wada https://github.com/yusukebe diff --git a/bun.lock b/bun.lock index 1258452..a77f6fd 100644 --- a/bun.lock +++ b/bun.lock @@ -4,14 +4,15 @@ "": { "name": "@hono/cli", "dependencies": { + "@hono/node-server": "^1.19.5", "commander": "^14.0.1", "esbuild": "^0.25.10", + "hono": "^4.9.12", }, "devDependencies": { "@hono/eslint-config": "^1.1.1", "@types/node": "^24.7.0", "eslint": "^9.37.0", - "hono": "^4.9.12", "np": "^10.2.0", "prettier": "^3.6.2", "publint": "^0.3.14", @@ -104,6 +105,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=="], @@ -112,37 +115,41 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@inquirer/ansi": ["@inquirer/ansi@1.0.0", "", {}, "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.1", "", {}, "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw=="], - "@inquirer/checkbox": ["@inquirer/checkbox@4.2.4", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw=="], + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5+Q3PKH35YsnoPTh75LucALdAxom6xh5D1oeY561x4cqBuH24ZFVyFREPe14xgnrtmGu3EEt1dIi60wRVSnGCw=="], - "@inquirer/confirm": ["@inquirer/confirm@5.1.18", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw=="], + "@inquirer/confirm": ["@inquirer/confirm@5.1.19", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ=="], - "@inquirer/core": ["@inquirer/core@10.2.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA=="], + "@inquirer/core": ["@inquirer/core@10.3.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA=="], - "@inquirer/editor": ["@inquirer/editor@4.2.20", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/external-editor": "^1.0.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g=="], + "@inquirer/editor": ["@inquirer/editor@4.2.21", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/external-editor": "^1.0.2", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ=="], - "@inquirer/expand": ["@inquirer/expand@4.0.20", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow=="], + "@inquirer/expand": ["@inquirer/expand@4.0.21", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+mScLhIcbPFmuvU3tAGBed78XvYHSvCl6dBiYMlzCLhpr0bzGzd8tfivMMeqND6XZiaZ1tgusbUHJEfc6YzOdA=="], "@inquirer/external-editor": ["@inquirer/external-editor@1.0.2", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ=="], - "@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="], + "@inquirer/figures": ["@inquirer/figures@1.0.14", "", {}, "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ=="], - "@inquirer/input": ["@inquirer/input@4.2.4", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw=="], + "@inquirer/input": ["@inquirer/input@4.2.5", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-7GoWev7P6s7t0oJbenH0eQ0ThNdDJbEAEtVt9vsrYZ9FulIokvd823yLyhQlWHJPGce1wzP53ttfdCZmonMHyA=="], - "@inquirer/number": ["@inquirer/number@3.0.20", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg=="], + "@inquirer/number": ["@inquirer/number@3.0.21", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5QWs0KGaNMlhbdhOSCFfKsW+/dcAVC2g4wT/z2MCiZM47uLgatC5N20kpkDQf7dHx+XFct/MJvvNGy6aYJn4Pw=="], - "@inquirer/password": ["@inquirer/password@4.0.20", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug=="], + "@inquirer/password": ["@inquirer/password@4.0.21", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xxeW1V5SbNFNig2pLfetsDb0svWlKuhmr7MPJZMYuDnCTkpVBI+X/doudg4pznc1/U+yYmWFFOi4hNvGgUo7EA=="], - "@inquirer/prompts": ["@inquirer/prompts@7.8.6", "", { "dependencies": { "@inquirer/checkbox": "^4.2.4", "@inquirer/confirm": "^5.1.18", "@inquirer/editor": "^4.2.20", "@inquirer/expand": "^4.0.20", "@inquirer/input": "^4.2.4", "@inquirer/number": "^3.0.20", "@inquirer/password": "^4.0.20", "@inquirer/rawlist": "^4.1.8", "@inquirer/search": "^3.1.3", "@inquirer/select": "^4.3.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig=="], + "@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], - "@inquirer/rawlist": ["@inquirer/rawlist@4.1.8", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg=="], + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.9", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-AWpxB7MuJrRiSfTKGJ7Y68imYt8P9N3Gaa7ySdkFj1iWjr6WfbGAhdZvw/UnhFXTHITJzxGUI9k8IX7akAEBCg=="], - "@inquirer/search": ["@inquirer/search@3.1.3", "", { "dependencies": { "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q=="], + "@inquirer/search": ["@inquirer/search@3.2.0", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a5SzB/qrXafDX1Z4AZW3CsVoiNxcIYCzYP7r9RzrfMpaLpB+yWi5U8BWagZyLmwR0pKbbL5umnGRd0RzGVI8bQ=="], - "@inquirer/select": ["@inquirer/select@4.3.4", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA=="], + "@inquirer/select": ["@inquirer/select@4.4.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kaC3FHsJZvVyIjYBs5Ih8y8Bj4P/QItQWrZW22WJax7zTN+ZPXVGuOM55vzbdCP9zKUiBd9iEJVdesujfF+cAA=="], - "@inquirer/type": ["@inquirer/type@3.0.8", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw=="], + "@inquirer/type": ["@inquirer/type@3.0.9", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w=="], + + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -230,29 +237,29 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="], + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/type-utils": "8.46.0", "@typescript-eslint/utils": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/type-utils": "8.46.1", "@typescript-eslint/utils": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.0", "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.0", "@typescript-eslint/tsconfig-utils": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], @@ -554,7 +561,7 @@ "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], - "inquirer": ["inquirer@12.9.6", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/prompts": "^7.8.6", "@inquirer/type": "^3.0.8", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw=="], + "inquirer": ["inquirer@12.10.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/prompts": "^7.9.0", "@inquirer/type": "^3.0.9", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-K/epfEnDBZj2Q3NMDcgXWZye3nhSPeoJnOh8lcKWrldw54UEZfS4EmAMsAsmVbl7qKi+vjAsy39Sz4fbgRMewg=="], "inquirer-autosubmit-prompt": ["inquirer-autosubmit-prompt@0.2.0", "", { "dependencies": { "chalk": "^2.4.1", "inquirer": "^6.2.1", "rxjs": "^6.3.3" } }, "sha512-mzNrusCk5L6kSzlN0Ioddn8yzrhYNLli+Sn2ZxMuLechMYAzakiFCIULxsxlQb5YKzthLGfrFACcWoAvM7p04Q=="], @@ -622,7 +629,7 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "ky": ["ky@1.11.0", "", {}, "sha512-NEyo0ICpS0cqSuyoJFMCnHOZJILqXsKhIZlHJGDYaH8OB5IFrGzuBpEwyoMZG6gUKMPrazH30Ax5XKaujvD8ag=="], + "ky": ["ky@1.12.0", "", {}, "sha512-YRLmSUHCwOJRBMArtqMRLOmO7fewn3yOoui6aB8ERkRVXupa0UiaQaKbIXteMt4jUElhbdqTMsLFHs8APxxUoQ=="], "latest-version": ["latest-version@9.0.0", "", { "dependencies": { "package-json": "^10.0.0" } }, "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA=="], @@ -854,7 +861,7 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -926,7 +933,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.46.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.0", "@typescript-eslint/parser": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/utils": "8.46.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw=="], + "typescript-eslint": ["typescript-eslint@8.46.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], @@ -944,7 +951,7 @@ "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], - "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], + "vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -1016,7 +1023,7 @@ "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "eslint-plugin-import-x/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "eslint-plugin-import-x/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], @@ -1122,7 +1129,7 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "boxen/string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -1132,8 +1139,6 @@ "cli-truncate/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="], - "eslint-plugin-import-x/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "ignore-walk/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -1204,7 +1209,7 @@ "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "widest-line/string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + "widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], diff --git a/package.json b/package.json index ffef64c..8b4b847 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,15 @@ }, "homepage": "https://hono.dev", "dependencies": { + "@hono/node-server": "^1.19.5", "commander": "^14.0.1", - "esbuild": "^0.25.10" + "esbuild": "^0.25.10", + "hono": "^4.9.12" }, "devDependencies": { "@hono/eslint-config": "^1.1.1", "@types/node": "^24.7.0", "eslint": "^9.37.0", - "hono": "^4.9.12", "np": "^10.2.0", "prettier": "^3.6.2", "publint": "^0.3.14", @@ -49,4 +50,4 @@ "vitest": "^3.2.4" }, "packageManager": "bun@1.2.20" -} +} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 2a6966c..c3ccc0d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url' import { docsCommand } from './commands/docs/index.js' import { requestCommand } from './commands/request/index.js' import { searchCommand } from './commands/search/index.js' +import { serveCommand } from './commands/serve/index.js' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -21,7 +22,8 @@ program // Register commands docsCommand(program) -requestCommand(program) searchCommand(program) +requestCommand(program) +serveCommand(program) program.parse() 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/builtin-map.ts b/src/commands/serve/builtin-map.ts new file mode 100644 index 0000000..7a2eb4e --- /dev/null +++ b/src/commands/serve/builtin-map.ts @@ -0,0 +1,86 @@ +// 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', + poweredBy: 'hono/powered-by', + + // 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', + + // Helpers - Accepts + accepts: 'hono/accepts', + + // Helpers - Adapter + env: 'hono/adapter', + getRuntimeKey: 'hono/adapter', + + // Helpers - Connection Info (Node.js specific) + getConnInfo: '@hono/node-server/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..1848b89 --- /dev/null +++ b/src/commands/serve/index.test.ts @@ -0,0 +1,259 @@ +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 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) => { + 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 new file mode 100644 index 0000000..af432e0 --- /dev/null +++ b/src/commands/serve/index.ts @@ -0,0 +1,105 @@ +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import type { Command } from 'commander' +import { Hono } from 'hono' +import { showRoutes } from 'hono/dev' +import { existsSync, realpathSync } from 'node:fs' +import { resolve } from 'node:path' +import { buildAndImportApp } from '../../utils/build.js' +import { builtinMap } from './builtin-map.js' + +// Keep serveStatic to prevent bundler removal +;[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 server') + .argument('[entry]', 'entry file') + .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 | undefined, + options: { port?: string; showRoutes?: boolean; use?: string[] } + ) => { + let app: Hono + + if (!entry) { + // Create a default Hono app if no entry is provided + app = new Hono() + } else { + const appPath = resolve(process.cwd(), entry) + + if (!existsSync(appPath)) { + // Create a default Hono app if entry file doesn't exist + app = new Hono() + } else { + const appFilePath = realpathSync(appPath) + app = await buildAndImportApp(appFilePath, { + external: ['@hono/node-server'], + }) + } + } + + // 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 || []) { + // 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) => { + const middleware = func(c, next, ...functionValues) + return typeof middleware === 'function' ? middleware(c, next) : middleware + }) + } + + baseApp.route('/', app) + + if (options.showRoutes) { + showRoutes(baseApp) + } + + serve( + { + fetch: baseApp.fetch, + port: options.port ? Number.parseInt(options.port) : 7070, + }, + (info) => { + console.log(`Listening on http://localhost:${info.port}`) + } + ) + } + ) +} 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 + } +} 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 +})