😸

yarn v1→npmに移行した

2024/06/10に公開

こんにちは!CastingONEの大沼です。

はじめに

弊社ではずっと使い慣れたyarn v1を使っていましたが、相当古くなってしまい、いよいよstorybookのバージョンを上げられないという事態に直面しました😢

[Bug]: string-width dependency stops storybook from executing

普通に考えたらyarnをアップグレードする必要があるのですが、yarnの独自機能は使わずにアップグレードするのは結構手間であまりモチベーションが湧きませんでした。。(だからこそ今までv1で頑張っていたんですよね)

https://zenn.dev/mizchi/articles/yarn-v1-to-v3

そんな状況で改めて標準搭載されているnpmの今のスペックを見ていたのですが、昔と違ってきちんとlockファイルを生成するし、workspaceも使えて、弊社の開発要件は満たせているので思い切ってnpmに乗り換えても良いなと思い始めました。
他の開発メンバーも「yarnをアップグレードするよりnpmに乗り換えた方が無難なのでは?」という意見をいただき、npmに乗り換えることを決心しました😊

移行作業

npmへの移行はパッと調べた感じ良さげな移行手順が見当たらなかったため、単純にnpm installして乗り換えることにしました。一応以下のドキュメントや記事をみるとyarn.lockも考慮してインストールされなそうな雰囲気はありましたが、できるだけパッケージの違いを小さくする対応を入れてからインストールするようにしました。

https://docs.npmjs.com/cli/v10/commands/npm-install
https://stackoverflow.com/questions/72853529/npm-and-yarn-lock/73005284#73005284

具体的には以下のような流れでやりました。

1. package.jsonのバージョンを固定

まずはパッケージのバージョンが大きくずれないように、package.jsonで指定しているバージョンは全てキャレットを外して固定するようにしました。

package.jsonで指定しているバージョンを固定する
 {
   "dependencies": {
-    "@mui/material": "^5.14.0"
+    "@mui/material": "5.14.0"
   }
 }

これによってnpm installで改めてインストールしてもバージョンが固定されます。ただし、さらにこのパッケージのdependenciesで指定されているものについてはこれだけだとずれてしまう可能性があるのは注意する必要があります。
例えば上の例で書いた@mui/materialは5.14.0と指定することはできましたが、これに依存している@babel/runtimereact-transition-groupなどのパッケージはキャレット付きで指定されているため、マイナーバージョンがずれる可能性があります。

yarn.lock
"@mui/material@5.14.0":
  version "5.14.0"
  resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.0.tgz#3d2afb4a3643774370cb5add873abcbbe8e7af27"
  integrity sha512-HP7CP71NhMkui2HUIEKl2/JfuHMuoarSUWAKlNw6s17bl/Num9rN61EM6uUzc2A2zHjj/00A66GnvDnmixEJEw==
  dependencies:
    "@babel/runtime" "^7.22.5"
    "@mui/base" "5.0.0-beta.7"
    "@mui/core-downloads-tracker" "^5.14.0"
    "@mui/system" "^5.14.0"
    "@mui/types" "^7.2.4"
    "@mui/utils" "^5.13.7"
    "@types/react-transition-group" "^4.4.6"
    clsx "^1.2.1"
    csstype "^3.1.2"
    prop-types "^15.8.1"
    react-is "^18.2.0"
    react-transition-group "^4.4.5"

これは少し気になるところですが、単純に全体的にパッケージのバージョンを上げたんだと考えることもできるので、全体的に触って問題なければこのままで進めることにしました。

2. 実際にnpm installを実行してエラーを潰していく

パッケージのバージョンを固定したので、次は実際にnpm installをしてみて、エラーになってしまったところを潰していきました。対応した内容をいくつか挙げると以下のようなものがありました。

casone-libのpeerDependenciesを調整

弊社ではプライベートの外部パッケージがあるのですが、以下のようなエラーが出てそのパッケージのpeerDependenciesで指定しているTypeScriptのバージョンと合わないと言われました。yarn v1では特に言われていなかったので、npmだとpeerDependenciesもちゃんとみるんだなと思いました👀

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: tenant-app@0.1.0
npm ERR! Found: typescript@5.1.6
npm ERR! node_modules/typescript
npm ERR!   dev typescript@"5.1.6" from tenant-app@0.1.0
npm ERR!   apps/tenant
npm ERR!     tenant-app@0.1.0
npm ERR!     node_modules/tenant-app
npm ERR!       workspace apps/tenant from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer typescript@"^4.0.2" from @casone-lib/dotenv@0.0.1
npm ERR! node_modules/@casone-lib/dotenv
npm ERR!   @casone-lib/dotenv@"git+ssh://git@github.com:CastingONE/frontend-package-dotenv.git#dist-v0.0.1" from tenant-app@0.1.0
npm ERR!   apps/tenant
npm ERR!     tenant-app@0.1.0
npm ERR!     node_modules/tenant-app
npm ERR!       workspace apps/tenant from the root project

