0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-21 18:18:57 +01:00

Merge branch 'main' into hide-annotations

This commit is contained in:
TATSUNO “Taz” Yasuhiro 2024-11-16 12:42:04 +09:00 committed by GitHub
commit 7c1e71b658
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1215 additions and 550 deletions

View File

@ -18,7 +18,7 @@ body:
required: true
- type: input
attributes:
label: What runtime/platform is your app running on?
label: What runtime/platform is your app running on? (with version if possible)
placeholder: Cloudflare Workers, Deno, Bun, etc.
validations:
required: true

View File

@ -20,6 +20,7 @@ jobs:
- bun
- fastly
- node
- deno
- workerd
- lambda
- lambda-edge
@ -50,9 +51,9 @@ jobs:
run: |
echo "::remove-matcher owner=eslint-compact::"
echo "::remove-matcher owner=eslint-stylish::"
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.1.16'
bun-version: '1.1.33'
- run: bun install
- run: bun run format
- run: bun run lint
@ -71,7 +72,7 @@ jobs:
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bunx jsr publish --dry-run
deno:
@ -85,6 +86,8 @@ jobs:
- run: env NAME=Deno deno test --coverage=coverage/raw/deno-runtime --allow-read --allow-env --allow-write --allow-net -c runtime-tests/deno/deno.json runtime-tests/deno
- run: deno test -c runtime-tests/deno-jsx/deno.precompile.json --coverage=coverage/raw/deno-precompile-jsx runtime-tests/deno-jsx
- run: deno test -c runtime-tests/deno-jsx/deno.react-jsx.json --coverage=coverage/raw/deno-react-jsx runtime-tests/deno-jsx
- run: grep -R '"url":' coverage | grep -v runtime-tests | sed -e 's/.*file:..//;s/.,//' | xargs deno cache --unstable-sloppy-imports
- run: deno coverage --lcov > coverage/deno-runtime-coverage-lcov.info
- uses: actions/upload-artifact@v4
with:
name: coverage-deno
@ -95,9 +98,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.1.16'
bun-version: '1.1.33'
- run: bun run test:bun
- uses: actions/upload-artifact@v4
with:
@ -108,7 +111,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bun run test:fastly
@ -128,7 +131,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bun run test:node
@ -146,7 +149,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bun run test:workerd
@ -160,7 +163,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bun run test:lambda
@ -174,7 +177,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run build
- run: bun run test:lambda-edge
@ -184,11 +187,12 @@ jobs:
path: coverage/
perf-measures-type-check-on-pr:
name: 'Type Check on PR'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install
- uses: actions/cache/restore@v4
with:
@ -211,11 +215,12 @@ jobs:
name: display comparison
perf-measures-type-check-on-main:
name: 'Type Check on Main'
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun scripts/generate-app.ts
working-directory: perf-measures/type-check
@ -225,3 +230,44 @@ jobs:
with:
path: perf-measures/type-check/previous-result.txt
key: type-check-perf-previous-result-${{ github.sha }}
perf-measures-bundle-check-on-pr:
name: 'Bundle Check on PR'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bunx esbuild --minify --bundle dist/index.js --format=esm --outfile=perf-measures/bundle-check/generated/after.js
- uses: actions/cache/restore@v4
with:
path: perf-measures/bundle-check/generated/before.js
restore-keys: |
perf-measures-bundle-check-previous-file-
key: perf-measures-bundle-check-previous-file-
- run: |
{
echo 'COMPARISON<<EOF'
bun scripts/process-results.ts | column -s '|' -t
echo 'EOF'
} >> "$GITHUB_ENV"
working-directory: perf-measures/bundle-check
- run: echo "$COMPARISON"
name: display comparison
perf-measures-bundle-check-on-main:
name: 'Bundle Check on Main'
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- run: bunx esbuild --minify --bundle dist/index.js --format=esm --outfile=perf-measures/bundle-check/generated/before.js
- uses: actions/cache/save@v4
with:
path: perf-measures/bundle-check/generated/before.js
key: perf-measures-bundle-check-previous-file-${{ github.sha }}

26
.github/workflows/no-response.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Close stale issues with "not bug" label
on:
schedule:
- cron: '0 0 * * *'
permissions:
contents: write
issues: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Close stale issues with "not bug" label
uses: actions/stale@v8
with:
days-before-stale: 7
days-before-close: 2
stale-issue-message: 'This issue has been marked as stale due to inactivity.'
close-issue-message: 'Closing this issue due to inactivity.'
exempt-issue-labels: ''
stale-issue-label: 'stale'
only-labels: 'not bug'
operations-per-run: 30
remove-stale-when-updated: true

View File

@ -16,11 +16,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install deno
uses: denoland/setup-deno@v1
uses: denoland/setup-deno@v2
with:
deno-version: v1.x
deno-version: v2.x
- run: deno install --no-lock --allow-scripts
- name: Publish to JSR
run: deno run -A jsr:@david/publish-on-tag@0.1.3
run: deno run -A jsr:@david/publish-on-tag@0.1.4

View File

@ -47,7 +47,7 @@ npm create hono@latest
## Features
- **Ultrafast** 🚀 - The router `RegExpRouter` is really fast. Not using linear loops. Fast.
- **Lightweight** 🪶 - The `hono/tiny` preset is under 13kB. Hono has zero dependencies and uses only the Web Standard API.
- **Lightweight** 🪶 - The `hono/tiny` preset is under 12kB. Hono has zero dependencies and uses only the Web Standard API.
- **Multi-runtime** 🌍 - Works on Cloudflare Workers, Fastly Compute, Deno, Bun, AWS Lambda, Lambda@Edge, or Node.js. The same code runs on all platforms.
- **Batteries Included** 🔋 - Hono has built-in middleware, custom middleware, and third-party middleware. Batteries included.
- **Delightful DX** 😃 - Super clean APIs. First-class TypeScript support. Now, we've got "Types".

View File

@ -7,9 +7,11 @@
"license": "MIT",
"dependencies": {
"fast-querystring": "^1.1.1",
"mitata": "^0.1.6"
"mitata": "^0.1.6",
"qs": "^6.13.0"
},
"devDependencies": {
"@types/qs": "^6.9.17",
"tsx": "^3.12.2"
}
},
@ -395,12 +397,76 @@
"node": ">=12"
}
},
"node_modules/@types/qs": {
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
"dev": true,
"license": "MIT"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.14.tgz",
@ -465,6 +531,34 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-tsconfig": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz",
@ -474,11 +568,133 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mitata": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/mitata/-/mitata-0.1.6.tgz",
"integrity": "sha512-VKQ0r3jriTOU9E2Z+mwbZrUmbg4Li4QyFfi7kfHKl6reZhGzL0AYlu3wE0VPXzIwA5xnFzmEQoBwCcNT8stUkA=="
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@ -5,10 +5,12 @@
},
"license": "MIT",
"devDependencies": {
"@types/qs": "^6.9.17",
"tsx": "^3.12.2"
},
"dependencies": {
"fast-querystring": "^1.1.1",
"mitata": "^0.1.6"
"mitata": "^0.1.6",
"qs": "^6.13.0"
}
}

View File

@ -1,6 +1,7 @@
import { run, group, bench } from 'mitata'
import fastQuerystring from './fast-querystring.mts'
import hono from './hono.mts'
import qs from './qs.mts'
;[
{
url: 'http://example.com/?page=1',
@ -36,6 +37,7 @@ import hono from './hono.mts'
group(JSON.stringify(data), () => {
bench('hono', () => hono(url, key))
bench('fastQuerystring', () => fastQuerystring(url, key))
bench('qs', () => qs(url, key))
})
})

View File

@ -0,0 +1,12 @@
import qs from 'qs'
const getQueryStringFromURL = (url: string): string => {
const queryIndex = url.indexOf('?', 8)
const result = queryIndex !== -1 ? url.slice(queryIndex + 1) : ''
return result
}
export default (url, key?) => {
const data = qs.parse(getQueryStringFromURL(url))
return key !== undefined ? data[key] : data
}

View File

