├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yml │ ├── 2-feature-request.yml │ └── config.yml ├── actions │ └── perf-measures │ │ └── action.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── no-response.yml │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── .prettierrc ├── .vitest.config ├── jsx-runtime-default.ts ├── jsx-runtime-dom.ts └── setup-vitest.ts ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── benchmarks ├── deno │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── fast.ts │ ├── faster.ts │ ├── hono.ts │ ├── magalo.ts │ ├── oak.ts │ └── opine.ts ├── handle-event │ ├── index.js │ ├── package.json │ └── yarn.lock ├── jsx │ ├── package.json │ ├── src │ │ ├── benchmark.ts │ │ ├── hono.ts │ │ ├── nano.ts │ │ ├── page-react.tsx │ │ ├── page.tsx │ │ ├── preact.ts │ │ ├── react-jsx │ │ │ ├── benchmark.ts │ │ │ ├── hono.ts │ │ │ ├── nano.ts │ │ │ ├── page-hono.tsx │ │ │ ├── page-nano.tsx │ │ │ ├── page-preact.tsx │ │ │ ├── page-react.tsx │ │ │ ├── preact.ts │ │ │ ├── react.ts │ │ │ └── tsconfig.json │ │ └── react.ts │ ├── tsconfig.json │ └── yarn.lock ├── query-param │ ├── bun.lockb │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── bench.mts │ │ ├── fast-querystring.mts │ │ ├── hono.mts │ │ └── qs.mts ├── routers-deno │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── deno.json │ ├── deno.lock │ └── src │ │ ├── bench.mts │ │ ├── find-my-way.mts │ │ ├── hono.mts │ │ ├── koa-router.mts │ │ ├── koa-tree-router.mts │ │ ├── medley-router.mts │ │ ├── tool.mts │ │ └── trek-router.mts ├── routers │ ├── README.md │ ├── package.json │ ├── src │ │ ├── bench-includes-init.mts │ │ ├── bench.mts │ │ ├── express.mts │ │ ├── find-my-way.mts │ │ ├── hono.mts │ │ ├── koa-router.mts │ │ ├── koa-tree-router.mts │ │ ├── medley-router.mts │ │ ├── memoirist.mts │ │ ├── radix3.mts │ │ ├── rou3.mts │ │ ├── tool.mts │ │ └── trek-router.mts │ ├── tsconfig.json │ └── yarn.lock ├── utils │ ├── .gitignore │ ├── package.json │ └── src │ │ ├── get-path.ts │ │ └── loop.js └── webapp │ ├── .gitignore │ ├── hono.js │ ├── itty-router.js │ ├── package.json │ └── sunder.js ├── build ├── build.ts ├── remove-private-fields-worker.test.ts ├── remove-private-fields-worker.ts ├── remove-private-fields.ts ├── validate-exports.test.ts └── validate-exports.ts ├── bun.lock ├── bunfig.toml ├── codecov.yml ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── MIGRATION.md └── images │ ├── hono-logo.png │ ├── hono-logo.pxm │ ├── hono-logo.svg │ ├── hono-title.png │ └── hono-title.pxm ├── eslint.config.mjs ├── jsr.json ├── package.cjs.json ├── package.json ├── perf-measures ├── .octocov.tsc.perf-measures.main.yml ├── .octocov.tsc.perf-measures.yml ├── .octocov.tsgo.perf-measures.main.yml ├── .octocov.tsgo.perf-measures.yml ├── bundle-check │ ├── .gitignore │ ├── generated │ │ └── .gitkeep │ └── scripts │ │ └── check-bundle-size.ts ├── tsconfig.json └── type-check │ ├── .gitignore │ ├── client.ts │ ├── generated │ └── .gitkeep │ ├── scripts │ ├── generate-app.ts │ └── process-results.ts │ └── tsconfig.build.json ├── runtime-tests ├── bun │ ├── .static │ │ └── plain.txt │ ├── favicon.ico │ ├── index.test.tsx │ ├── static-absolute-root │ │ └── plain.txt │ ├── static │ │ ├── download │ │ ├── hello.world │ │ │ └── index.html │ │ ├── helloworld │ │ │ └── index.html │ │ └── plain.txt │ └── vitest.config.ts ├── deno-jsx │ ├── deno.precompile.json │ ├── deno.react-jsx.json │ └── jsx.test.tsx ├── deno │ ├── .static │ │ └── plain.txt │ ├── .vscode │ │ └── settings.json │ ├── deno.json │ ├── deno.lock │ ├── favicon.ico │ ├── hono.test.ts │ ├── middleware.test.tsx │ ├── ssg.test.tsx │ ├── static-absolute-root │ │ └── plain.txt │ ├── static │ │ ├── download │ │ ├── hello.world │ │ │ └── index.html │ │ ├── helloworld │ │ │ └── index.html │ │ └── plain.txt │ └── stream.test.ts ├── fastly │ ├── index.test.ts │ └── vitest.config.ts ├── lambda-edge │ ├── index.test.ts │ └── vitest.config.ts ├── lambda │ ├── index.test.ts │ ├── mock.ts │ ├── stream-mock.ts │ ├── stream.test.ts │ └── vitest.config.ts ├── node │ ├── index.test.ts │ └── vitest.config.ts ├── tsconfig.json └── workerd │ ├── index.test.ts │ ├── index.ts │ └── vitest.config.ts ├── src ├── adapter │ ├── aws-lambda │ │ ├── handler.test.ts │ │ ├── handler.ts │ │ ├── index.ts │ │ └── types.ts │ ├── bun │ │ ├── conninfo.test.ts │ │ ├── conninfo.ts │ │ ├── index.ts │ │ ├── serve-static.ts │ │ ├── server.test.ts │ │ ├── server.ts │ │ ├── ssg.ts │ │ ├── websocket.test.ts │ │ └── websocket.ts │ ├── cloudflare-pages │ │ ├── handler.test.ts │ │ ├── handler.ts │ │ └── index.ts │ ├── cloudflare-workers │ │ ├── conninfo.test.ts │ │ ├── conninfo.ts │ │ ├── index.ts │ │ ├── serve-static-module.ts │ │ ├── serve-static.test.ts │ │ ├── serve-static.ts │ │ ├── utils.test.ts │ │ ├── utils.ts │ │ ├── websocket.test.ts │ │ └── websocket.ts │ ├── deno │ │ ├── conninfo.test.ts │ │ ├── conninfo.ts │ │ ├── deno.d.ts │ │ ├── index.ts │ │ ├── serve-static.ts │ │ ├── ssg.ts │ │ ├── websocket.test.ts │ │ └── websocket.ts │ ├── lambda-edge │ │ ├── conninfo.test.ts │ │ ├── conninfo.ts │ │ ├── handler.test.ts │ │ ├── handler.ts │ │ └── index.ts │ ├── netlify │ │ ├── handler.ts │ │ ├── index.ts │ │ └── mod.ts │ ├── service-worker │ │ ├── handler.test.ts │ │ ├── handler.ts │ │ ├── index.ts │ │ └── types.ts │ └── vercel │ │ ├── conninfo.test.ts │ │ ├── conninfo.ts │ │ ├── handler.test.ts │ │ ├── handler.ts │ │ └── index.ts ├── client │ ├── client.test.ts │ ├── client.ts │ ├── index.ts │ ├── types.test.ts │ ├── types.ts │ ├── utils.test.ts │ └── utils.ts ├── compose.test.ts ├── compose.ts ├── context.test.ts ├── context.ts ├── helper │ ├── accepts │ │ ├── accepts.test.ts │ │ ├── accepts.ts │ │ └── index.ts │ ├── adapter │ │ ├── index.test.ts │ │ └── index.ts │ ├── conninfo │ │ ├── index.ts │ │ └── types.ts │ ├── cookie │ │ ├── index.test.ts │ │ └── index.ts │ ├── css │ │ ├── common.case.test.tsx │ │ ├── common.ts │ │ ├── index.test.tsx │ │ └── index.ts │ ├── dev │ │ ├── index.test.ts │ │ └── index.ts │ ├── factory │ │ ├── index.test.ts │ │ └── index.ts │ ├── html │ │ ├── index.test.ts │ │ └── index.ts │ ├── proxy │ │ ├── index.test.ts │ │ └── index.ts │ ├── ssg │ │ ├── index.ts │ │ ├── middleware.ts │ │ ├── ssg.test.tsx │ │ ├── ssg.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── streaming │ │ ├── index.ts │ │ ├── sse.test.tsx │ │ ├── sse.ts │ │ ├── stream.test.ts │ │ ├── stream.ts │ │ ├── text.test.ts │ │ ├── text.ts │ │ └── utils.ts │ ├── testing │ │ ├── index.test.ts │ │ └── index.ts │ └── websocket │ │ ├── index.test.ts │ │ └── index.ts ├── hono-base.ts ├── hono.test.ts ├── hono.ts ├── http-exception.test.ts ├── http-exception.ts ├── index.ts ├── jsx │ ├── base.ts │ ├── children.test.ts │ ├── children.ts │ ├── components.test.tsx │ ├── components.ts │ ├── constants.ts │ ├── context.ts │ ├── dom │ │ ├── client.test.tsx │ │ ├── client.ts │ │ ├── components.test.tsx │ │ ├── components.ts │ │ ├── context.test.tsx │ │ ├── context.ts │ │ ├── css.test.tsx │ │ ├── css.ts │ │ ├── hooks │ │ │ ├── index.test.tsx │ │ │ └── index.ts │ │ ├── index.test.tsx │ │ ├── index.ts │ │ ├── intrinsic-element │ │ │ ├── components.test.tsx │ │ │ └── components.ts │ │ ├── jsx-dev-runtime.ts │ │ ├── jsx-runtime.ts │ │ ├── render.ts │ │ ├── server.test.tsx │ │ ├── server.ts │ │ └── utils.ts │ ├── hooks │ │ ├── dom.test.tsx │ │ ├── index.ts │ │ └── string.test.tsx │ ├── index.test.tsx │ ├── index.ts │ ├── intrinsic-element │ │ ├── common.ts │ │ ├── components.test.tsx │ │ └── components.ts │ ├── intrinsic-elements.ts │ ├── jsx-dev-runtime.ts │ ├── jsx-runtime.test.tsx │ ├── jsx-runtime.ts │ ├── streaming.test.tsx │ ├── streaming.ts │ ├── types.ts │ ├── utils.test.ts │ └── utils.ts ├── middleware │ ├── basic-auth │ │ ├── index.test.ts │ │ └── index.ts │ ├── bearer-auth │ │ ├── index.test.ts │ │ └── index.ts │ ├── body-limit │ │ ├── index.test.ts │ │ └── index.ts │ ├── cache │ │ ├── index.test.ts │ │ └── index.ts │ ├── combine │ │ ├── index.test.ts │ │ └── index.ts │ ├── compress │ │ ├── index.test.ts │ │ └── index.ts │ ├── context-storage │ │ ├── index.test.ts │ │ └── index.ts │ ├── cors │ │ ├── index.test.ts │ │ └── index.ts │ ├── csrf │ │ ├── index.test.ts │ │ └── index.ts │ ├── etag │ │ ├── digest.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── ip-restriction │ │ ├── index.test.ts │ │ └── index.ts │ ├── jsx-renderer │ │ ├── index.test.tsx │ │ └── index.ts │ ├── jwk │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── jwk.ts │ │ └── keys.test.json │ ├── jwt │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── jwt.ts │ ├── language │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── language.ts │ ├── logger │ │ ├── index.test.ts │ │ └── index.ts │ ├── method-override │ │ ├── index.test.ts │ │ └── index.ts │ ├── powered-by │ │ ├── index.test.ts │ │ └── index.ts │ ├── pretty-json │ │ ├── index.test.ts │ │ └── index.ts │ ├── request-id │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── request-id.ts │ ├── secure-headers │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── permissions-policy.ts │ │ └── secure-headers.ts │ ├── serve-static │ │ ├── index.test.ts │ │ └── index.ts │ ├── timeout │ │ ├── index.test.ts │ │ └── index.ts │ ├── timing │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── timing.ts │ └── trailing-slash │ │ ├── index.test.ts │ │ └── index.ts ├── preset │ ├── quick.test.ts │ ├── quick.ts │ ├── tiny.test.ts │ └── tiny.ts ├── request.test.ts ├── request.ts ├── router.ts ├── router │ ├── common.case.test.ts │ ├── linear-router │ │ ├── index.ts │ │ ├── router.test.ts │ │ └── router.ts │ ├── pattern-router │ │ ├── index.ts │ │ ├── router.test.ts │ │ └── router.ts │ ├── reg-exp-router │ │ ├── index.ts │ │ ├── node.ts │ │ ├── router.test.ts │ │ ├── router.ts │ │ └── trie.ts │ ├── smart-router │ │ ├── index.ts │ │ ├── router.test.ts │ │ └── router.ts │ └── trie-router │ │ ├── index.ts │ │ ├── node.test.ts │ │ ├── node.ts │ │ ├── router.test.ts │ │ └── router.ts ├── types.test.ts ├── types.ts ├── utils │ ├── accept.test.ts │ ├── accept.ts │ ├── basic-auth.test.ts │ ├── basic-auth.ts │ ├── body.test.ts │ ├── body.ts │ ├── buffer.test.ts │ ├── buffer.ts │ ├── color.test.ts │ ├── color.ts │ ├── compress.ts │ ├── concurrent.test.ts │ ├── concurrent.ts │ ├── constants.ts │ ├── cookie.test.ts │ ├── cookie.ts │ ├── crypto.test.ts │ ├── crypto.ts │ ├── encode.test.ts │ ├── encode.ts │ ├── filepath.test.ts │ ├── filepath.ts │ ├── handler.ts │ ├── headers.ts │ ├── html.test.ts │ ├── html.ts │ ├── http-status.ts │ ├── ipaddr.test.ts │ ├── ipaddr.ts │ ├── jwt │ │ ├── index.ts │ │ ├── jwa.test.ts │ │ ├── jwa.ts │ │ ├── jws.ts │ │ ├── jwt.test.ts │ │ ├── jwt.ts │ │ ├── types.ts │ │ └── utf8.ts │ ├── mime.test.ts │ ├── mime.ts │ ├── stream.test.ts │ ├── stream.ts │ ├── types.test.ts │ ├── types.ts │ ├── url.test.ts │ └── url.ts └── validator │ ├── index.ts │ ├── validator.test.ts │ └── validator.ts ├── tsconfig.build.json ├── tsconfig.json └── vitest.config.ts /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/typescript-node:20 2 | 3 | # Install Deno 4 | ENV DENO_INSTALL=/usr/local 5 | RUN curl -fsSL https://deno.land/install.sh | sh 6 | 7 | # Install Bun 8 | ENV BUN_INSTALL=/usr/local 9 | RUN curl -fsSL https://bun.sh/install | bash 10 | 11 | WORKDIR /hono 12 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "Dockerfile" 4 | }, 5 | "containerEnv": { 6 | "HOME": "/home/node" 7 | }, 8 | "customizations": { 9 | "vscode": { 10 | "settings": { 11 | "deno.enable": false, 12 | "eslint.validate": [ 13 | "javascript", 14 | "javascriptreact", 15 | "typescript", 16 | "typescriptreact" 17 | ], 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.eslint": "explicit" 20 | } 21 | }, 22 | "extensions": [ 23 | "dbaeumer.vscode-eslint", 24 | "esbenp.prettier-vscode" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | hono: 5 | build: . 6 | container_name: hono 7 | volumes: 8 | - ../:/hono 9 | networks: 10 | - hono 11 | command: bash 12 | stdin_open: true 13 | tty: true 14 | restart: 'no' 15 | 16 | networks: 17 | hono: 18 | driver: bridge 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['yusukebe', 'usualoma'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report an issue that should be fixed 3 | labels: [triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting a bug report. It helps make Hono better. 9 | 10 | If you need help or support using Hono, and are not reporting a bug, please ask questions in [our Discord](https://discord.gg/KMh2eNSdxV) or [GitHub Discussions](https://github.com/orgs/honojs/discussions). 11 | 12 | Please try to include as much information as possible. 13 | - type: input 14 | attributes: 15 | label: What version of Hono are you using? 16 | placeholder: 0.0.0 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: What runtime/platform is your app running on? (with version if possible) 22 | placeholder: Cloudflare Workers, Deno, Bun, etc. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: What steps can reproduce the bug? 28 | description: Explain the bug and provide a code snippet that can reproduce it. 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: What is the expected behavior? 34 | - type: textarea 35 | attributes: 36 | label: What do you see instead? 37 | - type: textarea 38 | attributes: 39 | label: Additional information 40 | description: Is there anything else you think we should know? 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Suggest an idea, feature, or enhancement 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting an idea. It helps make Hono better. 9 | 10 | - type: textarea 11 | attributes: 12 | label: What is the feature you are proposing? 13 | description: A clear description of what you want to happen. 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: ❓ Questions 4 | url: https://github.com/orgs/honojs/discussions 5 | about: Ask your questions on the GitHub Discussions. 6 | - name: 🗣️ Discord 7 | url: https://discord.gg/KMh2eNSdxV 8 | about: Join our Discord server to chat. 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### The author should do the following, if applicable 2 | 3 | - [ ] Add tests 4 | - [ ] Run tests 5 | - [ ] `bun run format:fix && bun run lint:fix` to format the code 6 | - [ ] Add [TSDoc](https://tsdoc.org/)/[JSDoc](https://jsdoc.app/about-getting-started) to document the code 7 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues with "not bug" label 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | permissions: 8 | contents: write 9 | issues: write 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Close stale issues with "not bug" label 16 | uses: actions/stale@v8 17 | with: 18 | days-before-stale: 7 19 | days-before-close: 2 20 | stale-issue-message: 'This issue has been marked as stale due to inactivity.' 21 | close-issue-message: 'Closing this issue due to inactivity.' 22 | exempt-issue-labels: '' 23 | stale-issue-label: 'stale' 24 | only-labels: 'not bug' 25 | operations-per-run: 30 26 | remove-stale-when-updated: true 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | jsr: 10 | name: publish-to-jsr 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: read 15 | id-token: write 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install deno 20 | uses: denoland/setup-deno@v2 21 | with: 22 | deno-version: v2.x 23 | - run: deno install --no-lock --allow-scripts 24 | - name: Publish to JSR 25 | run: deno run -A jsr:@david/publish-on-tag@0.1.4 26 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: Setup 3 | init: bun install --no-save 4 | image: 5 | file: ./.devcontainer/Dockerfile 6 | vscode: 7 | extensions: 8 | - oven.bun-vscode 9 | - vitest.explorer 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": false, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /.vitest.config/jsx-runtime-default.ts: -------------------------------------------------------------------------------- 1 | import config from '../vitest.config' 2 | config.esbuild = { 3 | jsx: 'automatic', 4 | jsxImportSource: __dirname + '/../src/jsx', 5 | } 6 | if (config.test) { 7 | config.test.include = [ 8 | '**/src/jsx/dom/**/(*.)+(spec|test).+(ts|tsx|js)', 9 | 'src/jsx/hooks/dom.test.tsx', 10 | ] 11 | if (config.test.coverage) { 12 | config.test.coverage.reportsDirectory = './coverage/raw/jsx-runtime' 13 | } 14 | } 15 | export default config 16 | -------------------------------------------------------------------------------- /.vitest.config/jsx-runtime-dom.ts: -------------------------------------------------------------------------------- 1 | import config from '../vitest.config' 2 | config.esbuild = { 3 | jsx: 'automatic', 4 | jsxImportSource: __dirname + '/../src/jsx/dom', 5 | } 6 | if (config.test) { 7 | config.test.include = [ 8 | '**/src/jsx/dom/**/(*.)+(spec|test).+(ts|tsx|js)', 9 | 'src/jsx/hooks/dom.test.tsx', 10 | ] 11 | if (config.test.coverage) { 12 | config.test.coverage.reportsDirectory = './coverage/raw/jsx-dom' 13 | } 14 | } 15 | export default config 16 | -------------------------------------------------------------------------------- /.vitest.config/setup-vitest.ts: -------------------------------------------------------------------------------- 1 | import * as nodeCrypto from 'node:crypto' 2 | import { vi } from 'vitest' 3 | 4 | /** 5 | * crypto 6 | */ 7 | if (!globalThis.crypto) { 8 | vi.stubGlobal('crypto', nodeCrypto) 9 | vi.stubGlobal('CryptoKey', nodeCrypto.webcrypto.CryptoKey) 10 | } 11 | 12 | /** 13 | * Cache API 14 | */ 15 | type StoreMap = Map 16 | 17 | class MockCache { 18 | name: string 19 | store: StoreMap 20 | 21 | constructor(name: string, store: StoreMap) { 22 | this.name = name 23 | this.store = store 24 | } 25 | 26 | async match(key: Request | string): Promise { 27 | return this.store.get(key) || null 28 | } 29 | 30 | async keys() { 31 | return this.store.keys() 32 | } 33 | 34 | async put(key: Request | string, response: Response): Promise { 35 | this.store.set(key, response) 36 | } 37 | } 38 | 39 | const globalStore: Map = new Map() 40 | 41 | const caches = { 42 | open: (name: string) => { 43 | return new MockCache(name, globalStore) 44 | }, 45 | } 46 | 47 | vi.stubGlobal('caches', caches) 48 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": false, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "typescript", 7 | "typescriptreact" 8 | ], 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": "explicit" 11 | }, 12 | "typescript.tsdk": "node_modules/typescript/lib" 13 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 - present, Yusuke Wada and Hono contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmarks/deno/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite -------------------------------------------------------------------------------- /benchmarks/deno/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": true 10 | }, 11 | "deno.enable": true 12 | } -------------------------------------------------------------------------------- /benchmarks/deno/fast.ts: -------------------------------------------------------------------------------- 1 | import fast, { type Context } from 'https://deno.land/x/fast@4.0.0-beta.1/mod.ts' 2 | 3 | const app = fast() 4 | 5 | app.get('/user', () => {}) 6 | app.get('/user/comments', () => {}) 7 | app.get('/user/avatar', () => {}) 8 | app.get('/user/lookup/email/:address', () => {}) 9 | app.get('/event/:id', () => {}) 10 | app.get('/event/:id/comments', () => {}) 11 | app.post('/event/:id/comments', () => {}) 12 | app.post('/status', () => {}) 13 | app.get('/very/deeply/nested/route/hello/there', () => {}) 14 | app.get('/user/lookup/username/:username', (ctx: Context) => { 15 | return { message: `Hello ${ctx.params.username}` } 16 | }) 17 | 18 | await app.serve({ 19 | port: 8000, 20 | }) 21 | -------------------------------------------------------------------------------- /benchmarks/deno/faster.ts: -------------------------------------------------------------------------------- 1 | import { res, Server } from 'https://deno.land/x/faster@v5.7/mod.ts' 2 | const app = new Server() 3 | 4 | app.get('/user', () => {}) 5 | app.get('/user/comments', () => {}) 6 | app.get('/user/avatar', () => {}) 7 | app.get('/user/lookup/email/:address', () => {}) 8 | app.get('/event/:id', () => {}) 9 | app.get('/event/:id/comments', () => {}) 10 | app.post('/event/:id/comments', () => {}) 11 | app.post('/status', () => {}) 12 | app.get('/very/deeply/nested/route/hello/there', () => {}) 13 | app.get('/user/lookup/username/:username', res('json'), async (ctx: any, next: any) => { 14 | ctx.res.body = { message: `Hello ${ctx.params.username}` } 15 | await next() 16 | }) 17 | 18 | await app.listen({ port: 8000 }) 19 | -------------------------------------------------------------------------------- /benchmarks/deno/hono.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../src/index.ts' 2 | import { RegExpRouter } from '../../src/router/reg-exp-router/index.ts' 3 | 4 | const app = new Hono({ router: new RegExpRouter() }) 5 | 6 | app.get('/user', (c) => c.text('User')) 7 | app.get('/user/comments', (c) => c.text('User Comments')) 8 | app.get('/user/avatar', (c) => c.text('User Avatar')) 9 | app.get('/user/lookup/email/:address', (c) => c.text('User Lookup Email Address')) 10 | app.get('/event/:id', (c) => c.text('Event')) 11 | app.get('/event/:id/comments', (c) => c.text('Event Comments')) 12 | app.post('/event/:id/comments', (c) => c.text('POST Event Comments')) 13 | app.post('/status', (c) => c.text('Status')) 14 | app.get('/very/deeply/nested/route/hello/there', (c) => c.text('Very Deeply Nested Route')) 15 | app.get('/user/lookup/username/:username', (c) => { 16 | return c.json({ message: `Hello ${c.req.param('username')}` }) 17 | }) 18 | 19 | Deno.serve(app.fetch, { 20 | port: 8000, 21 | }) 22 | -------------------------------------------------------------------------------- /benchmarks/deno/magalo.ts: -------------------------------------------------------------------------------- 1 | import { Megalo } from 'https://deno.land/x/megalo@v0.3.0/mod.ts' 2 | 3 | const app = new Megalo() 4 | 5 | app.get('/user', () => {}) 6 | app.get('/user/comments', () => {}) 7 | app.get('/user/avatar', () => {}) 8 | app.get('/user/lookup/email/:address', () => {}) 9 | app.get('/event/:id', () => {}) 10 | app.get('/event/:id/comments', () => {}) 11 | app.post('/event/:id/comments', () => {}) 12 | app.post('/status', () => {}) 13 | app.get('/very/deeply/nested/route/hello/there', () => {}) 14 | app.get('/user/lookup/username/:username', ({ params }, res) => { 15 | res.json({ 16 | message: `Hello ${params.username}`, 17 | }) 18 | }) 19 | 20 | app.listen({ port: 8000 }) 21 | -------------------------------------------------------------------------------- /benchmarks/deno/oak.ts: -------------------------------------------------------------------------------- 1 | import { Application, Router } from 'https://deno.land/x/oak@v10.5.1/mod.ts' 2 | 3 | const router = new Router() 4 | 5 | router.get('/user', () => {}) 6 | router.get('/user/comments', () => {}) 7 | router.get('/user/avatar', () => {}) 8 | router.get('/user/lookup/email/:address', () => {}) 9 | router.get('/event/:id', () => {}) 10 | router.get('/event/:id/comments', () => {}) 11 | router.post('/event/:id/comments', () => {}) 12 | router.post('/status', () => {}) 13 | router.get('/very/deeply/nested/route/hello/there', () => {}) 14 | router.get('/user/lookup/username/:username', (ctx) => { 15 | ctx.response.body = { 16 | message: `Hello ${ctx.params.username}`, 17 | } 18 | }) 19 | 20 | const app = new Application() 21 | app.use(router.routes()) 22 | app.use(router.allowedMethods()) 23 | 24 | await app.listen({ port: 8000 }) 25 | -------------------------------------------------------------------------------- /benchmarks/deno/opine.ts: -------------------------------------------------------------------------------- 1 | import { opine } from 'https://deno.land/x/opine@2.2.0/mod.ts' 2 | 3 | const app = opine() 4 | 5 | app.get('/user', () => {}) 6 | app.get('/user/comments', () => {}) 7 | app.get('/user/avatar', () => {}) 8 | app.get('/user/lookup/email/:address', () => {}) 9 | app.get('/event/:id', () => {}) 10 | app.get('/event/:id/comments', () => {}) 11 | app.post('/event/:id/comments', () => {}) 12 | app.post('/status', () => {}) 13 | app.get('/very/deeply/nested/route/hello/there', () => {}) 14 | app.get('/user/lookup/username/:username', (req, res) => { 15 | res.send({ message: `Hello ${req.params.username}` }) 16 | }) 17 | 18 | app.listen(8000) 19 | -------------------------------------------------------------------------------- /benchmarks/handle-event/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-benchmark", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node --experimental-specifier-resolution=node index.js" 9 | }, 10 | "type": "module", 11 | "author": "Yusuke Wada (https://github.com/yusukebe)", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "benchmark": "^2.1.4", 15 | "edge-mock": "^0.0.15", 16 | "itty-router": "^3.0.11", 17 | "node-fetch": "^3.2.10", 18 | "sunder": "^0.10.1", 19 | "worktop": "^0.7.3" 20 | } 21 | } -------------------------------------------------------------------------------- /benchmarks/jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "bench:node": "esbuild --bundle src/benchmark.ts | node", 7 | "bench:bun": "bun run src/benchmark.ts", 8 | "bench:react-jsx:node": "esbuild --bundle src/react-jsx/benchmark.ts | node", 9 | "compare-bundle-size": "esbuild --minify --minify-syntax --tree-shaking=true --bundle src/{hono,react,preact,nano}.ts --outdir=dist" 10 | }, 11 | "license": "MIT", 12 | "devDependencies": { 13 | "esbuild": "^0.19.8", 14 | "node-html-parser": "^6.1.11" 15 | }, 16 | "dependencies": { 17 | "@types/benchmark": "^2.1.5", 18 | "@types/react": "^18.2.40", 19 | "@types/react-dom": "^18.2.17", 20 | "benchmark": "^2.1.4", 21 | "hono": "^3.10.4", 22 | "nano-jsx": "^0.1.0", 23 | "preact": "^10.19.2", 24 | "preact-render-to-string": "^6.3.1", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "typescript": "^5.3.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { Suite } from 'benchmark' 2 | import { parse } from 'node-html-parser' 3 | 4 | import { render as renderHono } from './hono' 5 | import { render as renderNano } from './nano' 6 | import { render as renderPreact } from './preact' 7 | import { render as renderReact } from './react' 8 | 9 | const suite = new Suite() 10 | 11 | ;[renderHono, renderReact, renderPreact, renderNano].forEach((render) => { 12 | const html = render() 13 | const doc = parse(html) 14 | if (doc.querySelector('p#c').textContent !== '3\nc') { 15 | throw new Error('Invalid output') 16 | } 17 | }) 18 | 19 | suite 20 | .add('Hono', () => { 21 | renderHono() 22 | }) 23 | .add('React', () => { 24 | renderReact() 25 | }) 26 | .add('Preact', () => { 27 | renderPreact() 28 | }) 29 | .add('Nano', () => { 30 | renderNano() 31 | }) 32 | .on('cycle', (ev) => { 33 | console.log(String(ev.target)) 34 | }) 35 | .on('complete', (ev) => { 36 | console.log(`Fastest is ${ev.currentTarget.filter('fastest').map('name')}`) 37 | }) 38 | .run({ async: true }) 39 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/hono.ts: -------------------------------------------------------------------------------- 1 | import { jsx, Fragment} from '../../../src/jsx' 2 | import { buildPage } from './page' 3 | 4 | export const render = () => buildPage({ jsx, Fragment })().toString() -------------------------------------------------------------------------------- /benchmarks/jsx/src/nano.ts: -------------------------------------------------------------------------------- 1 | import { h, Fragment, renderSSR } from 'nano-jsx' 2 | import { buildPage } from './page' 3 | 4 | export const render = () => renderSSR(buildPage({ jsx: h, Fragment })) -------------------------------------------------------------------------------- /benchmarks/jsx/src/page-react.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | /** @jsxFrag Fragment */ 3 | 4 | export const buildPage = ({ jsx, Fragment }: { jsx: any; Fragment: any }) => { 5 | const Content = () => ( 6 | <> 7 |