これは原因がハッキリとしているので外部パッケージのバージョンを全体的に上げてpeerDependenciesも5系に上げました。

@testing-library/react-hooksから@testing-library/reactに完全に乗り換えた

続いてのエラーは以下で、@testing-library/react-hooksのpeerDependenciesとマッチしていないと言われました。

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: tenant-app@0.1.0
npm ERR! Found: react@18.2.0
npm ERR! node_modules/react
npm ERR!   react@"18.2.0" from tenant-app@0.1.0
npm ERR!   apps/tenant
npm ERR!     tenant-app@0.1.0
npm ERR!     node_modules/tenant-app
npm ERR!       workspace apps/tenant from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.9.0 || ^17.0.0" from @testing-library/react-hooks@8.0.1
npm ERR! node_modules/@testing-library/react-hooks
npm ERR!   dev @testing-library/react-hooks@"8.0.1" from tenant-app@0.1.0
npm ERR!   apps/tenant
npm ERR!     tenant-app@0.1.0
npm ERR!     node_modules/tenant-app
npm ERR!       workspace apps/tenant from the root project

調べてみるとどうやらそもそもReact18系では@testing-library/react-hooksではなく@testing-library/reactを使うのが正しいようなので、そちらに乗り換えました。yarn installでは気づかなかったので、npm installすることで事前に気づけるのは良いですね😊

https://zenn.dev/k_kazukiiiiii/articles/9f48bdd20435d2

tanstack-queryのバージョンを揃える

これでひとまずinstall自体は完了することができて、Next.jsを起動してみるのですが、tanstack周りでエラーが出て画面が表示されませんでした。詳細を見るとどうやら@tanstack/react-query@tanstack/react-query-devtoolsでバージョンが4.32.04.29.15と微妙に違っていたのが原因でした。yarnだとよしなに調整していたんですかね🤔 そもそもバージョンは揃えるべきなので4.32.0で揃えることで解消されました。

3. CIでも動くように調整

最後にCIでも動くように調整しました。ローカルではインストールできましたが、CI上でのインストールでもいくつかハマったのでそれについて挙げていきます。

sshを使った外部パッケージの指定方法がyarnとnpmで変わった

弊社ではプライベートの外部パッケージがありますが、これはローカルではSSHで認証して、CIではトークン付きのHTTPSで認証してインストールしています。

GitHub Actionsで外部パッケージをyarn installする
- name: Install
  run: |
    git config --global url."https://${{ secrets.CASONE_BOT_TOKEN }}:x-oauth-basic@github.com/CastingONE".insteadOf "git@github.com:CastingONE"
    yarn install

ただこれを単純にnpm installだけを変えても上手くリポジトリを見つけることができず、最終的には以下のように変えたら上手くいきました。insteadOfgit@github.com:CastingONEからssh://git@github.com/CastingONEに変えただけです。

GitHub Actionsで外部パッケージをnpm installに変える
 - name: Install
   run: |
-    git config --global url."https://${{ secrets.CASONE_BOT_TOKEN }}:x-oauth-basic@github.com/CastingONE".insteadOf "git@github.com:CastingONE"
+    git config --global url."https://${{ secrets.CASONE_BOT_TOKEN }}:x-oauth-basic@github.com/CastingONE".insteadOf "ssh://git@github.com/CastingONE"
-    yarn install
+    npm ci

