どうも, みゅーとんです.
テックブログの執筆が軌道に乗ってて, 私は逆にサボるようになってしまいました. 良くないですね. 久しぶりに書きます.
メインプロダクトの Nuxt3 マイグレーション作業で, 一部影響範囲を外に切り出して private package 化をしたことがあるのですが, その際に Vue2 / Vue3 の双方のライブラリに依存する構成にせざるを得ず, その時の知見をまとめてみました.
はじめに
対象読者
- 利用するライブラリのマイグレーション作業が膨大で困ってる
- 依存ライブラリが複数バージョンをサポートするライブラリを作ろうとしている
前提事項
- ライブラリ公開や, その際に必要な package.json の設定項目については, 本記事では解説しません
2行でまとめ
- 任意のライブラリをリネームしてインポートする方法がある
- ライブラリとして公開する想定のコード内で利用する場合は tsconfig の path alias でごまかす
前提
複数のバージョンの同一名のライブラリを同時にインポートするのは, 一見奇妙かもしれないですが, 実際は以下のケースで対応せざるを得なくなります.
- 特定ライブラリを段階的にマイグレーションするとき
- 複数バージョンにまたがる互換性をもたせる node のパッケージを作るとき
それぞれのケース向けに考えてみます.
対応方法
段階的にマイグレーションするとき
例えば hoge というライブラリがあって、 v1 から v2 に置き換えることを想定します.
どうしても利用箇所が多くて段階的な対応が求められるときは, 複数バージョンが混在している状態にすることもやむなしかなと思います.
(フレームワーク関連のライブラリを対象にする場合は無理な場合があるので注意です)
この場合, hoge のライブラリの v2 のほうを hoge2 のようにリネームしてインポートする手段を取ることが出来ます.
サンプルとして, 以下のコードを書けるようになります.
import { doSomething as doSomethingLegacy } from "hoge" import { doSomething } from "hoge2"
このためには, package.json の dependencies に以下の記載をした状態でライブラリをインストールすればよいです
{ "dependencies": { "hoge": "^1.0.0", "hoge2": "npm:hoge@^2.0.0" } }
この仕様については, npm のマニュアル (以下2箇所) に記載があります.
- package.json dependencies の記法 .. package.json | npm Docs
- package の仕様 Alias について .. package-spec | npm Docs
ライブラリが 2 つ導入されている構成を作ってから, 徐々に古いバージョンのコードをなくしていけばいいですね.
マイグレーションのベストプラクティスかどうかはわかりませんが, 内容や状況次第で v1, v2 の違いを吸収できるラッパーを用意するなどの施策がとれそうですね. 以下はそのサンプルです.
// lib/hoge-helper.ts // ※ 中間状態のコードを用意するべきか否かは要議論です. import { doSomething as doSomethingLegacy } from "hoge" import { doSomething as doSomethingNew } from "hoge2" type LegacyArgs = Parameters<typeof doSomethingLegacy> type LegacyReturns = ReturnType<typeof doSomethingLegacy> type NewArgs = Parameters<typeof doSomethingNew> // 関数のオーバーライドを使って, 旧式と新式の双方の使い方を示す /** * @deprecated "hoge2" への移行のため, 古い機能は非推奨とします */ export function doSomething(arg: LegacyArgs): ReturnLegacy /** * */ export function doSomething(arg: NewArgs): ReturnNew export function doSomething(arg: LegacyArgs | NewArgs): ReturnLegacy | ReturnNew { if (/* 古い方を呼ぶ条件 */) { return doSomethingLegacy(arg) } // ゆくゆくは new だけを残す return doSomethingNew(arg) }
マイグレーションの方式としてどのようなステップを踏むべきかは, その対象がなにか, 変更規模等に左右されると思いますので, その都度最適な方法を検討すべきでしょう.
最後に、忘れずに “hoge2” から “hoge” にリネームしましょう.
ライブラリが複数バージョンをサポートする場合
作成しようとしているライブラリが “hoge” に依存していて, その v1, v2 の双方を, 互換性を維持しながらサポートしたい場合を考えます.
“hoge” が v1, v2 間でほとんどインターフェースや具体的な挙動が変更されないのであれば, 考慮する必要性はありません. しかし, 破壊的な変更があれば, 利用状況によっては対応が求められます.
このとき前述するようなインポートライブラリをリネームする手段をとり, 複数バージョン向けにコードを書くことが出来ます. しかし, ライブラリを利用する側は “hoge2” に当たるライブラリを認識できません.
互換性を保つコードを作るのは非常に大変ですが, もしもライブラリのバージョンごとに異なるエントリーポイントを用意する構成にできるなら, 話は変わります.
要件としては, v1, v2 どちらも import {} from "hoge" とかければよいのです.
まず, ビルド結果となるファイルが以下の構成になるようにビルドシステムを工夫します.
- dist/v1.js .. hoge v1 を利用するコード
- dist/v2.js .. hoge v2 を利用するコード
余談ですが, 個人的にはこの構成のビルドシステムを作るのは unjs/unbuild を使うのがおすすめです. package.json の exports を以下の形にすれば, ビルドシステム側が判断して dist 配下を作ってくれます.
{ "exports": { "./v1": { "import": "./dist/v1/index.mjs", "require": "./dist/v1/index.cjs", "types": "./dist/v1/index.d.ts" }, "./v2": { "import": "./dist/v2/index.mjs", "require": "./dist/v2/index.cjs", "types": "./dist/v2/index.d.ts" } } }
続いて, v1, v2 それぞれのソースコードを, フォルダを分けて管理できるように配置します.
上記の例でいうと, ソースコードの配置は以下のようになると思います
- src/v1
- index.ts ..
dist/v1/index.mjsにビルドされるエントリーポイント - tsconfig.json .. 必ず配置 (後述)
- index.ts ..
- src/v2
- index.ts ..
dist/v2/index.mjsにビルドされるエントリーポイント - tsconfig.json .. 必ず配置 (後述)
- index.ts ..
ここで v2 配下の tsconfig に以下の記述をします.
{ "compilerOptions": { "paths": { "hoge": "./node_modules/hoge2", "hoge/*": "./node_modules/hoge2/*" } } }
この設定によって, npm install された “hoge2” の名前のライブラリを src/v2 配下においては “hoge” という名前で利用可能になります.
もしかしたら, ビルドシステム側でも同様な設定が必要かもしれません. 必要に応じて path alias 設定をしてあげるとよいです. webpack を使ってる場合とかね.
まとめ
以上の設定と手法を用いることで, 同一名で異なるバージョンのライブラリを効果的に活用することが可能となります.
若干トリッキーな対応ではありますが, これらは段階的なマイグレーションや複数バージョンの互換性を持たせたライブラリを作成する際に役立つ知識です.
また, 環境や要件に応じて適切な方法を選択してください.
株式会社hacomonoでは一緒に働く仲間を募集しています。
エンジニア採用サイトや採用ウィッシュリストもぜひご覧ください!