8 | 1
a 9 |

10 |

11 | 2
b 12 |

13 |
3
c

' }} /> 14 | {null} 15 | {undefined} 16 | 17 | ) 18 | 19 | const Form = () => ( 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | ) 28 | 29 | return () => ( 30 | 31 | 32 | 33 |
34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/page.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | /** @jsxFrag Fragment */ 3 | 4 | export const buildPage = ({ jsx, Fragment }: { jsx: any; Fragment: any }) => { 5 | const Content = () => ( 6 | <> 7 |

8 | 1
a 9 |

10 |

11 | 2
b 12 |

13 |
3
c

' }} /> 14 | {null} 15 | {undefined} 16 | 17 | ) 18 | 19 | const Form = () => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | 29 | return () => ( 30 | 31 | 32 | 33 |
34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/preact.ts: -------------------------------------------------------------------------------- 1 | import { h, Fragment} from 'preact' 2 | import { renderToString } from 'preact-render-to-string' 3 | import { buildPage } from './page' 4 | 5 | export const render = () => renderToString(buildPage({ jsx: h, Fragment })() as any) -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { Suite } from 'benchmark' 2 | import { parse } from 'node-html-parser' 3 | 4 | import { render as renderHono } from './hono' 5 | import { render as renderNano } from './nano' 6 | import { render as renderPreact } from './preact' 7 | import { render as renderReact } from './react' 8 | 9 | const suite = new Suite() 10 | 11 | ;[renderHono, renderReact, renderPreact, renderNano].forEach((render) => { 12 | const html = render() 13 | const doc = parse(html) 14 | if (doc.querySelector('p#c').textContent !== '3\nc') { 15 | throw new Error('Invalid output') 16 | } 17 | }) 18 | 19 | suite 20 | .add('Hono', () => { 21 | renderHono() 22 | }) 23 | .add('React', () => { 24 | renderReact() 25 | }) 26 | .add('Preact', () => { 27 | renderPreact() 28 | }) 29 | .add('Nano', () => { 30 | renderNano() 31 | }) 32 | .on('cycle', (ev) => { 33 | console.log(String(ev.target)) 34 | }) 35 | .on('complete', (ev) => { 36 | console.log(`Fastest is ${ev.currentTarget.filter('fastest').map('name')}`) 37 | }) 38 | .run({ async: true }) 39 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/hono.ts: -------------------------------------------------------------------------------- 1 | import { buildPage } from './page-hono' 2 | 3 | export const render = () => buildPage()().toString() -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/nano.ts: -------------------------------------------------------------------------------- 1 | import { renderSSR } from 'nano-jsx' 2 | import { buildPage } from './page-nano.tsx' 3 | 4 | export const render = () => renderSSR(buildPage()) -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/page-hono.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource ../../../../src/jsx **/ 2 | 3 | export const buildPage = () => { 4 | const Content = () => ( 5 | <> 6 |

7 | 1
a 8 |

9 |

10 | 2
b 11 |

12 |
3
c

' }} /> 13 | {null} 14 | {undefined} 15 | 16 | ) 17 | 18 | const Form = () => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | return () => ( 29 | 30 | 31 | 32 |
33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/page-nano.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource nano-jsx/lib **/ 2 | 3 | export const buildPage = () => { 4 | const Content = () => ( 5 | <> 6 |

7 | 1
a 8 |

9 |

10 | 2
b 11 |

12 |
3
c

' }} /> 13 | {null} 14 | {undefined} 15 | 16 | ) 17 | 18 | const Form = () => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | return () => ( 29 | 30 | 31 | 32 |
33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/page-preact.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact **/ 2 | 3 | export const buildPage = () => { 4 | const Content = () => ( 5 | <> 6 |

7 | 1
a 8 |

9 |

10 | 2
b 11 |

12 |
3
c

' }} /> 13 | {null} 14 | {undefined} 15 | 16 | ) 17 | 18 | const Form = () => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | return () => ( 29 | 30 | 31 | 32 |
33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/page-react.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react **/ 2 | 3 | export const buildPage = () => { 4 | const Content = () => ( 5 | <> 6 |

7 | 1
a 8 |

9 |

10 | 2
b 11 |

12 |
3
c

' }} /> 13 | {null} 14 | {undefined} 15 | 16 | ) 17 | 18 | const Form = () => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | return () => ( 29 | 30 | 31 | 32 |
33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/preact.ts: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'preact-render-to-string' 2 | import { buildPage } from './page-preact' 3 | 4 | export const render = () => renderToString(buildPage()() as any) -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/react.ts: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'react-dom/server' 2 | import { buildPage } from './page-react.tsx' 3 | 4 | export const render = () => renderToString(buildPage()() as any) 5 | -------------------------------------------------------------------------------- /benchmarks/jsx/src/react-jsx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | } 5 | } -------------------------------------------------------------------------------- /benchmarks/jsx/src/react.ts: -------------------------------------------------------------------------------- 1 | import { createElement, Fragment } from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import { buildPage } from './page-react' 4 | 5 | export const render = () => renderToString(buildPage({ jsx: createElement, Fragment })() as any) 6 | -------------------------------------------------------------------------------- /benchmarks/jsx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | } 5 | } -------------------------------------------------------------------------------- /benchmarks/query-param/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/benchmarks/query-param/bun.lockb -------------------------------------------------------------------------------- /benchmarks/query-param/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "bench:node": "tsx ./src/bench.mts", 4 | "bench:bun": "bun run ./src/bench.mts" 5 | }, 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@types/qs": "^6.9.17", 9 | "tsx": "^3.12.2" 10 | }, 11 | "dependencies": { 12 | "fast-querystring": "^1.1.1", 13 | "mitata": "^0.1.6", 14 | "qs": "^6.13.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /benchmarks/query-param/src/bench.mts: -------------------------------------------------------------------------------- 1 | import { run, group, bench } from 'mitata' 2 | import { getQueryStrings } from '../../../src/utils/url' 3 | import fastQuerystring from './fast-querystring.mts' 4 | import hono from './hono.mts' 5 | import qs from './qs.mts' 6 | ;[ 7 | { 8 | url: 'http://example.com/?page=1', 9 | key: 'page', 10 | }, 11 | { 12 | url: 'http://example.com/?url=http://example.com&page=1', 13 | key: 'page', 14 | }, 15 | { 16 | url: 'http://example.com/?page=1', 17 | key: undefined, 18 | }, 19 | { 20 | url: 'http://example.com/?url=http://example.com&page=1', 21 | key: undefined, 22 | }, 23 | { 24 | url: 'http://example.com/?url=http://example.com/very/very/deep/path/to/something&search=very-long-search-string', 25 | key: undefined, 26 | }, 27 | { 28 | url: 'http://example.com/?search=Hono+is+a+small,+simple,+and+ultrafast+web+framework+for+the+Edge.&page=1', 29 | key: undefined, 30 | }, 31 | { 32 | url: 'http://example.com/?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=10', 33 | key: undefined, 34 | }, 35 | ].forEach((data) => { 36 | const { url, key } = data 37 | 38 | group(JSON.stringify(data), () => { 39 | bench('hono', () => hono(url, key)) 40 | bench('fastQuerystring', () => fastQuerystring(url, key)) 41 | bench('qs', () => qs(url, key)) 42 | bench('URLSearchParams', () => { 43 | const params = new URLSearchParams(getQueryStrings(url)) 44 | if (key) { 45 | return params.get(key) 46 | } 47 | const obj = {} 48 | for (const [k, v] of params) { 49 | obj[k] = v 50 | } 51 | return obj 52 | }) 53 | }) 54 | }) 55 | 56 | run() 57 | -------------------------------------------------------------------------------- /benchmarks/query-param/src/fast-querystring.mts: -------------------------------------------------------------------------------- 1 | import { parse } from 'fast-querystring' 2 | 3 | const getQueryStringFromURL = (url: string): string => { 4 | const queryIndex = url.indexOf('?', 8) 5 | const result = queryIndex !== -1 ? url.slice(queryIndex + 1) : '' 6 | return result 7 | } 8 | 9 | export default (url, key?) => { 10 | const data = parse(getQueryStringFromURL(url)) 11 | return key !== undefined ? data[key] : data 12 | } 13 | -------------------------------------------------------------------------------- /benchmarks/query-param/src/hono.mts: -------------------------------------------------------------------------------- 1 | import { getQueryParam } from '../../../src/utils/url' 2 | 3 | export default (url, key?) => { 4 | return getQueryParam(url, key) 5 | } 6 | -------------------------------------------------------------------------------- /benchmarks/query-param/src/qs.mts: -------------------------------------------------------------------------------- 1 | import qs from 'qs' 2 | 3 | const getQueryStringFromURL = (url: string): string => { 4 | const queryIndex = url.indexOf('?', 8) 5 | const result = queryIndex !== -1 ? url.slice(queryIndex + 1) : '' 6 | return result 7 | } 8 | 9 | export default (url, key?) => { 10 | const data = qs.parse(getQueryStringFromURL(url)) 11 | return key !== undefined ? data[key] : data 12 | } 13 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": true 10 | }, 11 | "deno.enable": true 12 | } -------------------------------------------------------------------------------- /benchmarks/routers-deno/README.md: -------------------------------------------------------------------------------- 1 | # Router benchmarks 2 | 3 | Benchmark of the most commonly used HTTP routers. 4 | 5 | Tested routes: 6 | 7 | - [find-my-way](https://github.com/delvedor/find-my-way) 8 | - [koa-router](https://github.com/alexmingoia/koa-router) 9 | - [koa-tree-router](https://github.com/steambap/koa-tree-router) 10 | - [trek-router](https://www.npmjs.com/package/trek-router) 11 | - [@medley/router](https://www.npmjs.com/package/@medley/router) 12 | - [Hono RegExpRouter](https://github.com/honojs/hono) 13 | - [Hono TrieRouter](https://github.com/honojs/hono) 14 | 15 | For Deno: 16 | 17 | ``` 18 | deno run --allow-read --allow-run src/bench.mts 19 | ``` 20 | 21 | This project is heavily impaired by [delvedor/router-benchmark](https://github.com/delvedor/router-benchmark) 22 | 23 | ## License 24 | 25 | MIT 26 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "npm/": "https://unpkg.com/" 4 | } 5 | } -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/bench.mts: -------------------------------------------------------------------------------- 1 | import { run, bench, group } from 'npm:mitata' 2 | import { findMyWayRouter } from './find-my-way.mts' 3 | import { regExpRouter, trieRouter, patternRouter } from './hono.mts' 4 | import { koaRouter } from './koa-router.mts' 5 | import { koaTreeRouter } from './koa-tree-router.mts' 6 | import { medleyRouter } from './medley-router.mts' 7 | import type { Route, RouterInterface } from './tool.mts' 8 | import { trekRouter } from './trek-router.mts' 9 | 10 | const routers: RouterInterface[] = [ 11 | regExpRouter, 12 | trieRouter, 13 | patternRouter, 14 | medleyRouter, 15 | findMyWayRouter, 16 | koaTreeRouter, 17 | trekRouter, 18 | koaRouter, 19 | ] 20 | 21 | medleyRouter.match({ method: 'GET', path: '/user' }) 22 | 23 | const routes: (Route & { name: string })[] = [ 24 | { 25 | name: 'short static', 26 | method: 'GET', 27 | path: '/user', 28 | }, 29 | { 30 | name: 'static with same radix', 31 | method: 'GET', 32 | path: '/user/comments', 33 | }, 34 | { 35 | name: 'dynamic route', 36 | method: 'GET', 37 | path: '/user/lookup/username/hey', 38 | }, 39 | { 40 | name: 'mixed static dynamic', 41 | method: 'GET', 42 | path: '/event/abcd1234/comments', 43 | }, 44 | { 45 | name: 'post', 46 | method: 'POST', 47 | path: '/event/abcd1234/comment', 48 | }, 49 | { 50 | name: 'long static', 51 | method: 'GET', 52 | path: '/very/deeply/nested/route/hello/there', 53 | }, 54 | { 55 | name: 'wildcard', 56 | method: 'GET', 57 | path: '/static/index.html', 58 | }, 59 | ] 60 | 61 | for (const route of routes) { 62 | group(`${route.name} - ${route.method} ${route.path}`, () => { 63 | for (const router of routers) { 64 | bench(router.name, async () => { 65 | router.match(route) 66 | }) 67 | } 68 | }) 69 | } 70 | 71 | group('all together', () => { 72 | for (const router of routers) { 73 | bench(router.name, async () => { 74 | for (const route of routes) { 75 | router.match(route) 76 | } 77 | }) 78 | } 79 | }) 80 | 81 | await run() 82 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/find-my-way.mts: -------------------------------------------------------------------------------- 1 | import type { HTTPMethod } from 'npm:find-my-way' 2 | import findMyWay from 'npm:find-my-way' 3 | import type { RouterInterface } from './tool.mts' 4 | import { routes, handler } from './tool.mts' 5 | 6 | const name = 'find-my-way' 7 | const router = findMyWay() 8 | 9 | for (const route of routes) { 10 | router.on(route.method as HTTPMethod, route.path, handler) 11 | } 12 | 13 | export const findMyWayRouter: RouterInterface = { 14 | name, 15 | match: (route) => { 16 | router.find(route.method as HTTPMethod, route.path) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/hono.mts: -------------------------------------------------------------------------------- 1 | import type { Router } from '../../../src/router.ts' 2 | import { RegExpRouter } from '../../../src/router/reg-exp-router/index.ts' 3 | import { TrieRouter } from '../../../src/router/trie-router/index.ts' 4 | import { PatternRouter } from '../../../src/router/pattern-router/index.ts' 5 | import type { RouterInterface } from './tool.mts' 6 | import { routes, handler } from './tool.mts' 7 | 8 | const createHonoRouter = (name: string, router: Router): RouterInterface => { 9 | for (const route of routes) { 10 | router.add(route.method, route.path, handler) 11 | } 12 | return { 13 | name: `Hono ${name}`, 14 | match: (route) => { 15 | router.match(route.method, route.path) 16 | }, 17 | } 18 | } 19 | 20 | export const regExpRouter = createHonoRouter('RegExpRouter', new RegExpRouter()) 21 | export const trieRouter = createHonoRouter('TrieRouter', new TrieRouter()) 22 | export const patternRouter = createHonoRouter('PatternRouter', new PatternRouter()) 23 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/koa-router.mts: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'npm:koa-router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'koa-router' 6 | const router = new KoaRouter() 7 | 8 | for (const route of routes) { 9 | if (route.method === 'GET') { 10 | router.get(route.path.replace('*', '(.*)'), handler) 11 | } else { 12 | router.post(route.path, handler) 13 | } 14 | } 15 | 16 | export const koaRouter: RouterInterface = { 17 | name, 18 | match: (route) => { 19 | router.match(route.path, route.method) // only matching 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/koa-tree-router.mts: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'npm:koa-tree-router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'koa-tree-router' 6 | const router = new KoaRouter() 7 | 8 | for (const route of routes) { 9 | router.on(route.method, route.path.replace('*', '*foo'), handler) 10 | } 11 | 12 | export const koaTreeRouter: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-ignore 17 | router.find(route.method, route.path) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/medley-router.mts: -------------------------------------------------------------------------------- 1 | import Router from 'npm:@medley/router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = '@medley/router' 6 | const router = new Router() 7 | 8 | for (const route of routes) { 9 | const store = router.register(route.path) 10 | store[route.method] = handler 11 | } 12 | 13 | export const medleyRouter: RouterInterface = { 14 | name, 15 | match: (route) => { 16 | const match = router.find(route.path) 17 | match.store[route.method] // get handler 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/tool.mts: -------------------------------------------------------------------------------- 1 | export const handler = () => {} 2 | 3 | export type Route = { 4 | method: 'GET' | 'POST' 5 | path: string 6 | } 7 | 8 | export interface RouterInterface { 9 | name: string 10 | match: (route: Route) => unknown 11 | } 12 | 13 | export const routes: Route[] = [ 14 | { method: 'GET', path: '/user' }, 15 | { method: 'GET', path: '/user/comments' }, 16 | { method: 'GET', path: '/user/avatar' }, 17 | { method: 'GET', path: '/user/lookup/username/:username' }, 18 | { method: 'GET', path: '/user/lookup/email/:address' }, 19 | { method: 'GET', path: '/event/:id' }, 20 | { method: 'GET', path: '/event/:id/comments' }, 21 | { method: 'POST', path: '/event/:id/comment' }, 22 | { method: 'GET', path: '/map/:location/events' }, 23 | { method: 'GET', path: '/status' }, 24 | { method: 'GET', path: '/very/deeply/nested/route/hello/there' }, 25 | { method: 'GET', path: '/static/*' }, 26 | ] 27 | -------------------------------------------------------------------------------- /benchmarks/routers-deno/src/trek-router.mts: -------------------------------------------------------------------------------- 1 | import TrekRouter from 'npm:trek-router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'trek-router' 6 | 7 | const router = new TrekRouter() 8 | for (const route of routes) { 9 | router.add(route.method, route.path, handler()) 10 | } 11 | 12 | export const trekRouter: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | router.find(route.method, route.path) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /benchmarks/routers/README.md: -------------------------------------------------------------------------------- 1 | # Router benchmarks 2 | 3 | Benchmark of the most commonly used HTTP routers. 4 | 5 | Tested routes: 6 | 7 | - [find-my-way](https://github.com/delvedor/find-my-way) 8 | - [express](https://www.npmjs.com/package/express) 9 | - [koa-router](https://github.com/alexmingoia/koa-router) 10 | - [koa-tree-router](https://github.com/steambap/koa-tree-router) 11 | - [trek-router](https://www.npmjs.com/package/trek-router) 12 | - [@medley/router](https://www.npmjs.com/package/@medley/router) 13 | - [Hono RegExpRouter](https://github.com/honojs/hono) 14 | - [Hono TrieRouter](https://github.com/honojs/hono) 15 | 16 | For Node.js: 17 | 18 | ``` 19 | yarn bench:node 20 | ``` 21 | 22 | For Bun: 23 | 24 | ``` 25 | yarn bench:bun 26 | ``` 27 | 28 | This project is heavily impaired by [delvedor/router-benchmark](https://github.com/delvedor/router-benchmark) 29 | 30 | ## License 31 | 32 | MIT 33 | -------------------------------------------------------------------------------- /benchmarks/routers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "bench:node": "tsx ./src/bench.mts", 4 | "bench:bun": "bun run ./src/bench.mts", 5 | "bench-includes-init:node": "tsx ./src/bench-includes-init.mts", 6 | "bench-includes-init:bun": "bun run ./src/bench-includes-init.mts" 7 | }, 8 | "license": "MIT", 9 | "devDependencies": { 10 | "tsx": "^3.12.2" 11 | }, 12 | "dependencies": { 13 | "@medley/router": "^0.2.1", 14 | "express": "^4.19.2", 15 | "find-my-way": "^8.2.0", 16 | "koa-router": "^12.0.1", 17 | "koa-tree-router": "^0.12.1", 18 | "memoirist": "^0.2.0", 19 | "mitata": "^0.1.6", 20 | "radix3": "^1.1.2", 21 | "rou3": "^0.1.0", 22 | "trek-router": "^1.2.0" 23 | } 24 | } -------------------------------------------------------------------------------- /benchmarks/routers/src/express.mts: -------------------------------------------------------------------------------- 1 | import routerFunc from 'express/lib/router/index.js' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const router = routerFunc() 6 | const name = 'express (WARNING: includes handling)' 7 | 8 | for (const route of routes) { 9 | if (route.method === 'GET') { 10 | router.route(route.path).get(handler) 11 | } else { 12 | router.route(route.path).post(handler) 13 | } 14 | } 15 | 16 | export const expressRouter: RouterInterface = { 17 | name, 18 | match: (route) => { 19 | router.handle({ method: route.method, url: route.path }) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/routers/src/find-my-way.mts: -------------------------------------------------------------------------------- 1 | import type { HTTPMethod } from 'find-my-way' 2 | import findMyWay from 'find-my-way' 3 | import type { RouterInterface } from './tool.mts' 4 | import { routes, handler } from './tool.mts' 5 | 6 | const name = 'find-my-way' 7 | const router = findMyWay() 8 | 9 | for (const route of routes) { 10 | router.on(route.method as HTTPMethod, route.path, handler) 11 | } 12 | 13 | export const findMyWayRouter: RouterInterface = { 14 | name, 15 | match: (route) => { 16 | router.find(route.method as HTTPMethod, route.path) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /benchmarks/routers/src/hono.mts: -------------------------------------------------------------------------------- 1 | import { RegExpRouter } from '../../../src/router/reg-exp-router/index.ts' 2 | import { TrieRouter } from '../../../src/router/trie-router/index.ts' 3 | import { PatternRouter } from '../../../src/router/pattern-router/index.ts' 4 | import type { Router } from '../../../src/router.ts' 5 | import type { RouterInterface } from './tool.mts' 6 | import { routes, handler } from './tool.mts' 7 | 8 | const createHonoRouter = (name: string, router: Router): RouterInterface => { 9 | for (const route of routes) { 10 | router.add(route.method, route.path, handler) 11 | } 12 | return { 13 | name: `Hono ${name}`, 14 | match: (route) => { 15 | router.match(route.method, route.path) 16 | }, 17 | } 18 | } 19 | 20 | export const regExpRouter = createHonoRouter('RegExpRouter', new RegExpRouter()) 21 | export const trieRouter = createHonoRouter('TrieRouter', new TrieRouter()) 22 | export const patternRouter = createHonoRouter('PatternRouter', new PatternRouter()) 23 | -------------------------------------------------------------------------------- /benchmarks/routers/src/koa-router.mts: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'koa-router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'koa-router' 6 | const router = new KoaRouter() 7 | 8 | for (const route of routes) { 9 | if (route.method === 'GET') { 10 | router.get(route.path.replace('*', '(.*)'), handler) 11 | } else { 12 | router.post(route.path, handler) 13 | } 14 | } 15 | 16 | export const koaRouter: RouterInterface = { 17 | name, 18 | match: (route) => { 19 | router.match(route.path, route.method) // only matching 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/routers/src/koa-tree-router.mts: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'koa-tree-router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'koa-tree-router' 6 | const router = new KoaRouter() 7 | 8 | for (const route of routes) { 9 | router.on(route.method, route.path.replace('*', '*foo'), handler) 10 | } 11 | 12 | export const koaTreeRouter: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-ignore 17 | router.find(route.method, route.path) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/routers/src/medley-router.mts: -------------------------------------------------------------------------------- 1 | import Router from '@medley/router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = '@medley/router' 6 | const router = new Router() 7 | 8 | for (const route of routes) { 9 | const store = router.register(route.path) 10 | store[route.method] = handler 11 | } 12 | 13 | export const medleyRouter: RouterInterface = { 14 | name, 15 | match: (route) => { 16 | const match = router.find(route.path) 17 | match.store[route.method] // get handler 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/routers/src/memoirist.mts: -------------------------------------------------------------------------------- 1 | import { Memoirist } from 'memoirist' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'Memoirist' 6 | const router = new Memoirist() 7 | 8 | for (const route of routes) { 9 | router.add(route.method, route.path, handler) 10 | } 11 | 12 | export const memoiristRouter: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | router.find(route.method, route.path) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /benchmarks/routers/src/radix3.mts: -------------------------------------------------------------------------------- 1 | import { createRouter } from 'radix3' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'radix3' 6 | const router = createRouter() 7 | 8 | for (const route of routes) { 9 | router.insert(route.path, handler) 10 | } 11 | 12 | export const radix3Router: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | router.lookup(route.path) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /benchmarks/routers/src/rou3.mts: -------------------------------------------------------------------------------- 1 | import { addRoute, createRouter, findRoute } from 'rou3' 2 | import type { RouterInterface } from './tool.mts' 3 | import { handler, routes } from './tool.mts' 4 | 5 | const name = 'rou3' 6 | const router = createRouter() 7 | 8 | for (const route of routes) { 9 | addRoute(router, route.path, route.method, handler) 10 | } 11 | 12 | export const rou3Router: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | findRoute(router, route.path, route.method, { 16 | ignoreParams: false, // Don't ignore params 17 | }) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/routers/src/tool.mts: -------------------------------------------------------------------------------- 1 | export const handler = () => {} 2 | 3 | export type Route = { 4 | method: 'GET' | 'POST' 5 | path: string 6 | } 7 | 8 | export interface RouterInterface { 9 | name: string 10 | match: (route: Route) => unknown 11 | } 12 | 13 | export const routes: Route[] = [ 14 | { method: 'GET', path: '/user' }, 15 | { method: 'GET', path: '/user/comments' }, 16 | { method: 'GET', path: '/user/avatar' }, 17 | { method: 'GET', path: '/user/lookup/username/:username' }, 18 | { method: 'GET', path: '/user/lookup/email/:address' }, 19 | { method: 'GET', path: '/event/:id' }, 20 | { method: 'GET', path: '/event/:id/comments' }, 21 | { method: 'POST', path: '/event/:id/comment' }, 22 | { method: 'GET', path: '/map/:location/events' }, 23 | { method: 'GET', path: '/status' }, 24 | { method: 'GET', path: '/very/deeply/nested/route/hello/there' }, 25 | { method: 'GET', path: '/static/*' }, 26 | ] 27 | -------------------------------------------------------------------------------- /benchmarks/routers/src/trek-router.mts: -------------------------------------------------------------------------------- 1 | import TrekRouter from 'trek-router' 2 | import type { RouterInterface } from './tool.mts' 3 | import { routes, handler } from './tool.mts' 4 | 5 | const name = 'trek-router' 6 | 7 | const router = new TrekRouter() 8 | for (const route of routes) { 9 | router.add(route.method, route.path, handler()) 10 | } 11 | 12 | export const trekRouter: RouterInterface = { 13 | name, 14 | match: (route) => { 15 | router.find(route.method, route.path) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /benchmarks/routers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext" 6 | }, 7 | "include": [ 8 | "./src" 9 | ] 10 | } -------------------------------------------------------------------------------- /benchmarks/utils/.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | bun.lockb -------------------------------------------------------------------------------- /benchmarks/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "devDependencies": { 4 | "mitata": "^0.1.11" 5 | } 6 | } -------------------------------------------------------------------------------- /benchmarks/utils/src/loop.js: -------------------------------------------------------------------------------- 1 | import { run, group, bench } from 'mitata' 2 | 3 | const arr = new Array(100000).fill(Math.random()) 4 | 5 | bench('noop', () => {}) 6 | 7 | group('loop', () => { 8 | bench('map', () => { 9 | const newArr = [] 10 | arr.map((e) => { 11 | newArr.push(e) 12 | }) 13 | }) 14 | 15 | bench('forEach', () => { 16 | const newArr = [] 17 | arr.forEach((e) => { 18 | newArr.push(e) 19 | }) 20 | }) 21 | 22 | bench('for of', () => { 23 | const newArr = [] 24 | for (const e of arr) { 25 | newArr.push(e) 26 | } 27 | }) 28 | 29 | bench('for', () => { 30 | const newArr = [] 31 | for (let i = 0; i < arr.length; i++) { 32 | newArr.push(arr[i]) 33 | } 34 | }) 35 | }) 36 | 37 | run() 38 | -------------------------------------------------------------------------------- /benchmarks/webapp/.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock -------------------------------------------------------------------------------- /benchmarks/webapp/hono.js: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../dist/hono' 2 | //import { Hono } from 'hono' 3 | 4 | const hono = new Hono() 5 | hono.get('/user', (c) => c.text('User')) 6 | hono.get('/user/comments', (c) => c.text('User Comments')) 7 | hono.get('/user/avatar', (c) => c.text('User Avatar')) 8 | hono.get('/user/lookup/email/:address', (c) => c.text('User Lookup Email Address')) 9 | hono.get('/event/:id', (c) => c.text('Event')) 10 | hono.get('/event/:id/comments', (c) => c.text('Event Comments')) 11 | hono.post('/event/:id/comments', (c) => c.text('POST Event Comments')) 12 | hono.post('/status', (c) => c.text('Status')) 13 | hono.get('/very/deeply/nested/route/hello/there', (c) => c.text('Very Deeply Nested Route')) 14 | hono.get('/user/lookup/username/:username', (c) => { 15 | return new Response(`Hello ${c.req.param('username')}`) 16 | }) 17 | 18 | hono.fire() 19 | -------------------------------------------------------------------------------- /benchmarks/webapp/itty-router.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'itty-router' 2 | 3 | const ittyRouter = Router() 4 | ittyRouter.get('/user', () => new Response('User')) 5 | ittyRouter.get('/user/comments', () => new Response('User Comments')) 6 | ittyRouter.get('/user/avatar', () => new Response('User Avatar')) 7 | ittyRouter.get('/user/lookup/email/:address', () => new Response('User Lookup Email Address')) 8 | ittyRouter.get('/event/:id', () => new Response('Event')) 9 | ittyRouter.get('/event/:id/comments', () => new Response('Event Comments')) 10 | ittyRouter.post('/event/:id/comments', () => new Response('POST Event Comments')) 11 | ittyRouter.post('/status', () => new Response('Status')) 12 | ittyRouter.get( 13 | '/very/deeply/nested/route/hello/there', 14 | () => new Response('Very Deeply Nested Route') 15 | ) 16 | ittyRouter.get('/user/lookup/username/:username', ({ params }) => { 17 | return new Response(`Hello ${params.username}`, { 18 | status: 200, 19 | headers: { 20 | 'Content-Type': 'text/plain;charset=UTF-8', 21 | }, 22 | }) 23 | }) 24 | 25 | addEventListener('fetch', (event) => event.respondWith(ittyRouter.handle(event.request))) 26 | -------------------------------------------------------------------------------- /benchmarks/webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start:hono": "wrangler dev hono.js --local --port 8787", 7 | "start:itty-router": "wrangler dev itty-router.js --local --port 8788", 8 | "start:sunder": "wrangler dev sunder.js --local --port 8789" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "itty-router": "^2.6.1", 13 | "sunder": "^0.10.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benchmarks/webapp/sunder.js: -------------------------------------------------------------------------------- 1 | import { Sunder, Router } from 'sunder' 2 | 3 | const sunderRouter = new Router() 4 | sunderRouter.get('/user', (ctx) => { 5 | ctx.response.body = 'User' 6 | }) 7 | sunderRouter.get('/user/comments', (ctx) => { 8 | ctx.response.body = 'User Comments' 9 | }) 10 | sunderRouter.get('/user/avatar', (ctx) => { 11 | ctx.response.body = 'User Avatar' 12 | }) 13 | sunderRouter.get('/user/lookup/email/:address', (ctx) => { 14 | ctx.response.body = 'User Lookup Email Address' 15 | }) 16 | sunderRouter.get('/event/:id', (ctx) => { 17 | ctx.response.body = 'Event' 18 | }) 19 | sunderRouter.get('/event/:id/comments', (ctx) => { 20 | ctx.response.body = 'Event Comments' 21 | }) 22 | sunderRouter.post('/event/:id/comments', (ctx) => { 23 | ctx.response.body = 'POST Event Comments' 24 | }) 25 | sunderRouter.post('/status', (ctx) => { 26 | ctx.response.body = 'Status' 27 | }) 28 | sunderRouter.get('/very/deeply/nested/route/hello/there', (ctx) => { 29 | ctx.response.body = 'Very Deeply Nested Route' 30 | }) 31 | //sunderRouter.get('/static/*', () => {}) 32 | sunderRouter.get('/user/lookup/username/:username', (ctx) => { 33 | ctx.response.body = `Hello ${ctx.params.username}` 34 | }) 35 | const sunderApp = new Sunder() 36 | sunderApp.use(sunderRouter.middleware) 37 | 38 | addEventListener('fetch', (event) => { 39 | event.respondWith(sunderApp.handle(event)) 40 | }) 41 | -------------------------------------------------------------------------------- /build/remove-private-fields-worker.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import fs from 'node:fs/promises' 4 | import os from 'node:os' 5 | import path from 'node:path' 6 | import { removePrivateFields } from './remove-private-fields-worker' 7 | 8 | describe('removePrivateFields', () => { 9 | it('Works', async () => { 10 | const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'removePrivateFields')) 11 | const tsPath = path.join(tmpDir, 'class.ts') 12 | await fs.writeFile(tsPath, 'class X { #private: number = 0; a: number = 0 }') 13 | expect(removePrivateFields(tsPath)).toBe(`class X { 14 | a: number = 0; 15 | } 16 | `) 17 | }) 18 | it('Should throw error when path does not exist', () => { 19 | expect(() => removePrivateFields('./unknown.ts')).toThrowError(Error) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /build/remove-private-fields.ts: -------------------------------------------------------------------------------- 1 | import { cpus } from 'node:os' 2 | import type { WorkerInput, WorkerOutput } from './remove-private-fields-worker' 3 | 4 | const workers = Array.from({ length: Math.ceil(cpus().length / 2) }).map( 5 | () => new Worker(`${import.meta.dirname}/remove-private-fields-worker.ts`, { type: 'module' }) 6 | ) 7 | let workerIndex = 0 8 | let taskId = 0 9 | 10 | export async function removePrivateFields(file: string): Promise { 11 | const currentTaskId = taskId++ 12 | const worker = workers[workerIndex] 13 | workerIndex = (workerIndex + 1) % workers.length 14 | 15 | return new Promise((resolve, reject) => { 16 | const abortController = new AbortController() 17 | worker.addEventListener( 18 | 'message', 19 | ({ data: { type, value, taskId } }: { data: WorkerOutput }) => { 20 | if (taskId === currentTaskId) { 21 | if (type === 'success') { 22 | resolve(value) 23 | } else { 24 | reject(value) 25 | } 26 | 27 | abortController.abort() 28 | } 29 | }, 30 | { signal: abortController.signal } 31 | ) 32 | worker.postMessage({ file, taskId: currentTaskId } satisfies WorkerInput) 33 | }) 34 | } 35 | 36 | export function cleanupWorkers() { 37 | for (const worker of workers) { 38 | worker.terminate() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build/validate-exports.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { validateExports } from './validate-exports' 4 | 5 | const mockExports1 = { 6 | './a': './a.ts', 7 | './b': './b.ts', 8 | './c/a': './c.ts', 9 | './d/*': './d/*.ts', 10 | } 11 | 12 | const mockExports2 = { 13 | './a': './a.ts', 14 | './b': './b.ts', 15 | './c/a': './c.ts', 16 | './d/a': './d/a.ts', 17 | } 18 | 19 | const mockExports3 = { 20 | './a': './a.ts', 21 | './c/a': './c.ts', 22 | './d/*': './d/*.ts', 23 | } 24 | 25 | describe('validateExports', () => { 26 | it('Works', async () => { 27 | expect(() => validateExports(mockExports1, mockExports1, 'package.json')).not.toThrowError() 28 | expect(() => validateExports(mockExports1, mockExports2, 'jsr.json')).not.toThrowError() 29 | expect(() => validateExports(mockExports1, mockExports3, 'package.json')).toThrowError() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /build/validate-exports.ts: -------------------------------------------------------------------------------- 1 | export const validateExports = ( 2 | source: Record, 3 | target: Record, 4 | fileName: string 5 | ) => { 6 | const isEntryInTarget = (entry: string): boolean => { 7 | if (entry in target) { 8 | return true 9 | } 10 | 11 | // e.g., "./utils/*" -> "./utils" 12 | const wildcardPrefix = entry.replace(/\/\*$/, '') 13 | if (entry.endsWith('/*')) { 14 | return Object.keys(target).some( 15 | (targetEntry) => 16 | targetEntry.startsWith(wildcardPrefix + '/') && targetEntry !== wildcardPrefix 17 | ) 18 | } 19 | 20 | const separatedEntry = entry.split('/') 21 | while (separatedEntry.length > 0) { 22 | const pattern = `${separatedEntry.join('/')}/*` 23 | if (pattern in target) { 24 | return true 25 | } 26 | separatedEntry.pop() 27 | } 28 | 29 | return false 30 | } 31 | 32 | Object.keys(source).forEach((sourceEntry) => { 33 | if (!isEntryInTarget(sourceEntry)) { 34 | throw new Error(`Missing "${sourceEntry}" in '${fileName}'`) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | coverage = true 3 | coverageReporter = ["text", "lcov"] 4 | coverageDir = "coverage/raw/bun" 5 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # Edit "test.coverage.exclude" option in vitest.config.ts to exclude specific files from coverage reports. 2 | # We can also use "ignore" option in codecov.yml, but it is not recognized by vitest, so results may differ on local. 3 | 4 | coverage: 5 | status: 6 | patch: 7 | default: 8 | target: 80% 9 | informational: true # Don't fail the build even if coverage is below target 10 | project: 11 | default: 12 | target: 75% 13 | threshold: 1% 14 | -------------------------------------------------------------------------------- /docs/images/hono-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/docs/images/hono-logo.png -------------------------------------------------------------------------------- /docs/images/hono-logo.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/docs/images/hono-logo.pxm -------------------------------------------------------------------------------- /docs/images/hono-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/images/hono-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/docs/images/hono-title.png -------------------------------------------------------------------------------- /docs/images/hono-title.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/docs/images/hono-title.pxm -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from '@hono/eslint-config' 2 | 3 | export default [...baseConfig] 4 | -------------------------------------------------------------------------------- /package.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } -------------------------------------------------------------------------------- /perf-measures/.octocov.tsc.perf-measures.main.yml: -------------------------------------------------------------------------------- 1 | locale: "en" 2 | repository: ${GITHUB_REPOSITORY}/perf-measures 3 | coverage: 4 | if: false 5 | codeToTestRatio: 6 | if: false 7 | testExecutionTime: 8 | if: false 9 | report: 10 | datastores: 11 | - artifact://${GITHUB_REPOSITORY} 12 | summary: 13 | if: true 14 | -------------------------------------------------------------------------------- /perf-measures/.octocov.tsc.perf-measures.yml: -------------------------------------------------------------------------------- 1 | locale: "en" 2 | repository: ${GITHUB_REPOSITORY}/perf-measures 3 | coverage: 4 | if: false 5 | codeToTestRatio: 6 | if: false 7 | testExecutionTime: 8 | if: false 9 | diff: 10 | datastores: 11 | - artifact://${GITHUB_REPOSITORY} 12 | comment: 13 | if: is_pull_request 14 | summary: 15 | if: true 16 | -------------------------------------------------------------------------------- /perf-measures/.octocov.tsgo.perf-measures.main.yml: -------------------------------------------------------------------------------- 1 | locale: "en" 2 | repository: ${GITHUB_REPOSITORY}/perf-measures/tsgo 3 | coverage: 4 | if: false 5 | codeToTestRatio: 6 | if: false 7 | testExecutionTime: 8 | if: false 9 | report: 10 | datastores: 11 | - artifact://${GITHUB_REPOSITORY} 12 | summary: 13 | if: true 14 | -------------------------------------------------------------------------------- /perf-measures/.octocov.tsgo.perf-measures.yml: -------------------------------------------------------------------------------- 1 | locale: "en" 2 | repository: ${GITHUB_REPOSITORY}/perf-measures/tsgo 3 | coverage: 4 | if: false 5 | codeToTestRatio: 6 | if: false 7 | testExecutionTime: 8 | if: false 9 | diff: 10 | datastores: 11 | - artifact://${GITHUB_REPOSITORY} 12 | comment: 13 | if: is_pull_request 14 | summary: 15 | if: true 16 | -------------------------------------------------------------------------------- /perf-measures/bundle-check/.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | !generated/.gitkeep 3 | size.json 4 | -------------------------------------------------------------------------------- /perf-measures/bundle-check/generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/perf-measures/bundle-check/generated/.gitkeep -------------------------------------------------------------------------------- /perf-measures/bundle-check/scripts/check-bundle-size.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild' 2 | import * as fs from 'node:fs' 3 | import * as os from 'os' 4 | import * as path from 'path' 5 | 6 | async function main() { 7 | const tempDir = os.tmpdir() 8 | const tempFilePath = path.join(tempDir, 'bundle.tmp.js') 9 | 10 | try { 11 | await esbuild.build({ 12 | entryPoints: ['dist/index.js'], 13 | bundle: true, 14 | minify: true, 15 | format: 'esm' as esbuild.Format, 16 | target: 'es2022', 17 | outfile: tempFilePath, 18 | }) 19 | 20 | const bundleSize = fs.statSync(tempFilePath).size 21 | const metrics = [] 22 | 23 | metrics.push({ 24 | key: 'bundle-size-b', 25 | name: 'Bundle Size (B)', 26 | value: bundleSize, 27 | unit: 'B', 28 | }) 29 | 30 | metrics.push({ 31 | key: 'bundle-size-kb', 32 | name: 'Bundle Size (KB)', 33 | value: parseFloat((bundleSize / 1024).toFixed(2)), 34 | unit: 'K', 35 | }) 36 | 37 | const benchmark = { 38 | key: 'bundle-size-check', 39 | name: 'Bundle size check', 40 | metrics, 41 | } 42 | console.log(JSON.stringify(benchmark, null, 2)) 43 | } catch (error) { 44 | console.error('Build failed:', error) 45 | } finally { 46 | if (fs.existsSync(tempFilePath)) { 47 | fs.unlinkSync(tempFilePath) 48 | } 49 | } 50 | } 51 | 52 | main() 53 | -------------------------------------------------------------------------------- /perf-measures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "noEmit": true, 6 | "rootDir": "..", 7 | "strict": true 8 | }, 9 | "include": [ 10 | "**/*.ts", 11 | "**/*.tsx" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /perf-measures/type-check/.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | !generated/.gitkeep 3 | trace 4 | *result.txt 5 | diagnostics.json 6 | -------------------------------------------------------------------------------- /perf-measures/type-check/client.ts: -------------------------------------------------------------------------------- 1 | import { hc } from '../../src/client' 2 | import type { app } from './generated/app' 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const client = hc('/') 6 | -------------------------------------------------------------------------------- /perf-measures/type-check/generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/perf-measures/type-check/generated/.gitkeep -------------------------------------------------------------------------------- /perf-measures/type-check/scripts/generate-app.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs' 2 | import * as path from 'node:path' 3 | 4 | const count = 200 5 | 6 | const generateRoutes = (count: number) => { 7 | let routes = `import { Hono } from '../../../src' 8 | export const app = new Hono()` 9 | for (let i = 1; i <= count; i++) { 10 | routes += ` 11 | .get('/route${i}/:id', (c) => { 12 | return c.json({ 13 | ok: true 14 | }) 15 | })` 16 | } 17 | return routes 18 | } 19 | 20 | const routes = generateRoutes(count) 21 | 22 | writeFile(path.join(import.meta.dirname, '../generated/app.ts'), routes, (err) => { 23 | if (err) { 24 | throw err 25 | } 26 | console.log(`${count} routes have been written to app.ts`) 27 | }) 28 | -------------------------------------------------------------------------------- /perf-measures/type-check/scripts/process-results.ts: -------------------------------------------------------------------------------- 1 | import * as readline from 'node:readline' 2 | 3 | async function main() { 4 | const rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout, 7 | terminal: false, 8 | }) 9 | const tsImplLabel = process.env['BENCHMARK_TS_IMPL_LABEL'] 10 | if (!tsImplLabel) { 11 | throw new Error('BENCHMARK_TS_IMPL_LABEL must be set') 12 | } 13 | 14 | const toKebabCase = (str: string): string => { 15 | return str 16 | .replace(/([a-z])([A-Z])/g, '$1-$2') 17 | .replace(/[\s_\/]+/g, '-') 18 | .toLowerCase() 19 | } 20 | const metrics = [] 21 | for await (const line of rl) { 22 | if (!line || line.trim() === '') { 23 | continue 24 | } 25 | const [name, value] = line.split(':') 26 | const unitMatch = value?.trim().match(/^(\d+(\.\d+)?)([a-zA-Z]*)$/) 27 | if (unitMatch) { 28 | const [, number, , unit] = unitMatch 29 | metrics.push({ 30 | key: toKebabCase(name?.trim()), 31 | name: name?.trim(), 32 | value: parseFloat(number), 33 | unit: unit || undefined, 34 | }) 35 | } else { 36 | metrics.push({ 37 | key: toKebabCase(name?.trim()), 38 | name: name?.trim(), 39 | value: parseFloat(value?.trim()), 40 | }) 41 | } 42 | } 43 | const benchmark = { 44 | key: 'diagnostics', 45 | name: `Compiler Diagnostics (${tsImplLabel})`, 46 | metrics, 47 | } 48 | console.log(JSON.stringify(benchmark, null, 2)) 49 | } 50 | 51 | main() 52 | -------------------------------------------------------------------------------- /perf-measures/type-check/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["client.ts"], 4 | "compilerOptions": { 5 | "skipLibCheck": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /runtime-tests/bun/.static/plain.txt: -------------------------------------------------------------------------------- 1 | Bun!! -------------------------------------------------------------------------------- /runtime-tests/bun/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/runtime-tests/bun/favicon.ico -------------------------------------------------------------------------------- /runtime-tests/bun/static-absolute-root/plain.txt: -------------------------------------------------------------------------------- 1 | Bun! -------------------------------------------------------------------------------- /runtime-tests/bun/static/download: -------------------------------------------------------------------------------- 1 | download -------------------------------------------------------------------------------- /runtime-tests/bun/static/hello.world/index.html: -------------------------------------------------------------------------------- 1 | Hi 2 | -------------------------------------------------------------------------------- /runtime-tests/bun/static/helloworld/index.html: -------------------------------------------------------------------------------- 1 | Hi 2 | -------------------------------------------------------------------------------- /runtime-tests/bun/static/plain.txt: -------------------------------------------------------------------------------- 1 | Bun! -------------------------------------------------------------------------------- /runtime-tests/bun/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import config from '../../vitest.config' 4 | 5 | export default defineConfig({ 6 | test: { 7 | globals: true, 8 | include: ['**/runtime-tests/bun/**/*.+(ts|tsx|js)'], 9 | coverage: { 10 | ...config.test?.coverage, 11 | reportsDirectory: './coverage/raw/runtime-bun', 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /runtime-tests/deno-jsx/deno.precompile.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "precompile", 4 | "jsxImportSource": "hono/jsx", 5 | "lib": [ 6 | "deno.ns", 7 | "dom", 8 | "dom.iterable" 9 | ] 10 | }, 11 | "unstable": [ 12 | "sloppy-imports" 13 | ], 14 | "imports": { 15 | "@std/assert": "jsr:@std/assert@^1.0.3", 16 | "hono/jsx/jsx-runtime": "../../src/jsx/jsx-runtime.ts" 17 | } 18 | } -------------------------------------------------------------------------------- /runtime-tests/deno-jsx/deno.react-jsx.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "jsxImportSource": "hono/jsx", 5 | "lib": [ 6 | "deno.ns", 7 | "dom", 8 | "dom.iterable" 9 | ] 10 | }, 11 | "unstable": [ 12 | "sloppy-imports" 13 | ], 14 | "imports": { 15 | "@std/assert": "jsr:@std/assert@^1.0.3", 16 | "hono/jsx/jsx-runtime": "../../src/jsx/jsx-runtime.ts" 17 | } 18 | } -------------------------------------------------------------------------------- /runtime-tests/deno/.static/plain.txt: -------------------------------------------------------------------------------- 1 | Deno!! -------------------------------------------------------------------------------- /runtime-tests/deno/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "deno.enable": true 7 | } 8 | -------------------------------------------------------------------------------- /runtime-tests/deno/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "jsxImportSource": "hono/jsx", 5 | "lib": [ 6 | "deno.ns", 7 | "dom", 8 | "dom.iterable" 9 | ] 10 | }, 11 | "unstable": [ 12 | "sloppy-imports" 13 | ], 14 | "imports": { 15 | "@std/assert": "jsr:@std/assert@^1.0.3", 16 | "@std/path": "jsr:@std/path@^1.0.3", 17 | "@std/testing": "jsr:@std/testing@^1.0.1", 18 | "hono/jsx/jsx-runtime": "../../src/jsx/jsx-runtime.ts" 19 | } 20 | } -------------------------------------------------------------------------------- /runtime-tests/deno/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@std/assert@^1.0.3": "1.0.10", 5 | "jsr:@std/assert@^1.0.6": "1.0.10", 6 | "jsr:@std/internal@^1.0.5": "1.0.5", 7 | "jsr:@std/path@^1.0.3": "1.0.6", 8 | "jsr:@std/testing@^1.0.1": "1.0.3", 9 | "npm:@types/node@*": "22.5.4" 10 | }, 11 | "jsr": { 12 | "@std/assert@1.0.10": { 13 | "integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3", 14 | "dependencies": [ 15 | "jsr:@std/internal" 16 | ] 17 | }, 18 | "@std/internal@1.0.5": { 19 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" 20 | }, 21 | "@std/path@1.0.6": { 22 | "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" 23 | }, 24 | "@std/testing@1.0.3": { 25 | "integrity": "f98c2bee53860a5916727d7e7d3abe920dd6f9edace022e2d059f00d05c2cf42", 26 | "dependencies": [ 27 | "jsr:@std/assert@^1.0.6" 28 | ] 29 | } 30 | }, 31 | "npm": { 32 | "@types/node@22.5.4": { 33 | "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", 34 | "dependencies": [ 35 | "undici-types" 36 | ] 37 | }, 38 | "undici-types@6.19.8": { 39 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" 40 | } 41 | }, 42 | "workspace": { 43 | "dependencies": [ 44 | "jsr:@std/assert@^1.0.3", 45 | "jsr:@std/path@^1.0.3", 46 | "jsr:@std/testing@^1.0.1" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /runtime-tests/deno/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/hono/53656e126df4855969afa55603cd063343d65552/runtime-tests/deno/favicon.ico -------------------------------------------------------------------------------- /runtime-tests/deno/hono.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '@std/assert' 2 | 3 | import { Context } from '../../src/context.ts' 4 | import { env, getRuntimeKey } from '../../src/helper/adapter/index.ts' 5 | import { Hono } from '../../src/hono.ts' 6 | 7 | // Test just only minimal patterns. 8 | // Because others are tested well in Cloudflare Workers environment already. 9 | 10 | Deno.env.set('NAME', 'Deno') 11 | 12 | Deno.test('Hello World', async () => { 13 | const app = new Hono() 14 | app.get('/:foo', (c) => { 15 | c.header('x-param', c.req.param('foo')) 16 | c.header('x-query', c.req.query('q') || '') 17 | return c.text('Hello Deno!') 18 | }) 19 | 20 | const res = await app.request('/foo?q=bar') 21 | assertEquals(res.status, 200) 22 | assertEquals(await res.text(), 'Hello Deno!') 23 | assertEquals(res.headers.get('x-param'), 'foo') 24 | assertEquals(res.headers.get('x-query'), 'bar') 25 | }) 26 | 27 | Deno.test('runtime', () => { 28 | assertEquals(getRuntimeKey(), 'deno') 29 | }) 30 | 31 | Deno.test('environment variables', () => { 32 | const c = new Context(new Request('http://localhost/')) 33 | const { NAME } = env<{ NAME: string }>(c) 34 | assertEquals(NAME, 'Deno') 35 | }) 36 | -------------------------------------------------------------------------------- /runtime-tests/deno/ssg.test.tsx: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '@std/assert' 2 | import { toSSG } from '../../src/adapter/deno/ssg.ts' 3 | import { Hono } from '../../src/hono.ts' 4 | 5 | Deno.test('toSSG function', async () => { 6 | const app = new Hono() 7 | app.get('/', (c) => c.text('Hello, World!')) 8 | app.get('/about', (c) => c.text('About Page')) 9 | app.get('/about/some', (c) => c.text('About Page 2tier')) 10 | app.post('/about/some/thing', (c) => c.text('About Page 3tier')) 11 | app.get('/bravo', (c) => c.html('Bravo Page')) 12 | app.get('/Charlie', async (c, next) => { 13 | c.setRenderer((content) => { 14 | return c.html( 15 | 16 | 17 |

{content}

18 | 19 | 20 | ) 21 | }) 22 | await next() 23 | }) 24 | app.get('/Charlie', (c) => { 25 | return c.render('Hello!') 26 | }) 27 | 28 | const result = await toSSG(app, { dir: './ssg-static' }) 29 | assertEquals(result.success, true) 30 | assertEquals(result.error, undefined) 31 | assertEquals(result.files !== undefined, true) 32 | 33 | await deleteDirectory('./ssg-static') 34 | }) 35 | 36 | async function deleteDirectory(dirPath: string): Promise { 37 | try { 38 | const stat = await Deno.stat(dirPath) 39 | 40 | if (stat.isDirectory) { 41 | for await (const dirEntry of Deno.readDir(dirPath)) { 42 | const entryPath = `${dirPath}/${dirEntry.name}` 43 | await deleteDirectory(entryPath) 44 | } 45 | await Deno.remove(dirPath) 46 | } else { 47 | await Deno.remove(dirPath) 48 | } 49 | } catch (error) { 50 | console.error(`Error deleting directory: ${error}`) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /runtime-tests/deno/static-absolute-root/plain.txt: -------------------------------------------------------------------------------- 1 | Deno! -------------------------------------------------------------------------------- /runtime-tests/deno/static/download: -------------------------------------------------------------------------------- 1 | download -------------------------------------------------------------------------------- /runtime-tests/deno/static/hello.world/index.html: -------------------------------------------------------------------------------- 1 | Hi 2 | -------------------------------------------------------------------------------- /runtime-tests/deno/static/helloworld/index.html: -------------------------------------------------------------------------------- 1 | Hi 2 | -------------------------------------------------------------------------------- /runtime-tests/deno/static/plain.txt: -------------------------------------------------------------------------------- 1 | Deno! -------------------------------------------------------------------------------- /runtime-tests/fastly/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import fastlyCompute from 'vite-plugin-fastly-js-compute' 3 | import { defineConfig } from 'vitest/config' 4 | import config from '../../vitest.config' 5 | 6 | export default defineConfig({ 7 | plugins: [fastlyCompute()], 8 | test: { 9 | globals: true, 10 | include: ['**/runtime-tests/fastly/**/(*.)+(test).+(ts|tsx)'], 11 | exclude: ['**/runtime-tests/fastly/vitest.config.ts'], 12 | coverage: { 13 | ...config.test?.coverage, 14 | reportsDirectory: './coverage/raw/runtime-fastly', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /runtime-tests/lambda-edge/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import config from '../../vitest.config' 4 | 5 | export default defineConfig({ 6 | test: { 7 | env: { 8 | NAME: 'Node', 9 | }, 10 | globals: true, 11 | include: ['**/runtime-tests/lambda-edge/**/*.+(ts|tsx|js)'], 12 | exclude: ['**/runtime-tests/lambda-edge/vitest.config.ts'], 13 | coverage: { 14 | ...config.test?.coverage, 15 | reportsDirectory: './coverage/raw/runtime-lambda-edge', 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /runtime-tests/lambda/mock.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import type { 3 | APIGatewayProxyEvent, 4 | APIGatewayProxyEventV2, 5 | LambdaFunctionUrlEvent, 6 | } from '../../src/adapter/aws-lambda/handler' 7 | import type { LambdaContext } from '../../src/adapter/aws-lambda/types' 8 | 9 | type StreamifyResponseHandler = ( 10 | handlerFunc: ( 11 | event: APIGatewayProxyEvent | APIGatewayProxyEventV2 | LambdaFunctionUrlEvent, 12 | responseStream: NodeJS.WritableStream, 13 | context: LambdaContext 14 | ) => Promise 15 | ) => (event: APIGatewayProxyEvent, context: LambdaContext) => Promise 16 | 17 | const mockStreamifyResponse: StreamifyResponseHandler = (handlerFunc) => { 18 | return async (event, context) => { 19 | const mockWritableStream: NodeJS.WritableStream = new (require('stream').Writable)({ 20 | write(chunk, encoding, callback) { 21 | console.log('Writing chunk:', chunk.toString()) 22 | callback() 23 | }, 24 | final(callback) { 25 | console.log('Finalizing stream.') 26 | callback() 27 | }, 28 | }) 29 | mockWritableStream.on('finish', () => { 30 | console.log('Stream has finished') 31 | }) 32 | await handlerFunc(event, mockWritableStream, context) 33 | mockWritableStream.end() 34 | return mockWritableStream 35 | } 36 | } 37 | 38 | const awslambda = { 39 | streamifyResponse: mockStreamifyResponse, 40 | } 41 | 42 | vi.stubGlobal('awslambda', awslambda) 43 | -------------------------------------------------------------------------------- /runtime-tests/lambda/stream-mock.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import { Writable } from 'node:stream' 3 | import type { 4 | APIGatewayProxyEvent, 5 | APIGatewayProxyEventV2, 6 | } from '../../src/adapter/aws-lambda/handler' 7 | import type { LambdaContext } from '../../src/adapter/aws-lambda/types' 8 | 9 | type StreamifyResponseHandler = ( 10 | handlerFunc: ( 11 | event: APIGatewayProxyEvent | APIGatewayProxyEventV2, 12 | responseStream: Writable, 13 | context: LambdaContext 14 | ) => Promise 15 | ) => (event: APIGatewayProxyEvent, context: LambdaContext) => Promise 16 | 17 | const mockStreamifyResponse: StreamifyResponseHandler = (handlerFunc) => { 18 | return async (event, context) => { 19 | const chunks = [] 20 | const mockWritableStream = new Writable({ 21 | write(chunk, encoding, callback) { 22 | chunks.push(chunk) 23 | callback() 24 | }, 25 | }) 26 | mockWritableStream.chunks = chunks 27 | await handlerFunc(event, mockWritableStream, context) 28 | mockWritableStream.end() 29 | return mockWritableStream 30 | } 31 | } 32 | 33 | const awslambda = { 34 | streamifyResponse: mockStreamifyResponse, 35 | HttpResponseStream: { 36 | from: (stream: Writable, httpResponseMetadata: unknown): Writable => { 37 | stream.write(Buffer.from(JSON.stringify(httpResponseMetadata))) 38 | return stream 39 | }, 40 | }, 41 | } 42 | 43 | vi.stubGlobal('awslambda', awslambda) 44 | -------------------------------------------------------------------------------- /runtime-tests/lambda/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import config from '../../vitest.config' 4 | 5 | export default defineConfig({ 6 | test: { 7 | env: { 8 | NAME: 'Node', 9 | }, 10 | globals: true, 11 | include: ['**/runtime-tests/lambda/**/*.+(ts|tsx|js)'], 12 | exclude: [ 13 | '**/runtime-tests/lambda/vitest.config.ts', 14 | '**/runtime-tests/lambda/mock.ts', 15 | '**/runtime-tests/lambda/stream-mock.ts', 16 | ], 17 | coverage: { 18 | ...config.test?.coverage, 19 | reportsDirectory: './coverage/raw/runtime-lambda', 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /runtime-tests/node/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import config from '../../vitest.config' 4 | 5 | export default defineConfig({ 6 | test: { 7 | env: { 8 | NAME: 'Node', 9 | }, 10 | globals: true, 11 | include: ['**/runtime-tests/node/**/*.+(ts|tsx|js)'], 12 | exclude: ['**/runtime-tests/node/vitest.config.ts'], 13 | coverage: { 14 | ...config.test?.coverage, 15 | reportsDirectory: './coverage/raw/runtime-node', 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /runtime-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "**/*.ts", 5 | "**/*.tsx" 6 | ] 7 | } -------------------------------------------------------------------------------- /runtime-tests/workerd/index.ts: -------------------------------------------------------------------------------- 1 | import { upgradeWebSocket } from '../../src/adapter/cloudflare-workers' 2 | import { env, getRuntimeKey } from '../../src/helper/adapter' 3 | import { Hono } from '../../src/hono' 4 | 5 | const app = new Hono() 6 | 7 | app.get('/', (c) => c.text(`Hello from ${getRuntimeKey()}`)) 8 | 9 | app.get('/env', (c) => { 10 | const { NAME } = env<{ NAME: string }>(c) 11 | return c.text(NAME) 12 | }) 13 | 14 | app.get( 15 | '/ws', 16 | upgradeWebSocket(() => { 17 | return { 18 | onMessage(event, ws) { 19 | ws.send(event.data as string) 20 | }, 21 | } 22 | }) 23 | ) 24 | 25 | export default app 26 | -------------------------------------------------------------------------------- /runtime-tests/workerd/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import config from '../../vitest.config' 4 | 5 | export default defineConfig({ 6 | test: { 7 | globals: true, 8 | include: ['**/runtime-tests/workerd/**/(*.)+(test).+(ts|tsx)'], 9 | exclude: ['**/runtime-tests/workerd/vitest.config.ts'], 10 | coverage: { 11 | ...config.test?.coverage, 12 | reportsDirectory: './coverage/raw/runtime-workerd', 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/adapter/aws-lambda/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * AWS Lambda Adapter for Hono. 4 | */ 5 | 6 | export { handle, streamHandle } from './handler' 7 | export type { APIGatewayProxyResult, LambdaEvent } from './handler' 8 | export type { 9 | ApiGatewayRequestContext, 10 | ApiGatewayRequestContextV2, 11 | ALBRequestContext, 12 | LambdaContext, 13 | } from './types' 14 | -------------------------------------------------------------------------------- /src/adapter/bun/conninfo.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../..' 2 | import type { GetConnInfo } from '../../helper/conninfo' 3 | import { getBunServer } from './server' 4 | 5 | /** 6 | * Get ConnInfo with Bun 7 | * @param c Context 8 | * @returns ConnInfo 9 | */ 10 | export const getConnInfo: GetConnInfo = (c: Context) => { 11 | const server = getBunServer(c) 12 | 13 | if (!server) { 14 | throw new TypeError('env has to include the 2nd argument of fetch.') 15 | } 16 | if (typeof server.requestIP !== 'function') { 17 | throw new TypeError('server.requestIP is not a function.') 18 | } 19 | const info = server.requestIP(c.req.raw) 20 | 21 | return { 22 | remote: { 23 | address: info.address, 24 | addressType: info.family === 'IPv6' || info.family === 'IPv4' ? info.family : undefined, 25 | port: info.port, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/adapter/bun/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Bun Adapter for Hono. 4 | */ 5 | 6 | export { serveStatic } from './serve-static' 7 | export { bunFileSystemModule, toSSG } from './ssg' 8 | export { createBunWebSocket } from './websocket' 9 | export type { BunWebSocketData, BunWebSocketHandler } from './websocket' 10 | export { getConnInfo } from './conninfo' 11 | -------------------------------------------------------------------------------- /src/adapter/bun/serve-static.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { stat } from 'node:fs/promises' 3 | import { serveStatic as baseServeStatic } from '../../middleware/serve-static' 4 | import type { ServeStaticOptions } from '../../middleware/serve-static' 5 | import type { Env, MiddlewareHandler } from '../../types' 6 | 7 | export const serveStatic = ( 8 | options: ServeStaticOptions 9 | ): MiddlewareHandler => { 10 | return async function serveStatic(c, next) { 11 | const getContent = async (path: string) => { 12 | path = path.startsWith('/') ? path : `./${path}` 13 | // @ts-ignore 14 | const file = Bun.file(path) 15 | return (await file.exists()) ? file : null 16 | } 17 | const pathResolve = (path: string) => { 18 | return path.startsWith('/') ? path : `./${path}` 19 | } 20 | const isDir = async (path: string) => { 21 | let isDir 22 | try { 23 | const stats = await stat(path) 24 | isDir = stats.isDirectory() 25 | } catch {} 26 | return isDir 27 | } 28 | return baseServeStatic({ 29 | ...options, 30 | getContent, 31 | pathResolve, 32 | isDir, 33 | })(c, next) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/adapter/bun/server.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context' 2 | import { getBunServer } from './server' 3 | import type { BunServer } from './server' 4 | 5 | describe('getBunServer', () => { 6 | it('Should success to pick Server', () => { 7 | const server = {} as BunServer 8 | 9 | expect(getBunServer(new Context(new Request('http://localhost/'), { env: server }))).toBe( 10 | server 11 | ) 12 | expect(getBunServer(new Context(new Request('http://localhost/'), { env: { server } }))).toBe( 13 | server 14 | ) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/adapter/bun/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Getting Bun Server Object for Bun adapters 3 | * @module 4 | */ 5 | import type { Context } from '../../context' 6 | 7 | /** 8 | * Bun Server Object 9 | */ 10 | export interface BunServer { 11 | requestIP?: (req: Request) => { 12 | address: string 13 | family: string 14 | port: number 15 | } 16 | upgrade( 17 | req: Request, 18 | options?: { 19 | data: T 20 | } 21 | ): boolean 22 | } 23 | 24 | /** 25 | * Get Bun Server Object from Context 26 | * @param c Context 27 | * @returns Bun Server 28 | */ 29 | export const getBunServer = (c: Context): BunServer | undefined => 30 | ('server' in c.env ? c.env.server : c.env) as BunServer | undefined 31 | -------------------------------------------------------------------------------- /src/adapter/bun/ssg.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { toSSG as baseToSSG } from '../../helper/ssg' 3 | import type { FileSystemModule, ToSSGAdaptorInterface } from '../../helper/ssg' 4 | 5 | // @ts-ignore 6 | const { write } = Bun 7 | 8 | /** 9 | * @experimental 10 | * `bunFileSystemModule` is an experimental feature. 11 | * The API might be changed. 12 | */ 13 | export const bunFileSystemModule: FileSystemModule = { 14 | writeFile: async (path, data) => { 15 | await write(path, data) 16 | }, 17 | mkdir: async () => {}, 18 | } 19 | 20 | /** 21 | * @experimental 22 | * `toSSG` is an experimental feature. 23 | * The API might be changed. 24 | */ 25 | export const toSSG: ToSSGAdaptorInterface = async (app, options) => { 26 | return baseToSSG(app, bunFileSystemModule, options) 27 | } 28 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-pages/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Cloudflare Pages Adapter for Hono. 4 | */ 5 | 6 | export { handle, handleMiddleware, serveStatic } from './handler' 7 | export type { EventContext } from './handler' 8 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/conninfo.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context' 2 | import { getConnInfo } from './conninfo' 3 | 4 | describe('getConnInfo', () => { 5 | it('Should getConnInfo works', () => { 6 | const address = Math.random().toString() 7 | const req = new Request('http://localhost/', { 8 | headers: { 9 | 'cf-connecting-ip': address, 10 | }, 11 | }) 12 | const c = new Context(req) 13 | 14 | const info = getConnInfo(c) 15 | 16 | expect(info.remote.address).toBe(address) 17 | expect(info.remote.addressType).toBeUndefined() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/conninfo.ts: -------------------------------------------------------------------------------- 1 | import type { GetConnInfo } from '../../helper/conninfo' 2 | 3 | export const getConnInfo: GetConnInfo = (c) => ({ 4 | remote: { 5 | address: c.req.header('cf-connecting-ip'), 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Cloudflare Workers Adapter for Hono. 4 | */ 5 | 6 | export { serveStatic } from './serve-static-module' 7 | export { upgradeWebSocket } from './websocket' 8 | export { getConnInfo } from './conninfo' 9 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/serve-static-module.ts: -------------------------------------------------------------------------------- 1 | // For ES module mode 2 | import type { Env, MiddlewareHandler } from '../../types' 3 | import type { ServeStaticOptions } from './serve-static' 4 | import { serveStatic } from './serve-static' 5 | 6 | const module = ( 7 | options: Omit, 'namespace'> 8 | ): MiddlewareHandler => { 9 | return serveStatic(options) 10 | } 11 | 12 | export { module as serveStatic } 13 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/serve-static.ts: -------------------------------------------------------------------------------- 1 | import { serveStatic as baseServeStatic } from '../../middleware/serve-static' 2 | import type { ServeStaticOptions as BaseServeStaticOptions } from '../../middleware/serve-static' 3 | import type { Env, MiddlewareHandler } from '../../types' 4 | import { getContentFromKVAsset } from './utils' 5 | 6 | export type ServeStaticOptions = BaseServeStaticOptions & { 7 | // namespace is KVNamespace 8 | namespace?: unknown 9 | manifest: object | string 10 | } 11 | 12 | /** 13 | * @deprecated 14 | * `serveStatic` in the Cloudflare Workers adapter is deprecated. 15 | * You can serve static files directly using Cloudflare Static Assets. 16 | * @see https://developers.cloudflare.com/workers/static-assets/ 17 | * Cloudflare Static Assets is currently in open beta. If this doesn't work for you, 18 | * please consider using Cloudflare Pages. You can start to create the Cloudflare Pages 19 | * application with the `npm create hono@latest` command. 20 | */ 21 | export const serveStatic = ( 22 | options: ServeStaticOptions 23 | ): MiddlewareHandler => { 24 | return async function serveStatic(c, next) { 25 | const getContent = async (path: string) => { 26 | return getContentFromKVAsset(path, { 27 | manifest: options.manifest, 28 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 29 | // @ts-ignore 30 | namespace: options.namespace 31 | ? options.namespace 32 | : c.env 33 | ? c.env.__STATIC_CONTENT 34 | : undefined, 35 | }) 36 | } 37 | return baseServeStatic({ 38 | ...options, 39 | getContent, 40 | })(c, next) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { getContentFromKVAsset } from './utils' 2 | 3 | // Mock 4 | const store: { [key: string]: string } = { 5 | 'index.abcdef.html': 'This is index', 6 | 'assets/static/plain.abcdef.txt': 'Asset text', 7 | } 8 | const manifest = JSON.stringify({ 9 | 'index.html': 'index.abcdef.html', 10 | 'assets/static/plain.txt': 'assets/static/plain.abcdef.txt', 11 | }) 12 | 13 | Object.assign(global, { __STATIC_CONTENT_MANIFEST: manifest }) 14 | Object.assign(global, { 15 | __STATIC_CONTENT: { 16 | get: (path: string) => { 17 | return store[path] 18 | }, 19 | }, 20 | }) 21 | 22 | describe('Utils for Cloudflare Workers', () => { 23 | it('getContentFromKVAsset', async () => { 24 | let content = await getContentFromKVAsset('not-found.txt') 25 | expect(content).toBeFalsy() 26 | content = await getContentFromKVAsset('index.html') 27 | expect(content).toBeTruthy() 28 | expect(content).toBe('This is index') 29 | content = await getContentFromKVAsset('assets/static/plain.txt') 30 | expect(content).toBeTruthy() 31 | expect(content).toBe('Asset text') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/utils.ts: -------------------------------------------------------------------------------- 1 | // __STATIC_CONTENT is KVNamespace 2 | declare const __STATIC_CONTENT: unknown 3 | declare const __STATIC_CONTENT_MANIFEST: string 4 | 5 | export type KVAssetOptions = { 6 | manifest?: object | string 7 | // namespace is KVNamespace 8 | namespace?: unknown 9 | } 10 | 11 | export const getContentFromKVAsset = async ( 12 | path: string, 13 | options?: KVAssetOptions 14 | ): Promise => { 15 | let ASSET_MANIFEST: Record 16 | 17 | if (options && options.manifest) { 18 | if (typeof options.manifest === 'string') { 19 | ASSET_MANIFEST = JSON.parse(options.manifest) 20 | } else { 21 | ASSET_MANIFEST = options.manifest as Record 22 | } 23 | } else { 24 | if (typeof __STATIC_CONTENT_MANIFEST === 'string') { 25 | ASSET_MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST) 26 | } else { 27 | ASSET_MANIFEST = __STATIC_CONTENT_MANIFEST 28 | } 29 | } 30 | 31 | // ASSET_NAMESPACE is KVNamespace 32 | let ASSET_NAMESPACE: unknown 33 | if (options && options.namespace) { 34 | ASSET_NAMESPACE = options.namespace 35 | } else { 36 | ASSET_NAMESPACE = __STATIC_CONTENT 37 | } 38 | 39 | const key = ASSET_MANIFEST[path] || path 40 | if (!key) { 41 | return null 42 | } 43 | 44 | // @ts-expect-error ASSET_NAMESPACE is not typed 45 | const content = await ASSET_NAMESPACE.get(key, { type: 'stream' }) 46 | if (!content) { 47 | return null 48 | } 49 | return content as unknown as ReadableStream 50 | } 51 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/websocket.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../..' 2 | import { Context } from '../../context' 3 | import { upgradeWebSocket } from '.' 4 | 5 | describe('upgradeWebSocket middleware', () => { 6 | const server = new EventTarget() 7 | 8 | // @ts-expect-error Cloudflare API 9 | globalThis.WebSocketPair = class { 10 | 0: WebSocket // client 11 | 1: WebSocket // server 12 | constructor() { 13 | this[0] = {} as WebSocket 14 | this[1] = server as WebSocket 15 | } 16 | } 17 | 18 | const app = new Hono() 19 | 20 | const wsPromise = new Promise((resolve) => 21 | app.get( 22 | '/ws', 23 | upgradeWebSocket(() => ({ 24 | onMessage(evt, ws) { 25 | resolve([evt.data, ws.readyState || 1]) 26 | }, 27 | })) 28 | ) 29 | ) 30 | it('Should receive message and readyState is valid', async () => { 31 | const sendingData = Math.random().toString() 32 | await app.request('/ws', { 33 | headers: { 34 | Upgrade: 'websocket', 35 | }, 36 | }) 37 | server.dispatchEvent( 38 | new MessageEvent('message', { 39 | data: sendingData, 40 | }) 41 | ) 42 | 43 | expect([sendingData, 1]).toStrictEqual(await wsPromise) 44 | }) 45 | it('Should call next() when header does not have upgrade', async () => { 46 | const next = vi.fn() 47 | await upgradeWebSocket(() => ({}))( 48 | new Context( 49 | new Request('http://localhost', { 50 | headers: { 51 | Upgrade: 'example', 52 | }, 53 | }) 54 | ), 55 | next 56 | ) 57 | expect(next).toBeCalled() 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/adapter/cloudflare-workers/websocket.ts: -------------------------------------------------------------------------------- 1 | import { WSContext, defineWebSocketHelper } from '../../helper/websocket' 2 | import type { UpgradeWebSocket, WSEvents, WSReadyState } from '../../helper/websocket' 3 | 4 | // Based on https://github.com/honojs/hono/issues/1153#issuecomment-1767321332 5 | export const upgradeWebSocket: UpgradeWebSocket< 6 | WebSocket, 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | any, 9 | Omit, 'onOpen'> 10 | > = defineWebSocketHelper(async (c, events) => { 11 | const upgradeHeader = c.req.header('Upgrade') 12 | if (upgradeHeader !== 'websocket') { 13 | return 14 | } 15 | 16 | // @ts-expect-error WebSocketPair is not typed 17 | const webSocketPair = new WebSocketPair() 18 | const client: WebSocket = webSocketPair[0] 19 | const server: WebSocket = webSocketPair[1] 20 | 21 | const wsContext = new WSContext({ 22 | close: (code, reason) => server.close(code, reason), 23 | get protocol() { 24 | return server.protocol 25 | }, 26 | raw: server, 27 | get readyState() { 28 | return server.readyState as WSReadyState 29 | }, 30 | url: server.url ? new URL(server.url) : null, 31 | send: (source) => server.send(source), 32 | }) 33 | 34 | // note: cloudflare workers doesn't support 'open' event 35 | 36 | if (events.onClose) { 37 | server.addEventListener('close', (evt: CloseEvent) => events.onClose?.(evt, wsContext)) 38 | } 39 | if (events.onMessage) { 40 | server.addEventListener('message', (evt: MessageEvent) => events.onMessage?.(evt, wsContext)) 41 | } 42 | if (events.onError) { 43 | server.addEventListener('error', (evt: Event) => events.onError?.(evt, wsContext)) 44 | } 45 | 46 | // @ts-expect-error - server.accept is not typed 47 | server.accept?.() 48 | return new Response(null, { 49 | status: 101, 50 | // @ts-expect-error - webSocket is not typed 51 | webSocket: client, 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/adapter/deno/conninfo.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context' 2 | import { getConnInfo } from './conninfo' 3 | 4 | describe('getConnInfo', () => { 5 | it('Should info is valid', () => { 6 | const transport = 'tcp' 7 | const address = Math.random().toString() 8 | const port = Math.floor(Math.random() * (65535 + 1)) 9 | const c = new Context(new Request('http://localhost/'), { 10 | env: { 11 | remoteAddr: { 12 | transport, 13 | hostname: address, 14 | port, 15 | }, 16 | }, 17 | }) 18 | const info = getConnInfo(c) 19 | 20 | expect(info.remote.port).toBe(port) 21 | expect(info.remote.address).toBe(address) 22 | expect(info.remote.addressType).toBeUndefined() 23 | expect(info.remote.transport).toBe(transport) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/adapter/deno/conninfo.ts: -------------------------------------------------------------------------------- 1 | import type { GetConnInfo } from '../../helper/conninfo' 2 | 3 | /** 4 | * Get conninfo with Deno 5 | * @param c Context 6 | * @returns ConnInfo 7 | */ 8 | export const getConnInfo: GetConnInfo = (c) => { 9 | const { remoteAddr } = c.env 10 | return { 11 | remote: { 12 | address: remoteAddr.hostname, 13 | port: remoteAddr.port, 14 | transport: remoteAddr.transport, 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/adapter/deno/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Deno Adapter for Hono. 4 | */ 5 | 6 | export { serveStatic } from './serve-static' 7 | export { toSSG, denoFileSystemModule } from './ssg' 8 | export { upgradeWebSocket } from './websocket' 9 | export { getConnInfo } from './conninfo' 10 | -------------------------------------------------------------------------------- /src/adapter/deno/serve-static.ts: -------------------------------------------------------------------------------- 1 | import type { ServeStaticOptions } from '../../middleware/serve-static' 2 | import { serveStatic as baseServeStatic } from '../../middleware/serve-static' 3 | import type { Env, MiddlewareHandler } from '../../types' 4 | 5 | const { open, lstatSync, errors } = Deno 6 | 7 | export const serveStatic = ( 8 | options: ServeStaticOptions 9 | ): MiddlewareHandler => { 10 | return async function serveStatic(c, next) { 11 | const getContent = async (path: string) => { 12 | try { 13 | if (isDir(path)) { 14 | return null 15 | } 16 | 17 | const file = await open(path) 18 | return file.readable 19 | } catch (e) { 20 | if (!(e instanceof errors.NotFound)) { 21 | console.warn(`${e}`) 22 | } 23 | return null 24 | } 25 | } 26 | const pathResolve = (path: string) => { 27 | return path.startsWith('/') ? path : `./${path}` 28 | } 29 | const isDir = (path: string) => { 30 | let isDir 31 | try { 32 | const stat = lstatSync(path) 33 | isDir = stat.isDirectory 34 | } catch {} 35 | return isDir 36 | } 37 | 38 | return baseServeStatic({ 39 | ...options, 40 | getContent, 41 | pathResolve, 42 | isDir, 43 | })(c, next) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/adapter/deno/ssg.ts: -------------------------------------------------------------------------------- 1 | import { toSSG as baseToSSG } from '../../helper/ssg/index' 2 | import type { FileSystemModule, ToSSGAdaptorInterface } from '../../helper/ssg/index' 3 | 4 | /** 5 | * @experimental 6 | * `denoFileSystemModule` is an experimental feature. 7 | * The API might be changed. 8 | */ 9 | export const denoFileSystemModule: FileSystemModule = { 10 | writeFile: async (path, data) => { 11 | const uint8Data = 12 | typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data) 13 | await Deno.writeFile(path, uint8Data) 14 | }, 15 | mkdir: async (path, options) => { 16 | return Deno.mkdir(path, { recursive: options?.recursive ?? false }) 17 | }, 18 | } 19 | 20 | /** 21 | * @experimental 22 | * `toSSG` is an experimental feature. 23 | * The API might be changed. 24 | */ 25 | export const toSSG: ToSSGAdaptorInterface = async (app, options) => { 26 | return baseToSSG(app, denoFileSystemModule, options) 27 | } 28 | -------------------------------------------------------------------------------- /src/adapter/deno/websocket.ts: -------------------------------------------------------------------------------- 1 | import type { UpgradeWebSocket, WSReadyState } from '../../helper/websocket' 2 | import { WSContext, defineWebSocketHelper } from '../../helper/websocket' 3 | 4 | export const upgradeWebSocket: UpgradeWebSocket = 5 | defineWebSocketHelper(async (c, events, options) => { 6 | if (c.req.header('upgrade') !== 'websocket') { 7 | return 8 | } 9 | 10 | const { response, socket } = Deno.upgradeWebSocket(c.req.raw, options ?? {}) 11 | 12 | const wsContext: WSContext = new WSContext({ 13 | close: (code, reason) => socket.close(code, reason), 14 | get protocol() { 15 | return socket.protocol 16 | }, 17 | raw: socket, 18 | get readyState() { 19 | return socket.readyState as WSReadyState 20 | }, 21 | url: socket.url ? new URL(socket.url) : null, 22 | send: (source) => socket.send(source), 23 | }) 24 | socket.onopen = (evt) => events.onOpen?.(evt, wsContext) 25 | socket.onmessage = (evt) => events.onMessage?.(evt, wsContext) 26 | socket.onclose = (evt) => events.onClose?.(evt, wsContext) 27 | socket.onerror = (evt) => events.onError?.(evt, wsContext) 28 | 29 | return response 30 | }) 31 | -------------------------------------------------------------------------------- /src/adapter/lambda-edge/conninfo.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context' 2 | import { getConnInfo } from './conninfo' 3 | import type { CloudFrontEdgeEvent } from './handler' 4 | 5 | describe('getConnInfo', () => { 6 | it('Should info is valid', () => { 7 | const clientIp = Math.random().toString() 8 | const env = { 9 | event: { 10 | Records: [ 11 | { 12 | cf: { 13 | request: { 14 | clientIp, 15 | }, 16 | }, 17 | }, 18 | ], 19 | } as CloudFrontEdgeEvent, 20 | } 21 | 22 | const c = new Context(new Request('http://localhost/'), { env }) 23 | const info = getConnInfo(c) 24 | 25 | expect(info.remote.address).toBe(clientIp) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/adapter/lambda-edge/conninfo.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../../context' 2 | import type { GetConnInfo } from '../../helper/conninfo' 3 | import type { CloudFrontEdgeEvent } from './handler' 4 | 5 | type Env = { 6 | Bindings: { 7 | event: CloudFrontEdgeEvent 8 | } 9 | } 10 | 11 | export const getConnInfo: GetConnInfo = (c: Context) => ({ 12 | remote: { 13 | address: c.env.event.Records[0].cf.request.clientIp, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/adapter/lambda-edge/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Lambda@Edge Adapter for Hono. 4 | */ 5 | 6 | export { handle } from './handler' 7 | export { getConnInfo } from './conninfo' 8 | export type { 9 | Callback, 10 | CloudFrontConfig, 11 | CloudFrontRequest, 12 | CloudFrontResponse, 13 | CloudFrontEdgeEvent, 14 | } from './handler' 15 | -------------------------------------------------------------------------------- /src/adapter/netlify/handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { Hono } from '../../hono' 3 | 4 | export const handle = ( 5 | app: Hono 6 | ): ((req: Request, context: any) => Response | Promise) => { 7 | return (req: Request, context: any) => { 8 | return app.fetch(req, { context }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/adapter/netlify/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Netlify Adapter for Hono. 4 | */ 5 | 6 | export * from './mod' 7 | -------------------------------------------------------------------------------- /src/adapter/netlify/mod.ts: -------------------------------------------------------------------------------- 1 | export { handle } from './handler' 2 | -------------------------------------------------------------------------------- /src/adapter/service-worker/handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Handler for Service Worker 3 | * @module 4 | */ 5 | 6 | import type { Hono } from '../../hono' 7 | import type { FetchEvent } from './types' 8 | 9 | type Handler = (evt: FetchEvent) => void 10 | 11 | /** 12 | * Adapter for Service Worker 13 | */ 14 | export const handle = ( 15 | app: Hono, 16 | opts: { 17 | fetch?: typeof fetch 18 | } = { 19 | // To use `fetch` on a Service Worker correctly, bind it to `globalThis`. 20 | fetch: globalThis.fetch.bind(globalThis), 21 | } 22 | ): Handler => { 23 | return (evt) => { 24 | evt.respondWith( 25 | (async () => { 26 | const res = await app.fetch(evt.request) 27 | if (opts.fetch && res.status === 404) { 28 | return await opts.fetch(evt.request) 29 | } 30 | return res 31 | })() 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/adapter/service-worker/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Service Worker Adapter for Hono. 3 | * @module 4 | */ 5 | export { handle } from './handler' 6 | -------------------------------------------------------------------------------- /src/adapter/service-worker/types.ts: -------------------------------------------------------------------------------- 1 | interface ExtendableEvent extends Event { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | waitUntil(f: Promise): void 4 | } 5 | 6 | export interface FetchEvent extends ExtendableEvent { 7 | readonly clientId: string 8 | readonly handled: Promise 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | readonly preloadResponse: Promise 11 | readonly request: Request 12 | readonly resultingClientId: string 13 | respondWith(r: Response | PromiseLike): void 14 | } 15 | -------------------------------------------------------------------------------- /src/adapter/vercel/conninfo.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context' 2 | import { getConnInfo } from './conninfo' 3 | 4 | describe('getConnInfo', () => { 5 | it('Should getConnInfo works', () => { 6 | const address = Math.random().toString() 7 | const req = new Request('http://localhost/', { 8 | headers: { 9 | 'x-real-ip': address, 10 | }, 11 | }) 12 | const c = new Context(req) 13 | 14 | const info = getConnInfo(c) 15 | 16 | expect(info.remote.address).toBe(address) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/adapter/vercel/conninfo.ts: -------------------------------------------------------------------------------- 1 | import type { GetConnInfo } from '../../helper/conninfo' 2 | 3 | export const getConnInfo: GetConnInfo = (c) => ({ 4 | remote: { 5 | // https://github.com/vercel/vercel/blob/b70bfb5fbf28a4650d4042ce68ca5c636d37cf44/packages/edge/src/edge-headers.ts#L10-L12C32 6 | address: c.req.header('x-real-ip'), 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/adapter/vercel/handler.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../hono' 2 | import { handle } from './handler' 3 | 4 | describe('Adapter for Next.js', () => { 5 | it('Should return 200 response', async () => { 6 | const app = new Hono() 7 | app.get('/api/author/:name', async (c) => { 8 | const name = c.req.param('name') 9 | return c.json({ 10 | path: '/api/author/:name', 11 | name, 12 | }) 13 | }) 14 | const handler = handle(app) 15 | const req = new Request('http://localhost/api/author/hono') 16 | const res = await handler(req) 17 | expect(res.status).toBe(200) 18 | expect(await res.json()).toEqual({ 19 | path: '/api/author/:name', 20 | name: 'hono', 21 | }) 22 | }) 23 | 24 | it('Should not use `route()` if path argument is not passed', async () => { 25 | const app = new Hono().basePath('/api') 26 | 27 | app.onError((e) => { 28 | throw e 29 | }) 30 | app.get('/error', () => { 31 | throw new Error('Custom Error') 32 | }) 33 | 34 | const handler = handle(app) 35 | const req = new Request('http://localhost/api/error') 36 | expect(() => handler(req)).toThrowError('Custom Error') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/adapter/vercel/handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { Hono } from '../../hono' 3 | 4 | export const handle = 5 | (app: Hono) => 6 | (req: Request): Response | Promise => { 7 | return app.fetch(req) 8 | } 9 | -------------------------------------------------------------------------------- /src/adapter/vercel/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Vercel Adapter for Hono. 4 | */ 5 | 6 | export { handle } from './handler' 7 | export { getConnInfo } from './conninfo' 8 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * The HTTP Client for Hono. 4 | */ 5 | 6 | export { hc } from './client' 7 | export type { 8 | InferResponseType, 9 | InferRequestType, 10 | Fetch, 11 | ClientRequestOptions, 12 | ClientRequest, 13 | ClientResponse, 14 | } from './types' 15 | -------------------------------------------------------------------------------- /src/client/types.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { expectTypeOf } from 'vitest' 3 | import { Hono } from '..' 4 | import { upgradeWebSocket } from '../adapter/deno/websocket' 5 | import { hc } from '.' 6 | 7 | describe('WebSockets', () => { 8 | const app = new Hono() 9 | .get( 10 | '/ws', 11 | upgradeWebSocket(() => ({})) 12 | ) 13 | .get('/', (c) => c.json({})) 14 | const client = hc('/') 15 | 16 | it('WebSocket route', () => { 17 | expectTypeOf(client.ws).toMatchTypeOf<{ 18 | $ws: () => WebSocket 19 | }>() 20 | }) 21 | it('Not WebSocket Route', () => { 22 | expectTypeOf< 23 | typeof client.index extends { $ws: () => WebSocket } ? false : true 24 | >().toEqualTypeOf(true) 25 | }) 26 | }) 27 | 28 | describe('without the leading slash', () => { 29 | const app = new Hono() 30 | .get('foo', (c) => c.json({})) 31 | .get('foo/bar', (c) => c.json({})) 32 | .get('foo/:id/baz', (c) => c.json({})) 33 | const client = hc('') 34 | it('`foo` should have `$get`', () => { 35 | expectTypeOf(client.foo).toHaveProperty('$get') 36 | }) 37 | it('`foo.bar` should not have `$get`', () => { 38 | expectTypeOf(client.foo.bar).toHaveProperty('$get') 39 | }) 40 | it('`foo[":id"].baz` should have `$get`', () => { 41 | expectTypeOf(client.foo[':id'].baz).toHaveProperty('$get') 42 | }) 43 | }) 44 | 45 | describe('with the leading slash', () => { 46 | const app = new Hono() 47 | .get('/foo', (c) => c.json({})) 48 | .get('/foo/bar', (c) => c.json({})) 49 | .get('/foo/:id/baz', (c) => c.json({})) 50 | const client = hc('') 51 | it('`foo` should have `$get`', () => { 52 | expectTypeOf(client.foo).toHaveProperty('$get') 53 | }) 54 | it('`foo.bar` should not have `$get`', () => { 55 | expectTypeOf(client.foo.bar).toHaveProperty('$get') 56 | }) 57 | it('`foo[":id"].baz` should have `$get`', () => { 58 | expectTypeOf(client.foo[':id'].baz).toHaveProperty('$get') 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/helper/accepts/accepts.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../../context' 2 | import { parseAccept } from '../../utils/accept' 3 | import type { AcceptHeader } from '../../utils/headers' 4 | 5 | export interface Accept { 6 | type: string 7 | params: Record 8 | q: number 9 | } 10 | 11 | export interface acceptsConfig { 12 | header: AcceptHeader 13 | supports: string[] 14 | default: string 15 | } 16 | 17 | export interface acceptsOptions extends acceptsConfig { 18 | match?: (accepts: Accept[], config: acceptsConfig) => string 19 | } 20 | 21 | export const defaultMatch = (accepts: Accept[], config: acceptsConfig): string => { 22 | const { supports, default: defaultSupport } = config 23 | const accept = accepts.sort((a, b) => b.q - a.q).find((accept) => supports.includes(accept.type)) 24 | return accept ? accept.type : defaultSupport 25 | } 26 | 27 | /** 28 | * Match the accept header with the given options. 29 | * @example 30 | * ```ts 31 | * app.get('/users', (c) => { 32 | * const lang = accepts(c, { 33 | * header: 'Accept-Language', 34 | * supports: ['en', 'zh'], 35 | * default: 'en', 36 | * }) 37 | * }) 38 | * ``` 39 | */ 40 | export const accepts = (c: Context, options: acceptsOptions): string => { 41 | const acceptHeader = c.req.header(options.header) 42 | if (!acceptHeader) { 43 | return options.default 44 | } 45 | const accepts = parseAccept(acceptHeader) 46 | const match = options.match || defaultMatch 47 | 48 | return match(accepts, options) 49 | } 50 | -------------------------------------------------------------------------------- /src/helper/accepts/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Accepts Helper for Hono. 4 | */ 5 | 6 | export { accepts } from './accepts' 7 | -------------------------------------------------------------------------------- /src/helper/adapter/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../hono' 2 | import { env, getRuntimeKey } from '.' 3 | 4 | describe('getRuntimeKey', () => { 5 | it('Should return the current runtime key', () => { 6 | // Now, using the `bun run test` command. 7 | // But `vitest` depending Node.js will run this test so the RuntimeKey will be `node`. 8 | expect(getRuntimeKey()).toBe('node') 9 | }) 10 | }) 11 | 12 | describe('env', () => { 13 | describe('Types', () => { 14 | type Env = { 15 | Bindings: { 16 | MY_VAR: string 17 | } 18 | } 19 | 20 | it('Should not throw type errors with env has generics', () => { 21 | const app = new Hono() 22 | app.get('/var', (c) => { 23 | const { MY_VAR } = env<{ MY_VAR: string }>(c) 24 | expectTypeOf(MY_VAR) 25 | return c.json({ 26 | var: MY_VAR, 27 | }) 28 | }) 29 | }) 30 | 31 | it('Should not throw type errors with Hono has generics', () => { 32 | const app = new Hono() 33 | 34 | app.get('/var', (c) => { 35 | const { MY_VAR } = env(c) 36 | expectTypeOf(MY_VAR) 37 | return c.json({ 38 | var: MY_VAR, 39 | }) 40 | }) 41 | }) 42 | 43 | it('Should not throw type errors with env and Hono have generics', () => { 44 | const app = new Hono() 45 | 46 | app.get('/var', (c) => { 47 | const { MY_VAR } = env<{ MY_VAR: string }>(c) 48 | expectTypeOf(MY_VAR) 49 | return c.json({ 50 | var: MY_VAR, 51 | }) 52 | }) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/helper/conninfo/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * ConnInfo Helper for Hono. 4 | */ 5 | 6 | export type { AddressType, NetAddrInfo, ConnInfo, GetConnInfo } from './types' 7 | -------------------------------------------------------------------------------- /src/helper/conninfo/types.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../../context' 2 | 3 | export type AddressType = 'IPv6' | 'IPv4' | undefined 4 | 5 | export type NetAddrInfo = { 6 | /** 7 | * Transport protocol type 8 | */ 9 | transport?: 'tcp' | 'udp' 10 | /** 11 | * Transport port number 12 | */ 13 | port?: number 14 | 15 | address?: string 16 | addressType?: AddressType 17 | } & ( 18 | | { 19 | /** 20 | * Host name such as IP Addr 21 | */ 22 | address: string 23 | 24 | /** 25 | * Host name type 26 | */ 27 | addressType: AddressType 28 | } 29 | | {} 30 | ) 31 | 32 | /** 33 | * HTTP Connection information 34 | */ 35 | export interface ConnInfo { 36 | /** 37 | * Remote information 38 | */ 39 | remote: NetAddrInfo 40 | } 41 | 42 | /** 43 | * Helper type 44 | */ 45 | export type GetConnInfo = (c: Context) => ConnInfo 46 | -------------------------------------------------------------------------------- /src/helper/ssg/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * SSG Helper for Hono. 4 | */ 5 | 6 | export * from './ssg' 7 | export { 8 | X_HONO_DISABLE_SSG_HEADER_KEY, 9 | ssgParams, 10 | isSSGContext, 11 | disableSSG, 12 | onlySSG, 13 | } from './middleware' 14 | -------------------------------------------------------------------------------- /src/helper/streaming/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Streaming Helper for Hono. 4 | */ 5 | 6 | export { stream } from './stream' 7 | export type { SSEMessage } from './sse' 8 | export { streamSSE, SSEStreamingApi } from './sse' 9 | export { streamText } from './text' 10 | -------------------------------------------------------------------------------- /src/helper/streaming/stream.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../../context' 2 | import { StreamingApi } from '../../utils/stream' 3 | import { isOldBunVersion } from './utils' 4 | 5 | const contextStash: WeakMap = new WeakMap() 6 | 7 | export const stream = ( 8 | c: Context, 9 | cb: (stream: StreamingApi) => Promise, 10 | onError?: (e: Error, stream: StreamingApi) => Promise 11 | ): Response => { 12 | const { readable, writable } = new TransformStream() 13 | const stream = new StreamingApi(writable, readable) 14 | 15 | // Until Bun v1.1.27, Bun didn't call cancel() on the ReadableStream for Response objects from Bun.serve() 16 | if (isOldBunVersion()) { 17 | c.req.raw.signal.addEventListener('abort', () => { 18 | if (!stream.closed) { 19 | stream.abort() 20 | } 21 | }) 22 | } 23 | 24 | // in bun, `c` is destroyed when the request is returned, so hold it until the end of streaming 25 | contextStash.set(stream.responseReadable, c) 26 | ;(async () => { 27 | try { 28 | await cb(stream) 29 | } catch (e) { 30 | if (e === undefined) { 31 | // If reading is canceled without a reason value (e.g. by StreamingApi) 32 | // then the .pipeTo() promise will reject with undefined. 33 | // In this case, do nothing because the stream is already closed. 34 | } else if (e instanceof Error && onError) { 35 | await onError(e, stream) 36 | } else { 37 | console.error(e) 38 | } 39 | } finally { 40 | stream.close() 41 | } 42 | })() 43 | 44 | return c.newResponse(stream.responseReadable) 45 | } 46 | -------------------------------------------------------------------------------- /src/helper/streaming/text.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../context' 2 | import { streamText } from '.' 3 | 4 | describe('Text Streaming Helper', () => { 5 | const req = new Request('http://localhost/') 6 | let c: Context 7 | beforeEach(() => { 8 | c = new Context(req) 9 | }) 10 | 11 | it('Check streamText Response', async () => { 12 | const res = streamText(c, async (stream) => { 13 | for (let i = 0; i < 3; i++) { 14 | await stream.write(`${i}`) 15 | await stream.sleep(1) 16 | } 17 | }) 18 | 19 | expect(res.status).toBe(200) 20 | expect(res.headers.get('content-type')).toMatch(/^text\/plain/) 21 | expect(res.headers.get('x-content-type-options')).toBe('nosniff') 22 | expect(res.headers.get('transfer-encoding')).toBe('chunked') 23 | 24 | if (!res.body) { 25 | throw new Error('Body is null') 26 | } 27 | const reader = res.body.getReader() 28 | const decoder = new TextDecoder() 29 | for (let i = 0; i < 3; i++) { 30 | const { value } = await reader.read() 31 | expect(decoder.decode(value)).toEqual(`${i}`) 32 | } 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/helper/streaming/text.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../../context' 2 | import { TEXT_PLAIN } from '../../context' 3 | import type { StreamingApi } from '../../utils/stream' 4 | import { stream } from './' 5 | 6 | export const streamText = ( 7 | c: Context, 8 | cb: (stream: StreamingApi) => Promise, 9 | onError?: (e: Error, stream: StreamingApi) => Promise 10 | ): Response => { 11 | c.header('Content-Type', TEXT_PLAIN) 12 | c.header('X-Content-Type-Options', 'nosniff') 13 | c.header('Transfer-Encoding', 'chunked') 14 | return stream(c, cb, onError) 15 | } 16 | -------------------------------------------------------------------------------- /src/helper/streaming/utils.ts: -------------------------------------------------------------------------------- 1 | export let isOldBunVersion = (): boolean => { 2 | // @ts-expect-error @types/bun is not installed 3 | const version: string = typeof Bun !== 'undefined' ? Bun.version : undefined 4 | if (version === undefined) { 5 | return false 6 | } 7 | const result = version.startsWith('1.1') || version.startsWith('1.0') || version.startsWith('0.') 8 | // Avoid running this check on every call 9 | isOldBunVersion = () => result 10 | return result 11 | } 12 | -------------------------------------------------------------------------------- /src/helper/testing/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../hono' 2 | import { testClient } from '.' 3 | 4 | describe('hono testClient', () => { 5 | it('Should return the correct search result', async () => { 6 | const app = new Hono().get('/search', (c) => c.json({ hello: 'world' })) 7 | const res = await testClient(app).search.$get() 8 | expect(await res.json()).toEqual({ hello: 'world' }) 9 | }) 10 | 11 | it('Should return the correct environment variables value', async () => { 12 | type Bindings = { hello: string } 13 | const app = new Hono<{ Bindings: Bindings }>().get('/search', (c) => { 14 | return c.json({ hello: c.env.hello }) 15 | }) 16 | const res = await testClient(app, { hello: 'world' }).search.$get() 17 | expect(await res.json()).toEqual({ hello: 'world' }) 18 | }) 19 | 20 | it('Should return a correct URL with out throwing an error', async () => { 21 | const app = new Hono().get('/abc', (c) => c.json(0)) 22 | const url = testClient(app).abc.$url() 23 | expect(url.pathname).toBe('/abc') 24 | }) 25 | 26 | it('Should not throw an error with $ws()', async () => { 27 | vi.stubGlobal('WebSocket', class {}) 28 | const app = new Hono().get('/ws', (c) => c.text('Fake response of a WebSocket')) 29 | // @ts-expect-error $ws is not typed correctly 30 | expect(() => testClient(app).ws.$ws()).not.toThrowError() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/helper/testing/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Testing Helper for Hono. 4 | */ 5 | 6 | import { hc } from '../../client' 7 | import type { Client } from '../../client/types' 8 | import type { ExecutionContext } from '../../context' 9 | import type { Hono } from '../../hono' 10 | import type { Schema } from '../../types' 11 | import type { UnionToIntersection } from '../../utils/types' 12 | 13 | type ExtractEnv = T extends Hono ? E : never 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | export const testClient = >( 17 | app: T, 18 | Env?: ExtractEnv['Bindings'] | {}, 19 | executionCtx?: ExecutionContext 20 | ): UnionToIntersection> => { 21 | const customFetch = (input: RequestInfo | URL, init?: RequestInit) => { 22 | return app.request(input, init, Env, executionCtx) 23 | } 24 | 25 | return hc('http://localhost', { fetch: customFetch }) 26 | } 27 | -------------------------------------------------------------------------------- /src/hono.ts: -------------------------------------------------------------------------------- 1 | import { HonoBase } from './hono-base' 2 | import type { HonoOptions } from './hono-base' 3 | import { RegExpRouter } from './router/reg-exp-router' 4 | import { SmartRouter } from './router/smart-router' 5 | import { TrieRouter } from './router/trie-router' 6 | import type { BlankEnv, BlankSchema, Env, Schema } from './types' 7 | 8 | /** 9 | * The Hono class extends the functionality of the HonoBase class. 10 | * It sets up routing and allows for custom options to be passed. 11 | * 12 | * @template E - The environment type. 13 | * @template S - The schema type. 14 | * @template BasePath - The base path type. 15 | */ 16 | export class Hono< 17 | E extends Env = BlankEnv, 18 | S extends Schema = BlankSchema, 19 | BasePath extends string = '/' 20 | > extends HonoBase { 21 | /** 22 | * Creates an instance of the Hono class. 23 | * 24 | * @param options - Optional configuration options for the Hono instance. 25 | */ 26 | constructor(options: HonoOptions = {}) { 27 | super(options) 28 | this.router = 29 | options.router ?? 30 | new SmartRouter({ 31 | routers: [new RegExpRouter(), new TrieRouter()], 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/http-exception.test.ts: -------------------------------------------------------------------------------- 1 | import { HTTPException } from './http-exception' 2 | 3 | describe('HTTPException', () => { 4 | it('Should be 401 HTTP exception object', async () => { 5 | // We should throw an exception if is not authorized 6 | // because next handlers should not be fired. 7 | const exception = new HTTPException(401, { 8 | message: 'Unauthorized', 9 | }) 10 | const res = exception.getResponse() 11 | 12 | expect(res.status).toBe(401) 13 | expect(await res.text()).toBe('Unauthorized') 14 | expect(exception.status).toBe(401) 15 | expect(exception.message).toBe('Unauthorized') 16 | }) 17 | 18 | it('Should be accessible to the object causing the exception', async () => { 19 | // We should pass the cause of the error to the cause option 20 | // because it makes debugging easier. 21 | const error = new Error('Server Error') 22 | const exception = new HTTPException(500, { 23 | message: 'Internal Server Error', 24 | cause: error, 25 | }) 26 | const res = exception.getResponse() 27 | 28 | expect(res.status).toBe(500) 29 | expect(await res.text()).toBe('Internal Server Error') 30 | expect(exception.status).toBe(500) 31 | expect(exception.message).toBe('Internal Server Error') 32 | expect(exception.cause).toBe(error) 33 | }) 34 | 35 | it('Should prioritize the status code over the code in the response', async () => { 36 | const exception = new HTTPException(400, { 37 | res: new Response('An exception', { 38 | status: 200, 39 | }), 40 | }) 41 | const res = exception.getResponse() 42 | expect(res.status).toBe(400) 43 | expect(await res.text()).toBe('An exception') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * 4 | * Hono - Web Framework built on Web Standards 5 | * 6 | * @example 7 | * ```ts 8 | * import { Hono } from 'hono' 9 | * const app = new Hono() 10 | * 11 | * app.get('/', (c) => c.text('Hono!')) 12 | * 13 | * export default app 14 | * ``` 15 | */ 16 | 17 | import { Hono } from './hono' 18 | 19 | /** 20 | * Types for environment variables, error handlers, handlers, middleware handlers, and more. 21 | */ 22 | export type { 23 | Env, 24 | ErrorHandler, 25 | Handler, 26 | MiddlewareHandler, 27 | Next, 28 | NotFoundHandler, 29 | ValidationTargets, 30 | Input, 31 | Schema, 32 | ToSchema, 33 | TypedResponse, 34 | } from './types' 35 | /** 36 | * Types for context, context variable map, context renderer, and execution context. 37 | */ 38 | export type { Context, ContextVariableMap, ContextRenderer, ExecutionContext } from './context' 39 | /** 40 | * Type for HonoRequest. 41 | */ 42 | export type { HonoRequest } from './request' 43 | /** 44 | * Types for inferring request and response types and client request options. 45 | */ 46 | export type { InferRequestType, InferResponseType, ClientRequestOptions } from './client' 47 | 48 | /** 49 | * Hono framework for building web applications. 50 | */ 51 | export { Hono } 52 | -------------------------------------------------------------------------------- /src/jsx/children.test.ts: -------------------------------------------------------------------------------- 1 | import { Children } from './children' 2 | import { createElement } from '.' 3 | 4 | describe('map', () => { 5 | it('should map children', () => { 6 | const element = createElement('div', null, 1, 2, 3) 7 | const result = Children.map(element.children, (child) => (child as number) * 2) 8 | expect(result).toEqual([2, 4, 6]) 9 | }) 10 | }) 11 | 12 | describe('forEach', () => { 13 | it('should iterate children', () => { 14 | const element = createElement('div', null, 1, 2, 3) 15 | const result: number[] = [] 16 | Children.forEach(element.children, (child) => { 17 | result.push(child as number) 18 | }) 19 | expect(result).toEqual([1, 2, 3]) 20 | }) 21 | }) 22 | 23 | describe('count', () => { 24 | it('should count children', () => { 25 | const element = createElement('div', null, 1, 2, 3) 26 | const result = Children.count(element.children) 27 | expect(result).toBe(3) 28 | }) 29 | }) 30 | 31 | describe('only', () => { 32 | it('should return the only child', () => { 33 | const element = createElement('div', null, 1) 34 | const result = Children.only(element.children) 35 | expect(result).toBe(1) 36 | }) 37 | 38 | it('should throw an error if there are multiple children', () => { 39 | const element = createElement('div', null, 1, 2) 40 | expect(() => Children.only(element.children)).toThrowError( 41 | 'Children.only() expects only one child' 42 | ) 43 | }) 44 | }) 45 | 46 | describe('toArray', () => { 47 | it('should convert children to an array', () => { 48 | const element = createElement('div', null, 1, 2, 3) 49 | const result = Children.toArray(element.children) 50 | expect(result).toEqual([1, 2, 3]) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/jsx/children.ts: -------------------------------------------------------------------------------- 1 | import type { Child } from './base' 2 | 3 | export const toArray = (children: Child): Child[] => 4 | Array.isArray(children) ? children : [children] 5 | export const Children = { 6 | map: (children: Child[], fn: (child: Child, index: number) => Child): Child[] => 7 | toArray(children).map(fn), 8 | forEach: (children: Child[], fn: (child: Child, index: number) => void): void => { 9 | toArray(children).forEach(fn) 10 | }, 11 | count: (children: Child[]): number => toArray(children).length, 12 | only: (_children: Child[]): Child => { 13 | const children = toArray(_children) 14 | if (children.length !== 1) { 15 | throw new Error('Children.only() expects only one child') 16 | } 17 | return children[0] 18 | }, 19 | toArray, 20 | } 21 | -------------------------------------------------------------------------------- /src/jsx/constants.ts: -------------------------------------------------------------------------------- 1 | export const DOM_RENDERER = Symbol('RENDERER') 2 | export const DOM_ERROR_HANDLER = Symbol('ERROR_HANDLER') 3 | export const DOM_STASH = Symbol('STASH') 4 | export const DOM_INTERNAL_TAG = Symbol('INTERNAL') 5 | export const DOM_MEMO = Symbol('MEMO') 6 | export const PERMALINK = Symbol('PERMALINK') 7 | -------------------------------------------------------------------------------- /src/jsx/context.ts: -------------------------------------------------------------------------------- 1 | import { raw } from '../helper/html' 2 | import type { HtmlEscapedString } from '../utils/html' 3 | import { JSXFragmentNode } from './base' 4 | import { DOM_RENDERER } from './constants' 5 | import { createContextProviderFunction } from './dom/context' 6 | import type { FC, PropsWithChildren } from './' 7 | 8 | export interface Context extends FC> { 9 | values: T[] 10 | Provider: FC> 11 | } 12 | 13 | export const globalContexts: Context[] = [] 14 | 15 | export const createContext = (defaultValue: T): Context => { 16 | const values = [defaultValue] 17 | const context: Context = ((props): HtmlEscapedString | Promise => { 18 | values.push(props.value) 19 | let string 20 | try { 21 | string = props.children 22 | ? (Array.isArray(props.children) 23 | ? new JSXFragmentNode('', {}, props.children) 24 | : props.children 25 | ).toString() 26 | : '' 27 | } finally { 28 | values.pop() 29 | } 30 | 31 | if (string instanceof Promise) { 32 | return string.then((resString) => raw(resString, (resString as HtmlEscapedString).callbacks)) 33 | } else { 34 | return raw(string) 35 | } 36 | }) as Context 37 | context.values = values 38 | context.Provider = context 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | ;(context as any)[DOM_RENDERER] = createContextProviderFunction(values) 42 | 43 | globalContexts.push(context as Context) 44 | 45 | return context 46 | } 47 | 48 | export const useContext = (context: Context): T => { 49 | return context.values.at(-1) as T 50 | } 51 | -------------------------------------------------------------------------------- /src/jsx/dom/components.ts: -------------------------------------------------------------------------------- 1 | import type { Child, FC, PropsWithChildren } from '../' 2 | import type { ErrorHandler, FallbackRender } from '../components' 3 | import { DOM_ERROR_HANDLER } from '../constants' 4 | import { Fragment } from './jsx-runtime' 5 | 6 | /* eslint-disable @typescript-eslint/no-explicit-any */ 7 | export const ErrorBoundary: FC< 8 | PropsWithChildren<{ 9 | fallback?: Child 10 | fallbackRender?: FallbackRender 11 | onError?: ErrorHandler 12 | }> 13 | > = (({ children, fallback, fallbackRender, onError }: any) => { 14 | const res = Fragment({ children }) 15 | ;(res as any)[DOM_ERROR_HANDLER] = (err: any) => { 16 | if (err instanceof Promise) { 17 | throw err 18 | } 19 | onError?.(err) 20 | return fallbackRender?.(err) || fallback 21 | } 22 | return res 23 | }) as any 24 | 25 | export const Suspense: FC> = (({ 26 | children, 27 | fallback, 28 | }: any) => { 29 | const res = Fragment({ children }) 30 | ;(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => { 31 | if (!(err instanceof Promise)) { 32 | throw err 33 | } 34 | err.finally(retry) 35 | return fallback 36 | } 37 | return res 38 | }) as any 39 | /* eslint-enable @typescript-eslint/no-explicit-any */ 40 | -------------------------------------------------------------------------------- /src/jsx/dom/context.ts: -------------------------------------------------------------------------------- 1 | import type { Child } from '../base' 2 | import { DOM_ERROR_HANDLER } from '../constants' 3 | import type { Context } from '../context' 4 | import { globalContexts } from '../context' 5 | import { setInternalTagFlag } from './utils' 6 | 7 | export const createContextProviderFunction = 8 | (values: T[]): Function => 9 | ({ value, children }: { value: T; children: Child[] }) => { 10 | if (!children) { 11 | return undefined 12 | } 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | const props: { children: any } = { 16 | children: [ 17 | { 18 | tag: setInternalTagFlag(() => { 19 | values.push(value) 20 | }), 21 | props: {}, 22 | }, 23 | ], 24 | } 25 | if (Array.isArray(children)) { 26 | props.children.push(...children.flat()) 27 | } else { 28 | props.children.push(children) 29 | } 30 | props.children.push({ 31 | tag: setInternalTagFlag(() => { 32 | values.pop() 33 | }), 34 | props: {}, 35 | }) 36 | const res = { tag: '', props, type: '' } 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | ;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => { 39 | values.pop() 40 | throw err 41 | } 42 | return res 43 | } 44 | 45 | export const createContext = (defaultValue: T): Context => { 46 | const values = [defaultValue] 47 | const context: Context = createContextProviderFunction(values) as Context 48 | context.values = values 49 | context.Provider = context 50 | globalContexts.push(context as Context) 51 | return context 52 | } 53 | -------------------------------------------------------------------------------- /src/jsx/dom/jsx-dev-runtime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * This module provides the `hono/jsx/dom` dev runtime. 4 | */ 5 | 6 | import type { JSXNode, Props } from '../base' 7 | import * as intrinsicElementTags from './intrinsic-element/components' 8 | 9 | export const jsxDEV = (tag: string | Function, props: Props, key?: string): JSXNode => { 10 | if (typeof tag === 'string' && intrinsicElementTags[tag as keyof typeof intrinsicElementTags]) { 11 | tag = intrinsicElementTags[tag as keyof typeof intrinsicElementTags] 12 | } 13 | return { 14 | tag, 15 | type: tag, 16 | props, 17 | key, 18 | ref: props.ref, 19 | } as JSXNode 20 | } 21 | 22 | export const Fragment = (props: Record): JSXNode => jsxDEV('', props, undefined) 23 | -------------------------------------------------------------------------------- /src/jsx/dom/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * This module provides the `hono/jsx/dom` runtime. 4 | */ 5 | 6 | export { jsxDEV as jsx, Fragment } from './jsx-dev-runtime' 7 | export { jsxDEV as jsxs } from './jsx-dev-runtime' 8 | -------------------------------------------------------------------------------- /src/jsx/dom/utils.ts: -------------------------------------------------------------------------------- 1 | import { DOM_INTERNAL_TAG } from '../constants' 2 | 3 | export const setInternalTagFlag = (fn: Function): Function => { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | ;(fn as any)[DOM_INTERNAL_TAG] = true 6 | return fn 7 | } 8 | -------------------------------------------------------------------------------- /src/jsx/hooks/string.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource ../ */ 2 | import { useState, useSyncExternalStore } from '..' 3 | 4 | describe('useState', () => { 5 | it('should be rendered with initial state', () => { 6 | const Component = () => { 7 | const [state] = useState('hello') 8 | return {state} 9 | } 10 | const template = 11 | expect(template.toString()).toBe('hello') 12 | }) 13 | }) 14 | 15 | describe('useSyncExternalStore', () => { 16 | it('should be rendered with result of getServerSnapshot()', () => { 17 | const unsubscribe = vi.fn() 18 | const subscribe = vi.fn(() => unsubscribe) 19 | const getSnapshot = vi.fn() 20 | const getServerSnapshot = vi.fn(() => 100) 21 | const App = () => { 22 | const count = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) 23 | return
{count}
24 | } 25 | const template = 26 | expect(template.toString()).toBe('
100
') 27 | expect(unsubscribe).not.toBeCalled() 28 | expect(subscribe).not.toBeCalled() 29 | expect(getSnapshot).not.toBeCalled() 30 | }) 31 | 32 | it('should raise an error if getServerShot() is not provided', () => { 33 | const App = () => { 34 | const count = useSyncExternalStore(vi.fn(), vi.fn()) 35 | return
{count}
36 | } 37 | const template = 38 | expect(() => template.toString()).toThrowError() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/jsx/intrinsic-element/common.ts: -------------------------------------------------------------------------------- 1 | export const deDupeKeyMap: Record = { 2 | title: [], 3 | script: ['src'], 4 | style: ['data-href'], 5 | link: ['href'], 6 | meta: ['name', 'httpEquiv', 'charset', 'itemProp'], 7 | } 8 | 9 | export const domRenderers: Record = {} 10 | 11 | export const dataPrecedenceAttr = 'data-precedence' 12 | -------------------------------------------------------------------------------- /src/jsx/jsx-dev-runtime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * This module provides Hono's JSX dev runtime. 4 | */ 5 | 6 | import type { HtmlEscapedString } from '../utils/html' 7 | import { jsxFn } from './base' 8 | import type { JSXNode } from './base' 9 | export { Fragment } from './base' 10 | export type { JSX } from './base' 11 | 12 | export function jsxDEV( 13 | tag: string | Function, 14 | props: Record, 15 | key?: string 16 | ): JSXNode { 17 | let node: JSXNode 18 | if (!props || !('children' in props)) { 19 | node = jsxFn(tag, props, []) 20 | } else { 21 | const children = props.children as string | HtmlEscapedString 22 | node = Array.isArray(children) ? jsxFn(tag, props, children) : jsxFn(tag, props, [children]) 23 | } 24 | node.key = key 25 | return node 26 | } 27 | -------------------------------------------------------------------------------- /src/jsx/jsx-runtime.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime automatic **/ 2 | /** @jsxImportSource . **/ 3 | import { Hono } from '../hono' 4 | 5 | describe('jsx-runtime', () => { 6 | let app: Hono 7 | 8 | beforeEach(() => { 9 | app = new Hono() 10 | }) 11 | 12 | it('Should render HTML strings', async () => { 13 | app.get('/', (c) => { 14 | return c.html(

Hello

) 15 | }) 16 | const res = await app.request('http://localhost/') 17 | expect(res.status).toBe(200) 18 | expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8') 19 | expect(await res.text()).toBe('

Hello

') 20 | }) 21 | 22 | // https://en.reactjs.org/docs/jsx-in-depth.html#booleans-null-and-undefined-are-ignored 23 | describe('Booleans, Null, and Undefined Are Ignored', () => { 24 | it.each([true, false, undefined, null])('%s', (item) => { 25 | expect(({item}).toString()).toBe('') 26 | }) 27 | 28 | it('falsy value', () => { 29 | const template = {0} 30 | expect(template.toString()).toBe('0') 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/jsx/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * This module provides Hono's JSX runtime. 4 | */ 5 | 6 | export { jsxDEV as jsx, Fragment } from './jsx-dev-runtime' 7 | export { jsxDEV as jsxs } from './jsx-dev-runtime' 8 | export type { JSX } from './jsx-dev-runtime' 9 | import { html, raw } from '../helper/html' 10 | import type { HtmlEscapedString, StringBuffer, HtmlEscaped } from '../utils/html' 11 | import { escapeToBuffer, stringBufferToString } from '../utils/html' 12 | import { styleObjectForEach } from './utils' 13 | 14 | export { html as jsxTemplate } 15 | 16 | export const jsxAttr = ( 17 | key: string, 18 | v: string | Promise | Record 19 | ): HtmlEscapedString | Promise => { 20 | const buffer: StringBuffer = [`${key}="`] as StringBuffer 21 | if (key === 'style' && typeof v === 'object') { 22 | // object to style strings 23 | let styleStr = '' 24 | styleObjectForEach(v as Record, (property, value) => { 25 | if (value != null) { 26 | styleStr += `${styleStr ? ';' : ''}${property}:${value}` 27 | } 28 | }) 29 | escapeToBuffer(styleStr, buffer) 30 | buffer[0] += '"' 31 | } else if (typeof v === 'string') { 32 | escapeToBuffer(v, buffer) 33 | buffer[0] += '"' 34 | } else if (v === null || v === undefined) { 35 | return raw('') 36 | } else if (typeof v === 'number' || (v as unknown as HtmlEscaped).isEscaped) { 37 | buffer[0] += `${v}"` 38 | } else if (v instanceof Promise) { 39 | buffer.unshift('"', v) 40 | } else { 41 | escapeToBuffer(v.toString(), buffer) 42 | buffer[0] += '"' 43 | } 44 | 45 | return buffer.length === 1 ? raw(buffer[0]) : stringBufferToString(buffer, undefined) 46 | } 47 | 48 | export const jsxEscape = (value: string) => value 49 | -------------------------------------------------------------------------------- /src/jsx/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * All types exported from "hono/jsx" are in this file. 3 | */ 4 | import type { Child, JSXNode } from './base' 5 | import type { JSX } from './intrinsic-elements' 6 | 7 | export type { Child, JSXNode, FC } from './base' 8 | export type { RefObject } from './hooks' 9 | export type { Context } from './context' 10 | 11 | export type PropsWithChildren

= P & { children?: Child | undefined } 12 | export type CSSProperties = JSX.CSSProperties 13 | 14 | /** 15 | * React types 16 | */ 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | type ReactElement

= JSXNode & { 20 | type: T 21 | props: P 22 | key: string | null 23 | } 24 | type ReactNode = ReactElement | string | number | boolean | null | undefined 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | type ComponentClass

= unknown 27 | 28 | export type { ReactElement, ReactNode, ComponentClass } 29 | 30 | export type Event = globalThis.Event 31 | export type MouseEvent = globalThis.MouseEvent 32 | export type KeyboardEvent = globalThis.KeyboardEvent 33 | export type FocusEvent = globalThis.FocusEvent 34 | export type ClipboardEvent = globalThis.ClipboardEvent 35 | export type InputEvent = globalThis.InputEvent 36 | export type PointerEvent = globalThis.PointerEvent 37 | export type TouchEvent = globalThis.TouchEvent 38 | export type WheelEvent = globalThis.WheelEvent 39 | export type AnimationEvent = globalThis.AnimationEvent 40 | export type TransitionEvent = globalThis.TransitionEvent 41 | export type DragEvent = globalThis.DragEvent 42 | -------------------------------------------------------------------------------- /src/jsx/utils.ts: -------------------------------------------------------------------------------- 1 | const normalizeElementKeyMap: Map = new Map([ 2 | ['className', 'class'], 3 | ['htmlFor', 'for'], 4 | ['crossOrigin', 'crossorigin'], 5 | ['httpEquiv', 'http-equiv'], 6 | ['itemProp', 'itemprop'], 7 | ['fetchPriority', 'fetchpriority'], 8 | ['noModule', 'nomodule'], 9 | ['formAction', 'formaction'], 10 | ]) 11 | export const normalizeIntrinsicElementKey = (key: string): string => 12 | normalizeElementKeyMap.get(key) || key 13 | 14 | export const styleObjectForEach = ( 15 | style: Record, 16 | fn: (key: string, value: string | null) => void 17 | ): void => { 18 | for (const [k, v] of Object.entries(style)) { 19 | const key = 20 | k[0] === '-' || !/[A-Z]/.test(k) 21 | ? k // a CSS variable or a lowercase only property 22 | : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`) // a camelCase property. convert to kebab-case 23 | fn( 24 | key, 25 | v == null 26 | ? null 27 | : typeof v === 'number' 28 | ? !key.match( 29 | /^(?:a|border-im|column(?:-c|s)|flex(?:$|-[^b])|grid-(?:ar|[^a])|font-w|li|or|sca|st|ta|wido|z)|ty$/ 30 | ) 31 | ? `${v}px` 32 | : `${v}` 33 | : v 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/middleware/context-storage/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../hono' 2 | import { contextStorage, getContext } from '.' 3 | 4 | describe('Context Storage Middleware', () => { 5 | type Env = { 6 | Variables: { 7 | message: string 8 | } 9 | } 10 | 11 | const app = new Hono() 12 | 13 | app.use(contextStorage()) 14 | app.use(async (c, next) => { 15 | c.set('message', 'Hono is hot!!') 16 | await next() 17 | }) 18 | app.get('/', (c) => { 19 | return c.text(getMessage()) 20 | }) 21 | 22 | const getMessage = () => { 23 | return getContext().var.message 24 | } 25 | 26 | it('Should get context', async () => { 27 | const res = await app.request('/') 28 | expect(await res.text()).toBe('Hono is hot!!') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/middleware/context-storage/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Context Storage Middleware for Hono. 4 | */ 5 | 6 | import { AsyncLocalStorage } from 'node:async_hooks' 7 | import type { Context } from '../../context' 8 | import type { Env, MiddlewareHandler } from '../../types' 9 | 10 | const asyncLocalStorage = new AsyncLocalStorage() 11 | 12 | /** 13 | * Context Storage Middleware for Hono. 14 | * 15 | * @see {@link https://hono.dev/docs/middleware/builtin/context-storage} 16 | * 17 | * @returns {MiddlewareHandler} The middleware handler function. 18 | * 19 | * @example 20 | * ```ts 21 | * type Env = { 22 | * Variables: { 23 | * message: string 24 | * } 25 | * } 26 | * 27 | * const app = new Hono() 28 | * 29 | * app.use(contextStorage()) 30 | * 31 | * app.use(async (c, next) => { 32 | * c.set('message', 'Hono is hot!!) 33 | * await next() 34 | * }) 35 | * 36 | * app.get('/', async (c) => { c.text(getMessage()) }) 37 | * 38 | * const getMessage = () => { 39 | * return getContext().var.message 40 | * } 41 | * ``` 42 | */ 43 | export const contextStorage = (): MiddlewareHandler => { 44 | return async function contextStorage(c, next) { 45 | await asyncLocalStorage.run(c, next) 46 | } 47 | } 48 | 49 | export const getContext = (): Context => { 50 | const context = asyncLocalStorage.getStore() 51 | if (!context) { 52 | throw new Error('Context is not available') 53 | } 54 | return context 55 | } 56 | -------------------------------------------------------------------------------- /src/middleware/etag/digest.ts: -------------------------------------------------------------------------------- 1 | const mergeBuffers = (buffer1: ArrayBuffer | undefined, buffer2: Uint8Array): Uint8Array => { 2 | if (!buffer1) { 3 | return buffer2 4 | } 5 | const merged = new Uint8Array(buffer1.byteLength + buffer2.byteLength) 6 | merged.set(new Uint8Array(buffer1), 0) 7 | merged.set(buffer2, buffer1.byteLength) 8 | return merged 9 | } 10 | 11 | export const generateDigest = async ( 12 | stream: ReadableStream | null, 13 | generator: (body: Uint8Array) => ArrayBuffer | Promise 14 | ): Promise => { 15 | if (!stream) { 16 | return null 17 | } 18 | 19 | let result: ArrayBuffer | undefined = undefined 20 | 21 | const reader = stream.getReader() 22 | for (;;) { 23 | const { value, done } = await reader.read() 24 | if (done) { 25 | break 26 | } 27 | 28 | result = await generator(mergeBuffers(result, value)) 29 | } 30 | 31 | if (!result) { 32 | return null 33 | } 34 | 35 | return Array.prototype.map 36 | .call(new Uint8Array(result), (x) => x.toString(16).padStart(2, '0')) 37 | .join('') 38 | } 39 | -------------------------------------------------------------------------------- /src/middleware/jwk/index.ts: -------------------------------------------------------------------------------- 1 | export { jwk } from './jwk' 2 | -------------------------------------------------------------------------------- /src/middleware/jwt/index.ts: -------------------------------------------------------------------------------- 1 | import type { JwtVariables } from './jwt' 2 | export type { JwtVariables } 3 | export { jwt, verify, decode, sign } from './jwt' 4 | import type {} from '../..' 5 | 6 | declare module '../..' { 7 | interface ContextVariableMap extends JwtVariables {} 8 | } 9 | -------------------------------------------------------------------------------- /src/middleware/language/index.ts: -------------------------------------------------------------------------------- 1 | import type { LanguageVariables, DetectorOptions, DetectorType, CacheType } from './language' 2 | export type { LanguageVariables, DetectorOptions, DetectorType, CacheType } 3 | export { 4 | languageDetector, 5 | detectFromCookie, 6 | detectFromHeader, 7 | detectFromPath, 8 | detectFromQuery, 9 | } from './language' 10 | declare module '../..' { 11 | interface ContextVariableMap extends LanguageVariables {} 12 | } 13 | -------------------------------------------------------------------------------- /src/middleware/powered-by/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../hono' 2 | import { poweredBy } from '.' 3 | 4 | describe('Powered by Middleware', () => { 5 | const app = new Hono() 6 | 7 | app.use('/poweredBy/*', poweredBy()) 8 | app.get('/poweredBy', (c) => c.text('root')) 9 | 10 | app.use('/poweredBy2/*', poweredBy()) 11 | app.use('/poweredBy2/*', poweredBy()) 12 | app.get('/poweredBy2', (c) => c.text('root')) 13 | 14 | app.use('/poweredBy3/*', poweredBy({ serverName: 'Foo' })) 15 | app.get('/poweredBy3', (c) => c.text('root')) 16 | 17 | it('Should return with X-Powered-By header', async () => { 18 | const res = await app.request('http://localhost/poweredBy') 19 | expect(res).not.toBeNull() 20 | expect(res.status).toBe(200) 21 | expect(res.headers.get('X-Powered-By')).toBe('Hono') 22 | }) 23 | 24 | it('Should not return duplicate values', async () => { 25 | const res = await app.request('http://localhost/poweredBy2') 26 | expect(res).not.toBeNull() 27 | expect(res.status).toBe(200) 28 | expect(res.headers.get('X-Powered-By')).toBe('Hono') 29 | }) 30 | 31 | it('Should return custom serverName', async () => { 32 | const res = await app.request('http://localhost/poweredBy3') 33 | expect(res).not.toBeNull() 34 | expect(res.status).toBe(200) 35 | expect(res.headers.get('X-Powered-By')).toBe('Foo') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/middleware/powered-by/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Powered By Middleware for Hono. 4 | */ 5 | import type { MiddlewareHandler } from '../../types' 6 | 7 | type PoweredByOptions = { 8 | /** 9 | * The value for X-Powered-By header. 10 | * @default Hono 11 | */ 12 | serverName?: string 13 | } 14 | 15 | /** 16 | * Powered By Middleware for Hono. 17 | * 18 | * @param options - The options for the Powered By Middleware. 19 | * @returns {MiddlewareHandler} The middleware handler function. 20 | * 21 | * @example 22 | * ```ts 23 | * import { poweredBy } from 'hono/powered-by' 24 | * 25 | * const app = new Hono() 26 | * 27 | * app.use(poweredBy()) // With options: poweredBy({ serverName: "My Server" }) 28 | * ``` 29 | */ 30 | export const poweredBy = (options?: PoweredByOptions): MiddlewareHandler => { 31 | return async function poweredBy(c, next) { 32 | await next() 33 | c.res.headers.set('X-Powered-By', options?.serverName ?? 'Hono') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/middleware/pretty-json/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from '../../hono' 2 | import { prettyJSON } from '.' 3 | 4 | describe('JSON pretty by Middleware', () => { 5 | it('Should return pretty JSON output', async () => { 6 | const app = new Hono() 7 | app.use('*', prettyJSON()) 8 | app.get('/', (c) => { 9 | return c.json({ message: 'Hono!' }) 10 | }) 11 | 12 | const res = await app.request('http://localhost/?pretty') 13 | expect(res).not.toBeNull() 14 | expect(res.status).toBe(200) 15 | expect(await res.text()).toBe(`{ 16 | "message": "Hono!" 17 | }`) 18 | }) 19 | 20 | it('Should return pretty JSON output with 4 spaces', async () => { 21 | const app = new Hono() 22 | app.use('*', prettyJSON({ space: 4 })) 23 | app.get('/', (c) => { 24 | return c.json({ message: 'Hono!' }) 25 | }) 26 | 27 | const res = await app.request('http://localhost/?pretty') 28 | expect(res).not.toBeNull() 29 | expect(res.status).toBe(200) 30 | expect(await res.text()).toBe(`{ 31 | "message": "Hono!" 32 | }`) 33 | }) 34 | 35 | it('Should return pretty JSON output when middleware received custom query', async () => { 36 | const targetQuery = 'format' 37 | 38 | const app = new Hono() 39 | app.use( 40 | '*', 41 | prettyJSON({ 42 | query: targetQuery, 43 | }) 44 | ) 45 | app.get('/', (c) => 46 | c.json({ 47 | message: 'Hono!', 48 | }) 49 | ) 50 | 51 | const prettyText = await (await app.request(`?${targetQuery}`)).text() 52 | expect(prettyText).toBe(`{ 53 | "message": "Hono!" 54 | }`) 55 | const nonPrettyText = await (await app.request('?pretty')).text() 56 | expect(nonPrettyText).toBe('{"message":"Hono!"}') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/middleware/pretty-json/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Pretty JSON Middleware for Hono. 4 | */ 5 | 6 | import type { MiddlewareHandler } from '../../types' 7 | 8 | interface PrettyOptions { 9 | /** 10 | * Number of spaces for indentation. 11 | * @default 2 12 | */ 13 | space?: number 14 | 15 | /** 16 | * Query conditions for when to Pretty. 17 | * @default 'pretty' 18 | */ 19 | query?: string 20 | } 21 | 22 | /** 23 | * Pretty JSON Middleware for Hono. 24 | * 25 | * @see {@link https://hono.dev/docs/middleware/builtin/pretty-json} 26 | * 27 | * @param options - The options for the pretty JSON middleware. 28 | * @returns {MiddlewareHandler} The middleware handler function. 29 | * 30 | * @example 31 | * ```ts 32 | * const app = new Hono() 33 | * 34 | * app.use(prettyJSON()) // With options: prettyJSON({ space: 4 }) 35 | * app.get('/', (c) => { 36 | * return c.json({ message: 'Hono!' }) 37 | * }) 38 | * ``` 39 | */ 40 | export const prettyJSON = (options?: PrettyOptions): MiddlewareHandler => { 41 | const targetQuery = options?.query ?? 'pretty' 42 | return async function prettyJSON(c, next) { 43 | const pretty = c.req.query(targetQuery) || c.req.query(targetQuery) === '' 44 | await next() 45 | if (pretty && c.res.headers.get('Content-Type')?.startsWith('application/json')) { 46 | const obj = await c.res.json() 47 | c.res = new Response(JSON.stringify(obj, null, options?.space ?? 2), c.res) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/middleware/request-id/index.ts: -------------------------------------------------------------------------------- 1 | import type { RequestIdVariables } from './request-id' 2 | export type { RequestIdVariables } 3 | export { requestId } from './request-id' 4 | 5 | declare module '../..' { 6 | interface ContextVariableMap extends RequestIdVariables {} 7 | } 8 | -------------------------------------------------------------------------------- /src/middleware/request-id/request-id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Request ID Middleware for Hono. 4 | */ 5 | 6 | import type { Context } from '../../context' 7 | import type { MiddlewareHandler } from '../../types' 8 | 9 | export type RequestIdVariables = { 10 | requestId: string 11 | } 12 | 13 | export type RequestIdOptions = { 14 | limitLength?: number 15 | headerName?: string 16 | generator?: (c: Context) => string 17 | } 18 | 19 | /** 20 | * Request ID Middleware for Hono. 21 | * 22 | * @param {object} options - Options for Request ID middleware. 23 | * @param {number} [options.limitLength=255] - The maximum length of request id. 24 | * @param {string} [options.headerName=X-Request-Id] - The header name used in request id. 25 | * @param {generator} [options.generator=() => crypto.randomUUID()] - The request id generation function. 26 | * 27 | * @returns {MiddlewareHandler} The middleware handler function. 28 | * 29 | * @example 30 | * ```ts 31 | * type Variables = RequestIdVariables 32 | * const app = new Hono<{Variables: Variables}>() 33 | * 34 | * app.use(requestId()) 35 | * app.get('/', (c) => { 36 | * console.log(c.get('requestId')) // Debug 37 | * return c.text('Hello World!') 38 | * }) 39 | * ``` 40 | */ 41 | export const requestId = ({ 42 | limitLength = 255, 43 | headerName = 'X-Request-Id', 44 | generator = () => crypto.randomUUID(), 45 | }: RequestIdOptions = {}): MiddlewareHandler => { 46 | return async function requestId(c, next) { 47 | // If `headerName` is empty string, req.header will return the object 48 | let reqId = headerName ? c.req.header(headerName) : undefined 49 | if (!reqId || reqId.length > limitLength || /[^\w\-]/.test(reqId)) { 50 | reqId = generator(c) 51 | } 52 | 53 | c.set('requestId', reqId) 54 | if (headerName) { 55 | c.header(headerName, reqId) 56 | } 57 | await next() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/middleware/secure-headers/index.ts: -------------------------------------------------------------------------------- 1 | export type { ContentSecurityPolicyOptionHandler } from './secure-headers' 2 | export { NONCE, secureHeaders } from './secure-headers' 3 | import type { SecureHeadersVariables } from './secure-headers' 4 | export type { SecureHeadersVariables } 5 | 6 | declare module '../..' { 7 | interface ContextVariableMap extends SecureHeadersVariables {} 8 | } 9 | -------------------------------------------------------------------------------- /src/middleware/timeout/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Timeout Middleware for Hono. 4 | */ 5 | 6 | import type { Context } from '../../context' 7 | import { HTTPException } from '../../http-exception' 8 | import type { MiddlewareHandler } from '../../types' 9 | 10 | export type HTTPExceptionFunction = (context: Context) => HTTPException 11 | 12 | const defaultTimeoutException = new HTTPException(504, { 13 | message: 'Gateway Timeout', 14 | }) 15 | 16 | /** 17 | * Timeout Middleware for Hono. 18 | * 19 | * @param {number} duration - The timeout duration in milliseconds. 20 | * @param {HTTPExceptionFunction | HTTPException} [exception=defaultTimeoutException] - The exception to throw when the timeout occurs. Can be a function that returns an HTTPException or an HTTPException object. 21 | * @returns {MiddlewareHandler} The middleware handler function. 22 | * 23 | * @example 24 | * ```ts 25 | * const app = new Hono() 26 | * 27 | * app.use( 28 | * '/long-request', 29 | * timeout(5000) // Set timeout to 5 seconds 30 | * ) 31 | * 32 | * app.get('/long-request', async (c) => { 33 | * await someLongRunningFunction() 34 | * return c.text('Completed within time limit') 35 | * }) 36 | * ``` 37 | */ 38 | export const timeout = ( 39 | duration: number, 40 | exception: HTTPExceptionFunction | HTTPException = defaultTimeoutException 41 | ): MiddlewareHandler => { 42 | return async function timeout(context, next) { 43 | let timer: number | undefined 44 | const timeoutPromise = new Promise((_, reject) => { 45 | timer = setTimeout(() => { 46 | reject(typeof exception === 'function' ? exception(context) : exception) 47 | }, duration) as unknown as number 48 | }) 49 | 50 | try { 51 | await Promise.race([next(), timeoutPromise]) 52 | } finally { 53 | if (timer !== undefined) { 54 | clearTimeout(timer) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/middleware/timing/index.ts: -------------------------------------------------------------------------------- 1 | import type { TimingVariables } from './timing' 2 | export { TimingVariables } 3 | export { timing, setMetric, startTime, endTime } from './timing' 4 | 5 | declare module '../..' { 6 | interface ContextVariableMap extends TimingVariables {} 7 | } 8 | -------------------------------------------------------------------------------- /src/middleware/trailing-slash/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Trailing Slash Middleware for Hono. 4 | */ 5 | 6 | import type { MiddlewareHandler } from '../../types' 7 | 8 | /** 9 | * Trailing Slash Middleware for Hono. 10 | * 11 | * @see {@link https://hono.dev/docs/middleware/builtin/trailing-slash} 12 | * 13 | * @returns {MiddlewareHandler} The middleware handler function. 14 | * 15 | * @example 16 | * ```ts 17 | * const app = new Hono() 18 | * 19 | * app.use(trimTrailingSlash()) 20 | * app.get('/about/me/', (c) => c.text('With Trailing Slash')) 21 | * ``` 22 | */ 23 | export const trimTrailingSlash = (): MiddlewareHandler => { 24 | return async function trimTrailingSlash(c, next) { 25 | await next() 26 | 27 | if ( 28 | c.res.status === 404 && 29 | (c.req.method === 'GET' || c.req.method === 'HEAD') && 30 | c.req.path !== '/' && 31 | c.req.path.at(-1) === '/' 32 | ) { 33 | const url = new URL(c.req.url) 34 | url.pathname = url.pathname.substring(0, url.pathname.length - 1) 35 | 36 | c.res = c.redirect(url.toString(), 301) 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Append trailing slash middleware for Hono. 43 | * Append a trailing slash to the URL if it doesn't have one. For example, `/path/to/page` will be redirected to `/path/to/page/`. 44 | * 45 | * @see {@link https://hono.dev/docs/middleware/builtin/trailing-slash} 46 | * 47 | * @returns {MiddlewareHandler} The middleware handler function. 48 | * 49 | * @example 50 | * ```ts 51 | * const app = new Hono() 52 | * 53 | * app.use(appendTrailingSlash()) 54 | * ``` 55 | */ 56 | export const appendTrailingSlash = (): MiddlewareHandler => { 57 | return async function appendTrailingSlash(c, next) { 58 | await next() 59 | 60 | if ( 61 | c.res.status === 404 && 62 | (c.req.method === 'GET' || c.req.method === 'HEAD') && 63 | c.req.path.at(-1) !== '/' 64 | ) { 65 | const url = new URL(c.req.url) 66 | url.pathname += '/' 67 | 68 | c.res = c.redirect(url.toString(), 301) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/preset/quick.test.ts: -------------------------------------------------------------------------------- 1 | import { getRouterName } from '../helper/dev' 2 | import { Hono } from './quick' 3 | 4 | describe('hono/quick preset', () => { 5 | it('Should have SmartRouter + LinearRouter', async () => { 6 | const app = new Hono() 7 | expect(getRouterName(app)).toBe('SmartRouter + LinearRouter') 8 | }) 9 | }) 10 | 11 | describe('Generics for Bindings and Variables', () => { 12 | interface CloudflareBindings { 13 | MY_VARIABLE: string 14 | } 15 | 16 | it('Should not throw type errors', () => { 17 | // @ts-expect-error Bindings should extend object 18 | new Hono<{ 19 | Bindings: number 20 | }>() 21 | 22 | const appWithInterface = new Hono<{ 23 | Bindings: CloudflareBindings 24 | }>() 25 | 26 | appWithInterface.get('/', (c) => { 27 | expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf() 28 | return c.text('/') 29 | }) 30 | 31 | const appWithType = new Hono<{ 32 | Bindings: { 33 | foo: string 34 | } 35 | }>() 36 | 37 | appWithType.get('/', (c) => { 38 | expectTypeOf(c.env.foo).toMatchTypeOf() 39 | return c.text('Hello Hono!') 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/preset/quick.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * The preset that uses `LinearRouter`. 4 | */ 5 | 6 | import { HonoBase } from '../hono-base' 7 | import type { HonoOptions } from '../hono-base' 8 | import { LinearRouter } from '../router/linear-router' 9 | import { SmartRouter } from '../router/smart-router' 10 | import { TrieRouter } from '../router/trie-router' 11 | import type { BlankEnv, BlankSchema, Env, Schema } from '../types' 12 | 13 | export class Hono< 14 | E extends Env = BlankEnv, 15 | S extends Schema = BlankSchema, 16 | BasePath extends string = '/' 17 | > extends HonoBase { 18 | constructor(options: HonoOptions = {}) { 19 | super(options) 20 | this.router = new SmartRouter({ 21 | routers: [new LinearRouter(), new TrieRouter()], 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/preset/tiny.test.ts: -------------------------------------------------------------------------------- 1 | import { getRouterName } from '../helper/dev' 2 | import { Hono } from './tiny' 3 | 4 | describe('hono/tiny preset', () => { 5 | it('Should have PatternRouter', async () => { 6 | const app = new Hono() 7 | expect(getRouterName(app)).toBe('PatternRouter') 8 | }) 9 | }) 10 | 11 | describe('Generics for Bindings and Variables', () => { 12 | interface CloudflareBindings { 13 | MY_VARIABLE: string 14 | } 15 | 16 | it('Should not throw type errors', () => { 17 | // @ts-expect-error Bindings should extend object 18 | new Hono<{ 19 | Bindings: number 20 | }>() 21 | 22 | const appWithInterface = new Hono<{ 23 | Bindings: CloudflareBindings 24 | }>() 25 | 26 | appWithInterface.get('/', (c) => { 27 | expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf() 28 | return c.text('/') 29 | }) 30 | 31 | const appWithType = new Hono<{ 32 | Bindings: { 33 | foo: string 34 | } 35 | }>() 36 | 37 | appWithType.get('/', (c) => { 38 | expectTypeOf(c.env.foo).toMatchTypeOf() 39 | return c.text('Hello Hono!') 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/preset/tiny.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * The preset that uses `PatternRouter`. 4 | */ 5 | 6 | import { HonoBase } from '../hono-base' 7 | import type { HonoOptions } from '../hono-base' 8 | import { PatternRouter } from '../router/pattern-router' 9 | import type { BlankEnv, BlankSchema, Env, Schema } from '../types' 10 | 11 | export class Hono< 12 | E extends Env = BlankEnv, 13 | S extends Schema = BlankSchema, 14 | BasePath extends string = '/' 15 | > extends HonoBase { 16 | constructor(options: HonoOptions = {}) { 17 | super(options) 18 | this.router = new PatternRouter() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/router/linear-router/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * LinearRouter for Hono. 4 | */ 5 | 6 | export { LinearRouter } from './router' 7 | -------------------------------------------------------------------------------- /src/router/linear-router/router.test.ts: -------------------------------------------------------------------------------- 1 | import { UnsupportedPathError } from '../../router' 2 | import { runTest } from '../common.case.test' 3 | import { LinearRouter } from './router' 4 | 5 | describe('LinearRouter', () => { 6 | runTest({ 7 | skip: [ 8 | { 9 | reason: 'UnsupportedPath', 10 | tests: [ 11 | 'Multi match > `params` per a handler > GET /entry/123/show', 12 | 'Capture regex pattern has trailing wildcard > GET /foo/bar/file.html', 13 | ], 14 | }, 15 | { 16 | reason: 'LinearRouter allows trailing slashes', 17 | tests: ['Trailing slash > GET /book/'], 18 | }, 19 | ], 20 | newRouter: () => new LinearRouter(), 21 | }) 22 | 23 | describe('Multi match', () => { 24 | describe('`params` per a handler', () => { 25 | const router = new LinearRouter() 26 | 27 | beforeEach(() => { 28 | router.add('ALL', '*', 'middleware a') 29 | router.add('GET', '/entry/:id/*', 'middleware b') 30 | router.add('GET', '/entry/:id/:action', 'action') 31 | }) 32 | 33 | it('GET /entry/123/show', () => { 34 | expect(() => { 35 | router.match('GET', '/entry/123/show') 36 | }).toThrowError(UnsupportedPathError) 37 | }) 38 | }) 39 | }) 40 | 41 | describe('Trailing slash', () => { 42 | const router = new LinearRouter() 43 | 44 | beforeEach(() => { 45 | router.add('GET', '/book', 'GET /book') 46 | router.add('GET', '/book/:id', 'GET /book/:id') 47 | }) 48 | 49 | it('GET /book/', () => { 50 | const [res] = router.match('GET', '/book/') 51 | expect(res.length).toBe(1) 52 | expect(res[0][0]).toBe('GET /book') 53 | }) 54 | }) 55 | 56 | describe('Skip part', () => { 57 | const router = new LinearRouter() 58 | 59 | beforeEach(() => { 60 | router.add('GET', '/products/:id{d+}', 'GET /products/:id{d+}') 61 | }) 62 | 63 | it('GET /products/list', () => { 64 | const [res] = router.match('GET', '/products/list') 65 | expect(res.length).toBe(0) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/router/pattern-router/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * PatternRouter for Hono. 4 | */ 5 | 6 | export { PatternRouter } from './router' 7 | -------------------------------------------------------------------------------- /src/router/pattern-router/router.test.ts: -------------------------------------------------------------------------------- 1 | import { UnsupportedPathError } from '../../router' 2 | import { runTest } from '../common.case.test' 3 | import { PatternRouter } from './router' 4 | 5 | describe('Pattern', () => { 6 | runTest({ 7 | skip: [ 8 | { 9 | reason: 'UnsupportedPath', 10 | tests: ['Duplicate param name > self'], 11 | }, 12 | { 13 | reason: 'PatternRouter allows trailing slashes', 14 | tests: ['Trailing slash > GET /book/'], 15 | }, 16 | ], 17 | newRouter: () => new PatternRouter(), 18 | }) 19 | 20 | describe('Duplicate param name', () => { 21 | it('self', () => { 22 | const router = new PatternRouter() 23 | expect(() => { 24 | router.add('GET', '/:id/:id', 'foo') 25 | }).toThrowError(UnsupportedPathError) 26 | }) 27 | }) 28 | describe('Trailing slash', () => { 29 | const router = new PatternRouter() 30 | 31 | beforeEach(() => { 32 | router.add('GET', '/book', 'GET /book') 33 | router.add('GET', '/book/:id', 'GET /book/:id') 34 | }) 35 | 36 | it('GET /book/', () => { 37 | const [res] = router.match('GET', '/book/') 38 | expect(res.length).toBe(1) 39 | expect(res[0][0]).toBe('GET /book') 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/router/pattern-router/router.ts: -------------------------------------------------------------------------------- 1 | import type { Params, Result, Router } from '../../router' 2 | import { METHOD_NAME_ALL, UnsupportedPathError } from '../../router' 3 | 4 | type Route = [RegExp, string, T] // [pattern, method, handler] 5 | 6 | const emptyParams = Object.create(null) 7 | 8 | export class PatternRouter implements Router { 9 | name: string = 'PatternRouter' 10 | #routes: Route[] = [] 11 | 12 | add(method: string, path: string, handler: T) { 13 | const endsWithWildcard = path.at(-1) === '*' 14 | if (endsWithWildcard) { 15 | path = path.slice(0, -2) 16 | } 17 | if (path.at(-1) === '?') { 18 | path = path.slice(0, -1) 19 | this.add(method, path.replace(/\/[^/]+$/, ''), handler) 20 | } 21 | 22 | const parts = (path.match(/\/?(:\w+(?:{(?:(?:{[\d,]+})|[^}])+})?)|\/?[^\/\?]+/g) || []).map( 23 | (part) => { 24 | const match = part.match(/^\/:([^{]+)(?:{(.*)})?/) 25 | return match 26 | ? `/(?<${match[1]}>${match[2] || '[^/]+'})` 27 | : part === '/*' 28 | ? '/[^/]+' 29 | : part.replace(/[.\\+*[^\]$()]/g, '\\$&') 30 | } 31 | ) 32 | 33 | try { 34 | this.#routes.push([ 35 | new RegExp(`^${parts.join('')}${endsWithWildcard ? '' : '/?$'}`), 36 | method, 37 | handler, 38 | ]) 39 | } catch { 40 | throw new UnsupportedPathError() 41 | } 42 | } 43 | 44 | match(method: string, path: string): Result { 45 | const handlers: [T, Params][] = [] 46 | 47 | for (let i = 0, len = this.#routes.length; i < len; i++) { 48 | const [pattern, routeMethod, handler] = this.#routes[i] 49 | 50 | if (routeMethod === method || routeMethod === METHOD_NAME_ALL) { 51 | const match = pattern.exec(path) 52 | if (match) { 53 | handlers.push([handler, match.groups || emptyParams]) 54 | } 55 | } 56 | } 57 | 58 | return [handlers] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/router/reg-exp-router/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * RegExpRouter for Hono. 4 | */ 5 | 6 | export { RegExpRouter } from './router' 7 | -------------------------------------------------------------------------------- /src/router/smart-router/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * SmartRouter for Hono. 4 | */ 5 | 6 | export { SmartRouter } from './router' 7 | -------------------------------------------------------------------------------- /src/router/smart-router/router.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '../common.case.test' 2 | import { RegExpRouter } from '../reg-exp-router' 3 | import { TrieRouter } from '../trie-router' 4 | import { SmartRouter } from './router' 5 | 6 | describe('SmartRouter', () => { 7 | runTest({ 8 | newRouter: () => 9 | new SmartRouter({ 10 | routers: [new RegExpRouter(), new TrieRouter()], 11 | }), 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/router/smart-router/router.ts: -------------------------------------------------------------------------------- 1 | import type { Result, Router } from '../../router' 2 | import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from '../../router' 3 | 4 | export class SmartRouter implements Router { 5 | name: string = 'SmartRouter' 6 | #routers: Router[] = [] 7 | #routes?: [string, string, T][] = [] 8 | 9 | constructor(init: { routers: Router[] }) { 10 | this.#routers = init.routers 11 | } 12 | 13 | add(method: string, path: string, handler: T) { 14 | if (!this.#routes) { 15 | throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT) 16 | } 17 | 18 | this.#routes.push([method, path, handler]) 19 | } 20 | 21 | match(method: string, path: string): Result { 22 | if (!this.#routes) { 23 | throw new Error('Fatal error') 24 | } 25 | 26 | const routers = this.#routers 27 | const routes = this.#routes 28 | 29 | const len = routers.length 30 | let i = 0 31 | let res 32 | for (; i < len; i++) { 33 | const router = routers[i] 34 | try { 35 | for (let i = 0, len = routes.length; i < len; i++) { 36 | router.add(...routes[i]) 37 | } 38 | res = router.match(method, path) 39 | } catch (e) { 40 | if (e instanceof UnsupportedPathError) { 41 | continue 42 | } 43 | throw e 44 | } 45 | 46 | this.match = router.match.bind(router) 47 | this.#routers = [router] 48 | this.#routes = undefined 49 | break 50 | } 51 | 52 | if (i === len) { 53 | // not found 54 | throw new Error('Fatal error') 55 | } 56 | 57 | // e.g. "SmartRouter + RegExpRouter" 58 | this.name = `SmartRouter + ${this.activeRouter.name}` 59 | 60 | return res as Result 61 | } 62 | 63 | get activeRouter(): Router { 64 | if (this.#routes || this.#routers.length !== 1) { 65 | throw new Error('No active router has been determined yet.') 66 | } 67 | 68 | return this.#routers[0] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/router/trie-router/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * TrieRouter for Hono. 4 | */ 5 | 6 | export { TrieRouter } from './router' 7 | -------------------------------------------------------------------------------- /src/router/trie-router/router.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '../common.case.test' 2 | import { TrieRouter } from './router' 3 | 4 | describe('TrieRouter', () => { 5 | runTest({ 6 | newRouter: () => new TrieRouter(), 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/router/trie-router/router.ts: -------------------------------------------------------------------------------- 1 | import type { Result, Router } from '../../router' 2 | import { checkOptionalParameter } from '../../utils/url' 3 | import { Node } from './node' 4 | 5 | export class TrieRouter implements Router { 6 | name: string = 'TrieRouter' 7 | #node: Node 8 | 9 | constructor() { 10 | this.#node = new Node() 11 | } 12 | 13 | add(method: string, path: string, handler: T) { 14 | const results = checkOptionalParameter(path) 15 | if (results) { 16 | for (let i = 0, len = results.length; i < len; i++) { 17 | this.#node.insert(method, results[i], handler) 18 | } 19 | return 20 | } 21 | 22 | this.#node.insert(method, path, handler) 23 | } 24 | 25 | match(method: string, path: string): Result { 26 | return this.#node.search(method, path) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/basic-auth.test.ts: -------------------------------------------------------------------------------- 1 | import { auth } from './basic-auth' 2 | 3 | describe('auth', () => { 4 | it('auth() - not include Authorization Header', () => { 5 | const res = auth(new Request('http://localhost/auth')) 6 | expect(res).toBeUndefined() 7 | }) 8 | 9 | it('auth() - invalid Authorization Header format', () => { 10 | const res = auth( 11 | new Request('http://localhost/auth', { 12 | headers: { Authorization: 'InvalidAuthHeader' }, 13 | }) 14 | ) 15 | expect(res).toBeUndefined() 16 | }) 17 | 18 | it('auth() - invalid Base64 string in Authorization Header', () => { 19 | const res = auth( 20 | new Request('http://localhost/auth', { 21 | headers: { Authorization: 'Basic InvalidBase64' }, 22 | }) 23 | ) 24 | expect(res).toBeUndefined() 25 | }) 26 | 27 | it('auth() - valid Authorization Header', () => { 28 | const validBase64 = btoa('username:password') 29 | const res = auth( 30 | new Request('http://localhost/auth', { 31 | headers: { Authorization: `Basic ${validBase64}` }, 32 | }) 33 | ) 34 | expect(res).toEqual({ username: 'username', password: 'password' }) 35 | }) 36 | 37 | it('auth() - empty username', () => { 38 | const validBase64 = btoa(':password') 39 | const res = auth( 40 | new Request('http://localhost/auth', { 41 | headers: { Authorization: `Basic ${validBase64}` }, 42 | }) 43 | ) 44 | expect(res).toEqual({ username: '', password: 'password' }) 45 | }) 46 | 47 | it('auth() - empty password', () => { 48 | const validBase64 = btoa('username:') 49 | const res = auth( 50 | new Request('http://localhost/auth', { 51 | headers: { Authorization: `Basic ${validBase64}` }, 52 | }) 53 | ) 54 | expect(res).toEqual({ username: 'username', password: '' }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/utils/basic-auth.ts: -------------------------------------------------------------------------------- 1 | import { decodeBase64 } from './encode' 2 | 3 | const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/ 4 | const USER_PASS_REGEXP = /^([^:]*):(.*)$/ 5 | const utf8Decoder = new TextDecoder() 6 | 7 | export type Auth = (req: Request) => { username: string; password: string } | undefined 8 | 9 | export const auth: Auth = (req: Request) => { 10 | const match = CREDENTIALS_REGEXP.exec(req.headers.get('Authorization') || '') 11 | if (!match) { 12 | return undefined 13 | } 14 | 15 | let userPass = undefined 16 | // If an invalid string is passed to atob(), it throws a `DOMException`. 17 | try { 18 | userPass = USER_PASS_REGEXP.exec(utf8Decoder.decode(decodeBase64(match[1]))) 19 | } catch {} // Do nothing 20 | 21 | if (!userPass) { 22 | return undefined 23 | } 24 | 25 | return { username: userPass[1], password: userPass[2] } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Buffer utility. 4 | */ 5 | 6 | import { sha256 } from './crypto' 7 | 8 | export const equal = (a: ArrayBuffer, b: ArrayBuffer): boolean => { 9 | if (a === b) { 10 | return true 11 | } 12 | if (a.byteLength !== b.byteLength) { 13 | return false 14 | } 15 | 16 | const va = new DataView(a) 17 | const vb = new DataView(b) 18 | 19 | let i = va.byteLength 20 | while (i--) { 21 | if (va.getUint8(i) !== vb.getUint8(i)) { 22 | return false 23 | } 24 | } 25 | 26 | return true 27 | } 28 | 29 | export const timingSafeEqual = async ( 30 | a: string | object | boolean, 31 | b: string | object | boolean, 32 | hashFunction?: Function 33 | ): Promise => { 34 | if (!hashFunction) { 35 | hashFunction = sha256 36 | } 37 | 38 | const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]) 39 | 40 | if (!sa || !sb) { 41 | return false 42 | } 43 | 44 | return sa === sb && a === b 45 | } 46 | 47 | export const bufferToString = (buffer: ArrayBuffer): string => { 48 | if (buffer instanceof ArrayBuffer) { 49 | const enc = new TextDecoder('utf-8') 50 | return enc.decode(buffer) 51 | } 52 | return buffer 53 | } 54 | 55 | export const bufferToFormData = ( 56 | arrayBuffer: ArrayBuffer, 57 | contentType: string 58 | ): Promise => { 59 | const response = new Response(arrayBuffer, { 60 | headers: { 61 | 'Content-Type': contentType, 62 | }, 63 | }) 64 | return response.formData() 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/color.test.ts: -------------------------------------------------------------------------------- 1 | import { getColorEnabled } from './color' 2 | 3 | describe('getColorEnabled() - With colors enabled', () => { 4 | it('should return true', async () => { 5 | expect(getColorEnabled()).toBe(true) 6 | }) 7 | }) 8 | 9 | describe('getColorEnabled() - With NO_COLOR environment variable set', () => { 10 | beforeAll(() => { 11 | vi.stubEnv('NO_COLOR', '1') 12 | }) 13 | 14 | afterAll(() => { 15 | vi.unstubAllEnvs() 16 | }) 17 | 18 | it('should return false', async () => { 19 | expect(getColorEnabled()).toBe(false) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Color utility. 4 | */ 5 | 6 | /** 7 | * Get whether color change on terminal is enabled or disabled. 8 | * If `NO_COLOR` environment variable is set, this function returns `false`. 9 | * @see {@link https://no-color.org/} 10 | * 11 | * @returns {boolean} 12 | */ 13 | export function getColorEnabled(): boolean { 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | const { process, Deno } = globalThis as any 16 | 17 | const isNoColor = 18 | typeof Deno?.noColor === 'boolean' 19 | ? (Deno.noColor as boolean) 20 | : process !== undefined 21 | ? // eslint-disable-next-line no-unsafe-optional-chaining 22 | 'NO_COLOR' in process?.env 23 | : false 24 | 25 | return !isNoColor 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/compress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Constants for compression. 4 | */ 5 | 6 | /** 7 | * Match for compressible content type. 8 | */ 9 | export const COMPRESSIBLE_CONTENT_TYPE_REGEX = 10 | /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i 11 | -------------------------------------------------------------------------------- /src/utils/concurrent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Concurrent utility. 4 | */ 5 | 6 | const DEFAULT_CONCURRENCY = 1024 7 | 8 | export interface Pool { 9 | run(fn: () => T): Promise 10 | } 11 | 12 | export const createPool = ({ 13 | concurrency, 14 | interval, 15 | }: { 16 | concurrency?: number 17 | interval?: number 18 | } = {}): Pool => { 19 | concurrency ||= DEFAULT_CONCURRENCY 20 | 21 | if (concurrency === Infinity) { 22 | // unlimited 23 | return { 24 | run: async (fn) => fn(), 25 | } 26 | } 27 | 28 | const pool: Set<{}> = new Set() 29 | const run = async ( 30 | fn: () => T, 31 | promise?: Promise, 32 | resolve?: (result: T) => void 33 | ): Promise => { 34 | if (pool.size >= (concurrency as number)) { 35 | promise ||= new Promise((r) => (resolve = r)) 36 | setTimeout(() => run(fn, promise, resolve)) 37 | return promise 38 | } 39 | const marker = {} 40 | pool.add(marker) 41 | const result = await fn() 42 | if (interval) { 43 | setTimeout(() => pool.delete(marker), interval) 44 | } else { 45 | pool.delete(marker) 46 | } 47 | if (resolve) { 48 | resolve(result) 49 | return promise as Promise 50 | } else { 51 | return result 52 | } 53 | } 54 | return { run } 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constant used to mark a composed handler. 3 | */ 4 | export const COMPOSED_HANDLER = '__COMPOSED_HANDLER' 5 | -------------------------------------------------------------------------------- /src/utils/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto' 2 | import { md5, sha1, sha256 } from './crypto' 3 | 4 | describe('crypto', () => { 5 | it('sha256', async () => { 6 | expect(await sha256('hono')).toBe( 7 | '8b3dc17add91b7e8f0b5109a389927d66001139cd9b03fa7b95f83126e1b2b23' 8 | ) 9 | expect(await sha256('炎')).toBe( 10 | '1fddc5a562ee1fbeb4fc6def7d4be4911fcdae4273b02ae3a507b170ba0ea169' 11 | ) 12 | expect(await sha256('abcdedf')).not.toBe('abcdef') 13 | }) 14 | 15 | it('sha1', async () => { 16 | expect(await sha1('hono')).toBe('28c7e86f5732391917876b45c06c626c04d77f39') 17 | expect(await sha1('炎')).toBe('d56e09ae2421b2b8a0b5ee5fdceaed663c8c9472') 18 | expect(await sha1('abcdedf')).not.toBe('abcdef') 19 | }) 20 | 21 | // MD5 is not part of the WebCrypto standard. 22 | // Node.js' Web Crypto API does not support it (But Cloudflare Workers supports it). 23 | // We should skip this test in a Node.js environment. 24 | it.skip('md5', async () => { 25 | expect(await md5('hono')).toBe('cf22a160789a91dd5f737cd3b2640cc2') 26 | expect(await md5('炎')).toBe('f620d89a5a782c22b4420acb39121be3') 27 | expect(await md5('abcdedf')).not.toBe('abcdef') 28 | }) 29 | 30 | it('Should not be the same values - compare difference objects', async () => { 31 | expect(await sha256({ foo: 'bar' })).not.toEqual( 32 | await sha256({ 33 | bar: 'foo', 34 | }) 35 | ) 36 | }) 37 | 38 | it('Should create hash for Buffer', async () => { 39 | const hash = createHash('sha256').update(new Uint8Array(1)).digest('hex') 40 | expect(await sha256(new Uint8Array(1))).toBe(hash) 41 | expect(await sha256(new Uint8Array(1))).not.toEqual(await sha256(new Uint8Array(2))) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Crypto utility. 4 | */ 5 | 6 | import type { JSONValue } from './types' 7 | 8 | type Algorithm = { 9 | name: string 10 | alias: string 11 | } 12 | 13 | type Data = string | boolean | number | JSONValue | ArrayBufferView | ArrayBuffer 14 | 15 | export const sha256 = async (data: Data): Promise => { 16 | const algorithm: Algorithm = { name: 'SHA-256', alias: 'sha256' } 17 | const hash = await createHash(data, algorithm) 18 | return hash 19 | } 20 | 21 | export const sha1 = async (data: Data): Promise => { 22 | const algorithm: Algorithm = { name: 'SHA-1', alias: 'sha1' } 23 | const hash = await createHash(data, algorithm) 24 | return hash 25 | } 26 | 27 | export const md5 = async (data: Data): Promise => { 28 | const algorithm: Algorithm = { name: 'MD5', alias: 'md5' } 29 | const hash = await createHash(data, algorithm) 30 | return hash 31 | } 32 | 33 | export const createHash = async (data: Data, algorithm: Algorithm): Promise => { 34 | let sourceBuffer: ArrayBufferView | ArrayBuffer 35 | 36 | if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) { 37 | sourceBuffer = data 38 | } else { 39 | if (typeof data === 'object') { 40 | data = JSON.stringify(data) 41 | } 42 | sourceBuffer = new TextEncoder().encode(String(data)) 43 | } 44 | 45 | if (crypto && crypto.subtle) { 46 | const buffer = await crypto.subtle.digest( 47 | { 48 | name: algorithm.name, 49 | }, 50 | sourceBuffer as ArrayBuffer 51 | ) 52 | const hash = Array.prototype.map 53 | .call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)) 54 | .join('') 55 | return hash 56 | } 57 | return null 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/encode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Encode utility. 4 | */ 5 | 6 | export const decodeBase64Url = (str: string): Uint8Array => { 7 | return decodeBase64(str.replace(/_|-/g, (m) => ({ _: '/', '-': '+' }[m] ?? m))) 8 | } 9 | 10 | export const encodeBase64Url = (buf: ArrayBufferLike): string => 11 | encodeBase64(buf).replace(/\/|\+/g, (m) => ({ '/': '_', '+': '-' }[m] ?? m)) 12 | 13 | // This approach is written in MDN. 14 | // btoa does not support utf-8 characters. So we need a little bit hack. 15 | export const encodeBase64 = (buf: ArrayBufferLike): string => { 16 | let binary = '' 17 | const bytes = new Uint8Array(buf) 18 | for (let i = 0, len = bytes.length; i < len; i++) { 19 | binary += String.fromCharCode(bytes[i]) 20 | } 21 | return btoa(binary) 22 | } 23 | 24 | // atob does not support utf-8 characters. So we need a little bit hack. 25 | export const decodeBase64 = (str: string): Uint8Array => { 26 | const binary = atob(str) 27 | const bytes = new Uint8Array(new ArrayBuffer(binary.length)) 28 | const half = binary.length / 2 29 | for (let i = 0, j = binary.length - 1; i <= half; i++, j--) { 30 | bytes[i] = binary.charCodeAt(i) 31 | bytes[j] = binary.charCodeAt(j) 32 | } 33 | return bytes 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/filepath.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * FilePath utility. 4 | */ 5 | 6 | type FilePathOptions = { 7 | filename: string 8 | root?: string 9 | defaultDocument?: string 10 | } 11 | 12 | export const getFilePath = (options: FilePathOptions): string | undefined => { 13 | let filename = options.filename 14 | const defaultDocument = options.defaultDocument || 'index.html' 15 | 16 | if (filename.endsWith('/')) { 17 | // /top/ => /top/index.html 18 | filename = filename.concat(defaultDocument) 19 | } else if (!filename.match(/\.[a-zA-Z0-9_-]+$/)) { 20 | // /top => /top/index.html 21 | filename = filename.concat('/' + defaultDocument) 22 | } 23 | 24 | const path = getFilePathWithoutDefaultDocument({ 25 | root: options.root, 26 | filename, 27 | }) 28 | 29 | return path 30 | } 31 | 32 | export const getFilePathWithoutDefaultDocument = ( 33 | options: Omit 34 | ): string | undefined => { 35 | let root = options.root || '' 36 | let filename = options.filename 37 | 38 | if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) { 39 | return 40 | } 41 | 42 | // /foo.html => foo.html 43 | filename = filename.replace(/^\.?[\/\\]/, '') 44 | 45 | // foo\bar.txt => foo/bar.txt 46 | filename = filename.replace(/\\/, '/') 47 | 48 | // assets/ => assets 49 | root = root.replace(/\/$/, '') 50 | 51 | // ./assets/foo.html => assets/foo.html 52 | let path = root ? root + '/' + filename : filename 53 | path = path.replace(/^\.?\//, '') 54 | 55 | if (root[0] !== '/' && path[0] === '/') { 56 | return 57 | } 58 | 59 | return path 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Handler utility. 4 | */ 5 | 6 | import { COMPOSED_HANDLER } from './constants' 7 | 8 | export const isMiddleware = (handler: Function) => handler.length > 1 9 | export const findTargetHandler = (handler: Function): Function => { 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | return (handler as any)[COMPOSED_HANDLER] 12 | ? // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | findTargetHandler((handler as any)[COMPOSED_HANDLER]) 14 | : handler 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/html.test.ts: -------------------------------------------------------------------------------- 1 | import { escapeToBuffer } from './html' 2 | import type { StringBuffer } from './html' 3 | 4 | describe('HTML utilities', () => { 5 | describe('escapeToBuffer', () => { 6 | it('Should escape special characters', () => { 7 | let buffer: StringBuffer = [''] 8 | escapeToBuffer('I think this is good.', buffer) 9 | expect(buffer[0]).toBe('I <b>think</b> this is good.') 10 | 11 | buffer = [''] 12 | escapeToBuffer('John "Johnny" Smith', buffer) 13 | expect(buffer[0]).toBe('John "Johnny" Smith') 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/utils/http-status.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * HTTP Status utility. 4 | */ 5 | 6 | export type InfoStatusCode = 100 | 101 | 102 | 103 7 | export type SuccessStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 8 | export type DeprecatedStatusCode = 305 | 306 9 | export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | DeprecatedStatusCode | 307 | 308 10 | export type ClientErrorStatusCode = 11 | | 400 12 | | 401 13 | | 402 14 | | 403 15 | | 404 16 | | 405 17 | | 406 18 | | 407 19 | | 408 20 | | 409 21 | | 410 22 | | 411 23 | | 412 24 | | 413 25 | | 414 26 | | 415 27 | | 416 28 | | 417 29 | | 418 30 | | 421 31 | | 422 32 | | 423 33 | | 424 34 | | 425 35 | | 426 36 | | 428 37 | | 429 38 | | 431 39 | | 451 40 | export type ServerErrorStatusCode = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 41 | 42 | /** 43 | * `UnofficialStatusCode` can be used to specify an unofficial status code. 44 | * @example 45 | * 46 | * ```ts 47 | * app.get('/unknown', (c) => { 48 | * return c.text("Unknown Error", 520 as UnofficialStatusCode) 49 | * }) 50 | * ``` 51 | */ 52 | export type UnofficialStatusCode = -1 53 | 54 | /** 55 | * @deprecated 56 | * Use `UnofficialStatusCode` instead. 57 | */ 58 | export type UnOfficalStatusCode = UnofficialStatusCode 59 | 60 | /** 61 | * If you want to use an unofficial status, use `UnofficialStatusCode`. 62 | */ 63 | export type StatusCode = 64 | | InfoStatusCode 65 | | SuccessStatusCode 66 | | RedirectStatusCode 67 | | ClientErrorStatusCode 68 | | ServerErrorStatusCode 69 | | UnofficialStatusCode 70 | 71 | export type ContentlessStatusCode = 101 | 204 | 205 | 304 72 | export type ContentfulStatusCode = Exclude 73 | -------------------------------------------------------------------------------- /src/utils/jwt/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * JWT utility. 4 | */ 5 | 6 | import { decode, sign, verify, verifyFromJwks } from './jwt' 7 | export const Jwt = { sign, verify, decode, verifyFromJwks } 8 | -------------------------------------------------------------------------------- /src/utils/jwt/jwa.test.ts: -------------------------------------------------------------------------------- 1 | import { AlgorithmTypes } from './jwa' 2 | 3 | describe('Types', () => { 4 | it('AlgorithmTypes', () => { 5 | expect('HS256' as AlgorithmTypes).toBe(AlgorithmTypes.HS256) 6 | expect('HS384' as AlgorithmTypes).toBe(AlgorithmTypes.HS384) 7 | expect('HS512' as AlgorithmTypes).toBe(AlgorithmTypes.HS512) 8 | expect('RS256' as AlgorithmTypes).toBe(AlgorithmTypes.RS256) 9 | expect('RS384' as AlgorithmTypes).toBe(AlgorithmTypes.RS384) 10 | expect('RS512' as AlgorithmTypes).toBe(AlgorithmTypes.RS512) 11 | 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 13 | // @ts-ignore 14 | expect(undefined as AlgorithmTypes).toBe(undefined) 15 | expect('' as AlgorithmTypes).toBe('') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/jwt/jwa.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * JSON Web Algorithms (JWA) 4 | * https://datatracker.ietf.org/doc/html/rfc7518 5 | */ 6 | 7 | export enum AlgorithmTypes { 8 | HS256 = 'HS256', 9 | HS384 = 'HS384', 10 | HS512 = 'HS512', 11 | RS256 = 'RS256', 12 | RS384 = 'RS384', 13 | RS512 = 'RS512', 14 | PS256 = 'PS256', 15 | PS384 = 'PS384', 16 | PS512 = 'PS512', 17 | ES256 = 'ES256', 18 | ES384 = 'ES384', 19 | ES512 = 'ES512', 20 | EdDSA = 'EdDSA', 21 | } 22 | 23 | export type SignatureAlgorithm = keyof typeof AlgorithmTypes 24 | -------------------------------------------------------------------------------- /src/utils/jwt/utf8.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Functions for encoding/decoding UTF8. 4 | */ 5 | 6 | export const utf8Encoder: TextEncoder = new TextEncoder() 7 | export const utf8Decoder: TextDecoder = new TextDecoder() 8 | -------------------------------------------------------------------------------- /src/utils/mime.test.ts: -------------------------------------------------------------------------------- 1 | import { getExtension, getMimeType } from './mime' 2 | 3 | const mime = { 4 | m3u8: 'application/vnd.apple.mpegurl', 5 | ts: 'video/mp2t', 6 | } 7 | 8 | describe('mime', () => { 9 | it('getMimeType', () => { 10 | expect(getMimeType('hello.txt')).toBe('text/plain; charset=utf-8') 11 | expect(getMimeType('hello.html')).toBe('text/html; charset=utf-8') 12 | expect(getMimeType('hello.json')).toBe('application/json') 13 | expect(getMimeType('favicon.ico')).toBe('image/x-icon') 14 | expect(getMimeType('good.morning.hello.gif')).toBe('image/gif') 15 | expect(getMimeType('goodmorninghellogif')).toBeUndefined() 16 | expect(getMimeType('indexjs.abcd')).toBeUndefined() 17 | }) 18 | 19 | it('getMimeType with custom mime', () => { 20 | expect(getMimeType('morning-routine.m3u8', mime)).toBe('application/vnd.apple.mpegurl') 21 | expect(getMimeType('morning-routine1.ts', mime)).toBe('video/mp2t') 22 | expect(getMimeType('readme.txt', mime)).toBeUndefined() 23 | }) 24 | 25 | it('getExtension', () => { 26 | expect(getExtension('audio/aac')).toBe('aac') 27 | expect(getExtension('video/x-msvideo')).toBe('avi') 28 | expect(getExtension('image/avif')).toBe('avif') 29 | expect(getExtension('text/css')).toBe('css') 30 | expect(getExtension('text/html')).toBe('htm') 31 | expect(getExtension('image/jpeg')).toBe('jpeg') 32 | expect(getExtension('text/javascript')).toBe('js') 33 | expect(getExtension('application/json')).toBe('json') 34 | expect(getExtension('audio/mpeg')).toBe('mp3') 35 | expect(getExtension('video/mp4')).toBe('mp4') 36 | expect(getExtension('application/pdf')).toBe('pdf') 37 | expect(getExtension('image/png')).toBe('png') 38 | expect(getExtension('application/zip')).toBe('zip') 39 | expect(getExtension('non/existent')).toBeUndefined() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/validator/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Validator for Hono. 4 | */ 5 | 6 | export { validator } from './validator' 7 | export type { ValidationFunction } from './validator' 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "rootDir": "./src/", 6 | "outDir": "./dist/types/", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | }, 10 | "include": [ 11 | "src/**/*.ts", 12 | "src/**/*.mts" 13 | ], 14 | "exclude": [ 15 | "src/mod.ts", 16 | "src/helper.ts", 17 | "src/middleware.ts", 18 | "src/deno/**/*.ts", 19 | "src/test-utils/*.ts", 20 | "src/**/*.test.ts", 21 | "src/**/*.test.tsx" 22 | ] 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "declaration": true, 5 | "moduleResolution": "Bundler", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": false, 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "types": [ 14 | "node", 15 | "vitest/globals" 16 | ], 17 | "jsx": "react", 18 | "jsxFactory": "jsx", 19 | "jsxFragmentFactory": "Fragment", 20 | }, 21 | "include": [ 22 | "src/**/*.ts", 23 | "src/**/*.d.ts", 24 | "src/**/*.mts", 25 | "src/**/*.test.ts", 26 | "src/**/*.test.tsx" 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { configDefaults, defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | esbuild: { 6 | jsx: 'automatic', 7 | jsxImportSource: __dirname + '/../src/jsx', 8 | }, 9 | test: { 10 | globals: true, 11 | include: ['**/src/**/(*.)+(spec|test).+(ts|tsx|js)', '**/scripts/**/(*.)+(spec|test).+(ts|tsx|js)', '**/build/**/(*.)+(spec|test).+(ts|tsx|js)'], 12 | exclude: [...configDefaults.exclude, '**/sandbox/**', '**/*.case.test.+(ts|tsx|js)'], 13 | setupFiles: ['./.vitest.config/setup-vitest.ts'], 14 | coverage: { 15 | enabled: true, 16 | provider: 'v8', 17 | reportsDirectory: './coverage/raw/default', 18 | reporter: ['json', 'text', 'html'], 19 | exclude: [ 20 | ...(configDefaults.coverage.exclude ?? []), 21 | 'benchmarks', 22 | 'runtime-tests', 23 | 'build/build.ts', 24 | 'src/test-utils', 25 | 'perf-measures', 26 | 27 | // types are compile-time only, so their coverage cannot be measured 28 | 'src/**/types.ts', 29 | 'src/jsx/intrinsic-elements.ts', 30 | 'src/utils/http-status.ts', 31 | ], 32 | }, 33 | pool: 'forks', 34 | }, 35 | }) 36 | --------------------------------------------------------------------------------