理由はあんまり分かっていないですが、yarn.lockがある状態でnpm installすると以下のような差分が出ていたため、指定するパスも:から/に変える必要があったのかなと思っています🤔(ssh://も追加したのは念の為)

ルートディレクトリでnpm installしないとfailした

packagesディレクトリ配下のパッケージはworking-directoryを指定して該当のディレクトリでインストールからテストの実行などのタスクを実行していましたが、上手くインストールできませんでした。具体的に言うとインストール自体はできていますが、その後のpostinstallで自前のESLintルールをコンパイルしようとすると失敗していました。

パッケージコンポーネントのCIテスト
jobs:
  test:
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    defaults:
      run:
        working-directory: ./packages/components

    steps:
      - uses: actions/checkout@v3
        with:
          token: ${{ secrets.CASONE_BOT_TOKEN }}

      - uses: actions/setup-node@v3
        with:
          node-version-file: "package.json"
          cache: "npm"

      # postinstallでeslintのカスタムルールをコンパイルしているのだが、それに失敗する
      - name: Install
        run: |
          git config --global url."https://${{ secrets.CASONE_BOT_TOKEN }}:x-oauth-basic@github.com/CastingONE".insteadOf "ssh://git@github.com/CastingONE"
          npm ci

      - name: Run Testing
        run: npm run test

インストールに成功するCIとそうでないCIがあって原因特定に苦労しましたが、どうやらnpmでインストールする場合はルートディレクトリで行わないといけないようで、サブディレクトリでインストールすると、そのディレクトリにあるpackage.jsonのものしかインストールしないようです。yarn v1系ではどこにいてもルートのyarn.lockを見てインストールしてくれていましたが、npmだとその辺も注意する必要があるようです。

npm installができるようにルートディレクトリから実行する
 jobs:
   test:
     runs-on: ubuntu-22.04
     timeout-minutes: 10
     defaults:
       run:
         working-directory: ./packages/components

     steps:
       - uses: actions/checkout@v3
         with:
           token: ${{ secrets.CASONE_BOT_TOKEN }}

       - uses: actions/setup-node@v3
         with:
           node-version-file: "package.json"
           cache: "npm"

       - name: Install
         run: |
           git config --global url."https://${{ secrets.CASONE_BOT_TOKEN }}:x-oauth-basic@github.com/CastingONE".insteadOf "ssh://git@github.com/CastingONE"
           npm ci
+        # ルートディレクトリからじゃないと全てのパッケージをインストールしないため、ルートディレクトリを指定する
+        working-directory: ./

       - name: Run Testing
         run: npm run test

運用面での調整

以上で無事npmで動くようになりました!🎉 後は今後の運用にあたっていくつか設定を加えました。

yarnでinstallされないように only-allow npm を設定

今までyarnを使ってきたため、ついついyarn installしちゃうと思うため、それを防止する設定を入れました。only-allowというパッケージを使うと特定のパッケージマネージャーのみに絞ることができるため、これをpreinstallに仕込んでおきます。

package.json
{
  "scripts": {
    "preinstall": "npx only-allow npm"
  }
}

パッケージの追加は今後固定のみ受け付けるようにlintを入れる

今回念の為package.jsonに書かれているパッケージのバージョンを固定しましたが、今後は最初から固定になっていた方が安心なので以下の記事などを参考にして、バージョンが固定されているかをCIでチェックするようにしました。

https://zenn.dev/taka_shino/articles/b3d3a491008bed

ちなみに、デフォルトではnpm installでパッケージを追加すると^がついてしまいますが、それを取りたい場合はnpm config set save-exact trueを実行するか、インストール時に-Eオプションをつけると^なしでインストールできるようです。

https://dackdive.hateblo.jp/entry/2016/10/04/095200#:~:text=^ (キャレット) をつけずにインストールする方法はないのか?

その他

その他で作業中に気づいたこと以下に残します。

npm installするとyarn.lockも更新されるが、両方使うことは難しそう

npm installでもyarn.lockを見てくれるという情報がありましたが、その影響かyarn.lockファイルがある状態でnpm installするとyarn.lockファイルに変更が入ります。
これによってyarn installでも同じパッケージがインストールされるなら移行期間を設けても良かったかなぁと思いましたが、以下のような変更が出て、併用するのは難しそうでした。

  • registry先がyarnpkgからnpmjsに変わる
    • registry先が変わってしまうため、そのままyarn installしようとしても上手くいかなかった
  • workspaceのパッケージが追加されるが、場所がローカルファイルのパスで入ってしまう
    • ローカルファイルパスをgit commitしたくないのでこのdiffは困る
npm installしたことによるyarn.lockの差分の一部
 "@mui/material@5.14.0":
   version "5.14.0"
-  resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.0.tgz#3d2afb4a3643774370cb5add873abcbbe8e7af27"
+  resolved "https://registry.npmjs.org/@mui/material/-/material-5.14.0.tgz"

+"@casone/components@file:[個人のファイルパス]":
+  version "1.0.0"
+  resolved "file:packages/components"
+  dependencies:
+    "@mui/x-date-pickers" "6.8.0"
+    "@tanstack/react-table" "8.15.3"
+    colord "2.9.3"
+    deep-object-diff "1.1.9"
+    lodash-es "4.17.21"
+    react "18.2.0"
+    react-colorful "5.6.1"
+    react-cropper "2.3.3"
+    react-device-frameset "1.3.4"
+    react-dom "18.2.0"
+    react-nl2br "1.0.4"
+    react-swipeable-views "npm:@gromy/react-swipeable-views@0.15.1"
+    swiper "10.3.1"

タスクの実行自体はyarnを使っても問題ない

yarn installを防ぐためにnpx only-allow npmをpreinstallに入れましたが、これはあくまでinstall時にチェックが走るだけで、通常のタスク実行ではスルーされます。したがって yarn workspace @casone/components storybook みたいにコマンドを叩いてもそのまま実行されてしまいますが、特に問題なく動いていました。パッケージを入れる時が重要なだけで、タスクの実行自体は何でやっても変わらないんですかね🤔 問題なさそうな感じだったので一旦はCIやpackage.json内で書かれているyarnを使ってタスクを実行しているものはそのままにして、徐々にnpmに変えていこうかなと思っています。

おわりに

以上がyarn v1からnpmに移行した流れでした。
npmの方がyarn v1よりも厳格で、パッケージの依存関係も綺麗になって良かったです。
また、npmで安定稼働を確認した後にstorybookのアップデートも試しましたが、無事アップデートすることができました😊

弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/836878
https://www.wantedly.com/projects/1130967
https://www.wantedly.com/projects/1244229

Discussion