🧶

プロジェクトを壊さず安全に npm から yarn4 へ移行する

2023/07/08に公開
1

これはなに

長らく node パッケージマネージャーとして npm が使われてきたプロジェクトを yarn v4 に移行する手順をまとめたものです。

単純に移行するだけなら何も考えず yarn を導入して終わりですが、依存する node パッケージのバージョンを一切変更することなく移行するとなれば一筋縄ではいかなくなります。立ち上げ時から増改築を繰り返して膨大な数の node パッケージに依存しているプロジェクトですと、ほんの少しバージョンがズレただけで壊れてしまうことも珍しくありません。本稿ではそのような類のプロジェクトでも可能な限り安全に npm から yarn4 へ移行する手順をご紹介します。

前提

  • プロジェクトが依存するパッケージ情報は ^ 付きで package.json にバージョンが記載されているものとする。
  • プロジェクトには package-lock.json があるものとする。
  • プロジェクトに Renovate や dependabot の類は導入されていないものとする。
  • プロジェクトの Node.js は v18 以上であるものとする[1]

移行手順

1. yarn.lock を生成する

package.json^ 付きでバージョンが記載されているということは、依存するパッケージの本当のバージョンは package-lock.json にのみ記載されていることになります。つまり各種依存パッケージのバージョンを一切変更することなく yarn に移行するには、 package-lock.json の内容をそっくりそのまま yarn.lock に移行せねばなりません。

yarn には package-lock.json から yarn.lock を生成する import という機能があるため、これを利用します。

yarn import

https://chore-update--yarnpkg.netlify.app/ja/docs/cli/import

「なんだ簡単じゃないか」と思われたでしょうが、この機能を使うには以下の要件をすべて満たす必要があります

  • yarn v1 である。
    • yarn v2, v3, v4 は NG。
    • yarn v1 は corepack 経由でインストールできないため、homebrew 等でグローバルインストールする必要がある。
  • 使用する Node.js のバージョンが以下に該当する。
    • ^14.18.0 || ^16.14.0 || >=18.0.0 [2]

上記の要件をすべて満たした上で yarn import コマンドを実行すると package-lock.json を元に yarn.lock が生成されます。公式ドキュメントには「ロックファイルと既存の依存関係ツリーとの間の差異を可能な限り少なくするもの」と記載されているとおり 100% の精度ではないとのことですが、package.json の記載内容から生成するよりは遥かに信頼できると言って良いでしょう。

yarn.lock の生成に成功したら package-lock.json はもう不要なので削除します。自動的に削除されないため、忘れずにやっておきましょう。

プロジェクトディレクトリー
 .
 ├── src/
-├── package-lock.json
 ├── package.json
+└── yarn.lock

2. yarn4 を導入する

https://yarnpkg.com/getting-started/install

yarn は corepack 経由でのインストールが推奨されているため、まずは corepack を有効化します。

corepack enable

次に yarn をインストールします。

yarn set version stable

上記コマンドを実行すると package.jsonpackageManager というフィールドが追加され、corepack 経由でインストールされた yarn のバージョンが表記されます。

package.json
{
  "packageManager": "yarn@4.0.0"
  // ...
}

これで yarn が利用可能となります。

yarn -v
4.0.0

3. yarn の設定を調整する

npm や yarn1 と違い、v2 以降の yarn は依存パッケージを node_modules ディレクトリー配下に実態をダウンロードせず pnp.cjs, pnp.locker.mjs というファイルで管理する仕様となっています。ですが元々 npm を使っていたプロジェクトであることを考慮すると、引き続き node_modules ディレクトリーを使う方式の方が適切です。そこで .yarnrc.yml というファイルをプロジェクトディレクトリー直下に新規作成し、以下のように記述します。

プロジェクトディレクトリー
 .
 ├── src/
+├── .yarnrc.yml
 ├── package.json
 └── yarn.lock
.yarnrc.yml
nodeLinker: node-modules

https://yarnpkg.com/configuration/yarnrc#nodeLinker

これで引き続き node_modules ディレクトリー配下に依存パッケージがダウンロードされるようになります。

また、インストールした node パッケージのバージョンを完全に固定するための設定もしておくと良いでしょう。

.yarnrc.yml
 nodeLinker: node-modules
+defaultSemverRangePrefix: ''

https://yarnpkg.com/configuration/yarnrc#defaultSemverRangePrefix

これで yarn add foo とした際に "foo": "1.0.3" のように exact version でインストールされます(^ が付かない)。つまり -E オプションを毎回付ける必要がなくなります。

4. .gitignore に yarn 用の設定を追記する

yarn は v2 から依存パッケージを .yarn ディレクトリー配下にキャッシュします。これはパフォーマンス向上のための挙動ですが、それらは git 管理下に置くようなものではないため、以下を追記して git 管理下から除外しておくのが賢明です。

.gitignore
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions

https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored

5. 依存パッケージを再インストールする

rm -rf node_modules # 念のため古い node_modules を掃除しておく。
yarn install

6. 誤って package-lock.json が生成されないようにする

せっかく yarn に移行しても npm install コマンドを実行すると再び package-lock.json が生成されてしまいます。これを予防するために npm * コマンドの使用を禁ずる設定をしておきます。

package.json
 {
+  "engines": {
+    "npm": "use yarn instead",
+    "yarn": "4.0.0"
+  },
   "packageManager": "yarn@4.0.0"
 }

package.jsonengines フィールドを追加し、その中に "npm": "use yarn instead" と記述します。このフィールドは当該プロジェクトで使用可能な各種エンジンのバージョンを明記するためのものですが、ここに実在しないバージョンを指定することでそのエンジンを使うコマンドは必ず失敗するようになります。よって "use yarn instead" でも "代わりに yarn を使ってください" でも構いません。ついでに yarn の適切なバージョンを指定しておくと良いでしょう。

次に .npmrc というファイルを作成して以下のように設定します。

.npmrc
engine-strict=true

package.json の設定だけでは不十分であり、このファイルと組み合わせることで初めて期待通りの動作が得られます。

https://docs.npmjs.com/cli/v10/using-npm/config#engine-strict

移行作業は以上です。あとはプロジェクトに応じて CI/CD や README を書き直せば完了です。

参考文献

https://yarnpkg.com/getting-started/migration

脚注
  1. Release: Yarn 4.0 🪄⚗️ | Yarn ↩︎

  2. このバージョンに該当しないと yarn import が失敗します。 ↩︎

Discussion

htnabehtnabe

とても参考になりました。ありがとうございます。