⚒️

Storybook の addon 作ってパネルを表示する

2022/09/03に公開

はじめに

Storybook の addon を作りたくなった。

最近触っているバージョンが 7.0-alpha なので、以降では Storybook 7.0-alpha.28 のバージョンで作っていく。
ただ、すでに非推奨な実装をしない限りは、7 でも 6 でも同様に動くと思う。

リポジトリのセットアップ

$ npm init -y

package.json

package.json
{
  "name": "test-storybook-addon",
  "version": "1.0.0",
  "description": "storybook の addon の実装練習",
  "type": "module",
  "main": "index.js",
  "scripts": {
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/sterashima78/test-storybook-addon.git"
  },
  "license": "MIT"
}

とりあえず Storybook が動くようにする

パッケージの追加をする。

package.json
{
  "name": "test-storybook-addon",
  "version": "1.0.0",
  "description": "storybook の addon の実装練習",
  "type": "module",
  "main": "index.js",
  "scripts": {
+    "dev": "storybook dev -p 6006",
+    "build:storybook": "storybook build"
  },
+  "devDependencies": {
+    "@types/react": "18.0.18",
+    "react": "18.2.0",
+    "react-dom": "18.2.0",
+    "storybook": "7.0.0-alpha.28",
+    "@storybook/react": "7.0.0-alpha.28",
+    "@storybook/react-vite": "7.0.0-alpha.28",
+    "@tsconfig/vite-react": "1.0.1",
+    "vite": "3.0.9",
+    "@vitejs/plugin-react": "2.0.1"
+  },
+  "peerDependencies": {
+  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/sterashima78/test-storybook-addon.git"
  },
  "license": "MIT"
}

諸々の設定ファイルら追加

.storybook/main.ts
export default {
    framework: "@storybook/react-vite",
    stories: [
        "../src/**/*.stories.tsx",
    ],
}
vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
  plugins: [react()],
})
tsconfig.json
{
    "extends": "@tsconfig/vite-react/tsconfig.json"
}

テスト用のコンポーネントと Story 追加

src/Test.tsx
export type TestProps = { msg: string }
export const Test = ({ msg }: TestProps)=> <div>{msg}</div>
src/Test.stories.tsx
import { Meta, Story } from "@storybook/react";
import { Test, TestProps } from "./Test";
export default {
    title: "test",
    component: Test
} as Meta<TestProps>

export const testStory: Story<TestProps> = {
    args: {
        msg: "hoge"
    }
}

これでOK

npm run dev

addon を作る

基本的には以下に書いてある。
https://storybook.js.org/docs/react/addons/writing-addons

ビルド設定など

jsx は React.createElement に変換されなくてはいけないみたいなので (上の説明で、@babel/preset-react を使っている)、tsconfig を修正する。

tsconfig.json
{
    "extends": "@tsconfig/vite-react/tsconfig.json",
    "compilerOptions": {
        "jsx": "react"
    }
}

前に上げたドキュメントではトランスパイルを babel で行っているが、ここでは tsup で行う。個人的にライブラリを作るならこれを使うことが多い。
https://github.com/egoist/tsup

依存関係に必要なパッケージを追加し、ビルド用のタスクを追加する。
以下で、tsup と以降で用いる Storybook のパッケージを追加しておく。

package.json
{
  "name": "test-storybook-addon",
  "version": "1.0.0",
  "description": "storybook の addon の実装練習",
  "type": "module",
  "main": "index.js",
  "scripts": {
+    "build": "tsup",
    "dev": "storybook dev -p 6006",
    "build:storybook": "storybook build"
  },
  "devDependencies": {
    "@types/react": "18.0.18",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "storybook": "7.0.0-alpha.28",
    "@storybook/react": "7.0.0-alpha.28",
    "@storybook/react-vite": "7.0.0-alpha.28",
    "@tsconfig/vite-react": "1.0.1",
    "vite": "3.0.9",
    "@vitejs/plugin-react": "2.0.1",
+    "tsup": "6.2.3",
+    "@storybook/addons": "7.0.0-alpha.28",
+    "@storybook/components": "7.0.0-alpha.28",
+    "@storybook/api": "7.0.0-alpha.28"
  },
  "peerDependencies": {
+    "react": ">= 16",
+    "react-dom": ">= 16",
+    "@storybook/components": "7.0.0-alpha.28",
+    "@storybook/addons": "7.0.0-alpha.28",
+    "@storybook/api": "7.0.0-alpha.28"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/sterashima78/test-storybook-addon.git"
  },
  "license": "MIT"
}

ここで、peerDependencies フィールドにもいくつかのパッケージを追加した。
これは、addon を使うユーザが自身でインストールするべきパッケージを記載する。
また、tsupdependenciespeerDependencies フィールドのパッケージを外部化し、バンドルしないでくれる。
これによって、Storybook 本体で使われている react とこの addon にバンドルされた react と2つの react がランタイムに存在することを回避できる。

tsup の設定を書く。
設定値はキーの名前から大体予想がつくと思う。詳しくはドキュメントを見てほしい。

今回は esm のみあればいいので、この形式で出力し、エントリポイントについては後述する。

tsup.config.ts
import { defineConfig } from 'tsup'

export default defineConfig({
  outDir: "libs",
  entry: ['addon/index.ts', "addon/addon.tsx"],
  splitting: false,
  sourcemap: true,
  clean: true,
  format: ["esm"],
})

