📕

pnpm workspaces (monorepo) 上に Storybook 7.0 を導入する

2023/04/17に公開

これはなに

pnpm workspaces で構築した monorepo プロジェクトで Storybook による UI カタログを構築するまでの手順をまとめたものです。

2023 年 4 月 3 日に Storybook v7 がリリースされました。このメジャーアップデートにより pnpm が正式にサポートされ、monorepo 構成下でも正常に動作するようになりました。

https://github.com/storybookjs/storybook/releases/tag/v7.0.2

Storybook 6.5 でも pnpm + monorepo 構成にて動作させることは可能でしたが、 v6.5 は TypeScript 5.x を正式にサポートしておらず、ビルドに失敗することがあります。これを解消するには Storybook を v7 にマイグレーションする必要があります。

本稿では、必要な node モジュールをイチから手動でインストールしてセットアップする手順と、既存の Storybook プロジェクトを CLI を使って対話形式でマイグレーションする手順をご紹介します。

サンプルプロジェクト

https://github.com/wakamsha/learn-monorepo-pnpm

本稿でご紹介する内容に則したプロジェクトのサンプルです[1]

.
├── apps/
│   ├── app1/ # Next.js で実装したアプリケーション
│   │   ├── src/
│   │   ├── next.config.js
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── app2/ # React + Vite で実装したアプリケーション
│   │   ├── src/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── vite.config.ts
│   │   └── tsconfig.json
│   └── catalog/ # storybook で実装した UI カタログ
│       ├── .storybook/
│       │   ├── main.js
│       │   └── preview.js
│       └── package.json
├── packages/
│   ├── core/ # app1, app2 から参照される汎用モジュールパッケージ
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── tsconfig/ # tsconfig の共通設定を管理するパッケージ
│       ├── package.json
│       └── tsconfig.json
├── .node-version
├── .npmrc
├── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml

monorepo 構成

ワークスペースを appspackages の 2 種類を用意します。apps には web アプリケーションとしてビルドおよびデプロイするもの、packages にはそれ以外のパッケージを配置します。Storybook による UI カタログも 「当該プロジェクト配下にある全 UI パーツを列挙するアプリケーション」 [2]と見なして単独のサブパッケージとして配置します。

app1, app2, core パッケージの設計について

Storybook のセットアップに入る前に他のサブパッケージの設計について言及しておきます。

まず app1, app2 はそれぞれ Next.js もしくは React で実装する web アプリです。monorepo 配下に複数の web アプリパッケージが存在するのは「カスタマー向けアプリ」「管理者(バックオフィス)向けアプリ」といった 1 つのサービスにつき複数のアプリケーションが存在するケースを想定しています。

core パッケージは app1, app2 からの参照を目的とした汎用的なモジュールのみを管理することを想定したものです。具体的にはボタンやセレクトボックスといった UI コンポーネントやカラーコードといったデザイントークン等です。サンプルコードでは FormLabel, LabeledSlider という 2 つのコンポーネントと useDebouncedState というカスタムフックを管理しています。

core

https://github.com/wakamsha/learn-monorepo-pnpm/tree/main/packages/core

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/packages/core/package.json

monorepo らしく name@learn-monorepo-pnpm/core としています。この値と version を使って app1, app2 から参照されるようにします。次の節で参照方法を説明します。

app1

https://github.com/wakamsha/learn-monorepo-pnpm/tree/main/apps/app1

app1 は Next.js + TypeScript で実装しています。そして core パッケージのモジュールを参照するため、 core パッケージをインストールします。

cd apps/app1
pnpm add @learn-monorepo-pnpm/core@workspace:1.0.0

従来の node モジュールをインストールするのに加えてバージョン指定に workspace: という接頭辞を付けることで同じ monorepo 内のパッケージをインストールできます。

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/apps/app1/package.json#L13-L15

次に next.config.jsに以下の設定を記述します。

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/apps/app1/next.config.js#L5

これにより core パッケージにある FormLabel というカスタムコンポーネントを以下のような絶対パスで参照できるようになります。

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/apps/app1/src/templates/home/index.tsx#L1

