こんにちは。TypeScript フロントエンド向けのビルド時 DI ライブラリ prewire を作りました。
このライブラリを作ることになったきっかけは、私が管理しているフロントエンドモノレポの構成でした。
複数事業のフロントエンドが 1 つの core/kit コードを共有しており、現在のアプリは Next.js を使っています。今後は一部の事業で TanStack Router を試してみたいと考えていましたが、だからといって共有コアが next/navigation や @tanstack/react-router に直接依存するようにはしたくありませんでした。
事業ごとに異なる実装を選びつつ、共有コアは修正しない構成が必要でした。
prewire では、共有コードが先に port と token を宣言します。
export interface RouterPort {
href(to: string): string
push(to: string): void
}
export const ROUTER = new InjectionToken<RouterPort>('router')
各アプリは、自分が使うフレームワークでこの port を実装します。
export const tanstackRouter = injectable(
{ logger: LOGGER },
({ logger }): RouterPort => ({
href: (to) => to,
push: (to) => {
logger.info(`navigate → ${to}`)
throw redirect({ to })
},
}),
{ provides: ROUTER },
)
共有コードを含む利用側では、実装がどこから来たかを知る必要なく、同じパスだけを import します。
import { router } from '#prewire'
prewire codegen は、アプリと共有パッケージの injectable() 宣言を静的に解析し、依存関係の順序どおりに普通の TypeScript composition root を生成します。
export const logger = consoleLoggerBinding.factory({})
export const router = tanstackRouterBinding.factory({ logger })
ランタイムコンテナや reflect-metadata、decorator は使いません。欠落した binding、循環依存、重複 binding のような問題は、実行中ではなくコード生成段階でビルドエラーとして処理します。生成されたコードは直接読むことができ、必要なら切り離して手動の composition root として使うこともできます。
共有パッケージの実装をアプリ側が勝手に上書きできないように、override もデフォルトで閉じています。共有コードが default: true を指定した binding だけをアプリ側で差し替えられます。意図としては Kotlin の open に近いです。
environment 軸も別に設けています。live/test、server/client、あるいは事業アプリ名のように、ユーザーが望む基準で異なる root を作れます。たとえばサーバー専用 binding は、クライアント root で単に実行されないだけでなく、生成されたコードに import 自体が含まれません。
現在リポジトリのサンプルには、1 つの共有 kit を次の 3 つのアプリがそれぞれ異なる形で配線する構成が入っています。
- Next.js
- TanStack Start
- React Router
ビルド連携については、Vite・webpack・rspack 向けの unplugin と、Next.js 向けの withPrewire() を提供しています。
ただし、まだ実際の製品コードに導入した状態ではありません。まず別ライブラリとサンプルで設計を検証して公開した段階です。現在 npm に 0.1.1 として公開されており、初期の 0.x バージョンなので API は変更される可能性があります。
また、単一アプリ・単一環境で binding が数個しかない場合、prewire を使う理由はあまり大きくありません。そうしたプロジェクトなら、composition root を 1 ファイル手書きするほうがシンプルです。同じ共有コードを複数のアプリ・環境・テスト構成で異なる形に配線する必要がある場合を主な対象として考えています。
私は主にバックエンドを開発してきて、フロントエンドモノレポを管理する中で、この問題を DI とビルド時コード生成で解決しました。そのため、解決策自体がかなりバックエンド寄りだという感覚もあります。
フロントエンドを専門に扱っている方々は、複数アプリが共有コアを使いつつ、ルーターのようにフレームワークごとの実装だけが異なる問題を、普段どのように解決しているのか気になります。prewire のようなビルド時 DI が適切に見えるか、あるいはもっとシンプルな方法やフロントエンドのエコシステムになじんだアプローチがあれば、意見を聞きたいです。
GitHub: https://github.com/clroot/prewire
npm: https://www.npmjs.com/package/@prewire/core
MIT ライセンスです。初期段階なので、設計や API に対する批判的なフィードバックも歓迎します。
まだコメントはありません。