addon の実装

addon のエントリポイントでは、読み込ませるファイルらを設定する必要がある。
読み込ませるファイルらは、require.resolve で解決させるようにする。

.storybook/main.js で利用するようなものは、それを返す関数を managerEntries という名前で公開し、.storybook/preview.js で利用するようなものは config という名前で公開するらしい。今回はパネルを増やすので、前者となる。

エントリーポイントは、 package.jsonmain フィールドや export フィールドで決まるものなのでファイル名はなんでもいい。ここでは addon/index.ts とする。

addon/index.ts
export const managerEntries = (entry = []) => {
    return [...entry, require.resolve("./addon.js")];
}

addon 本体も実装していく。
前に出したドキュメントと同じ内容を書く。
addon/index.tsrequire.resolve("./addon.js") を読ませるように書いているので、同じディレクトリに addon.js という名前にトランスパイルされるファイルを作る。

addon/addon.tsx
import React from 'react';
import { addons, types } from '@storybook/addons';

import { AddonPanel } from '@storybook/components';

const ADDON_ID = 'myaddon';
const PANEL_ID = `${ADDON_ID}/panel`;

const MyPanel = () => <div>MyAddon</div>;

addons.register(ADDON_ID, () => {
  addons.add(PANEL_ID, {
    type: types.PANEL,
    title: 'My Addon',
    render: ({ active, key }) => (
      <AddonPanel active={!!active} key={key}>
        <MyPanel />
      </AddonPanel>
    ),
  });
});

@storybook/addons から addons オブジェクトを取得し、いくつかのメソッドを実行している。
これによって、Story の下部に表示されるパネルを追加することができる。
各メソッドについては以下での説明で十分理解できると思う。
https://storybook.js.org/docs/react/addons/addons-api

動作確認

動作確認をする。
といってもそれほど難しくはなく、とりあえずビルドする。

$ npm run build

> test-storybook-addon@1.0.0 build
> tsup

CLI Building entry: addon/index.ts, addon/addon.tsx
CLI Using tsconfig: tsconfig.json
CLI tsup v6.2.3
CLI Using tsup config: /workspaces/test-storybook-addon/tsup.config.ts
CLI Target: node14
CLI Cleaning output folder
ESM Build start
ESM libs/index.js     550.00 B
ESM libs/addon.js     640.00 B
ESM libs/index.js.map 297.00 B
ESM libs/addon.js.map 992.00 B
ESM ⚡️ Build success in 21ms

次に、addon を読み込ませるための設定を .storybook/main.ts に追加する。

.storybook/main.ts
export default {
    framework: "@storybook/react-vite",
    stories: [
        "../src/**/*.stories.tsx",
    ],
+    addons: ["../libs/index.js"]
}

起動してみよう。

$ npm run dev

パネルが追加されている。

パラメータの取得

このままだとどの Story でも同じ内容が表示されるだけであまりおもしろくない。
Storyの記述フォーマットの CSF には parameters というフィールドがある。
この値を取得できるようにすることで、Story ごとに振る舞いを変えることができる。
値の取得には useParameter という hook を用いる。

addon.tsx
import React from 'react';
import { addons, types } from '@storybook/addons';

import { AddonPanel } from '@storybook/components';
+ import { useParameter } from '@storybook/api';

const ADDON_ID = 'myaddon';
const PANEL_ID = `${ADDON_ID}/panel`;
+ const PARAM_KEY = 'myAddon';

- const MyPanel = () => <div>MyAddon</div>;
+ const MyPanel = () => {
+  const value = useParameter<{ data: string } | null>(PARAM_KEY, null);
+  const item = value ? value.data : 'No story parameter defined';
+  return <div>{item}</div>;
+ };

addons.register(ADDON_ID, () => {
  addons.add(PANEL_ID, {
    type: types.PANEL,
    title: 'My Addon',
    render: ({ active, key }) => (
      <AddonPanel active={!!active} key={key}>
        <MyPanel />
      </AddonPanel>
    ),
  });
});

Story にもパラメーターを追加する。

src/Test.stories.tsx
import { Meta, Story } from "@storybook/react";
import { Test, TestProps } from "./Test";
export default {
    title: "test",
    component: Test
} as Meta<TestProps>

export const testStory: Story<TestProps> = {
    args: {
        msg: "hoge"
+    },
+    parameters: {
+        myAddon: {
+            data: 'this data is passed to the addon',
+        },
    }
}

addon を再ビルドして確認する。

$ npm run build
$ npm run dev

パラメータに記載した値が表示されている

あとは普通にパッケージを公開すれば外部からも使え、中に表示するものは好きに React コンポーネントを実装すればいい。

終わりに

他にも Toolbar や Tab にも自分で機能を埋め込むことができる。

これらを含めた説明は以下にもあり、
https://storybook.js.org/tutorials/create-an-addon/

その説明のベースであり、addon を作るためのベースとなるのが、以下となっている。
https://github.com/storybookjs/addon-kit

そのため、手っ取り早く手を動かしたい人はこれをイジるのがいいと思う。

個人的には更地から足していきながら API などを理解したいので、引き続き別の機能に触れていきたいと思った。

ここまでの内容は以下となっている。
https://github.com/sterashima78/test-storybook-addon/tree/panel

続き

https://zenn.dev/sterashima78/articles/473e1764962cbe

Discussion