app2

https://github.com/wakamsha/learn-monorepo-pnpm/tree/main/apps/app2

こちらは React + Vite + TypeScript で実装しています。基本的には app1 パッケージと大差ないため割愛します。

Storybook のセットアップ

https://github.com/wakamsha/learn-monorepo-pnpm/tree/main/apps/catalog

ビルダーは Vite を第一候補とする

Storybook は webpack もしくは Vite のいずれかでビルドします。デフォルトのビルダーは webpack であり、v6.5 までの Vite ビルダーは β 版扱いで動作もピーキーでしたが、v7 から見違えるほど安定して動作するようになりました。Vite はデフォルトで SCSS のトランスパイルや CSS Modules の読み込みをサポートしているのはもちろん、ビルド速度やバンドルサイズなど多くの点で webpack より優れています。そのため、特別な事情がない限り Vite を選択します。

https://storybook.js.org/blog/first-class-vite-support-in-storybook/

node モジュールをインストール

Storybook をビルド・起動するのに最低限必要な node モジュールをインストールします。

cd apps/catalog

pnpm add -D \
  react \
  react-dom \
  storybook \
  @storybook/react \
  @storybook/react-vite \
  @storybook/addon-{essentials,interactions,links}

v7 より CLI パッケージである storybook も必要となります。後述する Storybook の起動やビルド処理を実行するのに使います。また、v7 よりビルドモジュールは @storybook/react-vite に変わります。v6.5 までは @storybook/builder-vite に加えてこれがピア依存する大量のモジュールも併せてインストールする必要がありましたが、pnpm 正式サポートの影響なのか react-vite 1 つのみで済むようになりました。

pnpm と npm および yarn との大きな違いは react, react-dom を明示的にインストールする必要がある点です。npm, yarn であれば全ての依存モジュールが node_modules ディレクトリー配下にフラットに展開されるため、暗黙的にインストールされるこれら 2 つのモジュールは何もせずともよしなに参照してくれます。しかし pnpm はフラットに展開しないため、これら 2 つのモジュールも明示的にインストールせねばなりません。

npm scripts を定義

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/apps/catalog/package.json#L5-L8

v6.5 までは start-storybookbuild-storybook といった専用のコマンドがそれぞれ用意されていましたが、v7 より共通の CLI パッケージである storybook を呼びだす設計となりました。

.storybook/main.js

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/apps/catalog/.storybook/main.js

v6.5 までは framework, core フィールドを指定していましたが、 v7 から framework のみとなりました。

.storybook/main.js
+ framework: '@storybook/react-vite',
- framework: '@storybook/react',
- core: {
-   builder: '@storybook/builder-vite',
- },
- features: {
-   storyStoreV7: true,
- },

また、code splitting のために指定していた features/storyStoreV7 も v7 よりデフォルトで適用されるようになったため、明示的に有効化する必要がなくなりました。

.storybook/preview.js

https://github.com/wakamsha/learn-monorepo-pnpm/blob/main/apps/catalog/.storybook/preview.js

上記は Storybook CLI で自動生成されるコードと全く同じです。このファイルはマイグレーションの影響を受けないため、v6.5 と v7 とで内容は全く同じです。

Storybook を起動

packages/catalog ディレクトリーで以下の npm script を実行します。

pnpm start

正常にビルドされ、 http://localhost:6006 で UI カタログにアクセスできるはずです。

UI catalog

CLI を使って既存プロジェクトをマイグレーションする

依存ライブラリのアップデートや設定ファイルの書き換えを自動で行ってくれます。Storybook のあるディレクトリーに移動してマイグレーションコマンドを実行します。

cd apps/catalog

yarn dlx storybook@latest upgrade

実行すると対話形式でアップデートが進行します。

🔎 found a 'storybook-binary' migration
? Do you want to run the 'storybook-binary' migration on your project?

Y を選択します。 CLI パッケージである storybook がインストールされます。Storybook の起動やビルド処理を実行するのに使います。

🔎 found a 'sb-scripts' migration
? Do you want to run the 'sb-scripts' migration on your project?

