はじめに
ついにStorybookのバージョン7がリリースされ最新版になりました。
この記事ではReact×Viteの環境でバージョン6から7にアップデートするまでに行ったことをまとめました。
公式が公開する移行方法はこちらになります。
これまでの書き方
今回私がアップデートしたプロジェクトの設定ファイルやStoryの書き方を紹介します。
アップデート前の設定ファイルは以下のようになっています。
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,
},
});
},
};
import React from 'react';
import { AppProvider } from './../src/providers/app';
export const decorators = [
(Story) => (
<AppProvider>
<Story />
</AppProvider>
),
];
Storyは基本的に以下のように書かれていました。
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にはアップデートを自動でおこなってくれるコマンドがあるのでそれを用いて行いました。
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は以下のように設定されました。
"scripts": {
"storybook": "NODE_OPTIONS=--openssl-legacy-provider start-storybook",
"build-storybook": "NODE_OPTIONS=--openssl-legacy-provider build-storybook",
}
そのような設定が記載されていても検知してstorybook devとstorybook buildに置き換えてくれました。
"scripts": {
"storybook": "NODE_OPTIONS=--openssl-legacy-provider storybook dev",
"build-storybook": "NODE_OPTIONS=--openssl-legacy-provider storybook build",
}
置き換え後もNODE_OPTIONS=--openssl-legacy-providerがついてきますが、Storybook7ではこれらのオプションが不要になったのでコマンド実行後に削除しました。
"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を自動で変更されます。
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のドキュメントは自動生成されてきましたが、バージョン7では自動生成をさせないことがデフォルトになりました。これまで通り生成して欲しい場合はyを入力してください。以下のように.storybook/main.cjsに追加されるはずです。
docs: {
autodocs: true
}
私はドキュメントを利用していなかったのでnを入力しました(その場合何も変わりはありません)。'tag'にすると、Storyごとに自動生成させるかどうかを指定させることができます。
以上がコマンドによる変更でした。
main.cjsの書き換え
コマンド実行後、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を消します。そして元の状態のように整形すると以下のようになります。
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することが推奨されます。
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化は元々行っていたので後者のみ行いました。
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を利用するようになります。私が変更したプロジェクトでは以下のようになります。
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を使って書きます。
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> = {};
機能的な変更があるわけではないので、名前をひたすら変更するだけで対応することができます(ここで書いた例ではよりドキュメントに近い書き方に合わせています)。
今回移行したプロジェクトではCSF3で記述していたためStoryFnは利用しませんでした。この型はCSF2の記法で使われます。
export const CSF2Story: StoryFn<ButtonProps> = (args) => <Button {...args} />;
Storyが非推奨になった理由にCSF3への移行があるのでこの機会にCSF2を用いている場合はCSF3に変更した方が良いと考えています。
CSF2からCSF3へ記法を変更したい場合は
npx sb@next migrate csf-2-to-3 --glob="**/*.stories.js"
を使って変換することができます。変更についてはこちらに書かれています。
さいごに
以上でStorybook7への移行は完了です。
型名の変更によって全てのStoryに影響があり移行が大変でしたが、コマンドの書き換えやmain.cjsの書き換えなどは自動で実行されるため円滑に進められました。
コンポーネントごとに見た目を表示できるので実装が行いやすい点や、コンポーネントの管理がしやい点、UIに関するテストも合わせてかける点など、フロントエンドの開発で重宝される良いライブラリと考えているので、今後の発展を楽しみにしています(もちろん傍観しているだけではなく寄与していきたいと考えています)。