📖

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

2023/02/12に公開

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

これはなに

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

pnpm は依存モジュールの取り扱いが npm や yarn と大きく異なるため、一般的には Storybook の 2023 年 2 月現在の現行バージョンである v6.5 の導入が非推奨とされています。公式ドキュメントにも pnpm によるインストール手順は記載されていません。しかし実際は手順さえ怠らなければ pnpm の設計思想を侵すことなく Storybook を導入できます。本稿ではその手順をご紹介します。

また、本稿の後半では Storybook のビルダーを webpack から Vite に置き換える手順についても言及します。

サンプルコード

https://github.com/wakamsha/learn-monorepo-pnpm/tree/feat/storybook6-with-webpack

feat/storybook6-with-webpack ブランチ

本稿でご紹介する内容に則したプロジェクトのサンプルです[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/feat/storybook6-with-webpack/packages/core

https://github.com/wakamsha/learn-monorepo-pnpm/blob/feat/storybook6-with-webpack/packages/core/package.json

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

app1

https://github.com/wakamsha/learn-monorepo-pnpm/tree/feat/storybook6-with-webpack/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/feat/storybook6-with-webpack/apps/app1/package.json#L13-L15

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

https://github.com/wakamsha/learn-monorepo-pnpm/blob/feat/storybook6-with-webpack/apps/app1/next.config.js#L5

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

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

app2

https://github.com/wakamsha/learn-monorepo-pnpm/tree/feat/storybook6-with-webpack/apps/app2

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

Storybook のセットアップ(webpack)

https://github.com/wakamsha/learn-monorepo-pnpm/tree/feat/storybook6-with-webpack/apps/catalog

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

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

cd apps/catalog

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

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

.storybook/main.js

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

.storybook/preview.js

https://github.com/wakamsha/learn-monorepo-pnpm/blob/feat/storybook6-with-webpack/apps/catalog/.storybook/preview.js

上記は Storybook CLI で自動生成されるコードと全く同じです。今回はひとまずこれで OK。

npm scripts を定義

https://github.com/wakamsha/learn-monorepo-pnpm/blob/feat/storybook6-with-webpack/apps/catalog/package.json#L5-L8

こちらも Storybook CLI で自動生成されるコードと本質的には同じです。

Storybook を起動

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

pnpm start

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

UI catalog

error:0308010C:digital envelope routines::unsupported というエラーが発生してビルドに失敗する場合の対処法

Node.js のバージョンなど環境によっては error:0308010C:digital envelope routines::unsupported というエラーが発生してビルドに失敗することがあります。その場合は openssl-legacy-provider オプションを使用することで回避します。npm scripts を以下のように修正します。

{
  "scripts": {
-   "start": "start-storybook -p 6006",
-   "build": "build-storybook"
+   "start": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006",
+   "build": "NODE_OPTIONS=--openssl-legacy-provider build-storybook"
  }
}

Storybook のビルダーを Vite に置き換える

デフォルトのビルダーである webpack は安定しているものの非常にビルドが遅く、この課題を解消する手段として builder-vite が公式から提供されています。2023 年 2 月時点の最新バージョンが 0.4.0 であり、所々ピーキーなのは否めませんが、pnpm workspaces にも導入は可能です。

Vite に置き換えたコードのサンプルはこちら。

https://github.com/wakamsha/learn-monorepo-pnpm/tree/feat/storybook6-with-vite

feat/storybook6-with-vite ブランチ

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

Vite に置き換えることで builder-webpack5, manager-webpack5 は不要となるのであらかじめアンインストールします。

pnpm remove @storybook/{builder,manager}-webpack5

次に @storybook/builder-vite とこれを動作させるのに最低限必要な node モジュールをインストールします。

pnpm add -D \
  @storybook/builder-vite \
  @storybook/channel-{postmessage,websocket} \
  @storybook/client-api \
  @storybook/preview-web \
  @storybook/addons \
  @storybook/addon-{actions,backgrounds,docs,measure,outline} \
  vite \
  @vitejs/plugin-react

Vite を使うので vite, @vitejs/plugin-react はまだ察しが付きますが、その他に膨大な数の @storybook/* モジュールをインストールを要求されるところが重要なポイントです。これらは @storybook/builder-vite が直接依存するモジュールであり、npm, yarn であれば node_modules ディレクトリー配下にフラットに展開されるため、明示的なインストールは不要です。しかし pnpm の場合は react, react-dom の時と同様にフラットに展開しないことが理由で、これら全てを明示的にインストールする必要があります。流石にこれは面倒が過ぎるものの、@storybook/builder-vite がそのように実装されている以上どうしようもありません。

.storybook/main.js を編集

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

重要なのは features/storyStoreV7 フィールドです。code splitting に対応するために storyStoreV7 フラグを有効化します。これをしないと起動時に Couldn't find any stories in your Storybook. というエラーになって stories の読み込みに失敗してしまうため、この設定は必須です。

.storybook/preview.js

こちらは webpack のときと全く同じ設定のままで大丈夫です。

.storybook/preview-head.html

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

webpack のときは不要ですが、 Vite でビルドする際はこれが必要となります。

以上で Vite への置き換え作業は完了です。 pnpm start コマンドを実行して Storybook がビルド・起動すれば成功です。

Chromatic との連携も問題なく可能

https://storybook.js.org/tutorials/intro-to-storybook/react/en/deploy/

詳しい手順は割愛しますが、本稿で紹介してきた monorepo 構成でも上記にある手順で Chromatic との連携ならびに UI Tests の実施は問題なく可能です。

projects on chromatic

build log on chromatic

diff on chromatic

単純なホスティングだけならまだしも、Visual Regression Testing が手間いらずで実現できてしまうのは非常に大きなメリットと言えるでしょう。

締め

当初は v7.0.0-beta を使っての環境構築を試みたのですが、冒頭で述べた不具合を踏んでどうにもならなかったため、試行錯誤を重ねて今回の設計に至りました。ひとまず pnpm workspaces でも monorepo の UI カタログが構築可能であることは実証できました。

Test runner の導入が可能かどうかは未検証のため、引き続き調査を進めます。

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

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

Discussion