遂に来た！Storybookのバージョンを7にするまでに行ったこと

# はじめに
ついにStorybookのバージョン７がリリースされ最新版になりました。


https://github.com/storybookjs/storybook/releases/tag/v7.0.2


この記事ではReact×Viteの環境でバージョン6から7にアップデートするまでに行ったことをまとめました。
公式が公開する移行方法は[こちら](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-65x-to-700)になります。

# これまでの書き方
今回私がアップデートしたプロジェクトの設定ファイルやStoryの書き方を紹介します。
アップデート前の設定ファイルは以下のようになっています。
```js:.storybook/main.cjs
const { loadConfigFromFile, mergeConfig } = require('vite');
const svgr = require('vite-plugin-svgr');
const path = require('path');

const configEnvServe = {
  mode: 'development',
  command: 'serve',
  ssrBuild: false,
};

const configEnvBuild = {
  mode: 'production',
  command: 'build',
  ssrBuild: false,
};

module.exports = {
  stories: ['../src/components/**/*.stories.@(jsx|tsx)'],
  addons: [
    '@storybook/addon-a11y',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  staticDirs: ['./public'],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-vite',
  },
  features: {
    storyStoreV7: true,
  },
  async viteFinal(config, { configType }) {
    const isProduction = configType === 'PRODUCTION';
    const { config: userConfig } = await loadConfigFromFile(
      isProduction ? configEnvBuild : configEnvServe,
      path.resolve(__dirname, '../vite.config.ts'),
    );

    return mergeConfig(config, {
      ...userConfig,
      plugins: [svgr()],
      build: {
        ...config.build,
        sourcemap: !isProduction,
      },
    });
  },
};

```
```tsx:.storybook/preview.tsx
import React from 'react';
import { AppProvider } from './../src/providers/app';

export const decorators = [
  (Story) => (
    <AppProvider>
      <Story />
    </AppProvider>
  ),
];

```

Storyは基本的に以下のように書かれていました。
```tsx
import type { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import { SampleComponent } from './SampleComponent';

export default {
  component: SampleComponent,
} as ComponentMeta<typeof SampleComponent>;

export const Default: ComponentStoryObj<typeof SampleComponent> = {};
```
# バージョンアップ
まずは、storybookの一連のライブラリをアップデートしていきます。storybookにはアップデートを自動でおこなってくれるコマンドがあるのでそれを用いて行いました。
```sh
npx storybook upgrade
```
入力後`Ok to proceed?`と聞かれるので`y`を返してパッケージのアップデートを進めていきます。

次に`Do you want to run the 'storybook-binary' migration on your project?`と聞かれます。`y`と返すと、`storybook`という名前のライブラリが`devDependencies`に追加されます。
バージョン7では`start-storybook`や`build-storybook`が削除され、`cli`は`storybook`から始まるコマンドに置き換わるためです。推奨される方法は`start-storybook`が`storybook dev`、`build-storybook`が`storybook build`に変わりました。

そして、それらのコマンドの置き換えは次に聞かれる`Do you want to run the 'sb-scripts' migration on your project?`で`y`を返すと自動で置き換えられます。
Storybook6では一定のNodeバージョン以上で利用するとエラーが起き、Nodeのオプション`--openssl-legacy-provider`を設定する必要がありました。そのため私のscriptsは以下のように設定されました。
```json:package.json
  "scripts": {
    "storybook": "NODE_OPTIONS=--openssl-legacy-provider start-storybook",
    "build-storybook": "NODE_OPTIONS=--openssl-legacy-provider build-storybook",
  }
```
そのような設定が記載されていても検知して`storybook dev`と`storybook build`に置き換えてくれました。
```json:package.json
  "scripts": {
    "storybook": "NODE_OPTIONS=--openssl-legacy-provider storybook dev",
    "build-storybook": "NODE_OPTIONS=--openssl-legacy-provider storybook build",
  }
```
置き換え後も`NODE_OPTIONS=--openssl-legacy-provider`がついてきますが、Storybook7ではこれらのオプションが不要になったのでコマンド実行後に削除しました。
```json:package.json
  "scripts": {
    "storybook": "storybook dev",
    "build-storybook": "storybook build",
  }
```


