ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。
この記事はFindy Advent Calendar 2024 4日目の記事です。
Nxはモノレポ管理の便利なユーティリティとして @nx/devkit
を提供しています。
今回は @nx/devkit
を利用したStorybookの設定の自動化についてご紹介します。
Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。
モノレポでStorybookをどのように管理するか?
皆さんはモノレポでStorybookを運用されたことはありますでしょうか?
概ねどちらかの方法を採用することになるかと思います。
- 単一のStorybookで集中管理
- Storybook Composition を使って管理
前者はStorybookのデプロイ設定をシンプルにできますが、全プロジェクトのStorybookへのパスを記述する必要があります。後者は設定がやや複雑であり、プロジェクト毎にStorybookのデプロイ設定も必要です。
Findyのフロントエンドはモノレポで管理されており、フィーチャ単位に細分化された多数のプロジェクトを持つという性質と運用の難易度を考慮し、前者の手法を選びました。
どうやってパス設定を自動化するか?
単一のStorybookで集中管理する際に課題となるのは↓の部分でしょう。
// .storybook/main.js const config = { addons: ['@storybook/addon-essentials'], framework: { name: '@storybook/react-vite', options: {}, }, stories:[ // 👈 ここ!👇 '../apps/app1/src', '../libs/components/src', '../libs/app1/feature-a/src', '../libs/app1/feature-b/src', '../libs/app1/feature-c/src', // 以下、全プロジェクトのパスが続きます // : ] }; export default config;
数個程度であれば十分に運用できそうですが、モノレポ上となると話が違ってきます。
ちなみに、Findyのフロントエンドではプロジェクト数は 70 個でした。
※他プロダクトでは100個近くになる場合もあります
さすがに手動での管理には限界がありますので、ここはNxの力を借りましょう。
使用するのは createProjectGraphAsync
というユーティリティです。
Nxは各プロジェクトの依存関係を保持しており、そこからStorybookのパスを算出できます。
最終的に次のようなものを目指します。
// .storybook/main.js const config = { addons: [...], framework: {...}, stories: getStories(), // https://storybook.js.org/docs/configure#with-a-custom-implementation // 👇 こんな感じの配列を返して欲しい // [ // { titlePrefix: 'app1', directory: '../apps/app1/feature-a/src' }, // { titlePrefix: 'app1-feature-a', directory: '../libs/app1/feature-a/src' }, // { titlePrefix: 'app1-feature-b', directory: '../libs/app1/feature-b/src' }, // { titlePrefix: 'app1-feature-c', directory: '../libs/app1/feature-c/src' }, // ... // ] }; export default config;
実装してみよう!
方針が決まったところで実装していきましょう。
事前の準備として、Storybookのパスを取得したいプロジェクトに tags
を設定しておきましょう。Nxの @nx/enforce-module-boundaries
ルールによる依存の制御を導入している場合は自然と設定されてあると思います。
// apps/app1/project.json { "name": "app1", "tags": ["scope:app1"], ... }
// libs/app1/feature-a/project.json { "name": "app1-feature-a", "tags": ["scope:app1", "type:feature"], ... }
@nx/devkit
の createProjectGraphAsync
を利用して、各プロジェクトの name
および sourceRoot
を取得します。
// apps/app1/.storybook/main.js import { createProjectGraphAsync } from '@nx/devkit'; const getStories = async () => { const graph = await createProjectGraphAsync(); // Storybookが不要なプロジェクトは無視 const ignoredProjects = ['app1-e2e', 'app1-utils']; return Object.keys(graph.nodes) .filter((key) => graph.nodes[key].data.tags?.includes('scope:app1')) // 関連するStorybookの絞り込み .filter((key) => !ignoredProjects.includes(key)) .map((key) => { const project = graph.nodes[key].data; return { titlePrefix: project.name, directory: `../../../${project.sourceRoot}`, }; }); };
あとはこれを stories
に渡せば完成です。
// apps/app1/.storybook/main.js const config = { addons: ['@storybook/addon-essentials'], framework: { name: '@storybook/react-vite', options: {}, }, stories: getStories(), // 👈 プロジェクトの追加・削除に応じて自動的に設定されます }; export default config;
Storybookが表示されました!
Nxによる自動化のもう1つのメリットは、↓のように titlePrefix
にプロジェクト名を関連付けることで、フィーチャ毎の分類がより明確になる点だと思います。
const project = graph.nodes[key].data; return { titlePrefix: project.name, directory: `../../../${project.sourceRoot}`, };
開発メンバーからは、
「モノレポ構造とStoryの構造がリンクすることで画面の使い勝手が非常に良い」
「検索したときにStory名が同じでもどの階層にいるか判断して目的にたどり着ける」
といったフィードバックを受けることができました👏 検索性の向上に一役買えましたね!
今回のサンプルは次のリポジトリから動作を確認できます。是非お試しください。
まとめ
この記事では @nx/devkit
を利用したStorybookの設定の自動化についてご紹介しました。
Nxの機能を活用すれば「モノレポにプロジェクトを追加した後のStorybookの設定が漏れていた!」といった事とは無縁になるでしょう。
検証した時点では、@storybook/test-runner
と stories
をAsync Functionで渡すパターンとの相性がまだ悪いようでした。今後の更新に期待したいですね。
今回はStorybookとの組み合わせでしたが、同じ仕組みを使ってGraphQL Codegenの設定自動化も可能であると確認しています。また別の機会にご紹介できればと思います。
それではまた次回!
ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。