Open21

npm から pnpm に移行する取り組み

Futa HirakobaFuta Hirakoba

最近 pnpm がセキュリティ的な意味でアツい。minimumReleaseAge[1] が入ったり postInstall がデフォルトで実行されなくなったり。なんかセキュアらしい。

npm から yarn、そして npm へ戻ってきた過去があるが、今度は pnpm に行ってみるぞ

脚注
  1. なんか npm の --before より使いやすいらしい。 ↩︎

Futa HirakobaFuta Hirakoba

今回やったこと

ローカルマシン

  1. npm/npx の封印
  2. pnpm のグローバルインストール
  3. minimumReleaseAge の設定
    • pnpm config set --location global minimumReleaseAge 10080

リポジトリ(ローカル開発ができるようになるまで)

  1. リポジトリで使う pnpm バージョンを指定(package.json の packageManager フィールド)
  2. package-lock.json から pnpm-lock.yaml への移行(pnpm import
  3. package.json の npm スクリプト内にある npmnpx を置き換え
    • npm -> pnpm
    • npx -> pnpm dlx
  4. pnpm-workspace.yaml を作成
  5. pnpm-workspace.yaml に minimumReleaseAge: 10080 # 1 week を追加
  6. 動作確認

リポジトリ(CI/CD が通るようになるまで)

  1. pnpm 利用ジョブに pnpm/action-setup アクションを追加
  2. actions/setup-node アクションの with.cache を pnpm に変更
  3. actions/setup-node アクションの with.cache-dependency-path を pnpm-lock.yaml に変更
  4. npm cipnpm install --frozen-lockfile に置き換え
  5. npm runpnpm run に置き換え
    • スクリプト名の直後に -- がある場合、削除
  6. npm lspnpm ls に置き換え
    • --json の形式が npm と異なるため、後続処理を修正
  7. CI を走らせて、通るようになるまでがんばる

色々やったし、このリポジトリ特有の作業もした。
ある程度は自動化できる部分自動化したいな。

nixiemintonnixieminton

素敵なスクラップを作成頂きありがとうございます。大変参考になりました。
まとめコメントのみ読む私のような浅はかな読者向けに、一点だけ補足コメントです。

npx -> pnpm dlx

npxの用途としてローカルのnode_modules中のバージョンを実行したいケースがあると思うのですが、
その場合に常に最新版をフェッチして実行するpnpm dlxは不適当でした。
npxのうち「依存関係としてインストールせず実行する」という目的のものだけ置き換えるのが良さそうです。

Futa HirakobaFuta Hirakoba

グローバルインストール

npm はいい。node 入れたらくっついてくるから。
pnpm は自分でインストールしないといけない。

ランタイムの管理は mise を使っており、リポジトリごとに node がバラバラに存在する。その都度 pnpm をインストールするのはだるすぎるので、グローバルに一つ持っておきたい。mise と良い感じに組み合わせる方法はあるのか?

Futa HirakobaFuta Hirakoba

見つけた。

https://zenn.dev/euxn23/articles/399a6815ddac93

どうやら pnpm v10 からは package.json の packageManager フィールドの pnpm バージョンを勝手に使ってくれるとか。
ということは node とは別に mise で pnpm を管理する必要はないか。代わりに packageManager フィールドを使うことになるわけだけど。

となるので、 mise で pnpm v10 以上を入れておく (mise use pnpm) で良いと思います。

とりあえずこれを実践する。

❯ mise use pnpm --global
mise 2025.8.10 by @jdx – install ✓ installed                                                                                                                                                                             mise ~/.config/mise/config.toml tools: pnpm@10.16.1

❯ pnpm -v
10.16.1

入った。

Futa HirakobaFuta Hirakoba

npm/npx を封印

せっかく pnpm を入れても npm を使ってしまったら意味ない。
ここは npm/npx を使えないようにする。

Futa HirakobaFuta Hirakoba

使ったら失敗するようにする。真面目に考えてもいいけど、ここは雑に同名のエイリアスを ~/.zshrc のラストにツッコむ。

alias npx='echo "WARNING: npx は実行しないでください" && false'
alias npm='echo "WARNING: npm は実行しないでください" && false'

末尾にくっつけて source ~/.zshrc

❯ npm
WARNING: npm は実行しないでください

❯ npx
WARNING: npx は実行しないでください

甘えは捨てた。

Futa HirakobaFuta Hirakoba

リポジトリで npm から pnpm へ移行(ローカル環境編)

npm/npx を封印したことでいよいよ npm 使ってるリポジトリでローカル開発ができなくなった。
なんとかしないと。

Futa HirakobaFuta Hirakoba

最初に選ばれたリポジトリは自分のホムペリポジトリでした。

https://github.com/korosuke613/homepage-2nd

前述した pnpm v10 で corepack 不要で pnpm 自身のバージョン管理が可能に には次のように書いてある。

  1. ローカルの pnpm を v10 以上にする。
  2. プロジェクトの devDependencies に、使いたいバージョンの pnpm をインストールする
  3. プロジェクトの packageManager field に 2 と同じバージョンの pnpm を定義する

まずは pnpm を devDependencies としてインスコするか。
npm はもう使えないのでさっそく pnpm でインストールすることになる??

グローバルの pnpm と同じバージョンにするとちゃんと packageManager のバージョンを使ってくれるか確認できないので、ここは別バージョンにする。
何があるかな?

❯ pnpm info pnpm

pnpm@10.17.0 | MIT | deps: none | versions: 1143
Fast, disk space efficient package manager
https://pnpm.io

keywords: pnpm, pnpm10, dependencies, dependency manager, efficient, fast, hardlinks, install, installer, link, lockfile, modules, monorepo, multi-package, npm, package manager, package.json, packages, prune, rapid, remove, shrinkwrap, symlinks, uninstall, workspace

bin: pnpm, pnpx

dist
.tarball: https://registry.npmjs.org/pnpm/-/pnpm-10.17.0.tgz
.shasum: 530696ed31b10e1c6bf46a441a9b7c1a3159910c
.integrity: sha512-/Oij3Smk7S7FZvtT77sE2MRKDwW8bySnMEaRD7nDznr6NaCYBQBmj6NXM0W9ZEZE+pgzj6FoI1yA9KoXqhf77w==
.unpackedSize: 17.7 MB

maintainers:
- zkochan <z@kochan.io>
- pnpmuser <publish-bot@pnpm.io>

dist-tags:
dev: 6.23.7-202112041634  latest-2: 2.25.7          latest-5: 5.18.10         latest-8: 8.15.9          next-10: 10.17.0          next-8: 8.15.9
latest-10: 10.17.0        latest-3: 3.8.1           latest-6: 6.35.1          latest-9: 9.15.9          next-6: 6.35.1            next-9: 9.15.9
latest-1: 1.43.1          latest-4: 4.14.4          latest-7: 7.33.5          latest: 10.17.0           next-7: 7.33.7            pr4475: 0.0.0-pr4475.1

published yesterday by pnpmuser <publish-bot@pnpm.io>

npm infoと似たノリでpnpm info できた。ていうか v10 の最新が v10.17.0 なんだが??
mise で入れたグローバルの pnpm は v10.16.2 だ。最新バージョンがレジストリに反映されてないのだろうか。まあいい

リポジトリでは v10.17.0 を使うようにしよう。

`pnpm i -D pnpm@10.17.0`
❯ pnpm i -D pnpm@10.17.0
 WARN  Moving @biomejs/biome that was installed by a different package manager to "node_modules/.ignored"
 WARN  Moving @chromatic-com/storybook that was installed by a different package manager to "node_modules/.ignored"
 WARN  Moving @playwright/experimental-ct-react that was installed by a different package manager to "node_modules/.ignored"
 WARN  Moving @playwright/test that was installed by a different package manager to "node_modules/.ignored"
 WARN  Moving @proofdict/textlint-rule-proofdict that was installed by a different package manager to "node_modules/.ignored"
 WARN  55 other warnings

   ╭───────────────────────────────────────────╮
   │                                           │
   │   Update available! 10.16.1 → 10.17.0.    │
   │   Changelog: https://pnpm.io/v/10.17.0    │
   │   To update, run: pnpm add -g @pnpm/exe   │
   │                                           │
   ╰───────────────────────────────────────────╯

Downloading storybook@9.0.14: 8.79 MB/8.79 MB, done
Downloading react-icons@5.5.0: 22.22 MB/22.22 MB, done
Downloading sylvester@0.0.12: 7.08 MB/7.08 MB, done
Downloading wordnet-db@3.1.14: 10.55 MB/10.55 MB, done
Downloading @biomejs/cli-darwin-arm64@2.1.1: 13.43 MB/13.43 MB, done
Downloading @img/sharp-libvips-darwin-arm64@1.2.3: 7.57 MB/7.57 MB, done
Downloading kuromoji@0.1.2: 21.83 MB/21.83 MB, done
 WARN  4 deprecated subdependencies found: @types/minimatch@6.0.0, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0
Packages: +1337
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 1417, reused 182, downloaded 1155, added 1337, done
 WARN  Issues with peer dependencies found
.
├─┬ @storybook/react-vite 9.0.14
│ └─┬ @joshwooding/vite-plugin-react-docgen-typescript 0.6.0
│   └── ✕ unmet peer vite@"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0": found 7.1.6
└─┬ @storybook/addon-vitest 9.1.7
  └── ✕ unmet peer storybook@^9.1.7: found 9.0.14

dependencies:
+ @astrojs/check 0.9.4
+ @astrojs/db 0.15.1 (0.18.0 is available)
+ @astrojs/mdx 4.3.5
+ @astrojs/partytown 2.1.4
+ @astrojs/react 4.3.0 (4.3.1 is available)
+ @astrojs/rss 4.0.12
+ @astrojs/sitemap 3.6.0
+ @astrojs/tailwind 6.0.2
+ @docsearch/css 3.9.0 (4.0.1 is available)
+ @docsearch/react 3.9.0 (4.0.1 is available)
+ @google-analytics/data 4.12.1 (5.2.0 is available)
+ @tailwindcss/aspect-ratio 0.4.2
+ @tailwindcss/typography 0.5.16 (0.5.17 is available)
+ astro 5.13.9
+ astro-meta-tags 0.3.2 (0.4.0 is available)
+ astro-robots-txt 1.0.0
+ date-fns 4.1.0
+ emoji-regex 10.4.0
+ glob 11.0.3
+ natural 8.1.0
+ react 19.1.1
+ react-dom 19.1.1
+ react-icons 5.5.0
+ rehype-external-links 3.0.0
+ rehype-stringify 10.0.1
+ remark-extract-frontmatter 3.2.0
+ remark-frontmatter 5.0.0
+ remark-gfm 4.0.1
+ remark-parse 11.0.0
+ remark-rehype 11.1.2
+ simple-git 3.28.0
+ strip-ansi 7.1.0
+ tailwindcss 3.4.17 (4.1.13 is available)
+ typescript 5.9.2
+ unified 11.0.5

devDependencies:
+ @biomejs/biome 2.1.1 (2.2.4 is available)
+ @chromatic-com/storybook 4.0.1 (4.1.1 is available)
+ @playwright/experimental-ct-react 1.55.0
+ @playwright/test 1.55.0
+ @proofdict/textlint-rule-proofdict 3.1.2
+ @storybook/addon-a11y 9.0.14 (9.1.7 is available)
+ @storybook/addon-docs 9.0.14 (9.1.7 is available)
+ @storybook/addon-links 9.0.14 (9.1.7 is available)
+ @storybook/addon-vitest 9.1.7
+ @storybook/react-vite 9.0.14 (9.1.7 is available)
+ @types/react 19.1.11 (19.1.13 is available)
+ @vitest/browser 3.2.4
+ @vitest/coverage-v8 3.2.4
+ @vitest/ui 3.2.4
+ chromatic 11.29.0 (13.1.5 is available)
+ concurrently 9.2.0 (9.2.1 is available)
+ playwright 1.55.0
+ pnpm 10.17.0
+ storybook 9.0.14 (9.1.7 is available)
+ textlint 14.8.4 (15.2.2 is available)
+ textlint-filter-rule-comments 1.2.2
+ textlint-rule-preset-ja-spacing 2.4.3
+ textlint-rule-preset-ja-technical-writing 10.0.2 (12.0.2 is available)
+ textlint-rule-spellcheck-tech-word 5.0.0
+ vite-plugin-turbosnap 1.0.3
+ vitest 3.2.4

╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮
│                                                                                            │
│   Ignored build scripts: esbuild, protobufjs, sharp.                                       │
│   Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.   │
│                                                                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

Done in 55.5s using pnpm v10.16.1

色々でた!!!

Ignored build scripts: esbuild, protobufjs, sharp.

ビルドスクリプトがブロックされた。これがセキュリティ向上機能ってことか。
2. pnpmのビルドスクリプト警告対応 が参考になる。あとで考える。
ていうか protobufjs ってどこで使われてるんだ。protobuf と無縁のリポジトリなんだけど。

❯ pnpm ls --depth Infinity protobufjs
Legend: production dependency, optional only, dev only

korosuke613-homepage@2.0.0 /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd (PRIVATE)

dependencies:
@google-analytics/data 4.12.1
└─┬ google-gax 4.6.1
  ├─┬ @grpc/grpc-js 1.14.0
  │ └─┬ @grpc/proto-loader 0.8.0
  │   └── protobufjs 7.5.4
  ├─┬ @grpc/proto-loader 0.7.15
  │ └── protobufjs 7.5.4
  ├─┬ proto3-json-serializer 2.0.2
  │ └── protobufjs 7.5.4
  └── protobufjs 7.5.4

Google Analyticsェ...

Futa HirakobaFuta Hirakoba

pnpm-lock.yaml が爆誕してた。
yaml なんだ。

ていうか調べたら pnpm import という機能があるらしい。既存の package-lock.json から移行できると。

変更を取り消してまずは pnpm import する

❯ pnpm import
 WARN  `node_modules` is present. Lockfile only installation will make it out-of-date
 WARN  4 deprecated subdependencies found: @types/minimatch@6.0.0, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0
Progress: resolved 1416, reused 0, downloaded 0, added 0, done
 WARN  Issues with peer dependencies found
.
├─┬ @storybook/addon-vitest 9.1.7
│ └── ✕ unmet peer storybook@^9.1.7: found 9.0.14
└─┬ @storybook/react-vite 9.0.14
  └─┬ @joshwooding/vite-plugin-react-docgen-typescript 0.6.0
    └── ✕ unmet peer vite@"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0": found 7.1.6

色々出たが今は無視する。

再度 pnpm i -D pnpm@10.17.0

❯ pnpm i -D pnpm@10.17.0
 WARN  4 deprecated subdependencies found: @types/minimatch@6.0.0, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0
Already up to date
Progress: resolved 1417, reused 1337, downloaded 0, added 0, done
 WARN  Issues with peer dependencies found
.
├─┬ @storybook/addon-vitest 9.1.7
│ └── ✕ unmet peer storybook@^9.1.7: found 9.0.14
└─┬ @storybook/react-vite 9.0.14
  └─┬ @joshwooding/vite-plugin-react-docgen-typescript 0.6.0
    └── ✕ unmet peer vite@"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0": found 7.1.6

devDependencies:
+ pnpm 10.17.0

Done in 4.6s using pnpm v10.16.1

先に import したのでスリムな結果に。
package-lock.json くんはサヨナラする。

package.json に packageManager を加える。

❯ git diff package.json
diff --git a/package.json b/package.json
index 1cef60d..dcaf469 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "name": "Futa Hirakoba",
     "url": "https://github.com/korosuke613"
   },
+  "packageManager": "pnpm@10.17.0",
   "scripts": {
     "build": "astro check && tsc --noEmit && astro build",
     "build-types": "tsc --noEmit --pretty",

果たして 10.17.0 が使われるのか?

❯ pnpm -v
10.17.0

❯ cd ..

❯ pnpm -v
10.6.5

使われてる〜

じゃあ今度は devDependencies に入れてないバージョンだとどうなるか見るか。

❯ git diff package.json
diff --git a/package.json b/package.json
index 1cef60d..5a968c1 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "name": "Futa Hirakoba",
     "url": "https://github.com/korosuke613"
   },
+  "packageManager": "pnpm@10.16.0",
   "scripts": {
     "build": "astro check && tsc --noEmit && astro build",
     "build-types": "tsc --noEmit --pretty",

10.16.0 は未インストール。

❯ pnpm -v
10.16.0

ちょっと時間かかったけど普通に実行できたぞ???
これ devDependencies へ入れる必要ないのでは?

ただし、その pnpm 自体はどこかしらにインストールされている必要があり、一般的にはプロジェクトの devDependencies にインストールするものかと思われます。
pnpm v10 で corepack 不要で pnpm 自身のバージョン管理が可能に

なるほど。pnpm 自体は必要だよって書かれているだけなので、mise でグローバルに入れてるので事足りてる可能性あるのか。そういうことにしておこう。

Futa HirakobaFuta Hirakoba

動作確認する。

先ほどビルドスクリプトが走らなかったが、ホームページの dev サーバ起動、ビルド、静的解析、テストをやってみる。

  • 開発サーバ
    • pnpm start
  • SSG
    • pnpm run build && pnpm run preview
  • 静的解析
    • pnpm run lint
  • テスト
    • ユニットテスト
      • pnpm run test:unit
    • storybook を使ったやつ
      • pnpm run test:storybook
    • playwright を使ったやつ
      • pnpm exec playwright install
      • pnpm run test:playwright-ct
      • pnpm run test:playwright-e2e
  • VRT
    • pnpm run vrt:init
    • pnpm run vrt:regression


ページ遷移とかjsも動いたから良さそう

❯ pnpm run test:storybook

> korosuke613-homepage@2.0.0 test:storybook /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd
> vitest run --project=storybook --coverage

failed to load config from /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/vite.config.mts

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/@storybook+addon-vitest@9.1.7_@vitest+browser@3.2.4_@vitest+runner@3.2.4_react-dom@19.1_c150aa282900596a9da5ced895cd2ef0/node_modules/@storybook/addon-vitest/dist/vitest-plugin/index.mjs:10
import { optionalEnvToBoolean, getInterpretedFile, normalizeStories, validateConfigurationFiles, DEFAULT_FILES_PATTERN, resolvePathInStorybookCache } from 'storybook/internal/common';
         ^^^^^^^^^^^^^^^^^^^^
SyntaxError: The requested module 'storybook/internal/common' does not provide an export named 'optionalEnvToBoolean'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:220:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:321:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
    at async loadConfigFromBundledFile (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:36230:12)
    at async bundleAndLoadConfigFile (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:36116:17)
    at async loadConfigFromFile (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:36083:42)
    at async resolveConfig (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:35732:22)
    at async _createServer (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:28008:67)
    at async createViteServer (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.5.2_@vitest+browser@3.2.4_@vitest+ui@3.2.4_jiti@1.21.7_yaml@2.8.1/node_modules/vitest/dist/chunks/cli-api.BkDphVBG.js:6911:17)
    at async createVitest (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.5.2_@vitest+browser@3.2.4_@vitest+ui@3.2.4_jiti@1.21.7_yaml@2.8.1/node_modules/vitest/dist/chunks/cli-api.BkDphVBG.js:10202:17)



 ELIFECYCLE  Command failed with exit code 1.
❯ pnpm run test:unit

> korosuke613-homepage@2.0.0 test:unit /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd
> vitest run --project=unit --coverage

failed to load config from /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/vite.config.mts

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/@storybook+addon-vitest@9.1.7_@vitest+browser@3.2.4_@vitest+runner@3.2.4_react-dom@19.1_c150aa282900596a9da5ced895cd2ef0/node_modules/@storybook/addon-vitest/dist/vitest-plugin/index.mjs:10
import { optionalEnvToBoolean, getInterpretedFile, normalizeStories, validateConfigurationFiles, DEFAULT_FILES_PATTERN, resolvePathInStorybookCache } from 'storybook/internal/common';
         ^^^^^^^^^^^^^^^^^^^^
SyntaxError: The requested module 'storybook/internal/common' does not provide an export named 'optionalEnvToBoolean'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:220:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:321:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
    at async loadConfigFromBundledFile (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:36230:12)
    at async bundleAndLoadConfigFile (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:36116:17)
    at async loadConfigFromFile (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:36083:42)
    at async resolveConfig (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:35732:22)
    at async _createServer (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vite@7.1.6_@types+node@24.5.2_jiti@1.21.7_yaml@2.8.1/node_modules/vite/dist/node/chunks/dep-D5b0Zz6C.js:28008:67)
    at async createViteServer (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.5.2_@vitest+browser@3.2.4_@vitest+ui@3.2.4_jiti@1.21.7_yaml@2.8.1/node_modules/vitest/dist/chunks/cli-api.BkDphVBG.js:6911:17)
    at async createVitest (file:///Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.5.2_@vitest+browser@3.2.4_@vitest+ui@3.2.4_jiti@1.21.7_yaml@2.8.1/node_modules/vitest/dist/chunks/cli-api.BkDphVBG.js:10202:17)



 ELIFECYCLE  Command failed with exit code 1.

ユニットテストも storybook も vitest 使ってる。
なーんか vitest 関連が失敗してそう。一回 node_modules を吹き飛ばしてみたが状況変わらず。

そもそもいくつかのパッケージのバージョンが固定されてなかったため、バージョンが合わずに失敗していたっぽい。バージョンを固定した上で実行したら全て通った。
これはバージョンは固定しようねって話。

また、npm script で npx を使うものがあった[1]ので、pnpm dlx に置き換え。

❯ git diff package.json
diff --git a/package.json b/package.json
index fc42976..def4565 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "vrt:regression": "playwright test -c playwright-vrt.config.ts ./src/tests/vrt/regression.spec.ts",
     "storybook": "storybook dev -p 6006",
     "build-storybook": "storybook build --debug --disable-telemetry",
-    "chromatic": "npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
+    "chromatic": "pnpm dlx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
   },
   "dependencies": {
     "@astrojs/check": "0.9.4",

ローカルの環境は完成かも。

脚注
  1. ていうかなんで npx 使ってるんだろ?いらなくない? ↩︎

Futa HirakobaFuta Hirakoba

リポジトリで npm から pnpm へ移行(CI 編)

ローカルで移行できても CI では移行できていない。ここからが本当の戦いだ(たぶん CI を何度も実行し直すことになる)

Futa HirakobaFuta Hirakoba

さて、どうするか

とりあえず CI で npm を使ってそうな箇所を洗い出す。

`grep -r "npm" .github/workflows --include="*.yml" --include="*.yaml"`
❯ grep -r "npm" .github/workflows --include="*.yml" --include="*.yaml"
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/ci.yaml:      - run: npm run lint
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/ci.yaml:        run: npm run build
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/ci.yaml:        run: npm run test:unit
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/ci.yaml:        run: npm run test:playwright-e2e -- --retries=2 --workers=2
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/ci.yaml:        run: npm run test:playwright-ct
.github/workflows/ci.yaml:        run: npm run test:storybook
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/ci.yaml:          cache: 'npm'
.github/workflows/ci.yaml:        run: npm ci
.github/workflows/pages.yml:          cache: 'npm'
.github/workflows/pages.yml:        run: npm ci
.github/workflows/pages.yml:        run: npm run db:update
.github/workflows/pages.yml:          cache: 'npm'
.github/workflows/pages.yml:        run: npm ci
.github/workflows/pages.yml:          npm run build
.github/workflows/pages.yml:          npm run build -- --remote
.github/workflows/update-blogs-data.yaml:          cache: 'npm'
.github/workflows/update-blogs-data.yaml:        run: npm ci
.github/workflows/vrt-init.yaml:          cache: 'npm'
.github/workflows/vrt-init.yaml:        run: npm ci
.github/workflows/vrt-init.yaml:          npm run vrt:init
.github/workflows/vrt-regression.yaml:          cache: 'npm'
.github/workflows/vrt-regression.yaml:        run: npm ci
.github/workflows/vrt-regression.yaml:            npm run vrt:init
.github/workflows/vrt-regression.yaml:            npm run vrt:regression -- --retries=1 --grep="update dependencies"
.github/workflows/vrt-regression.yaml:            npm run vrt:regression -- --retries=1 --grep="add contents"
.github/workflows/copilot-setup-steps.yml:          cache: "npm"
.github/workflows/copilot-setup-steps.yml:        run: npm ci
.github/workflows/cache.yaml:          cache: 'npm'
.github/workflows/cache.yaml:        id: npm-install
.github/workflows/cache.yaml:          npm ci
.github/workflows/cache.yaml:          PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
.github/workflows/cache.yaml:          key: playwright-${{ steps.npm-install.outputs.PLAYWRIGHT_VERSION }}
.github/workflows/cache.yaml:        run: npm run build
.github/workflows/cache.yaml:        run: npm run test:unit
.github/workflows/cache.yaml:          cache: 'npm'
.github/workflows/cache.yaml:          npm ci

対象ファイル

  • .github/workflows/ci.yaml
  • .github/workflows/cache.yaml
  • .github/workflows/pages.yml
  • .github/workflows/vrt-init.yaml
  • .github/workflows/vrt-regression.yaml
  • .github/workflows/update-blogs-data.yaml
  • .github/workflows/copilot-setup-steps.yml

利用内容

  • パッケージインストール: npm ci
  • npm スクリプト実行: npm run
  • actions/setup-node のキャッシュ: cache: 'npm'
  • playwright のバージョン特定: npm ls
  • サブディレクトリ(./tools[1]
    • cache-dependency-path: 'package-lock.json'
    • cache-dependency-path: 'tools/package-lock.json'

上記に加え、pnpm/actions-setup を使って pnpm のセットアップが必要になる。

脚注
  1. そういえばサブディレクトリの ./tools の存在を忘れていた。こっちはまだ pnpm 移行できていないので後回しにする。 ↩︎

Futa HirakobaFuta Hirakoba

pnpm/actions-setup 入れる

まずは環境を整えるため、npm を使っているワークフローで pnpm/actions-setup を入れる。

Omit version input to use the version in the packageManager field in the package.json.

   steps:
     - uses: pnpm/action-setup@v4

https://github.com/pnpm/action-setup/blob/f2b2b233b538f500472c7274c7012f57857d8ce0/README.md#install-only-pnpm-with-packagemanager

どうやら with.version を設定しなければ、packageManager フィールドのバージョンを使ってくれるらしい。すばらしい。

      - name: Install pnpm
        uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

バージョン固定して上記を差し込む。pnpm/actions-setup の README では、setup-node より前に置いてたのでマネした。

Futa HirakobaFuta Hirakoba

pnpm をキャッシュの対象にする

自分は setup-node で npm のキャッシュを取るようにしていた。setup-node は pnpm のキャッシュにも対応している。pnpm に変える場合は次をする。

  • setup-node で cache: 'npm'cache: 'pnpm' に変える
  • setup-node で cache-dependency-path を設定している場合は、package-lock.json から pnpm-lock.yaml に変える。

単純に置換した。

Futa HirakobaFuta Hirakoba

npm コマンドの利用箇所を pnpm に変える

変えて行く。

  • npm ci -> pnpm install --frozen-lockfile
  • npm run -> pnpm run
  • npm ls -> pnpm ls

npm cinpm run の置き換えは特に困らないが、npm lspnpm ls と挙動が異なるようで、今までの使ってたコマンドが動かなくなった。

# pnpm ls はうまくいかない
❯ pnpm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version'
jq: error (at <stdin>:16): Cannot index array with string "dependencies"

# npm ls はうまく行く
❯ npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version'
1.55.0

json を見てみる。

npm ls --json @playwright/test | jqpnpm ls --json @playwright/test | jq
[
  {
    "name": "korosuke613-homepage",
    "version": "2.0.0",
    "path": "/Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd",
    "private": true,
    "devDependencies": {
      "@playwright/test": {
        "from": "@playwright/test",
        "version": "1.55.0",
        "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
        "path": "/Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd/node_modules/.pnpm/@playwright+test@1.55.0/node_modules/@playwright/test"
      }
    }
  }
]
npm ls --json @playwright/test | jq
{
  "version": "2.0.0",
  "name": "korosuke613-homepage",
  "dependencies": {
    ...
    "@playwright/test": {
      "version": "1.55.0",
      "resolved": "file:../.pnpm/@playwright+test@1.55.0/node_modules/@playwright/test",
      "overridden": false
    },
    ...
  }
}

全然 json の内容が違う。スキーマから違う。だから jq のクエリが失敗する。
トップレベルが配列になってるのと、dependenciesじゃなくてdevDependenciesになってるのが異なる部分。

pnpm ls @playwright/test --json | jq -r '.[].devDependencies."@playwright/test".version' で行けそう。

❯ pnpm ls @playwright/test --json | jq -r '.[].devDependencies."@playwright/test".version'
1.55.0

いけた。

Futa HirakobaFuta Hirakoba

CI が成功するようになるまで


ほとんどのジョブが成功した。及第点ではないか

pnpm run-- がいらなかった

playwright test 実行時にテストが見つからないとエラー。

> korosuke613-homepage@2.0.0 test:playwright-e2e /home/runner/work/homepage-2nd/homepage-2nd
> playwright test -c playwright-e2e.config.ts -- --retries=2 --workers=2

Error: No tests found.
Make sure that arguments are regular expressions matching test files.
You may need to escape symbols like "$" or "*" and quote the arguments.

 ELIFECYCLE  Command failed with exit code 1.

調べたら pnpm run -- がいらないらしい。

Options listed after the script's name are passed to the executed script.
https://pnpm.io/cli/run#options

これ結構移行してるとハマりそうだなー

Futa HirakobaFuta Hirakoba

VRT だけ失敗しちゃってるけどこれは不安定系なので今回は良しとする...(言い訳)

Futa HirakobaFuta Hirakoba

pnpm のセキュアな機能を使っていく

Futa HirakobaFuta Hirakoba

minimumReleaseAge

https://pnpm.io/settings#minimumreleaseage

minimumReleaseAge 設定してみる。ついでに .npmrc から pnpm-workspace.yaml に移行する。

.npmrc
# Expose Astro dependencies for `pnpm` users
shamefully-hoist=true
pnpm-workspace.yaml
shamefullyHoist: true
minimumReleaseAge: 10080  # 1 week
minimumReleaseAgeExclude:

期間は 1 週間とした。10080 の単位は「分」らしい。

minimumReleaseAgeExclude で特定のパッケージは除外できる。

この設定で最近出たパッケージをインストールしてみる。

astro@5.13.9 が昨日(2025/09/19)出てた。今使ってるバージョンは 5.10.1。

❯ pnpm add astro@5.13.9
 ERR_PNPM_NO_MATCHING_VERSION  No matching version found for astro@5.13.9 while fetching it from https://registry.npmjs.org/

This error happened while installing a direct dependency of /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd

The latest release of astro is "5.13.9".

Other releases are:
  * next--format-astro-url: 0.0.0-20220816201344
  * next--wasm: 0.0.0-wasm-20220921185024
  <省略>
  * experimental--adapter-sessions: 0.0.0-adapter-sessions-20250207124921
  * legacy: 4.16.19

If you need the full list of all 1227 published versions run "$ pnpm view astro versions".
Progress: resolved 59, reused 59, downloaded 0, added 0

ERR_PNPM_NO_MATCHING_VERSION  No matching version found for astro@5.13.9 while fetching it from https://registry.npmjs.org/

そんなバージョンねぇよとのこと。 
でもその下には The latest release of astro is "5.13.9". とある。矛盾してない?笑
pnpm view astro versions も実行したが、普通に 5.13.9 が出てきた。

とにかくインストールには失敗する。エラーメッセージがわかりづらすぎるが...

今度は astro を除外対象にしてみる。

pnpm-workspace.yaml
shamefullyHoist: true
minimumReleaseAge: 10080  # 1 week
minimumReleaseAgeExclude:
+   - "astro"

再度インストール。

❯ pnpm add astro@5.13.9   
 ERR_PNPM_NO_MATCHING_VERSION  No matching version found for vite@7.1.6 while fetching it from https://registry.npmjs.org/

This error happened while installing a direct dependency of /Users/korosuke613/ghq/github.com/korosuke613/homepage-2nd

The latest release of vite is "7.1.6".

Other releases are:
  * alpha: 6.0.0-alpha.24
  * beta: 7.1.0-beta.1
  * previous: 5.4.20

If you need the full list of all 674 published versions run "$ pnpm view vite versions".
Progress: resolved 1422, reused 1340, downloaded 2, added 0

失敗した!でも今度は vite 7.1.6 が存在しないって言われてる。
vite 7.1.6 は一昨日(2025/09/18)リリース。1週間以内だ。
なるほどちゃんと依存関係のリリース日も見てる。すばらしい。

セキュアではあるが、依存の依存を一個一個除外して行くのはきつそうだ。
エラーメッセージも不親切だし、やはり出たばかりの機能と言える。今後もっとよくなりそう。

リポジトリ内の設定として minimumReleaseAge を設定するのはリモートの環境でも適用させるため。 dependabot や renovate による依存関係のアップデートでなんらかの理由で1週間以内のリリースが入ってしまう場合でもインストールを失敗させられる。はず。