logoMiyauchi

StorybookでバンドラーにViteを使う

はじめに

Storybook で Vite を使ってビルドできるようになったので紹介します。

iframe 内のビルドを Webpack から vite に切り替えることで次の利点があります。

  • ビルド速度の改善
  • HMR の高速化
  • アセット処理の自動化
  • Vite プロジェクト設定との互換性
  • Vite のプラグインエコシステムへのアクセス

コンポーネントが少ない場合、速度の恩恵はあまり感じられない可能性があります。 Webpack ��������すると���ブラウザが表示されるまでのスピードは劇的に向上しますが、 ブラウザ上での読み込みに時間がかかるためです1

一方、コンポーネントが増えた場合に増加する時間は、抑えられると思います。 また、HMR での再ビルドは明らかな速度の差を感じられると思います。

個人的に vite に切り替える最大の利点は、アセット処理の設定が不要になることです。

Webpack ベースの場合、例えば sass を使うには、それ用の loader を設定する必要があります。 viteであれば、TypeScript、CSS プリプロセッサ、Static Assets の処理などは標準で備えているため設定不要です。

その反面、viteに切り替えることで、一部の Storybook アドオンが使用できなくなる可能性があります。 これは Storybook で webpack 5 を使用する場合にも同じことが言えます。 多くの Storybook アドオンを利用しているプロジェクトでは、十分な検証をしてから切り替えることをおすすめします。

ちなみに以下のアドオンは動作確認しました。

Storybook の準備

説明にはpreactを用います。preact を例にする理由は、preact 環境の場合、少しハマるポイントがあるためです。

まずはプロジェクトの雛形を作成します。

npm init @vitejs/app <project-name> -- --template preact-ts
cd <project-name>
npm i -D @mdx-js/preact

のちに必要になる storybook-builder-vitepeerDependency もインストールしています。

つづいて、Storybook の雛形を生成します。 スクラッチでももちろん環境構築できますが、次のコマンドで雛形を生成できます。

npx sb init --builder storybook-builder-vite

builder オプションに storybook-builder-vite を指定するとついでに storybook-builder-viteもインストールしてくれます。

さてこれで雛形は完成しました。

この時点でのディレクトリ構成は次の通りです。説明に不要なファイルは除いています。

.
├── .storybook
│   ├── main.js
│   └── preview.js
├── package.json
├── src
│   ├── stories
│   │   ├── Button.jsx
│   │   ├── Button.stories.jsx
│   │   └── Introduction.stories.mdx
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts

また、package.json には次のコマンドが追加されています。

{
 "scripts": {
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  }
}

この段階で start-storybook コマンド������行��て���、Introduction.stories.mdxのレンダリングに失敗します。

preactmdxファイルを扱う場合には設定が必要です。 プロジェクトにreactvue を選択した場合、次の作業は不要です。

また、もし preact 環境の場合でも、mdxを扱う必要がなければ、Introduction.stories.mdx を削除することで。 次のセクションを呼び飛ばすことができます。

Preact で mdx ファイルを扱う

preactmdx ファイルを Storybook で使うために、2つの変更が必要です。

Storybook の設定ファイルを変更する

続いて、.storybook/main.js ファイルを変更します。あとでこのファイルを .ts にして型安全にする方法を紹介します。

module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
  core: {
    builder: 'storybook-builder-vite'
  },

  viteFinal: (config) => {
    config.plugins = [
      ...config.plugins,
      require('@preact/preset-vite').default()
    ]
    return config
  }
}

storybook-builder-vite によって、viteFinalというフックが追加されます。 このフックから vite の設定を変更できます。 プラグインで @preact/preset-vite を使うように変更します。 ちなみに元々の構成では、以下のプラグインが有効になっています。

  • storybook-vite-code-generator-plugin
  • mock-core-js
  • vite-plugin-mdx
  • mdx:transclusion
  • storybook-vite-inject-export-order-plugin

これらも引き続き使う必要があるので、Destructuring assignmentによって、plugins にプラグインの配列を代入します。

jsx inject に対応する

@preact/preset-vite によって、[j,t]sxファイルには自動的に import { h, Fragment } from 'preact' が挿入されます。 雛形で Storybook を生成した場合、 生成されたファイルにはすでに import { h, Fragment } from 'preact'が宣言されています。  自動挿入により宣言が重複するので、雛形のすべてファイルから上の宣言を削除します。

これで準備は完了したので、start-storbybook コマンドを実行しましょう。 無事、正常にレンダリングされていると思います。

Storybook の設定ファイルを型安全にする

Storybook の設定ファイルは、デフォルトでは .jsです。 わたしは設定ファイルは何が何でも型安全にしたい病気なので、その方法を紹介します。

現在の状態は次の通りです。

