Nx活用術!モノレポ内のStorybookのパス設定自動化

ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。

この記事はFindy Advent Calendar 2024 4日目の記事です。

adventar.org

Nxはモノレポ管理の便利なユーティリティとして @nx/devkit を提供しています。

今回は @nx/devkit を利用したStorybookの設定の自動化についてご紹介します。

Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。

tech.findy.co.jp

モノレポでStorybookをどのように管理するか?

皆さんはモノレポでStorybookを運用されたことはありますでしょうか?

概ねどちらかの方法を採用することになるかと思います。

前者は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 というユーティリティです。

createProjectGraphAsync | Nx

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.dev

@nx/devkitcreateProjectGraphAsync を利用して、各プロジェクトの 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名が同じでもどの階層にいるか判断して目的にたどり着ける」

といったフィードバックを受けることができました👏 検索性の向上に一役買えましたね!

今回のサンプルは次のリポジトリから動作を確認できます。是非お試しください。

github.com

まとめ

この記事では @nx/devkit を利用したStorybookの設定の自動化についてご紹介しました。

Nxの機能を活用すれば「モノレポにプロジェクトを追加した後のStorybookの設定が漏れていた!」といった事とは無縁になるでしょう。

検証した時点では、@storybook/test-runnerstories をAsync Functionで渡すパターンとの相性がまだ悪いようでした。今後の更新に期待したいですね。

今回はStorybookとの組み合わせでしたが、同じ仕組みを使ってGraphQL Codegenの設定自動化も可能であると確認しています。また別の機会にご紹介できればと思います。

それではまた次回!


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers