Storybook の中から別の Storybook を参照できる Storybook composition を試してみる

Storybook 6.0 から,ある Storybook の中から別の Storybook を参照することができる "Storybook composition" という機能が導入された.

storybook.js.org

これは目玉機能として挙げられているものの,2021年1月現在この機能についてのドキュメンテーションが十分になされていない.そのため,不足している情報を補完することを目的としてこの記事を書くことにした.

モチベーション

HERP では herpism と呼ばれる社内用 UI コンポーネントライブラリを管理しており,プライベートな npm パケッジとして配布している.またherpism の Storybook は,社内のメンバのみがアクセス可能な環境にホスティングされている.

各フロントエンドのプロジェクトは herpism に依存している.

                               +---------------------+
                          +----|      Project A      |
                          |    +---------------------+
+-------------------+     |    +---------------------+
|      herpism      |<----+----|      Project B      |
+-------------------+     |    +---------------------+
                          |    +---------------------+
                          +----|      Project C      |
                               +---------------------+

またそれぞれのプロジェクトは,当該プロジェクト内でのみ用いられる UI コンポーネントを掲載した Storybook を持っている.

                               +---------------------+
                               | Project A Storybook |
                               +---------------------+
+-------------------+          +---------------------+
| herpism Storybook |          | Project B Storybook |
+-------------------+          +---------------------+
                               +---------------------+
                               | Project C Storybook |
                               +---------------------+

このような構成を取っているため,各プロジェクトの Storybook から,herpism に存在する UI コンポーネントを確認できると有用である.

                               +---------------------+
                          +----| Project A Storybook |
                          |    +---------------------+
+-------------------+     |    +---------------------+
| herpism Storybook |<----+----| Project B Storybook |
+-------------------+     |    +---------------------+
                          |    +---------------------+
                          +----| Project C Storybook |
                               +---------------------+

以下では,UI コンポーネントライブラリ (図中左側) を参照先,ライブラリを用いるプロジェクト (図中右側) を参照元と呼称する.また,参照元の Storybook は localhost で閲覧されることを前提している.

ドキュメント

composition についてのドキュメントとしては,以下の2ページが存在する.

storybook.js.org

storybook.js.org

前者は参照元.storybook/main.js に設定を書く手法であり,後者は参照先のパケッジの package.json に設定を書いておくことで,参照元の Storybook に自動的に読み込ませる手法である.個別のプロジェクトでの設定の手間を省くことができるため,今回は "package composition" と呼ばれている後者の手法を採用する.

package composition を実現するためには,以下の手順を踏む必要がある.

  • package.jsonstorybook フィールドを追加する
  • stories.json を配置する
  • metadata.json を配置する (任意)
  • CORS に関する設定を行う

以下では https://storybook.example.com で参照先の Storybook がホスティングされていることを前提する.

package.jsonstorybook フィールドを追加する

コンポーネントライブラリの package.json に,Storybook がホスティングされている URL を以下のように記載する.

@@ -1,4 +1,7 @@
 {
   "name": "herpism",
-  "version": "4.6.0"
+  "version": "4.6.0",
+  "storybook": {
+    "url": "https://storybook.example.com"
+  }
 }

このような package.json を持つパケッジが node_modules/ 配下に存在するプロジェクトで Storybook を立ち上げると,自動的に https://storybook.example.com でホストされている Storybook を埋め込もうとしてくれる.

stories.json を配置する

Storybook 内に存在するコンポーネントの情報を記載した stories.json というファイルが配信されている必要がある.@storybook/cli には stories.json を生成するための $ sb extract というサブコマンドが用意されており,$ npx build-storybook で静的な Storybook をビルドした後,そのディレクトリを引数に与えて実行すると,ディレクトリ内に stories.json を生成してくれる.これは puppeteer を通じてヘッドレスモードの Chromium が起動することで実現されている.

$ npx build-storybook -o ./dist
$ npx sb extract ./dist

うまくいけば以下のような内容をもつ ./dist/stories.json が生成される.公式ドキュメントには以下のような JSON が記載されている.

{
  "v": 2,
  "globalParameters": {},
  "kindParameters": {
    "components/myComponent": {
      "fileName": 445,
      "framework": "react"
    },
    "components/myOtherComponent": {
      "fileName": 447,
      "framework": "react"
    }
  },
  "stories": {
    "components-mycomponent--simple": {
      "id": "components-mycomponent--simple",
      "name": "Simple",
      "kind": "components/myComponent",
      "story": "Simple",
      "parameters": {
        "__id": "components-mycomponent--simple",
        "__isArgsStory": true
      }
    },
    "components-myothercomponent--simple": {
      "id": "components-myothercomponent--simple",
      "name": "Simple",
      "kind": "components/myothercomponent",
      "story": "Simple",
      "parameters": {
        "__id": "components-myothercomponent--simple",
        "__isArgsStory": true
      }
    }
  }
}

metadata.json を配置する (任意)

公式ドキュメントには,composition のためには metadata.json なるファイルが必要だと記載されているが,このファイルがどのようなものであるかについては言及されていない.加えて,このファイルの存在は必須ではなく任意である.GitHub の issue comment を参考にすると,以下のような内容であるべきだということがわかる.

{
  "versions": {
    "v4.6.0": "https://storybook.example.com"
  }
}

versions フィールドに複数のバージョンを記載し,異なるバージョンに対して異なる URL を割り当てておけば,参照元の Storybook からバージョンを切り替えることもできる.しかし HERP での事例では,過去のバージョンの Storybook を半永久的にホスティングし続けるのが面倒なので,単一のバージョンのみを記載している.このような JSON を生成する適当なスクリプトを用意しておき,ビルドプロセスに組み込んでおくのがいいだろう.

CORS に関する設定を行う

Storybook composition では,参照元の Storybook を開いているブラウザから Cross-Origin Resource Sharing (CORS) を用いて外部のリソースを読み込むため,参照先の Storybook をホストしているサーバ側で,CORS を許可するよう設定を行っておく必要がある.

HERP では Storybook を配信する Docker image を,Istio が入った Kubernetes クラスタにデプロイしているため,VirtualService に以下のような CorsPolicy を追加することで対応した.また,oauth2-proxy を用いたアクセス制限を施しているため,allowCredentials: true を指定している.

corsPolicy:
  # Access-Control-Allow-Credentials ヘッダに対応
  allowCredentials: true

  # Access-Control-Allow-Methods ヘッダに対応
  allowMethods:
  - GET

  # Access-Control-Allow-Origin ヘッダに対応
  allowOrigins:
  - regex: http://localhost:[0-9]+

  # Access-Control-Max-Age ヘッダに対応
  maxAge: 24h

前段に認証を挟む場合,Set-Cookie ヘッダに SameSite=None を指定することをお忘れなきよう.

設定がうまくいっていれば,参照元の Storybook を立ち上げた際に,サイドバーの下部から参照先の Storybook を参照できるようになっているはずである.

Referenced Storybook