.
└── .storybook
    ├── main.js
    └── preview.js

main.jspreview.js は利用するモジュールシステムが異なります。

File

Module

Ext

main.js

Commonjs

.js

preview.js

ES modules

.[j,t]sx?

main.jsNode.js での 利用を前提としているため、 Commonjs を採用しています。 一方、preview.js は ブラウザで実行されるため、 ES modules です。 こちらは TypeScript もサポートされています。

この両方を TypeScrit 化して、 ES modulesで書けるように変更します。 ちなみに、Gatsby なども似たような構成なので、同じ手法で型安全な設定ファイルを定義できます。

main.js を型安全にする

main.js はエントリーポイントなので、このファイルはそのまま残します。 新たに main.ts を作成し、 main.js の内容を ES modules に書き換えます。

import preact from '@preact/preset-vite'
const config = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
  core: {
    builder: 'storybook-builder-vite'
  },
  viteFinal: (config) => {
    config.plugins = [...config.plugins, preact()]
    if (process.env.NODE_ENV === 'production') {// [!code highlight]
      config.build.chunkSizeWarningLimit = 1200// [!code highlight]
    }// [!code highlight]
    return config
  }
}

export default config

このタイミングで chunkSizeWarningLimit を変更するコードを追加しています。 ビルド時のバンドルサイズの���告を抑える目的です。なくても問題ないです。

これに型注釈を与えます。型は storybook-builder-vite からは提供されていないので、自作する必要があります。

import { StorybookConfig, CoreConfig, Options } from '@storybook/core-common'
import { UserConfig } from 'vite'
import { Weaken } from 'utilitypes'

interface CustomizedCoreConfig extends Weaken<CoreConfig, 'builder'> {
  builder: CoreConfig['builder'] | 'storybook-builder-vite'
}
interface CustomizedStorybookConfig extends Weaken<StorybookConfig, 'core'> {
  core: CustomizedCoreConfig
  viteFinal?: (config: UserConfig, options: Options) => UserConfig
}

Storybook の StorybookConfig 型を拡張する必要があります。 interface ですでに存在するプロパティを拡張する場合は、そのプロパティを一旦 any にする必要があります。

Weaken は私が作っている utilitypesというプロジェクトで提供している便利な型です。 指定したプロパティを any した型を返します。

この記事の作成時はまだ、 beta です。

npm i -D utilitypes@beta

あとはこの型を型注釈で指定します。ファイル全体は次のようになります。

import preact from '@preact/preset-vite'
import { StorybookConfig, CoreConfig, Options } from '@storybook/core-common'
import { UserConfig } from 'vite'
import { Weaken } from 'utilitypes'

interface CustomizedCoreConfig extends Weaken<CoreConfig, 'builder'> {
  builder: CoreConfig['builder'] | 'storybook-builder-vite'
}
interface CustomizedStorybookConfig extends Weaken<StorybookConfig, 'core'> {
  core: CustomizedCoreConfig
  viteFinal?: (config: UserConfig, options: Options) => UserConfig
}

const config: CustomizedStorybookConfig = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
  core: {
    builder: 'storybook-builder-vite'
  },
  viteFinal: (config) => {
    config.plugins = [...config.plugins, preact()]
    if (process.env.NODE_ENV === 'production') {
      config.build.chunkSizeWarningLimit = 1200
    }
    return config
  }
}

export default config

続いて、 main.js から main.ts をインポートします。 main.jsCommonjs なので、 通常 TypeScript も ES modules も処理できません。

これを実現するために、 esbuild-registerというパッケージを使います。 esbuild-registerts-nodeesbuild 版です。型のチェックはありません。早いです。

npm i -D esbuild-registerbeta

main.js は次のようになります。

const { register } = require('esbuild-register/dist/node')
register({
  target: 'node15'
})
module.exports = require('./main.ts')

main.js は単なるエントリーポイントで、実際の設定は型安全に main.ts に書けるというわけです。

preview.js を型安全にする

preview.js の方は、 ES modules ですし、 TypeScripts も処理できるのでやることはほぼありません。

  • ファイルの拡張子を .ts もしくは .tsx へ変更
  • Storybook の型を型注釈する

こんな感じになります。

import { Parameters } from '@storybook/addons'
export const parameters: Parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/
    }
  }
}

Storybook は大量のファイルを提供しているので、型を見つけるのが大変でした💦。

これで型安全な開発ができますね。

プロダクションコードをプレビューする

完全におまけです。ビルド済みのコードを確認したいことありますよね。 Webpackから vite へ切り替えるような場合はなおさらです。

Storybook にはプレビュー用のコマンドが用意されていません。 なので、静的ファイルサーバーを自分で建てなければなりません。といっても簡単ですが。

npx http-server storybook-static

これでプレビューができますね。