Next.js + TypeScript + CSS ModulesでStorybookを使う
By kkoudev
Next.js + TypeScript + CSS ModulesでStorybookの環境構築をするにあたって、思ったよりも面倒だったので書き残しておきます。
(Storybook v5 & v6対応版です)
Storybookを使う場合、Next.js同等のwebpack設定を自分で書く必要がある
Next.js は内部で webpack を使ってビルドをしていますが、
その設定の殆どが自動設定されており、デフォルト設定で事足りるのであれば自分でwebpack設定を記述する機会は基本的にありません。
但し、Storybookを利用する場合はNext.jsとは別機能となるため、自分で webpack の設定を記述する必要があります。
これは言い方を変えると、Next.jsの内部で設定している webpack の設定と同等のものを、Storybookのために再現してあげる必要があるということです。
非常に面倒くさいですね。
ですが現状、Next.js (執筆時点ではv9.5)がStorybookと連携する仕組みを提供していないため、仕方なく自前で作ることにします。
必要な依存関係のインストール
まず今回利用する依存関係をインストールします。
yarn add -D @storybook/react @storybook/addon-actions @storybook/addon-docs @storybook/addon-knobs @storybook/addon-links @storybook/addon-viewport react-is react-docgen-typescript-loader
Storybook v5までは上記の依存関係だけで良かったのですが、v6からは必要なwebpackのloaderは自分で追加する必要があります。
そのため、v6の場合は以下の loader も追加します。
yarn add -D file-loader url-loader style-loader css-loader postcss-loader
また、core-jsのエラーが出るケースに対処するために core-js もインストールする必要があります。
yarn add core-js
Storybookのディレクトリ構成
Storybookの設定はリポジトリルートに .storybook
ディレクトリを作成し、以下のファイル構成とします。
.storybook/
├── preview.js
├── main.js
└── postcss.config.js
v5までは config.js が使えたのですが v6 からは完全に使えなくなったため、preview.js を使う必要が出てきます。
そのためv5の場合でも preview.js を使うようにしましょう。
tsconfig.json や babel.config.js についてはNext.jsと同じものが利用可能なので別途用意する必要はありません。
但し、postcss.config.js については別です。
というのも、Next.jsの postcss.config.js は、webpackのpostcss-loaderにそのまま渡すためのデータ形式になっていないためです。
postcss-loaderの postcss.config.js の形式
module.exports = {
plugins: {
'postcss-import': {},
....(省略)...
'postcss-reporter': {
clearReportedMessages: true,
},
},
};
Next.jsの postcss.config.js の形式
module.exports = {
plugins: [
'postcss-import',
....(省略)...
[
'postcss-reporter',
{
clearReportedMessages: true,
},
],
],
};
このように、Next.jsの postcss.config.js はbabel.config.jsと同じような配列形式になっています。
なので、これを参考に同じ設定となるように書き換えたものを用意すればいいのですが、いちいち手で書き換えていては手間なので、
.storybook/postcss.config.js
のファイルで以下のようにNext.jsの設定から自動変換するようにして設定を作成します。
const postcssConfig = require('../postcss.config');
const usePlugins = {};
// Convert a plugins format for postcss-loader.
postcssConfig.plugins.forEach((plugin) => {
// Has options?
if (Array.isArray(plugin) && plugin.length === 2) {
usePlugins[plugin[0]] = plugin[1];
} else {
usePlugins[plugin] = {};
}
});
module.exports = {
plugins: usePlugins,
};
preview.jsの設定
ここではaddonに関する設定や、全体で共通読み込みをさせたいファイルをインポートします。
私の環境ではデフォルトスタイルや変数の読み込み、addonの設定を以下のように行っています。
import '@/interfaces/ui/styles/global.css';
import { addParameters } from '@storybook/react';
import { DocsPage, DocsContainer } from '@storybook/addon-docs/blocks';
addParameters({
docs: {
container: DocsContainer,
page: DocsPage,
},
});
main.jsの設定
次に、 .storybook/main.js
の設定を行います。
ここにwebpackの設定を書く必要があります。以下の内容で作成しました。
各設定の意味と、またv5とv6で微妙に設定が異なるのでコメントにて注釈を記載しておきます。
const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.{tsx,mdx}'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-docs',
'@storybook/addon-knobs',
'@storybook/addon-links',
'@storybook/addon-viewport',
],
webpackFinal: async (config) => {
// TypeScript読み込み設定
// クラスヘッダやPropsのコメントからドキュメントを生成する場合に必要。
// v6からはデフォルトでTypeScript対応しているのでコメントからドキュメント生成しないのであれば特に不要
config.module.rules.push({
test: /\.tsx?$/,
use: [
'babel-loader',
{
loader: 'react-docgen-typescript-loader',
options: {
tsconfigPath: path.resolve(__dirname, '../tsconfig.json'),
},
},
]
});
// ※ v5の場合は必須。v6の場合はデフォルトで設定されているため不要
config.resolve.extensions.push('.ts', '.tsx');
// CSS
config.module.rules.forEach((rule) => {
// MDX storyの読み込み設定
// ※ v5の場合は不要。v6の場合は babel.config.js をデフォルトで読み込まない設定となっているため必須
// Next.jsと同じ babel.config.js を利用するために babel.config.js の読み込みを有効にする
if (rule.test.toString() === /\.(stories|story).mdx$/.toString()) {
rule.use.forEach((use) => {
// Babel loader
if (use.loader.toString() === require.resolve('babel-loader')) {
// Use Babel config file
use.options.configFile = true;
}
});
}
// CSS Modules読み込み設定
// Next.jsの場合は *.module.css がCSS Modulesファイルとなるので
// *.module.css のみを対象とするように変更する
if (rule.test.toString() === /\.css$/.toString()) {
rule.test = /\.module\.css$/;
rule.use.forEach((using) => {
// CSS loader
// Storybookのwebpack設定では css-loader は @storybook/core 配下の css-loader が利用される
if (using.loader === require.resolve('@storybook/core/node_modules/css-loader')) {
// change options
using.options = {
importLoaders: 1,
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
};
}
// PostCSS loader
if (using.loader === require.resolve('postcss-loader')) {
// Change config path
using.options.config.path = path.resolve(__dirname);
}
});
}
});
// CSS Modules以外のCSSファイルの読み込み設定
config.module.rules.push({
test: /^((?!\.?module).)*css$/,
exclude: /node_modules/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('@storybook/core/node_modules/css-loader'),
options: {
importLoaders: 1,
modules: false,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
config: {
path: path.resolve(__dirname),
},
},
},
]
});
// ライブラリのCSSを読み込む設定
config.module.rules.push({
test: /\.css$/,
include: /node_modules/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('@storybook/core/node_modules/css-loader'),
options: {
importLoaders: 1,
modules: false,
},
},
]
});
return config;
},
};
やってることを順番にまとめてみると、
1. Next.jsの babel.config.js の設定を元にTypeScriptのトランスパイル設定を babel-loader にて行います。
Storybookの公式ドキュメントをみるとv5の場合は ts-loader を使う例もあるのですが、Next.jsはBabelを使ってTypeScriptのトランスパイルをしているため、
出来る限りNext.jsの設定を流用するため(同じbabel.config.jsを使うため)に babel-loader を使うようにしています。
Storybook v6の場合はデフォルトでTypeScript読み込み設定がされているのですが、
次項の react-docgen-typescript-loader
を利用するためには結局設定が必要になります。
2. react-docgen-typescript-loader を使ってコードのコメントからドキュメント生成できるようにします。
これは @storybook/addon-docs
でReactコンポーネントやそのPropsにコメントした内容をStorybookで表示するために指定しています。
3. Storybookデフォルトの css-loader および postcss-loader の設定をNext.jsのCSS Modulesの仕様に合わせて変更します。
コメントにも記載していますが、
具体的には *.module.css
のファイルのみをCSS Modulesとして解釈し、それ以外の *.css
ファイルについてはCSS Modulesとして扱わないように変更します。
また、.storybook/postcss.config.js
を読み込むように config ディレクトリのパスを変更します。
MDXの記述
これで準備ができたので、あとはMDXにStoryを記述していきます。
MDXのファイルは、前回紹介したコンポーネントディレクトリに、以下のように index.stories.mdx
というファイルを追加しておきます。
DefaultButton
├── controller.ts
├── index.stories.mdx
├── index.ts
├── presenter.ts
├── props.ts
├── style.module.css
└── view.tsx
import { action } from '@storybook/addon-actions';
import { Story, Meta, Preview, Props, Source, Description } from '@storybook/addon-docs/blocks';
import { DefaultButton } from '.';
<Meta
title="Components/Atoms/DefaultButton"
parameters={{
component: DefaultButton
}}
/>
# DefaultButton
<Description of="." />
## Props
<Props of="." />
## Themes
### active
<Preview withSource="close">
<Story name="active" >
<DefaultButton title="アクティブボタン" theme="active" />
</Story>
</Preview>
Storybookの起動
以上で設定とStoryの記述が完了したので、最後にStorybookを起動するだけになります。
以下のコマンドを storybook という名前で npm-scripts へ追加し、 yarn storybook
で起動します。
"scripts": {
"storybook": "start-storybook -p 6006 -s ./public"
}
-s オプションにてNext.jsの静的ファイルディレクトリを指定しておきます。(画像ファイルなどの読み込みに必要です)
成功すると、以下のようにStorybookの画面が表示されます。
まとめ
Next.js + TypeScript + CSS ModulesでStorybookを使う際の設定を紹介させていただきました。
Storybookを使ってコンポーネント設計を意識したコーディングをしていくと、実際に画面に組み込む際も非常に簡単に組み込むことが可能となります。是非導入することをおすすめします。