0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HonoのマージされたPRを読んでいく#101 - #105

0
Last updated at Posted at 2025-12-22

この記事何?

エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。

でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono

#101 chore: update examples

サンプルプロジェクトを更新。相対パスインポートを修正し、不要な設定ファイルを削除。

#102 feat: add type to c.req.param key

ルートパラメータの型推論を追加。c.req.param('id') の引数に型チェックが効くように。

問題

変更前:

  app.get('/user/:id', (c) => {
    const id = c.req.param('id')    // OK
    const foo = c.req.param('foo')  // コンパイル通るが期待値はエラー
    return c.text(`User: ${id}`)
  })

問題: 存在しないパラメータ名を指定してもエラーにならない。

修正

src/hono.ts:

  // パスからパラメータ名を抽出する型
  type ParamKeyName<NameWithPattern> =
    NameWithPattern extends `${infer Name}{${infer _Pattern}`
      ? Name          // :id{[0-9]+} → id
      : NameWithPattern  // :id → id

  type ParamKey<Component> =
    Component extends `:${infer NameWithPattern}`
      ? ParamKeyName<NameWithPattern>  // :id → id
      : never

  type ParamKeys<Path> =
    Path extends `${infer Component}/${infer Rest}`
      ? ParamKey<Component> | ParamKeys<Rest>  // 再帰的に抽出
      : ParamKey<Path>

  // ハンドラーの型定義を変更
  get<Path extends string>(path: Path, ...args: Handler<ParamKeys<Path>>[]): Hono

動作:

  // '/user/:id' から 'id' を抽出
  type Keys = ParamKeys<'/user/:id'>
  // → 'id'

  // '/book/:category/:id' から 'category' | 'id' を抽出
  type Keys = ParamKeys<'/book/:category/:id'>
  // → 'category' | 'id'

使い方

変更後:

  app.get('/user/:id', (c) => {
    const id = c.req.param('id')    // OK - 'id' は有効
    const foo = c.req.param('foo')  // エラー
    return c.text(`User: ${id}`)
  })

  TypeScript の Template Literal Types

  // パターンマッチング
  type ParamKey<Component> =
    Component extends `:${infer Name}`  // ':' で始まる場合
      ? Name                             // → Name を抽出
      : never                            // そうでなければ never

  // 例
  type A = ParamKey<':id'>      // → 'id'
  type B = ParamKey<'users'>    // → never
  type C = ParamKey<':name'>    // → 'name'

#103: feat: add option for no strict routing

strict routing オプションを追加。末尾スラッシュの扱いを設定可能に。

問題

デフォルト動作(strict = true):

  app.get('/hello', (c) => c.text('Hello'))

  // GET /hello  → 200 OK
  // GET /hello/ → 404 Not Found(別のルートとして扱われる)

修正

src/hono.ts:

  type Init = {
    strict?: boolean
  }

  export class Hono {
    router: Router<Handler[]>
    middlewareRouters: Router<MiddlewareHandler>[]
    tempPath: string
  +  strict: boolean

    constructor(init: Init = { strict: true }) {
      this.router = new Router()
      this.middlewareRouters = []
      this.tempPath = '/'
  +    this.strict = init.strict  // strict routing - デフォルトは true
    }

    async dispatch(request: Request, env?: Env, event?: FetchEvent): Promise<Response> {
  -    const [method, path] = [request.method, getPathFromURL(request.url)]
  +    const path = getPathFromURL(request.url, { strict: this.strict })
      const method = request.method
      // ...
    }
  }

src/utils/url.ts:

  type Params = {
    strict: boolean
  }

  export const getPathFromURL = (url: string, params: Params = { strict: true }): string => {
    // strict が false なら末尾の / を削除
    if (!params.strict && url.endsWith('/')) {
      url = url.slice(0, -1)
    }

    const match = url.match(URL_REGEXP)
    if (match) {
      return match[5]
    }
    return ''
  }

#104 refactor: do some refactoring

コードのリファクタリング。型定義の改善とコードの整理。

#105 feat: nested route

BREAKING CHANGE
ネストルート機能を追加。app.route() の動作を変更。

変更前(チェーンルート):

  // これはもう動かない
  app.route('/')
    .get((c) => c.text('get /'))
    .post((c) => c.text('post /'))

変更後(ネストルート):

  // 新しい書き方
  const book = app.route('/book')
  book.get('/', (c) => c.text('List books'))      // GET /book
  book.get('/:id', (c) => {
    return c.text('Get Book: ' + c.req.param('id'))  // GET /book/:id
  })
  book.post('/', (c) => c.text('Create book'))    // POST /book

この辺はまだ今の形と違うなぁ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?