次に、`Do you want to run the 'new-frameworks' migration on your project?`と聞かれますが、これも`y`と返してください。これまではVite環境を構築するときに`@storybook/builder-vite`を利用していましたが、これからはViteとReactと組み合わせた環境では`@storybook/react-vite`を使うようになります(Vueだったら`@storybook/vue-vite`のようにUIライブラリとビルドツールを組み合わたライブラリを利用するようになります)。
そのため質問に`y`を返すことで`@storybook/builder-vite`をアンインストールして`@storybook/react-vite`をインストールします。さらに、バージョン7それらを用いた設定に`.storybook/main.cjs`を自動で変更されます。
```js:.stoybook/main.cjs
const {
  loadConfigFromFile,
  mergeConfig
} = require('vite');
const svgr = require('vite-plugin-svgr');
const path = require('path');
const configEnvServe = {
  mode: 'development',
  command: 'serve',
  ssrBuild: false,
};
const configEnvBuild = {
  mode: 'production',
  command: 'build',
  ssrBuild: false,
};
module.exports = {
  stories: ['../src/components/**/*.stories.@(jsx|tsx)'],
  addons: ['@storybook/addon-a11y', '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
  staticDirs: ['./public'],
  framework: {
    name: '@storybook/react-vite',
    options: {}
  },
  features: {
    storyStoreV7: true
  },
  async viteFinal(config, { configType }) {
    const { config: userConfig } =
      (await loadConfigFromFile(
        isProduction ? configEnvBuild : configEnvServe,
        path.resolve(__dirname, '../vite.config.ts'),
      )) ?? {};
    return mergeConfig(config, {
      ...userConfig,
      plugins: [svgr()],
      build: {
        ...config.build,
        sourcemap: !isProduction
      }
    });
  }
};
```
ここで行われた変更の調整は後ほど行います。これまでは`core.builder`でviteを指定して、`framework`ではreactを指定するようなバラバラな形式でしたが、バージョン7からは`framework`で両者を組み合わせたライブラリを指定するようになりました。

その後、`Do you want to run the 'github-flavored-markdown-mdx' migration on your project?`と聞かれます。今回移行したプロジェクトではmdxの利用を行っていなかったため`n`としました。利用状況によっては`y`とした方が良いと思います。

最後に、`Do you want to run the 'autodocsTrue' migration on your project? `と聞かれます。これまでのバージョンではStoryのドキュメントは自動生成されてきましたが、バージョン７では自動生成をさせないことがデフォルトになりました。これまで通り生成して欲しい場合は`y`を入力してください。以下のように`.storybook/main.cjs`に追加されるはずです。
```js:.storybook/main.cjs
  docs: {
    autodocs: true
  }
```
私はドキュメントを利用していなかったので`n`を入力しました(その場合何も変わりはありません)。`'tag'`にすると、Storyごとに自動生成させるかどうかを指定させることができます。

以上がコマンドによる変更でした。


# main.cjsの書き換え
コマンド実行後、`main.cjs`は以下のようになりました。
```js:.storybook/main.cjs
const {
  loadConfigFromFile,
  mergeConfig
} = require('vite');
const svgr = require('vite-plugin-svgr');
const path = require('path');
const configEnvServe = {
  mode: 'development',
  command: 'serve',
  ssrBuild: false,
};
const configEnvBuild = {
  mode: 'production',
  command: 'build',
  ssrBuild: false,
};
module.exports = {
  stories: ['../src/components/**/*.stories.@(jsx|tsx)'],
  addons: ['@storybook/addon-a11y', '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
  staticDirs: ['./public'],
  framework: {
    name: '@storybook/react-vite',
    options: {}
  },
  features: {
    storyStoreV7: true
  },
  async viteFinal(config, { configType }) {
    const { config: userConfig } =
      (await loadConfigFromFile(
        isProduction ? configEnvBuild : configEnvServe,
        path.resolve(__dirname, '../vite.config.ts'),
      )) ?? {};
    return mergeConfig(config, {
      ...userConfig,
      plugins: [svgr()],
      build: {
        ...config.build,
        sourcemap: !isProduction
      }
    });
  }
};
```
バージョンが7になったのでこのファイルから`features.storyStoreV7`を消します。そして元の状態のように整形すると以下のようになります。
```js:.storybook/main.cjs
const { loadConfigFromFile, mergeConfig } = require('vite');
const path = require('path');

const configEnvServe = {
  mode: 'development',
  command: 'serve',
  ssrBuild: false,
};

const configEnvBuild = {
  mode: 'production',
  command: 'build',
  ssrBuild: false,
};

module.exports = {
  stories: ['../src/components/**/*.stories.@(jsx|tsx)'],
  addons: [
    '@storybook/addon-a11y',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  staticDirs: ['./public'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  async viteFinal(config, { configType }) {
    const isProduction = configType === 'PRODUCTION';
    const { config: userConfig } =
      (await loadConfigFromFile(
        isProduction ? configEnvBuild : configEnvServe,
        path.resolve(__dirname, '../vite.config.ts'),
      )) ?? {};

    return mergeConfig(config, {
      ...userConfig,
      build: {
        ...config.build,
        sourcemap: !isProduction,
      },
    });
  },
};

```
このプロジェクトではsvgを扱うために`'vite-plugin-svgr'`を利用していて、Storybookでは`viteFinal`で改めて導入し直す必要があったのですが、このバージョンアップで不要になりました(むしろあったらエラーになります)。
Storybook7はここまでの変更で実行することが可能になります。`npm run storybook`コマンドでこれまで通り実行することができます。

