npm → pnpm v10移行(Turborepo編)
はじめに
今回は現在プロジェクトで使用しているturborepoというmonorepoをツール上で、npmからpnpm(v10.0.0)に移行した時の設定やつまづきポイントの備忘録となってます。
移行する上での変更点
- package.json周り
- .npmrcの管理
- nodejsのバージョン管理
- CI周り
1. package.json周り
i. workspaceの変更
npmからpnpmのworkspaceに移行する際は、
・pnpm-workspace.yamlに各アプリケーション、パッケージのpackage.jsonをrootで管理させるために作成します。
例)turborepoでは、apps, packages毎に区切られているので以下のように定義
packages:
- 'apps/*'
- 'packages/*'
公式:https://pnpm.io/ja/workspaces
そして、npmではworkspace内の内部パッケージの参照を@hoge/eslint-config: "*"
として参照していましたが、pnpmでは@hoge/eslint-config: "workspace*"
として定義することで内部パッケージを参照することができます。
例)内部パッケージを参照した時の定義
{
"name": "@hoge/ui",
"version": "0.0.0",
"private": true,
"exports": {
///
},
"devDependencies": {
- "@hoge/eslint-config": "*",
+ "@hoge/eslint-config": "workspace:*",
"@turbo/gen": "^2.4.0",
///
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
i. 各種コマンド変更
①npx
→ pnpm exec
・pnpm exex
は、インストール済みであるパッケージのシェルコマンドを実行するために使用します。
私はこの存在を知らず最新バージョンのパッケージを実行するpnpx(pnpm dlx)
に置き換えていて、後から誤っていることに気づきました💧
②npm run
→ pnpm
・pnpm
(or pnpm run
)でコマンド実行するので置き換え
i. packageManegerの変更
packageManagerをpnpmに変更。
後に紹介する、CI周りのバージョン参照やrenovateもバージョンアプデを参照してくれるのも助かります。
{
"name": "pnpm-turborepo",
"private": true,
///
- "packageManager": "npm@10.9.2",
+ "packageManager": "pnpm@10.0.0",
"engines": {
///
}
}
2. .npmrcの管理
- auto-install-peers = true
- engine-strict=true
+ link-workspace-packages=true
+ prefer-workspace-packages=true
+ public-hoist-pattern[]=*eslint*
上記の管理に変わりました。上から見ていきます。
auto-install-peers
の削除
① pnpm v10からデフォルトとなったので不要
公式:https://pnpm.io/next/npmrc#auto-install-peers
engine-strict
の削除
② 後に紹介しますが、pnpm.executionEnv.nodeVersion
でnodeVersionを管理することができたので外しました。
link-workspace-packages
の追加
③ workspace 内で管理されているパッケージが、package.json
に定義されたバージョンと一致する場合、シンボリックリンクとして扱われます。
通常、pnpm は外部パッケージ(react など)をデフォルトでシンボリックリンク化しますが、ワークスペース内の内部パッケージをシンボリックリンク化するには、この設定を true
にする必要があります。
・trueの場合:内部パッケージ(例:packages/ui
,packages/libs
)にreactがある場合、rootのnode_modules
に一度だけreactがインストールされ、それぞれ参照可能。
・falseの場合:逆にリンク化されていないと、packages/ui, packages/libsにreactがある場合それぞれのnode_modules内でreactをインストール・管理。
公式 https://pnpm.io/npmrc#link-workspace-packages
prefer-workspace-packages
の追加
④ ワークスペース内のパッケージが、npm レジストリのパッケージよりも優先して使用されます。」
そうすることで内部パッケージを変更した際、参照先にも即時反映されます。
例えば、workspace(monorepo)内にあるシンボリックリンクとして参照されるpackages/ui,packages/libs
があります。そして、apps/hoge
が packages/ui
を参照している場合に、packages/ui
を変更すれば、即座に apps/hoge
にも反映されます。
公式 https://pnpm.io/npmrc#prefer-workspace-packages
public-hoist-pattern[]=*eslint*
の追加
⑤ node_moduels
内でネストしているパッケージを、node_modules内のrootに引き上げる(hoist)するための設定です。
こちらは、筆者が最近直面したvscodeの拡張機能であるESLintが突如効いていないことがありました。
原因としては、VSCodeのESLintがnode_modules
内のネストされているeslintのパッケージを参照できていませんでした。
公式 https://pnpm.io/npmrc#hoist-workspace-packages
pnpmではnode_modules/.pnpm/
に依存パッケージがあり、node_modules配下のパッケージはシンボリックリンクされている構造です。そのnode_modules
の構造の読み込みが出来ていない関係でESLintが機能してなかったので注意が必要です。
3. nodejsのバージョン管理
今までは、リポジトリのnodejsバージョン管理をvoltaを活用していましたがpnpmで指定したnodejsのバージョンをコマンド実行することが可能になったのでvoltaを卒業しました。
具体的には、pnpm.executionEnv.nodeVersionの機能でpnpm run
などの実行時に、指定されたnodejsのバージョンで実行してくれます。
(CI上でも、pnpm実行時に定義されているnodeVersionを参照してくれます。)
公式: https://pnpm.io/ja/package_json#pnpmexecutionenvnodeversion
{
"name": "my-turborepo",
"private": true,
///
"packageManager": "pnpm@10.0.0",
- "engines": {
- "node": "20.x"
- },
- "volta": {
- "node": "20.18.1",
- "npm": "10.9.2"
- },
+ "pnpm": {
+ "executionEnv": {
+ "nodeVersion": "20.18.1"
+ }
+ }
}
4. CI周り
主にCIで変更した箇所は以下です。
-
actions/setup-node
→pnpm/action-setup
-
actions/cache
で指定してるpathをnode_modules
→**/node_modules
へ - 各種pnpmコマンドへ移行
nodeバージョン管理でも説明入れてますが、CI上のnodeバージョンもpnpmのコマンド実行時に、自動的に指定されたnodeバージョンで実行しているので、setup-node
が不要になりました。
2の**/node_modules
ですが、ワークスペースで運用している都合上、appsやpackages配下の各node_modules
を参照する必要があります。
github action周りの改修した内容
name: Checks on PullRequest
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: 'package.json'
- cache: 'npm'
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
- name: Cache node_modules
uses: actions/cache@v4
id: node_modules_cache_id
with:
+ path: node_modules
- key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/package-lock.json')) }}
- path: '**/node_modules'
+ key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/pnpm-lock.yaml')) }}
- name: Install Dependencies
if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
- run: npm ci
+ run: pnpm install --frozen-lockfile
lint-check-types:
needs: setup
name: Run lint and check-types
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Restore node_modules from cache
uses: actions/cache@v4
with:
- path: node_modules
- key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/package-lock.json')) }}
+ path: '**/node_modules'
+ key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/pnpm-lock.yaml')) }}
- name: Run lint
- run: npm run lint
+ run: pnpm lint
- name: Run check-types
- run: npm run check-types
+ run: pnpm check-types
test:
needs: setup
name: Run test
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Restore node_modules from cache
uses: actions/cache@v4
with:
- path: node_modules
- key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/package-lock.json')) }}
+ path: '**/node_modules'
+ key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/pnpm-lock.yaml')) }}
- name: Run test
- run: npm run test
+ run: pnpm test
build:
needs: setup
name: Run build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Restore node_modules from cache
uses: actions/cache@v4
with:
- path: node_modules
- key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/package-lock.json')) }}
+ path: '**/node_modules'
+ key: ${{ runner.os }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/pnpm-lock.yaml')) }}
- name: Run build
- run: npm run build
+ run: pnpm build
実際に変更してみて
- ローカルインストールの速度向上
- node_modulesのパッケージ管理の厳格化
- voltaをやめてpnpmだけでパッケージマネージャーとnodeのversion管理
が感じたメリットでした。
node_modules
のインストールは元々npm ci
でやってました。pnpmだと4倍近く早くなりました。
npm ci: 約80秒
pnpm install --frozen-lockfile: 17秒
また、モノレポで管理している内部Packagesのパッケージ依存関係をそれぞれ定義しているものの、npmだとnode_modules
内部はフラットに管理されているため、依存関係に関係なく参照ができてしまうため厳密な管理ができておらず、pnpmでだいぶ厳密管理することができました。
参考記事
Discussion