@ -7,14 +7,15 @@
/// <reference types="bun-types/bun" />
import fs, { write } from 'fs'
import path from 'path'
import arg from 'arg'
import { $, stdout } from 'bun'
import { build } from 'esbuild'
import type { Plugin, PluginBuild, BuildOptions } from 'esbuild'
import * as glob from 'glob'
import fs from 'fs'
import path from 'path'
import { removePrivateFields } from './remove-private-fields'
import { $, stdout } from 'bun'
import { validateExports } from './validate-exports'
const args = arg({
'--watch': Boolean,
@ -22,6 +23,14 @@ const args = arg({
const isWatch = args['--watch'] || false
const readJsonExports = (path: string) => JSON.parse(fs.readFileSync(path, 'utf-8')).exports
const [packageJsonExports, jsrJsonExports] = ['./package.json', './jsr.json'].map(readJsonExports)
// Validate exports of package.json and jsr.json
validateExports(packageJsonExports, jsrJsonExports, 'jsr.json')
validateExports(jsrJsonExports, packageJsonExports, 'package.json')
const entryPoints = glob.sync('./src/**/*.ts', {
ignore: ['./src/**/*.test.ts', './src/mod.ts', './src/middleware.ts', './src/deno/**/*.ts'],
})
@ -96,7 +105,7 @@ let lastOutputLength = 0
for (let i = 0; i < dtsEntries.length; i++) {
const entry = dtsEntries[i]
const message = `Removing private fields(${i}/${dtsEntries.length}): ${entry}`
const message = `Removing private fields(${i + 1}/${dtsEntries.length}): ${entry}`
writer.write(`\r${' '.repeat(lastOutputLength)}`)
lastOutputLength = message.length
writer.write(`\r${message}`)

View File

@ -0,0 +1,31 @@
/// <reference types="vitest/globals" />
import { validateExports } from './validate-exports'
const mockExports1 = {
'./a': './a.ts',
'./b': './b.ts',
'./c/a': './c.ts',
'./d/*': './d/*.ts',
}
const mockExports2 = {
'./a': './a.ts',
'./b': './b.ts',
'./c/a': './c.ts',
'./d/a': './d/a.ts',
}
const mockExports3 = {
'./a': './a.ts',
'./c/a': './c.ts',
'./d/*': './d/*.ts',
}
describe('validateExports', () => {
it('Works', async () => {
expect(() => validateExports(mockExports1, mockExports1, 'package.json')).not.toThrowError()
expect(() => validateExports(mockExports1, mockExports2, 'jsr.json')).not.toThrowError()
expect(() => validateExports(mockExports1, mockExports3, 'package.json')).toThrowError()
})
})

37
build/validate-exports.ts Normal file
View File

@ -0,0 +1,37 @@
export const validateExports = (
source: Record<string, unknown>,
target: Record<string, unknown>,
fileName: string
) => {
const isEntryInTarget = (entry: string): boolean => {
if (entry in target) {
return true
}
// e.g., "./utils/*" -> "./utils"
const wildcardPrefix = entry.replace(/\/\*$/, '')
if (entry.endsWith('/*')) {
return Object.keys(target).some(
(targetEntry) =>
targetEntry.startsWith(wildcardPrefix + '/') && targetEntry !== wildcardPrefix
)
}
const separatedEntry = entry.split('/')
while (separatedEntry.length > 0) {
const pattern = `${separatedEntry.join('/')}/*`
if (pattern in target) {
return true
}
separatedEntry.pop()
}
return false
}
Object.keys(source).forEach((sourceEntry) => {
if (!isEntryInTarget(sourceEntry)) {
throw new Error(`Missing "${sourceEntry}" in '${fileName}'`)
}
})
}

BIN
bun.lockb

Binary file not shown.

View File

@ -46,6 +46,7 @@
"./jsx/dom/css": "./src/jsx/dom/css.ts",
"./jsx/dom/server": "./src/jsx/dom/server.ts",
"./jwt": "./src/middleware/jwt/jwt.ts",
"./timeout": "./src/middleware/timeout/index.ts",
"./timing": "./src/middleware/timing/timing.ts",
"./logger": "./src/middleware/logger/index.ts",
"./method-override": "./src/middleware/method-override/index.ts",
@ -79,6 +80,7 @@
"./testing": "./src/helper/testing/index.ts",
"./dev": "./src/helper/dev/index.ts",
"./ws": "./src/helper/websocket/index.ts",
"./conninfo": "./src/helper/conninfo/index.ts",
"./utils/body": "./src/utils/body.ts",
"./utils/buffer": "./src/utils/buffer.ts",
"./utils/color": "./src/utils/color.ts",

View File

@ -1,6 +1,6 @@
{
"name": "hono",
"version": "4.6.8",
"version": "4.6.10",
"description": "Web framework built on Web Standards",
"main": "dist/cjs/index.js",
"type": "module",
@ -39,6 +39,11 @@
"import": "./dist/index.js",
"require": "./dist/cjs/index.js"
},
"./request": {
"types": "./dist/types/request.d.ts",
"import": "./dist/request.js",
"require": "./dist/cjs/request.js"
},
"./types": {
"types": "./dist/types/types.d.ts",
"import": "./dist/types.js",
@ -387,6 +392,9 @@
},
"typesVersions": {
"*": {
"request": [
"./dist/types/request"
],
"types": [
"./dist/types/types"
],
@ -619,29 +627,29 @@
],
"devDependencies": {
"@hono/eslint-config": "^1.0.2",
"@hono/node-server": "^1.8.2",
"@types/glob": "^8.0.0",
"@types/jsdom": "^21.1.4",
"@hono/node-server": "^1.13.5",
"@types/glob": "^8.1.0",
"@types/jsdom": "^21.1.7",
"@types/node": "20.11.4",
"@types/supertest": "^2.0.12",
"@types/supertest": "^2.0.16",
"@vitest/coverage-v8": "^2.0.5",
"arg": "^5.0.2",
"bun-types": "^1.1.30",
"esbuild": "^0.15.12",
"bun-types": "^1.1.34",
"esbuild": "^0.15.18",
"eslint": "^9.10.0",
"glob": "^11.0.0",
"jsdom": "^22.1.0",
"msw": "^2.3.0",
"msw": "^2.6.0",
"np": "7.7.0",
"prettier": "^2.6.2",
"publint": "^0.1.8",
"supertest": "^6.3.3",
"publint": "^0.1.16",
"supertest": "^6.3.4",
"typescript": "^5.3.3",
"vite-plugin-fastly-js-compute": "^0.4.2",
"vitest": "^2.0.5",
"wrangler": "3.58.0",
"ws": "^8.17.0",
"zod": "^3.20.2"
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=16.9.0"

2
perf-measures/bundle-check/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
generated
!generated/.gitkeep

View File

@ -0,0 +1,14 @@
import * as fs from 'node:fs/promises'
async function main() {
const currentResult = (await fs.readFile('./generated/after.js')).byteLength
let previousResult: number | null = null
try {
previousResult = (await fs.readFile('./generated/before.js')).byteLength
} catch (e) {}
const table = ['| | Current | Previous |', '| --- | --- | --- |']
table.push(`| Bundle Size | ${currentResult} | ${previousResult || 'N/A'} |`)
console.log(table.join('\n'))
}
main()

View File

@ -139,6 +139,22 @@ Deno.test('Serve Static middleware', async () => {
res = await app.request('http://localhost/static-absolute-root/plain.txt')
assertEquals(res.status, 200)
assertEquals(await res.text(), 'Deno!')
res = await app.request('http://localhost/static')
assertEquals(res.status, 404)
assertEquals(await res.text(), '404 Not Found')
res = await app.request('http://localhost/static/dir')
assertEquals(res.status, 404)
assertEquals(await res.text(), '404 Not Found')
res = await app.request('http://localhost/static/helloworld/nested')
assertEquals(res.status, 404)
assertEquals(await res.text(), '404 Not Found')
res = await app.request('http://localhost/static/helloworld/../')
assertEquals(res.status, 404)
assertEquals(await res.text(), '404 Not Found')
})
Deno.test('JWT Authentication middleware', async () => {

View File

@ -221,6 +221,7 @@ describe('compress', async () => {
{
fetch: externalApp.fetch,
port: 0,
hostname: '0.0.0.0',
},
(serverInfo) => {
resolve([server as Server, serverInfo])

View File

@ -21,14 +21,13 @@ describe('upgradeWebSocket middleware', () => {
app.get(
'/ws',
upgradeWebSocket(() => ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onMessage(evt, ws) {
resolve(evt.data)
resolve([evt.data, ws.readyState || 1])
},
}))
)
)
it('Should receive message is valid', async () => {
it('Should receive message and readyState is valid', async () => {
const sendingData = Math.random().toString()
await app.request('/ws', {
headers: {
@ -41,7 +40,7 @@ describe('upgradeWebSocket middleware', () => {
})
)
expect(sendingData).toBe(await wsPromise)
expect([sendingData, 1]).toStrictEqual(await wsPromise)
})
it('Should call next() when header does not have upgrade', async () => {
const next = vi.fn()

View File

@ -1,51 +1,53 @@
import { WSContext, defineWebSocketHelper } from '../../helper/websocket'
import type { UpgradeWebSocket, WSReadyState } from '../../helper/websocket'
import type { UpgradeWebSocket, WSEvents, WSReadyState } from '../../helper/websocket'
// Based on https://github.com/honojs/hono/issues/1153#issuecomment-1767321332
export const upgradeWebSocket: UpgradeWebSocket<WebSocket> = defineWebSocketHelper(
async (c, events) => {
const upgradeHeader = c.req.header('Upgrade')
if (upgradeHeader !== 'websocket') {
return
}
// @ts-expect-error WebSocketPair is not typed
const webSocketPair = new WebSocketPair()
const client: WebSocket = webSocketPair[0]
const server: WebSocket = webSocketPair[1]
const wsContext = new WSContext<WebSocket>({
close: (code, reason) => server.close(code, reason),
get protocol() {
return server.protocol
},
raw: server,
get readyState() {
return server.readyState as WSReadyState
},
url: server.url ? new URL(server.url) : null,
send: (source) => server.send(source),
})
if (events.onOpen) {
server.addEventListener('open', (evt: Event) => events.onOpen?.(evt, wsContext))
}
if (events.onClose) {
server.addEventListener('close', (evt: CloseEvent) => events.onClose?.(evt, wsContext))
}
if (events.onMessage) {
server.addEventListener('message', (evt: MessageEvent) => events.onMessage?.(evt, wsContext))
}
if (events.onError) {
server.addEventListener('error', (evt: Event) => events.onError?.(evt, wsContext))
}
// @ts-expect-error - server.accept is not typed
server.accept?.()
return new Response(null, {
status: 101,
// @ts-expect-error - webSocket is not typed
webSocket: client,
})
export const upgradeWebSocket: UpgradeWebSocket<
WebSocket,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
Omit<WSEvents<WebSocket>, 'onOpen'>
> = defineWebSocketHelper(async (c, events) => {
const upgradeHeader = c.req.header('Upgrade')
if (upgradeHeader !== 'websocket') {
return
}
)
// @ts-expect-error WebSocketPair is not typed
const webSocketPair = new WebSocketPair()
const client: WebSocket = webSocketPair[0]
const server: WebSocket = webSocketPair[1]
const wsContext = new WSContext<WebSocket>({
close: (code, reason) => server.close(code, reason),
get protocol() {
return server.protocol
},
raw: server,
get readyState() {
return server.readyState as WSReadyState
},
url: server.url ? new URL(server.url) : null,
send: (source) => server.send(source),
})
// note: cloudflare workers doesn't support 'open' event
if (events.onClose) {
server.addEventListener('close', (evt: CloseEvent) => events.onClose?.(evt, wsContext))
}
if (events.onMessage) {
server.addEventListener('message', (evt: MessageEvent) => events.onMessage?.(evt, wsContext))
}
if (events.onError) {
server.addEventListener('error', (evt: Event) => events.onError?.(evt, wsContext))
}
// @ts-expect-error - server.accept is not typed
server.accept?.()
return new Response(null, {
status: 101,
// @ts-expect-error - webSocket is not typed
webSocket: client,
})
})

View File

@ -10,6 +10,10 @@ export const serveStatic = <E extends Env = Env>(
return async function serveStatic(c, next) {
const getContent = async (path: string) => {
try {
if (isDir(path)) {
return null
}
const file = await open(path)
return file.readable
} catch (e) {
@ -30,6 +34,7 @@ export const serveStatic = <E extends Env = Env>(
} catch {}
return isDir
}
return baseServeStatic({
...options,
getContent,

View File

@ -1,5 +1,5 @@
/**
* Cloudflare Workers Adapter for Hono.
* Service Worker Adapter for Hono.
* @module
*/
export { handle } from './handler'

View File

@ -1,5 +1,4 @@
import { Context } from './context'
import type { ParamIndexMap, Params } from './router'
import type { Env, ErrorHandler, NotFoundHandler } from './types'
/**
@ -31,7 +30,7 @@ interface ComposeContext {
* @returns {(context: C, next?: Function) => Promise<C>} - A composed middleware function.
*/
export const compose = <C extends ComposeContext, E extends Env = Env>(
middleware: [[Function, unknown], ParamIndexMap | Params][],
middleware: [[Function, unknown], unknown][] | [[Function]][],
onError?: ErrorHandler<E>,
onNotFound?: NotFoundHandler<E>
): ((context: C, next?: Function) => Promise<C>) => {

View File

@ -623,11 +623,11 @@ export class Context<
return Object.fromEntries(this.#var)
}
newResponse: NewResponse = (
#newResponse(
data: Data | null,
arg?: StatusCode | ResponseInit,
headers?: HeaderRecord
): Response => {
): Response {
// Optimized
if (this.#isFresh && !headers && !arg && this.#status === 200) {
return new Response(data, {
@ -689,6 +689,8 @@ export class Context<
})
}
newResponse: NewResponse = (...args) => this.#newResponse(...(args as Parameters<NewResponse>))
/**
* `.body()` can return the HTTP response.
* You can set headers with `.header()` and set HTTP status code with `.status`.
@ -716,8 +718,8 @@ export class Context<
headers?: HeaderRecord
): Response => {
return typeof arg === 'number'
? this.newResponse(data, arg, headers)
: this.newResponse(data, arg)
? this.#newResponse(data, arg, headers)
: this.#newResponse(data, arg)
}
/**
@ -749,8 +751,8 @@ export class Context<
this.#preparedHeaders['content-type'] = TEXT_PLAIN
// @ts-expect-error `Response` due to missing some types-only keys
return typeof arg === 'number'
? this.newResponse(text, arg, headers)
: this.newResponse(text, arg)
? this.#newResponse(text, arg, headers)
: this.#newResponse(text, arg)
}
/**
@ -778,7 +780,7 @@ export class Context<
this.#preparedHeaders['content-type'] = 'application/json; charset=UTF-8'
/* eslint-disable @typescript-eslint/no-explicit-any */
return (
typeof arg === 'number' ? this.newResponse(body, arg, headers) : this.newResponse(body, arg)
typeof arg === 'number' ? this.#newResponse(body, arg, headers) : this.#newResponse(body, arg)
) as any
}
@ -793,14 +795,14 @@ export class Context<
if (typeof html === 'object') {
return resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then((html) => {
return typeof arg === 'number'
? this.newResponse(html, arg, headers)
: this.newResponse(html, arg)
? this.#newResponse(html, arg, headers)
: this.#newResponse(html, arg)
})
}
return typeof arg === 'number'
? this.newResponse(html as string, arg, headers)
: this.newResponse(html as string, arg)
? this.#newResponse(html as string, arg, headers)
: this.#newResponse(html as string, arg)
}
/**

View File

@ -227,6 +227,29 @@ describe('createHandler', () => {
expectTypeOf(routes).toEqualTypeOf<Expected>()
})
})
describe('Types - Context Env with Multiple Middlewares', () => {
const factory = createFactory()
const mw1 = createMiddleware<{ Variables: { foo: string } }>(async (c, next) => {
c.set('foo', 'bar')
await next()
})
const mw2 = createMiddleware<{ Variables: { bar: number } }>(async (c, next) => {
c.set('bar', 1)
await next()
})
it('Should set the correct type for context from multiple middlewares', () => {
factory.createHandlers(mw1, mw2, (c) => {
expectTypeOf(c.var.foo).toEqualTypeOf<string>()
expectTypeOf(c.var.bar).toEqualTypeOf<number>()
return c.json({ foo: c.var.foo, bar: c.var.bar })
})
})
})
})
describe('createApp', () => {

View File

@ -5,31 +5,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Hono } from '../../hono'
import type { Env, H, HandlerResponse, Input, MiddlewareHandler } from '../../types'
import type {
Env,
H,
HandlerResponse,
Input,
IntersectNonAnyTypes,
MiddlewareHandler,
} from '../../types'
type InitApp<E extends Env = Env> = (app: Hono<E>) => void
export interface CreateHandlersInterface<E extends Env, P extends string> {
<I extends Input = {}, R extends HandlerResponse<any> = any>(handler1: H<E, P, I, R>): [
H<E, P, I, R>
]
<I extends Input = {}, R extends HandlerResponse<any> = any, E2 extends Env = E>(
handler1: H<E2, P, I, R>
): [H<E2, P, I, R>]
// handler x2
<I extends Input = {}, I2 extends Input = I, R extends HandlerResponse<any> = any>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>
): [H<E, P, I, R>, H<E, P, I2, R>]
<
I extends Input = {},
I2 extends Input = I,
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = IntersectNonAnyTypes<[E, E2]>
>(
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>
): [H<E2, P, I, R>, H<E3, P, I2, R>]
// handler x3
<
I extends Input = {},
I2 extends Input = I,
I3 extends Input = I & I2,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>
): [H<E, P, I, R>, H<E, P, I2, R>, H<E, P, I3, R>]
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>
): [H<E2, P, I, R>, H<E3, P, I2, R>, H<E4, P, I3, R>]
// handler x4
<
@ -37,13 +54,17 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I2 extends Input = I,
I3 extends Input = I & I2,
I4 extends Input = I & I2 & I3,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>
): [H<E, P, I, R>, H<E, P, I2, R>, H<E, P, I3, R>, H<E, P, I4, R>]
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>
): [H<E2, P, I, R>, H<E3, P, I2, R>, H<E4, P, I3, R>, H<E5, P, I4, R>]
// handler x5
<
@ -52,14 +73,19 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I3 extends Input = I & I2,
I4 extends Input = I & I2 & I3,
I5 extends Input = I & I2 & I3 & I4,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = E,
E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>,
handler5: H<E, P, I5, R>
): [H<E, P, I, R>, H<E, P, I2, R>, H<E, P, I3, R>, H<E, P, I4, R>, H<E, P, I5, R>]
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>,
handler5: H<E6, P, I5, R>
): [H<E2, P, I, R>, H<E3, P, I2, R>, H<E4, P, I3, R>, H<E5, P, I4, R>, H<E6, P, I5, R>]
// handler x6
<
@ -69,15 +95,28 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I4 extends Input = I & I2 & I3,
I5 extends Input = I & I2 & I3 & I4,
I6 extends Input = I & I2 & I3 & I4 & I5,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = E,
E6 extends Env = E,
E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>,
handler5: H<E, P, I5, R>,
handler6: H<E, P, I6, R>
): [H<E, P, I, R>, H<E, P, I2, R>, H<E, P, I3, R>, H<E, P, I4, R>, H<E, P, I5, R>, H<E, P, I6, R>]
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>,
handler5: H<E6, P, I5, R>,
handler6: H<E7, P, I6, R>
): [
H<E2, P, I, R>,
H<E3, P, I2, R>,
H<E4, P, I3, R>,
H<E5, P, I4, R>,
H<E6, P, I5, R>,
H<E7, P, I6, R>
]
// handler x7
<
@ -88,23 +127,30 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I5 extends Input = I & I2 & I3 & I4,
I6 extends Input = I & I2 & I3 & I4 & I5,
I7 extends Input = I & I2 & I3 & I4 & I5 & I6,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = E,
E6 extends Env = E,
E7 extends Env = E,
E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>,
handler5: H<E, P, I5, R>,
handler6: H<E, P, I6, R>,
handler7: H<E, P, I7, R>
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>,
handler5: H<E6, P, I5, R>,
handler6: H<E7, P, I6, R>,
handler7: H<E8, P, I7, R>
): [
H<E, P, I, R>,
H<E, P, I2, R>,
H<E, P, I3, R>,
H<E, P, I4, R>,
H<E, P, I5, R>,
H<E, P, I6, R>,
H<E, P, I7, R>
H<E2, P, I, R>,
H<E3, P, I2, R>,
H<E4, P, I3, R>,
H<E5, P, I4, R>,
H<E6, P, I5, R>,
H<E7, P, I6, R>,
H<E8, P, I7, R>
]
// handler x8
@ -117,25 +163,33 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I6 extends Input = I & I2 & I3 & I4 & I5,
I7 extends Input = I & I2 & I3 & I4 & I5 & I6,
I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = E,
E6 extends Env = E,
E7 extends Env = E,
E8 extends Env = E,
E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>,
handler5: H<E, P, I5, R>,
handler6: H<E, P, I6, R>,
handler7: H<E, P, I7, R>,
handler8: H<E, P, I8, R>
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>,
handler5: H<E6, P, I5, R>,
handler6: H<E7, P, I6, R>,
handler7: H<E8, P, I7, R>,
handler8: H<E9, P, I8, R>
): [
H<E, P, I, R>,
H<E, P, I2, R>,
H<E, P, I3, R>,
H<E, P, I4, R>,
H<E, P, I5, R>,
H<E, P, I6, R>,
H<E, P, I7, R>,
H<E, P, I8, R>
H<E2, P, I, R>,
H<E3, P, I2, R>,
H<E4, P, I3, R>,
H<E5, P, I4, R>,
H<E6, P, I5, R>,
H<E7, P, I6, R>,
H<E8, P, I7, R>,
H<E9, P, I8, R>
]
// handler x9
@ -149,27 +203,36 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I7 extends Input = I & I2 & I3 & I4 & I5 & I6,
I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7,
I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = E,
E6 extends Env = E,
E7 extends Env = E,
E8 extends Env = E,
E9 extends Env = E,
E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>,
handler5: H<E, P, I5, R>,
handler6: H<E, P, I6, R>,
handler7: H<E, P, I7, R>,
handler8: H<E, P, I8, R>,
handler9: H<E, P, I9, R>
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>,
handler5: H<E6, P, I5, R>,
handler6: H<E7, P, I6, R>,
handler7: H<E8, P, I7, R>,
handler8: H<E9, P, I8, R>,
handler9: H<E10, P, I9, R>
): [
H<E, P, I, R>,
H<E, P, I2, R>,
H<E, P, I3, R>,
H<E, P, I4, R>,
H<E, P, I5, R>,
H<E, P, I6, R>,
H<E, P, I7, R>,
H<E, P, I8, R>,
H<E, P, I9, R>
H<E2, P, I, R>,
H<E3, P, I2, R>,
H<E4, P, I3, R>,
H<E5, P, I4, R>,
H<E6, P, I5, R>,
H<E7, P, I6, R>,
H<E8, P, I7, R>,
H<E9, P, I8, R>,
H<E10, P, I9, R>
]
// handler x10
@ -184,29 +247,39 @@ export interface CreateHandlersInterface<E extends Env, P extends string> {
I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7,
I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8,
I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9,
R extends HandlerResponse<any> = any
R extends HandlerResponse<any> = any,
E2 extends Env = E,
E3 extends Env = E,
E4 extends Env = E,
E5 extends Env = E,
E6 extends Env = E,
E7 extends Env = E,
E8 extends Env = E,
E9 extends Env = E,
E10 extends Env = E,
E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>
>(
handler1: H<E, P, I, R>,
handler2: H<E, P, I2, R>,
handler3: H<E, P, I3, R>,
handler4: H<E, P, I4, R>,
handler5: H<E, P, I5, R>,
handler6: H<E, P, I6, R>,
handler7: H<E, P, I7, R>,
handler8: H<E, P, I8, R>,
handler9: H<E, P, I9, R>,
handler10: H<E, P, I10, R>
handler1: H<E2, P, I, R>,
handler2: H<E3, P, I2, R>,
handler3: H<E4, P, I3, R>,
handler4: H<E5, P, I4, R>,
handler5: H<E6, P, I5, R>,
handler6: H<E7, P, I6, R>,
handler7: H<E8, P, I7, R>,
handler8: H<E9, P, I8, R>,
handler9: H<E10, P, I9, R>,
handler10: H<E11, P, I10, R>
): [
H<E, P, I, R>,
H<E, P, I2, R>,
H<E, P, I3, R>,
H<E, P, I4, R>,
H<E, P, I5, R>,
H<E, P, I6, R>,
H<E, P, I7, R>,
H<E, P, I8, R>,
H<E, P, I9, R>,
H<E, P, I10, R>
H<E2, P, I, R>,
H<E3, P, I2, R>,
H<E4, P, I3, R>,
H<E5, P, I4, R>,
H<E6, P, I5, R>,
H<E7, P, I6, R>,
H<E8, P, I7, R>,
H<E9, P, I8, R>,
H<E10, P, I9, R>,
H<E11, P, I10, R>
]
}

View File

@ -20,8 +20,8 @@ export interface WSEvents<T = unknown> {
/**
* Upgrade WebSocket Type
*/
export type UpgradeWebSocket<T = unknown, U = any> = (
createEvents: (c: Context) => WSEvents<T> | Promise<WSEvents<T>>,
export type UpgradeWebSocket<T = unknown, U = any, _WSEvents = WSEvents<T>> = (
createEvents: (c: Context) => _WSEvents | Promise<_WSEvents>,
options?: U
) => MiddlewareHandler<
any,

View File

@ -130,12 +130,10 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
if (typeof args1 === 'string') {
this.#path = args1
} else {
this.addRoute(method, this.#path, args1)
this.#addRoute(method, this.#path, args1)
}
args.forEach((handler) => {
if (typeof handler !== 'string') {
this.addRoute(method, this.#path, handler)
}
this.#addRoute(method, this.#path, handler)
})
return this as any
}
@ -147,7 +145,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
this.#path = p
for (const m of [method].flat()) {
handlers.map((handler) => {
this.addRoute(m.toUpperCase(), this.#path, handler)
this.#addRoute(m.toUpperCase(), this.#path, handler)
})
}
}
@ -163,7 +161,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
handlers.unshift(arg1)
}
handlers.forEach((handler) => {
this.addRoute(METHOD_NAME_ALL, this.#path, handler)
this.#addRoute(METHOD_NAME_ALL, this.#path, handler)
})
return this as any
}
@ -174,7 +172,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
this.getPath = strict ? options.getPath ?? getPath : getPathNoStrict
}
private clone(): Hono<E, S, BasePath> {
#clone(): Hono<E, S, BasePath> {
const clone = new Hono<E, S, BasePath>({
router: this.router,
getPath: this.getPath,
@ -183,8 +181,8 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
return clone
}
private notFoundHandler: NotFoundHandler = notFoundHandler
private errorHandler: ErrorHandler = errorHandler
#notFoundHandler: NotFoundHandler = notFoundHandler
#errorHandler: ErrorHandler = errorHandler
/**
* `.route()` allows grouping other Hono instance in routes.
@ -216,15 +214,15 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
const subApp = this.basePath(path)
app.routes.map((r) => {
let handler
if (app.errorHandler === errorHandler) {
if (app.#errorHandler === errorHandler) {
handler = r.handler
} else {
handler = async (c: Context, next: Next) =>
(await compose<Context>([], app.errorHandler)(c, () => r.handler(c, next))).res
(await compose<Context>([], app.#errorHandler)(c, () => r.handler(c, next))).res
;(handler as any)[COMPOSED_HANDLER] = r.handler
}
subApp.addRoute(r.method, r.path, handler)
subApp.#addRoute(r.method, r.path, handler)
})
return this
}
@ -243,7 +241,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
* ```
*/
basePath<SubPath extends string>(path: SubPath): Hono<E, S, MergePath<BasePath, SubPath>> {
const subApp = this.clone()
const subApp = this.#clone()
subApp._basePath = mergePath(this._basePath, path)
return subApp
}
@ -265,7 +263,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
* ```
*/
onError = (handler: ErrorHandler<E>): Hono<E, S, BasePath> => {
this.errorHandler = handler
this.#errorHandler = handler
return this
}
@ -285,7 +283,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
* ```
*/
notFound = (handler: NotFoundHandler<E>): Hono<E, S, BasePath> => {
this.notFoundHandler = handler
this.#notFoundHandler = handler
return this
}
@ -370,26 +368,26 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
await next()
}
this.addRoute(METHOD_NAME_ALL, mergePath(path, '*'), handler)
this.#addRoute(METHOD_NAME_ALL, mergePath(path, '*'), handler)
return this
}
private addRoute(method: string, path: string, handler: H) {
#addRoute(method: string, path: string, handler: H) {
method = method.toUpperCase()
path = mergePath(this._basePath, path)
const r: RouterRoute = { path: path, method: method, handler: handler }
const r: RouterRoute = { path, method, handler }
this.router.add(method, path, [handler, r])
this.routes.push(r)
}
private handleError(err: unknown, c: Context<E>) {
#handleError(err: unknown, c: Context<E>) {
if (err instanceof Error) {
return this.errorHandler(err, c)
return this.#errorHandler(err, c)
}
throw err
}
private dispatch(
#dispatch(
request: Request,
executionCtx: ExecutionContext | FetchEventLike | undefined,
env: E['Bindings'],
@ -398,7 +396,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
// Handle HEAD method
if (method === 'HEAD') {
return (async () =>
new Response(null, await this.dispatch(request, executionCtx, env, 'GET')))()
new Response(null, await this.#dispatch(request, executionCtx, env, 'GET')))()
}
const path = this.getPath(request, { env })
@ -409,7 +407,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
matchResult,
env,
executionCtx,
notFoundHandler: this.notFoundHandler,
notFoundHandler: this.#notFoundHandler,
})
// Do not `compose` if it has only one handler
@ -417,23 +415,23 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
let res: ReturnType<H>
try {
res = matchResult[0][0][0][0](c, async () => {
c.res = await this.notFoundHandler(c)
c.res = await this.#notFoundHandler(c)
})
} catch (err) {
return this.handleError(err, c)
return this.#handleError(err, c)
}
return res instanceof Promise
? res
.then(
(resolved: Response | undefined) =>
resolved || (c.finalized ? c.res : this.notFoundHandler(c))
resolved || (c.finalized ? c.res : this.#notFoundHandler(c))
)
.catch((err: Error) => this.handleError(err, c))
: res ?? this.notFoundHandler(c)
.catch((err: Error) => this.#handleError(err, c))
: res ?? this.#notFoundHandler(c)
}
const composed = compose<Context>(matchResult[0], this.errorHandler, this.notFoundHandler)
const composed = compose<Context>(matchResult[0], this.#errorHandler, this.#notFoundHandler)
return (async () => {
try {
@ -446,7 +444,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
return context.res
} catch (err) {
return this.handleError(err, c)
return this.#handleError(err, c)
}
})()
}
@ -467,7 +465,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
Env?: E['Bindings'] | {},
executionCtx?: ExecutionContext
) => Response | Promise<Response> = (request, ...rest) => {
return this.dispatch(request, rest[1], rest[0], request.method)
return this.#dispatch(request, rest[1], rest[0], request.method)
}
/**
@ -489,15 +487,17 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
executionCtx?: ExecutionContext
): Response | Promise<Response> => {
if (input instanceof Request) {
if (requestInit !== undefined) {
input = new Request(input, requestInit)
}
return this.fetch(input, Env, executionCtx)
return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx)
}
input = input.toString()
const path = /^https?:\/\//.test(input) ? input : `http://localhost${mergePath('/', input)}`
const req = new Request(path, requestInit)
return this.fetch(req, Env, executionCtx)
return this.fetch(
new Request(
/^https?:\/\//.test(input) ? input : `http://localhost${mergePath('/', input)}`,
requestInit
),
Env,
executionCtx
)
}
/**
@ -511,7 +511,7 @@ class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
addEventListener('fetch', (event: FetchEventLike): void => {
event.respondWith(this.dispatch(event.request, event, undefined, event.request.method))
event.respondWith(this.#dispatch(event.request, event, undefined, event.request.method))
})
}
}

View File

@ -166,6 +166,22 @@ describe('every', () => {
expect(await res.text()).toBe('Hello Middleware 1')
expect(middleware2).not.toBeCalled()
})
it('Should pass the path params to middlewares', async () => {
const app = new Hono()
app.use('*', nextMiddleware)
const paramMiddleware: MiddlewareHandler = async (c) => {
return c.json(c.req.param(), 200)
}
app.use('/:id', every(paramMiddleware))
app.get('/:id', (c) => {
return c.text('Hello World')
})
const res = await app.request('http://localhost/123')
expect(await res.json()).toEqual({ id: '123' })
})
})
describe('except', () => {

View File

@ -89,19 +89,22 @@ export const some = (...middleware: (MiddlewareHandler | Condition)[]): Middlewa
* ```
*/
export const every = (...middleware: (MiddlewareHandler | Condition)[]): MiddlewareHandler => {
const wrappedMiddleware = middleware.map((m) => async (c: Context, next: Next) => {
const res = await m(c, next)
if (res === false) {
throw new Error('Unmet condition')
}
return res
})
const handler = async (c: Context, next: Next) =>
compose<Context>(wrappedMiddleware.map((m) => [[m, undefined], c.req.param()]))(c, next)
return async function every(c, next) {
await handler(c, next)
const currentRouteIndex = c.req.routeIndex
await compose<Context>(
middleware.map((m) => [
[
async (c: Context, next: Next) => {
c.req.routeIndex = currentRouteIndex // should be unchanged in this context
const res = await m(c, next)
if (res === false) {
throw new Error('Unmet condition')
}
return res
},
],
])
)(c, next)
}
}

View File

@ -0,0 +1,42 @@
const mergeBuffers = (buffer1: ArrayBuffer | undefined, buffer2: Uint8Array): Uint8Array => {
if (!buffer1) {
return buffer2
}
const merged = new Uint8Array(buffer1.byteLength + buffer2.byteLength)
merged.set(new Uint8Array(buffer1), 0)
merged.set(buffer2, buffer1.byteLength)
return merged
}
export const generateDigest = async (
stream: ReadableStream<Uint8Array> | null
): Promise<string | null> => {
if (!stream || !crypto || !crypto.subtle) {
return null
}
let result: ArrayBuffer | undefined = undefined
const reader = stream.getReader()
for (;;) {
const { value, done } = await reader.read()
if (done) {
break
}
result = await crypto.subtle.digest(
{
name: 'SHA-1',
},
mergeBuffers(result, value)
)
}
if (!result) {
return null
}
return Array.prototype.map
.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, '0'))
.join('')
}

View File

@ -65,6 +65,63 @@ describe('Etag Middleware', () => {
expect(res.headers.get('ETag')).not.toBe(hash)
})
it('Should not be the same etag - ReadableStream', async () => {
const app = new Hono()
app.use('/etag/*', etag())
app.get('/etag/rs1', (c) => {
return c.body(
new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1]))
controller.enqueue(new Uint8Array([2]))
controller.close()
},
})
)
})
app.get('/etag/rs2', (c) => {
return c.body(
new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1]))
controller.enqueue(new Uint8Array([3]))
controller.close()
},
})
)
})
let res = await app.request('http://localhost/etag/rs1')
const hash = res.headers.get('Etag')
res = await app.request('http://localhost/etag/rs2')
expect(res.headers.get('ETag')).not.toBe(hash)
})
it('Should not return etag header when the stream is empty', async () => {
const app = new Hono()
app.use('/etag/*', etag())
app.get('/etag/abc', (c) => {
const stream = new ReadableStream({
start(controller) {
controller.close()
},
})
return c.body(stream)
})
const res = await app.request('http://localhost/etag/abc')
expect(res.status).toBe(200)
expect(res.headers.get('ETag')).toBeNull()
})
it('Should not return etag header when body is null', async () => {
const app = new Hono()
app.use('/etag/*', etag())
app.get('/etag/abc', () => new Response(null, { status: 500 }))
const res = await app.request('http://localhost/etag/abc')
expect(res.status).toBe(500)
expect(res.headers.get('ETag')).toBeNull()
})
it('Should return etag header - weak', async () => {
const app = new Hono()
app.use('/etag/*', etag({ weak: true }))
@ -179,4 +236,29 @@ describe('Etag Middleware', () => {
expect(res.headers.get('x-message-retain')).toBe(message)
expect(res.headers.get('x-message')).toBeFalsy()
})
describe('When crypto is not available', () => {
let _crypto: Crypto | undefined
beforeAll(() => {
_crypto = globalThis.crypto
Object.defineProperty(globalThis, 'crypto', {
value: {},
})
})
afterAll(() => {
Object.defineProperty(globalThis, 'crypto', {
value: _crypto,
})
})
it('Should not generate etag', async () => {
const app = new Hono()
app.use('/etag/*', etag())
app.get('/etag/no-digest', (c) => c.text('Hono is cool'))
const res = await app.request('/etag/no-digest')
expect(res.status).toBe(200)
expect(res.headers.get('ETag')).toBeNull()
})
})
})

View File

@ -4,7 +4,7 @@
*/
import type { MiddlewareHandler } from '../../types'
import { sha1 } from '../../utils/crypto'
import { generateDigest } from './digest'
type ETagOptions = {
retainedHeaders?: string[]
@ -63,7 +63,10 @@ export const etag = (options?: ETagOptions): MiddlewareHandler => {
let etag = res.headers.get('ETag')
if (!etag) {
const hash = await sha1(res.clone().body || '')
const hash = await generateDigest(res.clone().body)
if (hash === null) {
return
}
etag = weak ? `W/"${hash}"` : `"${hash}"`
}

View File

@ -28,6 +28,17 @@ describe('Logger by Middleware', () => {
return c.text(longRandomString)
})
app.get('/empty', (c) => c.text(''))
app.get('/redirect', (c) => {
return c.redirect('/empty', 301)
})
app.get('/server-error', (c) => {
const res = new Response('', { status: 511 })
if (c.req.query('status')) {
// test status code not yet supported by runtime `Response` object
Object.defineProperty(res, 'status', { value: parseInt(c.req.query('status') as string) })
}
return res
})
})
it('Log status 200 with empty body', async () => {
@ -62,6 +73,14 @@ describe('Logger by Middleware', () => {
expect(log).toMatch(/1s/)
})
it('Log status 301 with empty body', async () => {
const res = await app.request('http://localhost/redirect')
expect(res).not.toBeNull()
expect(res.status).toBe(301)
expect(log.startsWith('--> GET /redirect \x1b[36m301\x1b[0m')).toBe(true)
expect(log).toMatch(/m?s$/)
})
it('Log status 404', async () => {
const msg = 'Default 404 Not Found'
app.all('*', (c) => {
@ -73,6 +92,30 @@ describe('Logger by Middleware', () => {
expect(log.startsWith('--> GET /notfound \x1b[33m404\x1b[0m')).toBe(true)
expect(log).toMatch(/m?s$/)
})
it('Log status 511 with empty body', async () => {
const res = await app.request('http://localhost/server-error')
expect(res).not.toBeNull()
expect(res.status).toBe(511)
expect(log.startsWith('--> GET /server-error \x1b[31m511\x1b[0m')).toBe(true)
expect(log).toMatch(/m?s$/)
})
it('Log status 100', async () => {
const res = await app.request('http://localhost/server-error?status=100')
expect(res).not.toBeNull()
expect(res.status).toBe(100)
expect(log.startsWith('--> GET /server-error 100')).toBe(true)
expect(log).toMatch(/m?s$/)
})
it('Log status 700', async () => {
const res = await app.request('http://localhost/server-error?status=700')
expect(res).not.toBeNull()
expect(res.status).toBe(700)
expect(log.startsWith('--> GET /server-error 700')).toBe(true)
expect(log).toMatch(/m?s$/)
})
})
describe('Logger by Middleware in NO_COLOR', () => {

View File

@ -28,19 +28,22 @@ const time = (start: number) => {
const colorStatus = (status: number) => {
const colorEnabled = getColorEnabled()
const out: { [key: string]: string } = {
7: colorEnabled ? `\x1b[35m${status}\x1b[0m` : `${status}`,
5: colorEnabled ? `\x1b[31m${status}\x1b[0m` : `${status}`,
4: colorEnabled ? `\x1b[33m${status}\x1b[0m` : `${status}`,
3: colorEnabled ? `\x1b[36m${status}\x1b[0m` : `${status}`,
2: colorEnabled ? `\x1b[32m${status}\x1b[0m` : `${status}`,
1: colorEnabled ? `\x1b[32m${status}\x1b[0m` : `${status}`,
0: colorEnabled ? `\x1b[33m${status}\x1b[0m` : `${status}`,
if (colorEnabled) {
switch ((status / 100) | 0) {
case 5: // red = error
return `\x1b[31m${status}\x1b[0m`
case 4: // yellow = warning
return `\x1b[33m${status}\x1b[0m`
case 3: // cyan = redirect
return `\x1b[36m${status}\x1b[0m`
case 2: // green = success
return `\x1b[32m${status}\x1b[0m`
}
}
const calculateStatus = (status / 100) | 0
return out[calculateStatus]
// Fallback to unsupported status code.
// E.g.) Bun and Deno supports new Response with 101, but Node.js does not.
// And those may evolve to accept more status.
return `${status}`
}
type PrintFunc = (str: string, ...rest: string[]) => void

View File

@ -91,21 +91,21 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
param(key: string): string | undefined
param<P2 extends string = P>(): Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P2>>>>
param(key?: string): unknown {
return key ? this.getDecodedParam(key) : this.getAllDecodedParams()
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams()
}
private getDecodedParam(key: string): string | undefined {
#getDecodedParam(key: string): string | undefined {
const paramKey = this.#matchResult[0][this.routeIndex][1][key]
const param = this.getParamValue(paramKey)
const param = this.#getParamValue(paramKey)
return param ? (/\%/.test(param) ? tryDecodeURIComponent(param) : param) : undefined
}
private getAllDecodedParams(): Record<string, string> {
#getAllDecodedParams(): Record<string, string> {
const decoded: Record<string, string> = {}
const keys = Object.keys(this.#matchResult[0][this.routeIndex][1])
for (const key of keys) {
const value = this.getParamValue(this.#matchResult[0][this.routeIndex][1][key])
const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key])
if (value && typeof value === 'string') {
decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value
}
@ -114,7 +114,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
return decoded
}
private getParamValue(paramKey: any): string | undefined {
#getParamValue(paramKey: any): string | undefined {
return this.#matchResult[1] ? this.#matchResult[1][paramKey as any] : paramKey
}
@ -208,7 +208,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
return (this.bodyCache.parsedBody ??= await parseBody(this, options))
}
private cachedBody = (key: keyof Body) => {
#cachedBody = (key: keyof Body) => {
const { bodyCache, raw } = this
const cachedBody = bodyCache[key]
@ -242,7 +242,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
* ```
*/
json<T = any>(): Promise<T> {
return this.cachedBody('json')
return this.#cachedBody('json')
}
/**
@ -258,7 +258,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
* ```
*/
text(): Promise<string> {
return this.cachedBody('text')
return this.#cachedBody('text')
}
/**
@ -274,7 +274,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
* ```
*/
arrayBuffer(): Promise<ArrayBuffer> {
return this.cachedBody('arrayBuffer')
return this.#cachedBody('arrayBuffer')
}
/**
@ -288,7 +288,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
* @see https://hono.dev/docs/api/request#blob
*/
blob(): Promise<Blob> {
return this.cachedBody('blob')
return this.#cachedBody('blob')
}
/**
@ -302,7 +302,7 @@ export class HonoRequest<P extends string = '/', I extends Input['out'] = {}> {
* @see https://hono.dev/docs/api/request#formdata
*/
formData(): Promise<FormData> {
return this.cachedBody('formData')
return this.#cachedBody('formData')
}
/**

View File

@ -10,7 +10,7 @@ const splitPathRe = /\/(:\w+(?:{(?:(?:{[\d,]+})|[^}])+})?)|\/[^\/\?]+|(\?)/g
const splitByStarRe = /\*/
export class LinearRouter<T> implements Router<T> {
name: string = 'LinearRouter'
routes: [string, string, T][] = []
#routes: [string, string, T][] = []
add(method: string, path: string, handler: T) {
for (
@ -18,14 +18,14 @@ export class LinearRouter<T> implements Router<T> {
i < len;
i++
) {
this.routes.push([method, paths[i], handler])
this.#routes.push([method, paths[i], handler])
}
}
match(method: string, path: string): Result<T> {
const handlers: [T, Params][] = []
ROUTES_LOOP: for (let i = 0, len = this.routes.length; i < len; i++) {
const [routeMethod, routePath, handler] = this.routes[i]
ROUTES_LOOP: for (let i = 0, len = this.#routes.length; i < len; i++) {
const [routeMethod, routePath, handler] = this.#routes[i]
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
if (routePath === '*' || routePath === '/*') {
handlers.push([handler, emptyParams])

View File

@ -5,7 +5,7 @@ type Route<T> = [RegExp, string, T] // [pattern, method, handler, path]
export class PatternRouter<T> implements Router<T> {
name: string = 'PatternRouter'
private routes: Route<T>[] = []
#routes: Route<T>[] = []
add(method: string, path: string, handler: T) {
const endsWithWildcard = path[path.length - 1] === '*'
@ -34,14 +34,14 @@ export class PatternRouter<T> implements Router<T> {
} catch {
throw new UnsupportedPathError()
}
this.routes.push([re, method, handler])
this.#routes.push([re, method, handler])
}
match(method: string, path: string): Result<T> {
const handlers: [T, Params][] = []
for (let i = 0, len = this.routes.length; i < len; i++) {
const [pattern, routeMethod, handler] = this.routes[i]
for (let i = 0, len = this.#routes.length; i < len; i++) {
const [pattern, routeMethod, handler] = this.#routes[i]
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
const match = pattern.exec(path)

View File

@ -43,9 +43,9 @@ function compareKey(a: string, b: string): number {
}
export class Node {
index?: number
varIndex?: number
children: Record<string, Node> = Object.create(null)
#index?: number
#varIndex?: number
#children: Record<string, Node> = Object.create(null)
insert(
tokens: readonly string[],
@ -55,14 +55,14 @@ export class Node {
pathErrorCheckOnly: boolean
): void {
if (tokens.length === 0) {
if (this.index !== undefined) {
if (this.#index !== undefined) {
throw PATH_ERROR
}
if (pathErrorCheckOnly) {
return
}
this.index = index
this.#index = index
return
}
@ -88,10 +88,10 @@ export class Node {
}
}
node = this.children[regexpStr]
node = this.#children[regexpStr]
if (!node) {
if (
Object.keys(this.children).some(
Object.keys(this.#children).some(
(k) => k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
)
) {
@ -100,19 +100,19 @@ export class Node {
if (pathErrorCheckOnly) {
return
}
node = this.children[regexpStr] = new Node()
node = this.#children[regexpStr] = new Node()
if (name !== '') {
node.varIndex = context.varIndex++
node.#varIndex = context.varIndex++
}
}
if (!pathErrorCheckOnly && name !== '') {
paramMap.push([name, node.varIndex as number])
paramMap.push([name, node.#varIndex as number])
}
} else {
node = this.children[token]
node = this.#children[token]
if (!node) {
if (
Object.keys(this.children).some(
Object.keys(this.#children).some(
(k) =>
k.length > 1 && k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
)
@ -122,7 +122,7 @@ export class Node {
if (pathErrorCheckOnly) {
return
}
node = this.children[token] = new Node()
node = this.#children[token] = new Node()
}
}
@ -130,21 +130,21 @@ export class Node {
}
buildRegExpStr(): string {
const childKeys = Object.keys(this.children).sort(compareKey)
const childKeys = Object.keys(this.#children).sort(compareKey)
const strList = childKeys.map((k) => {
const c = this.children[k]
const c = this.#children[k]
return (
(typeof c.varIndex === 'number'
? `(${k})@${c.varIndex}`
(typeof c.#varIndex === 'number'
? `(${k})@${c.#varIndex}`
: regExpMetaChars.has(k)
? `\\${k}`
: k) + c.buildRegExpStr()
)
})
if (typeof this.index === 'number') {
strList.unshift(`#${this.index}`)
if (typeof this.#index === 'number') {
strList.unshift(`#${this.#index}`)
}
if (strList.length === 0) {

View File

@ -123,16 +123,17 @@ function findMiddleware<T>(
export class RegExpRouter<T> implements Router<T> {
name: string = 'RegExpRouter'
middleware?: Record<string, Record<string, HandlerWithMetadata<T>[]>>
routes?: Record<string, Record<string, HandlerWithMetadata<T>[]>>
#middleware?: Record<string, Record<string, HandlerWithMetadata<T>[]>>
#routes?: Record<string, Record<string, HandlerWithMetadata<T>[]>>
constructor() {
this.middleware = { [METHOD_NAME_ALL]: Object.create(null) }
this.routes = { [METHOD_NAME_ALL]: Object.create(null) }
this.#middleware = { [METHOD_NAME_ALL]: Object.create(null) }
this.#routes = { [METHOD_NAME_ALL]: Object.create(null) }
}
add(method: string, path: string, handler: T) {
const { middleware, routes } = this
const middleware = this.#middleware
const routes = this.#routes
if (!middleware || !routes) {
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT)
@ -207,7 +208,7 @@ export class RegExpRouter<T> implements Router<T> {
match(method: string, path: string): Result<T> {
clearWildcardRegExpCache() // no longer used.
const matchers = this.buildAllMatchers()
const matchers = this.#buildAllMatchers()
this.match = (method, path) => {
const matcher = (matchers[method] || matchers[METHOD_NAME_ALL]) as Matcher<T>
@ -229,27 +230,27 @@ export class RegExpRouter<T> implements Router<T> {
return this.match(method, path)
}
private buildAllMatchers(): Record<string, Matcher<T> | null> {
#buildAllMatchers(): Record<string, Matcher<T> | null> {
const matchers: Record<string, Matcher<T> | null> = Object.create(null)
Object.keys(this.routes!)
.concat(Object.keys(this.middleware!))
Object.keys(this.#routes!)
.concat(Object.keys(this.#middleware!))
.forEach((method) => {
matchers[method] ||= this.buildMatcher(method)
matchers[method] ||= this.#buildMatcher(method)
})
// Release cache
this.middleware = this.routes = undefined
this.#middleware = this.#routes = undefined
return matchers
}
private buildMatcher(method: string): Matcher<T> | null {
#buildMatcher(method: string): Matcher<T> | null {
const routes: [string, HandlerWithMetadata<T>[]][] = []
let hasOwnRoute = method === METHOD_NAME_ALL
;[this.middleware!, this.routes!].forEach((r) => {
;[this.#middleware!, this.#routes!].forEach((r) => {
const ownRoute = r[method]
? Object.keys(r[method]).map((path) => [path, r[method][path]])
: []

View File

@ -4,8 +4,8 @@ import { Node } from './node'
export type ReplacementMap = number[]
export class Trie {
context: Context = { varIndex: 0 }
root: Node = new Node()
#context: Context = { varIndex: 0 }
#root: Node = new Node()
insert(path: string, index: number, pathErrorCheckOnly: boolean): ParamAssocArray {
const paramAssoc: ParamAssocArray = []
@ -41,13 +41,13 @@ export class Trie {
}
}
this.root.insert(tokens, index, paramAssoc, this.context, pathErrorCheckOnly)
this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly)
return paramAssoc
}
buildRegExp(): [RegExp, ReplacementMap, ReplacementMap] {
let regexp = this.root.buildRegExpStr()
let regexp = this.#root.buildRegExpStr()
if (regexp === '') {
return [/^$/, [], []] // never match
}

View File

@ -3,27 +3,29 @@ import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from '../../ro
export class SmartRouter<T> implements Router<T> {
name: string = 'SmartRouter'
routers: Router<T>[] = []
routes?: [string, string, T][] = []
#routers: Router<T>[] = []
#routes?: [string, string, T][] = []
constructor(init: Pick<SmartRouter<T>, 'routers'>) {
Object.assign(this, init)
constructor(init: { routers: Router<T>[] }) {
this.#routers = init.routers
}
add(method: string, path: string, handler: T) {
if (!this.routes) {
if (!this.#routes) {
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT)
}
this.routes.push([method, path, handler])
this.#routes.push([method, path, handler])
}
match(method: string, path: string): Result<T> {
if (!this.routes) {
if (!this.#routes) {
throw new Error('Fatal error')
}
const { routers, routes } = this
const routers = this.#routers
const routes = this.#routes
const len = routers.length
let i = 0
let res
@ -42,8 +44,8 @@ export class SmartRouter<T> implements Router<T> {
}
this.match = router.match.bind(router)
this.routers = [router]
this.routes = undefined
this.#routers = [router]
this.#routes = undefined
break
}
@ -59,10 +61,10 @@ export class SmartRouter<T> implements Router<T> {
}
get activeRouter(): Router<T> {
if (this.routes || this.routers.length !== 1) {
if (this.#routes || this.#routers.length !== 1) {
throw new Error('No active router has been determined yet.')
}
return this.routers[0]
return this.#routers[0]
}
}

View File

@ -14,26 +14,26 @@ type HandlerParamsSet<T> = HandlerSet<T> & {
}
export class Node<T> {
methods: Record<string, HandlerSet<T>>[]
#methods: Record<string, HandlerSet<T>>[]
children: Record<string, Node<T>>
patterns: Pattern[]
order: number = 0
params: Record<string, string> = Object.create(null)
#children: Record<string, Node<T>>
#patterns: Pattern[]
#order: number = 0
#params: Record<string, string> = Object.create(null)
constructor(method?: string, handler?: T, children?: Record<string, Node<T>>) {
this.children = children || Object.create(null)
this.methods = []
this.#children = children || Object.create(null)
this.#methods = []
if (method && handler) {
const m: Record<string, HandlerSet<T>> = Object.create(null)
m[method] = { handler, possibleKeys: [], score: 0 }
this.methods = [m]
this.#methods = [m]
}
this.patterns = []
this.#patterns = []
}
insert(method: string, path: string, handler: T): Node<T> {
this.order = ++this.order
this.#order = ++this.#order
// eslint-disable-next-line @typescript-eslint/no-this-alias
let curNode: Node<T> = this
@ -44,8 +44,8 @@ export class Node<T> {
for (let i = 0, len = parts.length; i < len; i++) {
const p: string = parts[i]
if (Object.keys(curNode.children).includes(p)) {
curNode = curNode.children[p]
if (Object.keys(curNode.#children).includes(p)) {
curNode = curNode.#children[p]
const pattern = getPattern(p)
if (pattern) {
possibleKeys.push(pattern[1])
@ -53,18 +53,14 @@ export class Node<T> {
continue
}
curNode.children[p] = new Node()
curNode.#children[p] = new Node()
const pattern = getPattern(p)
if (pattern) {
curNode.patterns.push(pattern)
curNode.#patterns.push(pattern)
possibleKeys.push(pattern[1])
}
curNode = curNode.children[p]
}
if (!curNode.methods.length) {
curNode.methods = []
curNode = curNode.#children[p]
}
const m: Record<string, HandlerSet<T>> = Object.create(null)
@ -72,27 +68,27 @@ export class Node<T> {
const handlerSet: HandlerSet<T> = {
handler,
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
score: this.order,
score: this.#order,
}
m[method] = handlerSet
curNode.methods.push(m)
curNode.#methods.push(m)
return curNode
}
// getHandlerSets
private gHSets(
#getHandlerSets(
node: Node<T>,
method: string,
nodeParams: Record<string, string>,
params: Record<string, string>
): HandlerParamsSet<T>[] {
const handlerSets: HandlerParamsSet<T>[] = []
for (let i = 0, len = node.methods.length; i < len; i++) {
const m = node.methods[i]
for (let i = 0, len = node.#methods.length; i < len; i++) {
const m = node.#methods[i]
const handlerSet = (m[method] || m[METHOD_NAME_ALL]) as HandlerParamsSet<T>
const processedSet: Record<string, boolean> = Object.create(null)
const processedSet: Record<number, boolean> = {}
if (handlerSet !== undefined) {
handlerSet.params = Object.create(null)
for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) {
@ -111,7 +107,7 @@ export class Node<T> {
search(method: string, path: string): [[T, Params][]] {
const handlerSets: HandlerParamsSet<T>[] = []
this.params = Object.create(null)
this.#params = Object.create(null)
// eslint-disable-next-line @typescript-eslint/no-this-alias
const curNode: Node<T> = this
@ -125,34 +121,43 @@ export class Node<T> {
for (let j = 0, len2 = curNodes.length; j < len2; j++) {
const node = curNodes[j]
const nextNode = node.children[part]
const nextNode = node.#children[part]
if (nextNode) {
nextNode.params = node.params
nextNode.#params = node.#params
if (isLast) {
// '/hello/*' => match '/hello'
if (nextNode.children['*']) {
if (nextNode.#children['*']) {
handlerSets.push(
...this.gHSets(nextNode.children['*'], method, node.params, Object.create(null))
...this.#getHandlerSets(
nextNode.#children['*'],
method,
node.#params,
Object.create(null)
)
)
}
handlerSets.push(...this.gHSets(nextNode, method, node.params, Object.create(null)))
handlerSets.push(
...this.#getHandlerSets(nextNode, method, node.#params, Object.create(null))
)
} else {
tempNodes.push(nextNode)
}
}
for (let k = 0, len3 = node.patterns.length; k < len3; k++) {
const pattern = node.patterns[k]
for (let k = 0, len3 = node.#patterns.length; k < len3; k++) {
const pattern = node.#patterns[k]
const params = { ...node.params }
const params = { ...node.#params }
// Wildcard
// '/hello/*/foo' => match /hello/bar/foo
if (pattern === '*') {
const astNode = node.children['*']
const astNode = node.#children['*']
if (astNode) {
handlerSets.push(...this.gHSets(astNode, method, node.params, Object.create(null)))
handlerSets.push(
...this.#getHandlerSets(astNode, method, node.#params, Object.create(null))
)
tempNodes.push(astNode)
}
continue
@ -164,28 +169,28 @@ export class Node<T> {
const [key, name, matcher] = pattern
const child = node.children[key]
const child = node.#children[key]
// `/js/:filename{[a-z]+.js}` => match /js/chunk/123.js
const restPathString = parts.slice(i).join('/')
if (matcher instanceof RegExp && matcher.test(restPathString)) {
params[name] = restPathString
handlerSets.push(...this.gHSets(child, method, node.params, params))
handlerSets.push(...this.#getHandlerSets(child, method, node.#params, params))
continue
}
if (matcher === true || matcher.test(part)) {
if (typeof key === 'string') {
params[name] = part
if (isLast) {
handlerSets.push(...this.gHSets(child, method, params, node.params))
if (child.children['*']) {
handlerSets.push(...this.gHSets(child.children['*'], method, params, node.params))
}
} else {
child.params = params
tempNodes.push(child)
params[name] = part
if (isLast) {
handlerSets.push(...this.#getHandlerSets(child, method, params, node.#params))
if (child.#children['*']) {
handlerSets.push(
...this.#getHandlerSets(child.#children['*'], method, params, node.#params)
)
}
} else {
child.#params = params
tempNodes.push(child)
}
}
}

View File

@ -4,25 +4,25 @@ import { Node } from './node'
export class TrieRouter<T> implements Router<T> {
name: string = 'TrieRouter'
node: Node<T>
#node: Node<T>
constructor() {
this.node = new Node()
this.#node = new Node()
}
add(method: string, path: string, handler: T) {
const results = checkOptionalParameter(path)
if (results) {
for (let i = 0, len = results.length; i < len; i++) {
this.node.insert(method, results[i], handler)
this.#node.insert(method, results[i], handler)
}
return
}
this.node.insert(method, path, handler)
this.#node.insert(method, path, handler)
}
match(method: string, path: string): Result<T> {
return this.node.search(method, path)
return this.#node.search(method, path)
}
}

View File

@ -1985,7 +1985,7 @@ export type ExtractSchema<T> = UnionToIntersection<
>
type EnvOrEmpty<T> = T extends Env ? (Env extends T ? {} : T) : T
type IntersectNonAnyTypes<T extends any[]> = T extends [infer Head, ...infer Rest]
export type IntersectNonAnyTypes<T extends any[]> = T extends [infer Head, ...infer Rest]
? IfAnyThenEmptyObject<EnvOrEmpty<Head>> & IntersectNonAnyTypes<Rest>
: {}

View File

@ -30,6 +30,14 @@ describe('Parse cookie', () => {
expect(cookie['tasty_cookie']).toBeUndefined()
})
it('Should parse one cookie specified by name even if it is not found', () => {
const cookieString = 'yummy_cookie=choco; tasty_cookie = strawberry '
const cookie: Cookie = parse(cookieString, 'no_such_cookie')
expect(cookie['yummy_cookie']).toBeUndefined()
expect(cookie['tasty_cookie']).toBeUndefined()
expect(cookie['no_such_cookie']).toBeUndefined()
})
it('Should parse cookies with no value', () => {
const cookieString = 'yummy_cookie=; tasty_cookie = ; best_cookie= ; last_cookie=""'
const cookie: Cookie = parse(cookieString)

View File

@ -77,17 +77,22 @@ const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/
const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/
export const parse = (cookie: string, name?: string): Cookie => {
if (name && cookie.indexOf(name) === -1) {
// Fast-path: return immediately if the demanded-key is not in the cookie string
return {}
}
const pairs = cookie.trim().split(';')
return pairs.reduce((parsedCookie, pairStr) => {
const parsedCookie: Cookie = {}
for (let pairStr of pairs) {
pairStr = pairStr.trim()
const valueStartPos = pairStr.indexOf('=')
if (valueStartPos === -1) {
return parsedCookie
continue
}
const cookieName = pairStr.substring(0, valueStartPos).trim()
if ((name && name !== cookieName) || !validCookieNameRegEx.test(cookieName)) {
return parsedCookie
continue
}
let cookieValue = pairStr.substring(valueStartPos + 1).trim()
@ -96,10 +101,13 @@ export const parse = (cookie: string, name?: string): Cookie => {
}
if (validCookieValueRegEx.test(cookieValue)) {
parsedCookie[cookieName] = decodeURIComponent_(cookieValue)
if (name) {
// Fast-path: return only the demanded-key immediately. Other keys are not needed.
break
}
}
return parsedCookie
}, {} as Cookie)
}
return parsedCookie
}
export const parseSigned = async (
@ -162,7 +170,7 @@ const _serialize = (name: string, value: string, opt: CookieOptions = {}): strin
'Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration.'
)
}
cookie += `; Max-Age=${Math.floor(opt.maxAge)}`
cookie += `; Max-Age=${opt.maxAge | 0}`
}
if (opt.domain && opt.prefix !== 'host') {

View File

@ -3,12 +3,14 @@
* Crypto utility.
*/
import type { JSONValue } from './types'
type Algorithm = {
name: string
alias: string
}
type Data = string | boolean | number | object | ArrayBufferView | ArrayBuffer | ReadableStream
type Data = string | boolean | number | JSONValue | ArrayBufferView | ArrayBuffer
export const sha256 = async (data: Data): Promise<string | null> => {
const algorithm: Algorithm = { name: 'SHA-256', alias: 'sha256' }
@ -31,15 +33,6 @@ export const md5 = async (data: Data): Promise<string | null> => {
export const createHash = async (data: Data, algorithm: Algorithm): Promise<string | null> => {
let sourceBuffer: ArrayBufferView | ArrayBuffer
if (data instanceof ReadableStream) {
let body = ''
const reader = data.getReader()
await reader?.read().then(async (chuck) => {
const value = await createHash(chuck.value || '', algorithm)
body += value
})
return body
}
if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) {
sourceBuffer = data
} else {

View File

@ -74,7 +74,7 @@ export const verify = async (
if (!isTokenHeader(header)) {
throw new JwtHeaderInvalid(header)
}
const now = Math.floor(Date.now() / 1000)
const now = (Date.now() / 1000) | 0
if (payload.nbf && payload.nbf > now) {
throw new JwtTokenNotBefore(token)
}

View File

@ -32,58 +32,9 @@ export { baseMimes as mimes }
/**
* Union types for BaseMime
*/
export type BaseMime =
| 'audio/aac'
| 'video/x-msvideo'
| 'image/avif'
| 'video/av1'
| 'application/octet-stream'
| 'image/bmp'
| 'text/css'
| 'text/csv'
| 'application/vnd.ms-fontobject'
| 'application/epub+zip'
| 'image/gif'
| 'application/gzip'
| 'text/html'
| 'image/x-icon'
| 'text/calendar'
| 'image/jpeg'
| 'text/javascript'
| 'application/json'
| 'application/ld+json'
| 'audio/x-midi'
| 'audio/mpeg'
| 'video/mp4'
| 'video/mpeg'
| 'audio/ogg'
| 'video/ogg'
| 'application/ogg'
| 'audio/opus'
| 'font/otf'
| 'application/pdf'
| 'image/png'
| 'application/rtf'
| 'image/svg+xml'
| 'image/tiff'
| 'video/mp2t'
| 'font/ttf'
| 'text/plain'
| 'application/wasm'
| 'video/webm'
| 'audio/webm'
| 'image/webp'
| 'font/woff'
| 'font/woff2'
| 'application/xhtml+xml'
| 'application/xml'
| 'application/zip'
| 'video/3gpp'
| 'video/3gpp2'
| 'model/gltf+json'
| 'model/gltf-binary'
export type BaseMime = (typeof _baseMimes)[keyof typeof _baseMimes]
const baseMimes: Record<string, BaseMime> = {
const _baseMimes = {
aac: 'audio/aac',
avi: 'video/x-msvideo',
avif: 'image/avif',
@ -139,4 +90,6 @@ const baseMimes: Record<string, BaseMime> = {
'3g2': 'video/3gpp2',
gltf: 'model/gltf+json',
glb: 'model/gltf-binary',
}
} as const
const baseMimes: Record<string, BaseMime> = _baseMimes

View File

@ -8,7 +8,7 @@ export default defineConfig({
},
test: {
globals: true,
include: ['**/src/**/(*.)+(spec|test).+(ts|tsx|js)', '**/scripts/**/(*.)+(spec|test).+(ts|tsx|js)'],
include: ['**/src/**/(*.)+(spec|test).+(ts|tsx|js)', '**/scripts/**/(*.)+(spec|test).+(ts|tsx|js)', '**/build/**/(*.)+(spec|test).+(ts|tsx|js)'],
exclude: [...configDefaults.exclude, '**/sandbox/**', '**/*.case.test.+(ts|tsx|js)'],
setupFiles: ['./.vitest.config/setup-vitest.ts'],
coverage: {
@ -20,7 +20,7 @@ export default defineConfig({
...(configDefaults.coverage.exclude ?? []),
'benchmarks',
'runtime-tests',
'build.ts',
'build/build.ts',
'src/test-utils',
'perf-measures',

139
yarn.lock
View File

@ -1,6 +1,6 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
# bun ./bun.lockb --hash: 77B78F6BF9FCD5E8-99bd6727146efa0f-3A60C19E44E6AEA6-b935857e930aa307
# bun ./bun.lockb --hash: D365A55B3A797610-f2453af89509670e-C1C56F357DD69CBD-b406908528ddf7b3
"@ampproject/remapping@^2.3.0":
@ -684,10 +684,10 @@
eslint-plugin-import-x "^4.1.1"
eslint-plugin-n "^17.10.2"
"@hono/node-server@^1.8.2":
version "1.13.1"
resolved "https://registry.npmjs.org/@hono/node-server/-/node-server-1.13.1.tgz"
integrity sha512-TSxE6cT5RHnawbjnveexVN7H2Dpn1YaLxQrCOLCUwD+hFbqbFsnJBgdWcYtASqtWVjA+Qgi8uqFug39GsHjo5A==
"@hono/node-server@^1.13.5":
version "1.13.5"
resolved "https://registry.npmjs.org/@hono/node-server/-/node-server-1.13.5.tgz"
integrity sha512-lSo+CFlLqAFB4fX7ePqI9nauEn64wOfJHAfc9duYFTvAG3o416pC0nTGeNjuLHchLedH+XyWda5v79CVx1PIjg==
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
@ -699,50 +699,38 @@
resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz"
integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==
"@inquirer/confirm@^3.0.0":
version "3.2.0"
resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz"
integrity sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==
"@inquirer/confirm@^5.0.0":
version "5.0.1"
resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.1.tgz"
integrity sha512-6ycMm7k7NUApiMGfVc32yIPp28iPKxhGRMqoNDiUjq2RyTAkbs5Fx0TdzBqhabcKvniDdAAvHCmsRjnNfTsogw==
dependencies:
"@inquirer/core" "^9.1.0"
"@inquirer/type" "^1.5.3"
"@inquirer/core" "^10.0.1"
"@inquirer/type" "^3.0.0"
"@inquirer/core@^9.1.0":
version "9.2.1"
resolved "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz"
integrity sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==
"@inquirer/core@^10.0.1":
version "10.0.1"
resolved "https://registry.npmjs.org/@inquirer/core/-/core-10.0.1.tgz"
integrity sha512-KKTgjViBQUi3AAssqjUFMnMO3CM3qwCHvePV9EW+zTKGKafFGFF01sc1yOIYjLJ7QU52G/FbzKc+c01WLzXmVQ==
dependencies:
"@inquirer/figures" "^1.0.6"
"@inquirer/type" "^2.0.0"
"@types/mute-stream" "^0.0.4"
"@types/node" "^22.5.5"
"@types/wrap-ansi" "^3.0.0"
"@inquirer/figures" "^1.0.7"
"@inquirer/type" "^3.0.0"
ansi-escapes "^4.3.2"
cli-width "^4.1.0"
mute-stream "^1.0.0"
mute-stream "^2.0.0"
signal-exit "^4.1.0"
strip-ansi "^6.0.1"
wrap-ansi "^6.2.0"
yoctocolors-cjs "^2.1.2"
"@inquirer/figures@^1.0.6":
version "1.0.6"
resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz"
integrity sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==
"@inquirer/figures@^1.0.7":
version "1.0.7"
resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz"
integrity sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==
"@inquirer/type@^1.5.3":
version "1.5.5"
resolved "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz"
integrity sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==
dependencies:
mute-stream "^1.0.0"
"@inquirer/type@^2.0.0":
version "2.0.0"
resolved "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz"
integrity sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==
dependencies:
mute-stream "^1.0.0"
"@inquirer/type@^3.0.0":
version "3.0.0"
resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.0.tgz"
integrity sha512-YYykfbw/lefC7yKj7nanzQXILM7r3suIvyFlCcMskc99axmsSewXWkAfXKwMbgxL76iAFVmRwmYdwNZNc8gjog==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
@ -809,10 +797,10 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@mswjs/interceptors@^0.35.8":
version "0.35.8"
resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.8.tgz"
integrity sha512-PFfqpHplKa7KMdoQdj5td03uG05VK2Ng1dG0sP4pT9h0dGSX2v9txYt/AnrzPb/vAmfyBBC0NQV7VaBEX+efgQ==
"@mswjs/interceptors@^0.36.5":
version "0.36.9"
resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.9.tgz"
integrity sha512-mMRDUBwSNeCgjSMEWfjoh4Rm9fbyZ7xQ9SBq8eGHiiyRn1ieTip3pNEt0wxWVPPxR4i1Rv9bTkeEbkX7M4c15A==
dependencies:
"@open-draft/deferred-promise" "^2.2.0"
"@open-draft/logger" "^0.3.0"
@ -1147,7 +1135,7 @@
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/glob@^8.0.0":
"@types/glob@^8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz"
integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==
@ -1160,7 +1148,7 @@
resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz"
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
"@types/jsdom@^21.1.4":
"@types/jsdom@^21.1.7":
version "21.1.7"
resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz"
integrity sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==
@ -1196,13 +1184,6 @@
resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz"
integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
"@types/mute-stream@^0.0.4":
version "0.0.4"
resolved "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz"
integrity sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@20.11.4":
version "20.11.4"
resolved "https://registry.npmjs.org/@types/node/-/node-20.11.4.tgz"
@ -1217,7 +1198,7 @@
dependencies:
undici-types "~5.26.4"
"@types/node@*", "@types/node@^22.5.5":
"@types/node@*", "@types/node@>=18":
version "22.6.1"
resolved "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz"
integrity sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==
@ -1263,7 +1244,7 @@
"@types/methods" "^1.1.4"
"@types/cookiejar" "^2.1.5"
"@types/supertest@^2.0.12":
"@types/supertest@^2.0.16":
version "2.0.16"
resolved "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz"
integrity sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==
@ -1275,11 +1256,6 @@
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz"
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
"@types/wrap-ansi@^3.0.0":
version "3.0.0"
resolved "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz"
integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==
"@types/ws@~8.5.10":
version "8.5.12"
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz"
@ -1735,10 +1711,10 @@ builtins@^1.0.3:
resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz"
integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==
bun-types@^1.1.30:
version "1.1.30"
resolved "https://registry.npmjs.org/bun-types/-/bun-types-1.1.30.tgz"
integrity sha512-mGh7NLisOXskBU62DxLS+/nwmLlCYHYAkCzdo4DZ9+fzrpP41hAdOqaN4DO6tQfenHb4pYb0/shw29k4/6I2yQ==
bun-types@^1.1.34:
version "1.1.34"
resolved "https://registry.npmjs.org/bun-types/-/bun-types-1.1.34.tgz"
integrity sha512-br5QygTEL/TwB4uQOb96Ky22j4Gq2WxWH/8Oqv20fk5HagwKXo/akB+LiYgSfzexCt6kkcUaVm+bKiPl71xPvw==
dependencies:
"@types/ws" "~8.5.10"
"@types/node" "~20.12.8"
@ -2389,7 +2365,7 @@ es-module-lexer@^1.5.4:
resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz"
integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==
esbuild@^0.15.12:
esbuild@^0.15.18:
version "0.15.18"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz"
integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==
@ -4308,16 +4284,17 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msw@^2.3.0:
version "2.4.9"
resolved "https://registry.npmjs.org/msw/-/msw-2.4.9.tgz"
integrity sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==
msw@^2.6.0:
version "2.6.0"
resolved "https://registry.npmjs.org/msw/-/msw-2.6.0.tgz"
integrity sha512-n3tx2w0MZ3H4pxY0ozrQ4sNPzK/dGtlr2cIIyuEsgq2Bhy4wvcW6ZH2w/gXM9+MEUY6HC1fWhqtcXDxVZr5Jxw==
dependencies:
"@bundled-es-modules/cookie" "^2.0.0"
"@bundled-es-modules/statuses" "^1.0.1"
"@bundled-es-modules/tough-cookie" "^0.1.6"
"@inquirer/confirm" "^3.0.0"
"@mswjs/interceptors" "^0.35.8"
"@inquirer/confirm" "^5.0.0"
"@mswjs/interceptors" "^0.36.5"
"@open-draft/deferred-promise" "^2.2.0"
"@open-draft/until" "^2.1.0"
"@types/cookie" "^0.6.0"
"@types/statuses" "^2.0.4"
@ -4325,10 +4302,10 @@ msw@^2.3.0:
graphql "^16.8.1"
headers-polyfill "^4.0.2"
is-node-process "^1.2.0"
outvariant "^1.4.2"
outvariant "^1.4.3"
path-to-regexp "^6.3.0"
strict-event-emitter "^0.5.1"
type-fest "^4.9.0"
type-fest "^4.26.1"
yargs "^17.7.2"
mustache@^4.2.0:
@ -4346,10 +4323,10 @@ mute-stream@0.0.8:
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
mute-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz"
integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==
mute-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz"
integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==
nanoid@^3.3.3, nanoid@^3.3.7:
version "3.3.7"
@ -4604,7 +4581,7 @@ os-tmpdir@~1.0.2:
resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
outvariant@^1.4.0, outvariant@^1.4.2, outvariant@^1.4.3:
outvariant@^1.4.0, outvariant@^1.4.3:
version "1.4.3"
resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz"
integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==
@ -4919,7 +4896,7 @@ psl@^1.1.33:
resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
publint@^0.1.8:
publint@^0.1.16:
version "0.1.16"
resolved "https://registry.npmjs.org/publint/-/publint-0.1.16.tgz"
integrity sha512-wJgk7HnXDT5Ap0DjFYbGz78kPkN44iQvDiaq8P63IEEyNU9mYXvaMd2cAyIM6OgqXM/IA3CK6XWIsRq+wjNpgw==
@ -5600,7 +5577,7 @@ superagent@^8.1.2:
component-emitter "^1.3.0"
fast-safe-stringify "^2.1.1"
supertest@^6.3.3:
supertest@^6.3.4:
version "6.3.4"
resolved "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz"
integrity sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==
@ -5848,7 +5825,7 @@ type-fest@^0.21.3:
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-fest@^4.9.0:
type-fest@^4.26.1:
version "4.26.1"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz"
integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==
@ -6194,7 +6171,7 @@ write-file-atomic@^3.0.0:
is-typedarray "^1.0.0"
typedarray-to-buffer "^3.1.5"
ws@^8.11.0, ws@^8.13.0, ws@^8.17.0:
ws@^8.11.0, ws@^8.13.0, ws@^8.18.0:
version "8.18.0"
resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
@ -6289,7 +6266,7 @@ youch@^3.2.2:
mustache "^4.2.0"
stacktracey "^2.1.8"
zod@^3.20.2, zod@^3.20.6:
zod@^3.20.6, zod@^3.23.8:
version "3.23.8"
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==