Y を選択します。 start-storybookbuild-storybook といった専用のコマンドが CLI パッケージを呼びだすコマンドに置き換えられます。

Storybook v6 では一定より上のバージョンの Node.js だと OpenSSL の互換エラーが発生してビルドに失敗することがあります。これを回避する手段としてビルドコマンドを実行する際に NODE_OPTIONS=--openssl-legacy-provider というオプションを指定する必要がありました。v7 にてこの問題が解消されたため、コマンドの置き換え後にこのオプションは手動で撤去します。

package.json
{
  "scripts": {
-   "start": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006",
-   "build": "NODE_OPTIONS=--openssl-legacy-provider build-storybook"
+   "start": "storybook dev -p 6006",
+   "build": "storybook build"
  }
}
🔎 found a 'new-frameworks' migration
? Do you want to run the 'new-frameworks' migration on your project?

Y を選択します。ビルドモジュールが @storybook/builder-vite から @storybook/react-vite に置き換えられます。v6.5 までは @storybook/builder-vite がピア依存する大量のモジュールも併せてインストールする必要がありましたが、v7 からそれら全て不要となったため、置き換え後に手動でアンインストールします。

package.json
{
  "devDependencies": {
-   "@storybook/addon-actions": "7.0.5",
-   "@storybook/addon-backgrounds": "7.0.5",
-   "@storybook/addon-docs": "7.0.5",
    "@storybook/addon-essentials": "7.0.5",
    "@storybook/addon-interactions": "7.0.5",
    "@storybook/addon-links": "7.0.5",
-   "@storybook/addon-measure": "7.0.5",
-   "@storybook/addon-outline": "7.0.5",
-   "@storybook/addons": "7.0.5",
-   "@storybook/builder-vite": "0.4.0",
-   "@storybook/channel-postmessage": "7.0.5",
-   "@storybook/channel-websocket": "7.0.5",
-   "@storybook/client-api": "7.0.5",
-   "@storybook/preview-web": "7.0.5",
    "@storybook/react": "7.0.5",
+   "@storybook/react-vite": "7.0.5",
  }
}

.storybook/main.js も自動的に書き換えられます。v6.5 までは frameworkcore フィールドを指定していましたが、 v7 から framework のみとなりました。

.storybook/main.js
- framework: '@storybook/react',
- core: {
-   builder: '@storybook/builder-vite',
- },
+ framework: {
+   name: '@storybook/react-vite',
+   options: {},
+ },

また、code splitting のために指定していた features/storyStoreV7 も v7 よりデフォルトで適用されるため、明示的に有効化する必要がなくなりました。こちらは手動で削除します。

- features: {
-   storyStoreV7: true,
- },
🔎 found a 'github-flavored-markdown-mdx' migration
? Do you want to run the 'github-flavored-markdown-mdx' migration on your project?

MDX を利用しているプロジェクトであれば Y を選択します。本稿のサンプルプロジェクトでは利用していないため n を選択しました。

🔎 found a 'autodocsTrue' migration
? Do you want to run the 'autodocsTrue' migration on your project?

Y を選択します。各 Story のドキュメントを生成します。v6.5 までのドキュメントはタブ形式で表示されていましたが、v7 からは独立したページとして生成されるようになりました。

v6.5 v7
storybook6.5 storybook7

マイグレーションに必要な作業は以上です。

.storybook/preview-head.html は必須ではなくなった

https://github.com/wakamsha/learn-monorepo-pnpm/blob/feat/storybook6-with-vite/apps/catalog/.storybook/preview-head.html

v6.5 までは Vite でビルドする際に上記のワークアラウンドが必須でしたが、v7 からは不要となりました。よって .storybook/preview-head.html は他に理由がない限り不要となります。

参考文献

https://zenn.dev/wakamsha/articles/setup-storybook-with-pnpm-workspaces

脚注
  1. 本稿の内容と直接関係のないものは省略しています。 ↩︎

  2. 近頃の Storybook は必ずしもこの設計思想に則っていないようにも思えますが、少なくとも筆者はこの構成がしっくりきます。 ↩︎

Discussion