diff --git a/.vitepress/config.ts b/.vitepress/config.ts index bd7ef9bb..586c702c 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -101,6 +101,7 @@ const sidebars = (): DefaultTheme.SidebarItem[] => [ text: 'Guides', collapsed: true, items: [ + { text: 'Migrating to v5', link: '/docs/guides/migrating-to-v5' }, { text: 'create-hono', link: '/docs/guides/create-hono' }, { text: 'Middleware', link: '/docs/guides/middleware' }, { text: 'Helpers', link: '/docs/guides/helpers' }, @@ -231,9 +232,11 @@ const sidebars = (): DefaultTheme.SidebarItem[] => [ ], }, { - text: 'LLM', + text: 'Coding Agents', collapsed: true, items: [ + { text: 'Overview', link: '/docs/agents/' }, + { text: 'Hono CLI', link: '/docs/agents/cli' }, { text: 'Docs List', link: '/llms.txt', diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue index 10bb5bb4..beb1cf0a 100644 --- a/.vitepress/theme/Layout.vue +++ b/.vitepress/theme/Layout.vue @@ -1,5 +1,6 @@ + + diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index 32707927..7a822505 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -330,3 +330,55 @@ html.kawaii-mode .hero-image-slot { display: none; } } + +/* The create command under the hero buttons. Follows the hero's text-align, so it is + centered on narrow screens and left-aligned next to the code window. */ +.hero-command { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 20px; + padding: 0 6px 0 16px; + height: 40px; + border: 1px solid var(--vp-c-divider); + border-radius: 20px; + background-color: var(--vp-c-bg-soft); +} + +.hero-command-text { + font-family: var(--vp-font-family-mono); + font-size: 14px; + color: var(--vp-c-text-1); + white-space: nowrap; +} + +.hero-command-prompt { + color: var(--vp-c-text-3); + user-select: none; +} + +.hero-command-copy { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 14px; + color: var(--vp-c-text-2); + transition: color 0.25s, background-color 0.25s; +} + +.hero-command-copy:hover { + color: var(--vp-c-text-1); + background-color: var(--vp-c-default-soft); +} + +.hero-command-copy.copied { + color: var(--vp-c-brand-1); +} + +@media (min-width: 640px) { + .hero-command { + margin-top: 24px; + } +} diff --git a/docs/agents/cli.md b/docs/agents/cli.md new file mode 100644 index 00000000..3b75efe2 --- /dev/null +++ b/docs/agents/cli.md @@ -0,0 +1,224 @@ +# Hono CLI + +Hono CLI (`hono`) is a command-line tool for Hono, made for coding agents. It loads your Hono app directly, so an agent can inspect and test the app without starting a server. Commands print JSON by default. `routes`, `request`, `benchmark`, and `ssg` take `--plain` when a human reads the output. + +## Installation + +Install it in your project. Coding agents find it in `package.json`. + +```sh +npm install -D @hono/cli +``` + +Or globally: + +```sh +npm install -g @hono/cli +``` + +## Output + +Commands print JSON to stdout. `snapshot` prints batch JSONL lines instead. + +- Success: `{ "ok": true, "data": ... }` with exit code 0 +- Failure: `{ "ok": false, "error": { "code", "message", "suggestions", "docs" } }` with exit code 1 + +On failure, `error.suggestions` says what to try next, and `error.docs` points to a page on this site. Logs go to stderr. + +`hono --help` starts with a short note for coding agents, and `hono --help` has examples and notes for each command. An agent needs nothing else to use the CLI. + +## Commands + +| Command | What it does | +| ---------------------------- | ------------------------------------------------------ | +| `hono routes [file]` | Show routes of your Hono app | +| `hono request [path] [file]` | Send a request to your app using `app.request()` | +| `hono batch [file]` | Run multiple requests from JSONL using `app.request()` | +| `hono snapshot [file]` | Print the current behavior as batch JSONL lines | +| `hono benchmark [file]` | Measure the performance of your Hono app | +| `hono ssg [file]` | Generate static files from your Hono app | + +`file` is the path to your app file. When omitted, the app is found in `src/index.ts`, `src/index.tsx`, `src/index.js`, or `src/index.jsx`. TypeScript and JSX are supported. + +### routes + +Show all routes of your Hono app, like [`showRoutes()`](/docs/helpers/dev#showroutes). Routes are resolved from the real app instance, so mounted sub-apps and `basePath` are expanded. + +```sh +hono routes +hono routes --verbose src/app.ts +``` + +- `--verbose` - include middleware +- `--plain` - human-readable output instead of JSON +- `-e, --external ` - mark a package as external (can be used multiple times) + +```json +{ + "ok": true, + "data": { + "router": "SmartRouter + RegExpRouter", + "routes": [ + { + "method": "GET", + "path": "/", + "name": "[handler]", + "isMiddleware": false + }, + { + "method": "POST", + "path": "/posts", + "name": "[handler]", + "isMiddleware": false + } + ] + } +} +``` + +### request + +Send a request to your app through `app.request()`. No server is needed. `path` defaults to `/`. + +```sh +hono request / +hono request /users/123 +hono request /api/users -X POST -d '{"name":"Alice"}' +hono request /api/protected -H 'Authorization: Bearer token' +cat payload.json | hono request /api/users -X POST -d @- +hono request /api/users/123 --trace +hono request / --runtime bun +``` + +- `-X, --method ` - HTTP method (default: `GET`) +- `-d, --data ` - request body (`@file` reads a file, `@-` reads stdin) +- `-H, --header
` - custom header (can be used multiple times) +- `--trace` - include the matched routes in the output +- `--runtime ` - run the app on `node` (default), `bun`, `deno`, `workerd`, or `vite` +- `--compact` - one-line JSON without the headers +- `--no-bindings` - skip loading the local Cloudflare bindings +- `-w, --watch` - watch for changes and resend the request +- `-o, --output ` - write the response body to a file +- `--plain` - print the raw body like curl (`-i` adds the status and headers, `-I` shows only them) + +A JSON response body is embedded as an object: + +```json +{ + "ok": true, + "data": { + "status": 200, + "headers": { "content-type": "application/json" }, + "body": { "message": "Hello World" } + } +} +``` + +With `--trace`, the output has `matchedRoutes`: which middleware and handler matched, and which one responded. A 404 result suggests running it. + +You can also pass the app code from stdin with `-` as the file. `app` is predefined and exported for you: + +```sh +echo 'app.get("/hello", (c) => c.json({ ok: true }))' | hono request /hello - +``` + +#### Cloudflare bindings + +In a project with a wrangler config, `c.env` carries the real local bindings (KV, D1, R2, vars) automatically, while the app runs on Node.js. This works in `request`, `batch`, `snapshot`, and `ssg`. Skip it with `--no-bindings`. It needs [wrangler](https://developers.cloudflare.com/workers/wrangler/) installed in the project. + +`--runtime workerd` runs the whole app inside workerd instead, with the wrangler config. It is heavier, but it is the full runtime. The entry is `main` in the wrangler config, so pass no file argument. + +#### Vite + +`--runtime vite` sends the requests through the Vite dev server of the project, for an app that a Vite plugin builds. The app comes from the Vite config, so pass no file argument. In a project with `cloudflare.config.ts` and a Vite config, it is the default, and `c.env` has the bindings. This works in `request`, `batch`, and `snapshot`. A file argument, `--no-bindings`, `--trace`, or `--watch` runs the app on Node.js instead. + +### batch + +Run multiple requests from JSONL in one call, in order, against one app instance. In-memory state carries between steps. + +```sh +hono batch - <<'JSONL' +{"path":"/users","expect":{"status":200}} +{"method":"POST","path":"/users","body":{"name":"Momo"},"expect":{"status":201,"body":{"name":"Momo"}},"save":{"id":".id"}} +{"path":"/users/{{id}}","expect":{"status":200}} +{"method":"DELETE","path":"/users/{{id}}","expect":{"status":204}} +JSONL +``` + +One JSON object per line: `method`, `path`, `body`, `headers`, `expect`, and `save`. + +- `expect` declares the acceptance criteria. `status` matches exactly. `body` is a deep partial match: declared fields must match, and extra fields in the response are ignored. +- `save` stores a value from the response body by dot path, and later steps use it as `{{id}}`. +- A step without `expect` passes on any 2xx or 3xx and fails on a 4xx or 5xx. To accept a 4xx on purpose, declare it with `expect.status`. +- A failed step carries `diff`, one line per mismatch. + +The output has the actual `status` and `body` of each step, `pass` per step, and a `summary`. Rerun until `failed` is 0. + +- `-H, --header
` - a shared header for every step +- `--compact` - print only the failed steps and the summary +- `--runtime ` - `node` (default), `workerd`, or `vite` +- `--no-bindings` - skip loading the local Cloudflare bindings + +### snapshot + +Print the current behavior of the app as batch JSONL lines, to stdout. No file is written. + +```sh +hono snapshot +hono snapshot --status-only src/app.ts +``` + +Paramless GET routes are executed, and their actual status and body become the `expect`. Routes with params and non-GET routes are printed without one, for you to fill in. One probe line records the response for a path that matches no route. + +Capture before a refactor, then rerun the lines with `hono batch` until `failed` is 0. + +- `--status-only` - capture only the status codes, not the bodies +- `--runtime ` - `node` (default), `workerd`, or `vite` +- `--no-bindings` - skip loading the local Cloudflare bindings + +### benchmark + +Measure the performance of your Hono app. It is a micro benchmark of routing and handlers: `app.request()` is called directly, with no HTTP stack and no network. Each run happens in a fresh process, so results are comparable. + +```sh +hono benchmark +hono benchmark -P /users +hono benchmark -P /users -X POST -d '{"name":"Alice"}' -H 'Content-Type: application/json' +hono benchmark --hono 4.12.3 --hono 4.13.0 +hono benchmark --hono ../hono +``` + +- `-P, --path ` - benchmark only this path (can be used multiple times) +- `-X`, `-d`, `-H` - the method, body, and headers for `-P` paths +- `--duration ` - how long to measure each route (default: `500`) +- `--warmup ` - requests before measuring (default: `30`) +- `--hono ` - benchmark the same app with another Hono: an npm version, or a local checkout + +A few percent of difference is noise. To compare, run it more than once and check that the difference repeats. + +### ssg + +Generate static files from your Hono app, like the [SSG helper](/docs/helpers/ssg). + +```sh +hono ssg +hono ssg -o dist/static src/app.ts +hono ssg --exclude '/api/*' +``` + +- `-o, --outdir ` - output directory (default: `static`) +- `--include ` / `--exclude ` - select routes by path. `*` matches anything +- `--no-bindings` - skip loading the local Cloudflare bindings + +A page that does not answer 200 is not written. It is listed in `skipped` with its status, so check it with `hono request `. + +```json +{ + "ok": true, + "data": { + "output": "static", + "files": ["static/index.html", "static/about.html"], + "skipped": [{ "path": "/counter", "status": 500 }] + } +} +``` diff --git a/docs/agents/index.md b/docs/agents/index.md new file mode 100644 index 00000000..fae7e0ca --- /dev/null +++ b/docs/agents/index.md @@ -0,0 +1,67 @@ +# Hono for Coding Agents + +Hono is a good fit for coding agents. The API is small and built on Web Standards, so an agent already knows most of it. This page lists what we provide so that agents work well with Hono. + +## Getting started + +Create a project, then run your agent in it. + +```sh +npm create hono@next my-app +``` + +The templates come with what an agent needs: an `AGENTS.md` that tells the agent how to check the app, and [Hono CLI](/docs/agents/cli) as a dev dependency. Nothing else to set up. + +## Documentation for agents + +Every page on this site is available as Markdown. Fetch it with the `Accept: text/markdown` header: + +```sh +curl -H 'Accept: text/markdown' https://hono.dev/docs/api/routing +``` + +To find the right page, start from [`/llms.txt`](/llms.txt). It lists every page with a one-line description. [`/llms-full.txt`](/llms-full.txt) has the whole documentation in one file, and [`/llms-small.txt`](/llms-small.txt) is a shorter version. + +## Hono Skills + +[Hono Skills](https://github.com/honojs/skills) are [Agent Skills](https://agentskills.io) for Hono. The `hono` skill gives an agent an inline API reference and the way to test requests with Hono CLI. The `hono-jsx` skill covers UI with `hono/jsx`. + +Install them with [skills.sh](https://skills.sh) or [GitHub CLI](https://cli.github.com). Both put the skills where your agent reads them. + +```sh +# skills.sh +npx skills add honojs/skills + +# GitHub CLI +gh skill install honojs/skills hono +gh skill install honojs/skills hono-jsx +``` + +## Hono CLI + +[Hono CLI](/docs/agents/cli) loads your app directly, so an agent can inspect and test it without starting a server. Every command prints JSON. + +```sh +hono routes # all routes, without reading the source +hono request /users # one request, through app.request() +hono batch - # many requests with expectations, until "failed": 0 +hono snapshot # the current behavior, as batch lines +``` + +`hono --help` starts with a note for the agent, and `hono --help` has examples. No other setup. + +## AGENTS.md + +Agents follow the way your project says to run and check things. One line in `AGENTS.md` is enough to make them use Hono CLI instead of a dev server. The agent learns the rest from `hono --help`. The create-hono templates already have it. + +```md +## Hono CLI + +Run `npx hono --help` first, and use the CLI instead of a dev server. +``` + +Or say it to the agent directly. The same line works as a prompt. + +## How well do agents use Hono? + +We measure it. See [Agent DX](https://agent-dx.hono.dev) for the results. diff --git a/docs/guides/migrating-to-v5.md b/docs/guides/migrating-to-v5.md new file mode 100644 index 00000000..f2603589 --- /dev/null +++ b/docs/guides/migrating-to-v5.md @@ -0,0 +1,207 @@ +# Migrating to v5 + +Hono v5 keeps the core small and moves the rest around it. This page lists the breaking changes from v4 and how to update your code. + +## Try the release candidate + +```sh +npm install hono@next +``` + +## ESM only + +`hono` is published as ESM only. The CommonJS build is removed. + +On Node.js, version 22.12 or later is required. `require('hono')` still works there, because Node.js 22.12 can `require()` ES modules. + +## Runtime adapters are separate packages + +The runtime adapters under `hono/` are no longer bundled with `hono`. Install the corresponding package and import from it. + +| Before | After | +| ------------------------- | -------------------------- | +| `hono/aws-lambda` | `@hono/aws-lambda` | +| `hono/bun` | `@hono/bun` | +| `hono/cloudflare-workers` | `@hono/cloudflare-workers` | +| `hono/deno` | `@hono/deno` | +| `hono/lambda-edge` | `@hono/lambda-edge` | +| `hono/netlify` | `@hono/netlify` | +| `hono/service-worker` | `@hono/service-worker` | +| `hono/vercel` | `@hono/vercel` | + +```ts +// Before +import { serveStatic } from 'hono/bun' + +// After +import { serveStatic } from '@hono/bun' +``` + +`hono/cloudflare-pages` is removed without a replacement. Cloudflare recommends Workers with static assets, so use `hono` on Workers instead. + +`hono/adapter` (`env()` and `getRuntimeKey()`) stays in `hono`. + +## `onError()` and `notFound()` register routable middleware + +`onError()` now accepts middleware with the `(c, next)` signature instead of `(err, c)`. Read the error from `c.error`: + +```ts +// From +app.onError((err, c) => { + console.error(err) + return c.text('Custom Error', 500) +}) + +// To +app.onError((c) => { + console.error(c.error) + return c.text('Custom Error', 500) +}) +``` + +`notFound((c) => c.text('Not Found', 404))` keeps the same single-handler syntax. Both APIs also accept an optional path and multiple middleware: + +```ts +app.notFound( + '/api/*', + async (c, next) => { + c.header('x-not-found', 'api') + await next() + }, + (c) => c.text('API resource not found', 404) +) +``` + +The following routing rules apply to both APIs: + +- Registrations compose rather than replacing the previous handler. Return a response to stop the chain, or call `next()` to delegate. +- Paths are independent of the request's HTTP method and relative to `basePath()`. Omitting the path registers `*` within the current base path. +- `route()` imports sub-app handlers, including `notFound()` handlers. More deeply nested sub-apps take priority; at the same depth, handlers run in registration order, not path specificity order. +- Scopes match the request path, regardless of which app registered the original route. Request parameters still come from the original route, not the fallback scope. + +`notFound()` middleware runs for both unmatched requests and explicit `c.notFound()` calls. If the chain reaches its end without a finalized response, the built-in error or not-found handler is used. Errors thrown by `onError()` middleware are handled by the built-in error handler rather than escaping from `app.fetch()`. + +## Non-Error throws go to `onError` + +A non-Error value thrown from a handler or middleware, such as a string or a plain object, now goes to `onError` wrapped in an `Error`. The original value is available as `c.error.cause`, and a thrown string is also used as `c.error.message`. It no longer propagates out of `app.fetch()`. + +## `getColorEnabledAsync()` is removed + +`getColorEnabledAsync()` from `hono/utils/color` is removed. Use `getColorEnabled()` and pass the bindings on Cloudflare Workers. The `logger()` middleware does this by itself, so it needs no change. + +```ts +// Before +const enabled = await getColorEnabledAsync() + +// After +const enabled = getColorEnabled(c.env) +``` + +## `c.req.query()` may return `undefined` values + +`c.req.query()` and `c.req.queries()` without a key now return `Record` and `Record`, since a key may be absent. + +```ts +// Before +const { name } = c.req.query() // string + +// After +const { name } = c.req.query() // string | undefined +``` + +## `c.req.json()` returns `unknown` + +`c.req.json()` returns `Promise` instead of `Promise`. Pass the type explicitly, or use `c.req.valid()` with a validator. + +```ts +// Before +const body = await c.req.json() // any + +// After +const body = await c.req.json<{ name: string }>() +``` + +## `c.json()` throws for a value that is not JSON serializable + +`c.json(undefined)` used to return an empty body. It now throws a `TypeError`, like `Response.json()`. The same applies to a function or a symbol. + +## Wildcard matching is the same in every router + +A `*` at the end of a segment is a wildcard in every router, and it may match nothing. A `*` in the middle of a segment, such as `/x*y`, is not a wildcard. A segment that is only `*` matches one non-empty segment. + +| Route | Path | v5 | +| -------------- | ------------- | -------- | +| `/x*/y` | `/x/y` | Match | +| `/x*/y` | `/xz/y` | Match | +| `/x*/y` | `/x/z/y` | No match | +| `/x*y` | `/xay` | No match | +| `/wild/*/card` | `/wild//card` | No match | + +TrieRouter and PatternRouter did not match `/x*/y` to `/xz/y` before. In RegExpRouter, `/x*/y` and `/x/*` can not be registered together and throw `UnsupportedPathError`, so SmartRouter falls back to TrieRouter. + +## Removed deprecated features + +### `app.mount()` + +Use the [Mount Middleware](/docs/middleware/builtin/mount) instead. + +```ts +import { mount } from 'hono/mount' + +// Before +app.mount('/itty-router', ittyRouter.handle) + +// After +app.all('/itty-router/*', mount(ittyRouter.handle)) +``` + +### `app.fire()` + +Use `fire()` from `@hono/service-worker` instead. + +```ts +import { fire } from '@hono/service-worker' + +// Before +app.fire() + +// After +fire(app) +``` + +### `c.req.matchedRoutes` and `c.req.routePath` + +Use `matchedRoutes()` and `routePath()` from `hono/route` instead. + +```ts +import { routePath } from 'hono/route' + +// Before +app.get('/posts/:id', (c) => c.text(c.req.routePath)) + +// After +app.get('/posts/:id', (c) => c.text(routePath(c))) +``` + +### SSG hook options + +The `beforeRequestHook`, `afterResponseHook`, and `afterGenerateHook` options of `toSSG()` are removed. Pass them as a plugin via `plugins` instead. `defaultPlugin()`, which skips non-200 responses, is applied only when `plugins` is omitted, so add it explicitly if you need it. + +```ts +import { defaultPlugin, toSSG } from 'hono/ssg' + +// Before +toSSG(app, fs, { beforeRequestHook }) + +// After +toSSG(app, fs, { plugins: [{ beforeRequestHook }, defaultPlugin()] }) +``` + +### Others + +- Bearer Auth Middleware - `noAuthenticationHeaderMessage`, `invalidAuthenticationHeaderMessage`, and `invalidTokenMessage` are removed. Use `noAuthenticationHeader.message`, `invalidAuthenticationHeader.message`, and `invalidToken.message` instead. +- Serve Static Middleware - the `pathResolve` option is removed. It was no longer used. +- SSG Helper - `SSG_DISABLED_RESPONSE` is removed. Use `X_HONO_DISABLE_SSG_HEADER_KEY` instead. +- `timingSafeEqual()` in `hono/utils/buffer` only accepts strings. The `hashFunction` option of the Basic Auth and Bearer Auth middleware is typed as `(input: string) => string | null | Promise` accordingly. +- `getQueryStrings()` in `hono/utils/url` is removed. Use the `URL` API instead. +- `UnOfficalStatusCode` in `hono/utils/http-status` is removed. Use `UnofficialStatusCode` instead.