ここからは今後推奨される(であろう)書き方に直していきます。`.storybook/main.cjs`はESM形式にTypeScriptで書けるようになりました。今後は`default export`で必要な情報をexportすることが推奨されます。

```ts:.storybook/main.ts
import { StorybookConfig } from '@storybook/react-vite';
import { ConfigEnv, loadConfigFromFile, mergeConfig } from 'vite';
import path from 'path';

const configEnvServe: ConfigEnv = {
  mode: 'development',
  command: 'serve',
  ssrBuild: false,
};

const configEnvBuild: ConfigEnv = {
  mode: 'production',
  command: 'build',
  ssrBuild: false,
};

const config: StorybookConfig = {
  stories: ['../src/components/**/*.stories.@(jsx|tsx)'],
  addons: [
    '@storybook/addon-a11y',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  staticDirs: ['./public'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  async viteFinal(config, { configType }) {
    const isProduction = configType === 'PRODUCTION';
    const { config: userConfig } =
      (await loadConfigFromFile(
        isProduction ? configEnvBuild : configEnvServe,
        path.resolve(__dirname, '../vite.config.ts'),
      )) ?? {};

    return mergeConfig(config, {
      ...userConfig,
      plugins: [],
      build: {
        ...config.build,
        sourcemap: !isProduction,
      },
    });
  },
};

export default config;

```
ファイル名の変更を行い、全てのimportをCommonJSの形式(`require`）からESMの形式(`import {} from ...`)に変更しました。さらにこれまで`module.exports`で出荷していた部分を`config`で宣言する様にしてそれを`default export`するようにしました。
`config`の型は`'@storybook/react-vite'`などの`framework`に渡すライブラリが提供する`StorybookConfig`になります。その他、TypeScript化のために`configEnvBuild`など細かい点に型を持たせました。

以上が`main.cjs`の変更です。最低限の変更はStorybookの自動的な変更が行ってくれるので簡単に対応できました。

# preview.tsxの書き換え
このファイルはバージョンアップに伴う動作の担保のための変更はありませんでした。
`main.cjs`と同様にESM化、TypeScript化、`export default`によるexportが推奨されるようになりました。ESM化、TypeScript化は元々行っていたので後者のみ行いました。

```tsx:.storybook/preview.tsx
import React from 'react';
import { Preview } from '@storybook/react';
import { AppProvider } from './../src/providers/app';

const preview: Preview = {
  decorators: [
    (Story) => (
      <AppProvider>
        <Story />
      </AppProvider>
    ),
  ],
};

export default preview;

```
`'@storybook/react'`の`Preview`型を持った`preview`を詰め込んで、それを`default export`します。
`parameters`などの他の要素もこの`preview`に詰め込んで設定します。

# Storyの書き換え
Storybook7では`Story`、`ComponentStory`、`ComponentStoryObj`、`ComponentStoryFn`および`ComponentMeta`が非推奨の型になりました。代わりに、`Meta`、`StoryObj`、`StoryFn`を利用するようになります。私が変更したプロジェクトでは以下のようになります。
```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { SampleComponent } from './SampleComponent';

const meta: Meta<typeof SampleComponent> = {
  component: SampleComponent,
};
export default meta;

export const Default: StoryObj<typeof SampleComponent> = {};
```
または`satisfies`を使って書きます。
```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { SampleComponent } from './SampleComponent';

const meta = {
  component: SampleComponent,
} satisfies Meta<typeof SampleComponent>;
export default meta;

export const Default: StoryObj<typeof meta> = {};
```
機能的な変更があるわけではないので、名前をひたすら変更するだけで対応することができます(ここで書いた例ではより[ドキュメント](https://storybook.js.org/docs/7.0/react/writing-stories/args)に近い書き方に合わせています)。
今回移行したプロジェクトではCSF3で記述していたため`StoryFn`は利用しませんでした。この型はCSF2の記法で使われます。
```tsx
export const CSF2Story: StoryFn<ButtonProps> = (args) => <Button {...args} />;
```
`Story`が非推奨になった理由にCSF3への移行があるのでこの機会にCSF2を用いている場合はCSF3に変更した方が良いと考えています。

CSF2からCSF3へ記法を変更したい場合は
```sh
npx sb@next migrate csf-2-to-3 --glob="**/*.stories.js"
```
を使って変換することができます。変更については[こちら](https://storybook.js.org/docs/7.0/react/api/csf#upgrading-from-csf-2-to-csf-3)に書かれています。


# さいごに
以上でStorybook7への移行は完了です。
型名の変更によって全てのStoryに影響があり移行が大変でしたが、コマンドの書き換えや`main.cjs`の書き換えなどは自動で実行されるため円滑に進められました。
コンポーネントごとに見た目を表示できるので実装が行いやすい点や、コンポーネントの管理がしやい点、UIに関するテストも合わせてかける点など、フロントエンドの開発で重宝される良いライブラリと考えているので、今後の発展を楽しみにしています(もちろん傍観しているだけではなく寄与していきたいと考えています)。