SvelteKitでStorybookを使えるようにする

この記事は[SvelteKit Advent Calendar 2022](https://qiita.com/advent-calendar/2022/sveltekit)の9日目の記事です。

コンポーネントの開発の際にとても重宝するStorybookですが、これをSvelteKitを使っているプロジェクトに組み込むにはややデフォルトのコンフィグから変更する必要があるので動く状態まで持って行く方法を紹介したいと思います。

## ひとまずStorybookを入れる
この段階ではStorybook周りが何も入っていない、SvelteKitのプロジェクトを前提として説明します。

公式のチュートリアル( https://storybook.js.org/docs/react/get-started/install )にある通り、インストールします。

```bash
npx storybook init
```

途中で

> Do you want to run the 'eslintPlugin' migration on your project?

と聞かれるのでyesと答えておきます。

しばらく待つと必要なパッケージの追加とインストールが自動で走るので待ちましょう。
（自動で初期のコンフィグは追加してくれるのでそこは何もしなくても大丈夫です）

## そして動かない...ので直す！
```bash
yarn run storybook
```

`npx storybook init` のウィザードにある通りstorybookを実行しますが、残念ながら初期状態では動きません。

大量にエラーが出てそもそも異常終了してしまいます....😭

1つ1つ直して動くようにしていきましょう。

### Error [ERR_REQUIRE_ESM]: require() of ES Module ./svelte.config.js from ./.storybook/main.cjs not supported.

```
Error [ERR_REQUIRE_ESM]: require() of ES Module ./svelte.config.js from ./.storybook/main.cjs not supported.
Instead change the require of svelte.config.js in ./.storybook/main.cjs to a dynamic import() which is available in all CommonJS modules.
```

```diff
--- a/.storybook/main.cjs
+++ b/.storybook/main.cjs
@@ -12,9 +12,6 @@ module.exports = {
   "core": {
     "builder": "@storybook/builder-vite"
   },
-  "svelteOptions": {
-    "preprocess": require("../svelte.config.js").preprocess
-  },
   "features": {
     "storyStoreV7": true
   }
```

なにやらrequire周りでエラーが出ているようですが、これはimportに直してあげることで直ります。

が、しかしimportはPromiseを返してくるなど少し扱いづらいので今回はオプションごと消去して後述する方法で設定します。

### Error: error:0308010C:digital envelope routines::unsupported
```
info @storybook/svelte v6.5.14
info 
info => Loading presets

info => Ignoring cached manager due to change in manager config
ℹ ｢wdm｣: wait until bundle finished: 
node:internal/crypto/hash:71
  this[kHandle] = new _Hash(algorithm, xofLen);
                  ^

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:71:19)
    at Object.createHash (node:crypto:140:10)
```

今度は不思議なエラーが出ました。これについては、Storybookが古いwebpackに依存している都合上、比較的新しいバージョンのNode.jsと組み合わせるとレガシーなOpenSSLの機能を使っている都合でエラーになってしまうようです。(ファイルのハッシュサムを計算するために使っている模様)

一旦はworkaroundとしてレガシーなOpenSSLプロバイダーを使うように設定して回避することが出来るので、起動時にそのオプションを有効化してあげます。

```diff
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
     "test:unit": "vitest",
     "lint": "prettier --plugin-search-dir . --check . && eslint .",
     "format": "prettier --plugin-search-dir . --write .",
-    "storybook": "start-storybook -p 6006",
+    "storybook": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006",
     "build-storybook": "build-storybook"
   },
   "devDependencies": {
```

## 一旦起動してみる
`yarn run storybook` で起動してみましょう。立ち上がってきたポートをブラウザで開くとこのような画面が出てくると思われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41743/1a4508d2-21dc-03dc-4b2a-d95894497119.png)

Storybookは初期状態ではいくつかのサンプルのコンポーネントを含んだ状態でスタートするので、それらが正常に表示出来てれば問題ありません。

起動すらしなかったStorybookは起動するようになりました❗❗❗❗(しかし、まだ道のりは遠い)

## まだまだ動かないStorybook
さて、ここまで動いたのは良いのですが、コンポーネント内でSassが使えない、TypeScriptが使えない、正しくimport出来ないなどの問題に気づくと思います。

もう少し修正が必要です。

また、今回この過程で `@storybook/addon-svelte-csf` を追加でインストールしたので、インストールしてください。

最終的にこのような設定になりました。

```js:.storybook/main.cjs
const path = require('path');
const { loadConfigFromFile, mergeConfig } = require('vite');

module.exports = {
	stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'],
	addons: [
		'@storybook/addon-links',
		'@storybook/addon-essentials',
		'@storybook/addon-interactions',
		'@storybook/addon-svelte-csf'
	],
	framework: '@storybook/svelte',
	core: {
		builder: '@storybook/builder-vite'
	},
	features: {
		storyStoreV7: false
	},
	async viteFinal(config, { configType }) {
		const { resolve } = await import('path');
		const { config: userConfig } = await loadConfigFromFile(
			path.resolve(__dirname, '../vite.config.js')
		);

		return mergeConfig(
			{
				...userConfig,
				plugins: userConfig.plugins.flat(1).filter((p) => !p.name.startsWith('vite-plugin-svelte'))
			},
			{
				...config,
				resolve: {
					alias: {
						$lib: resolve('src/lib'),
						$app: path.resolve('../node_modules/@sveltejs/kit/src/runtime/app')
					}
				},
				define: {
					__SVELTEKIT_DEV__: true,
					__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: 1000
				}
			}
		);
	}
};
```

もはやデフォルトの設定から原型がないですが、一旦この設定で以下のことを行っています。

- 自動で `preprocess` の設定をする(`vite.config.js`経由でSvelteKitのViteプラグインを使って読み込ませる)
- 諸々のSvelteKitのViteプラグインによって設定される設定値を引き継ぐ
- `vite-plugin-svelte`から名前が始まるプラグインは全て除外する(入れてしまうとStorybookのビルドが壊れます)
- SvelteKitの機能に依存したコンポーネントのためにビルドエラーにならないように必要最低限のグローバル定数を定義しておく
- SvelteKitの`$app`を使えるように強引にaliasでねじ込む
- 参照に失敗しないようにエイリアス周りの設定をする

残念ながらかなりハックしないと動かないようです....😭

## 動作確認してみる
```vuejs:src/lib/components/TestComponent.svelte
<script lang="ts">
	export let startColor: string;
	export let endColor: string;
</script>

<div class="gradation" style="--start-color: {startColor};--end-color: {endColor};">
	Storybook最高
</div>

<style lang="scss">
	.gradation {
		font-size: 100px;
		font-weight: bolder;
		-webkit-background-clip: text;
		color: transparent;
		background-image: linear-gradient(var(--start-color), var(--end-color));
	}
</style>
```

```vue:src/stories/TestComponent.stories.svelte
<script lang="ts">
	import { Meta, Story, Template } from '@storybook/addon-svelte-csf';
	import TestComponent from '$lib/components/TestComponent.svelte';
</script>

<Meta title="Components/TestComponent" component={TestComponent} />

<Template let:args>
	<TestComponent {...args} />
</Template>

<Story name="Default" args={{ startColor: '#0014FF', endColor: '#00FFF6' }} />
```

試しに適当なコンポーネントを配置してみます。

`yarn run storybook`で起動してみると....

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41743/f2b1d411-13cc-79da-4385-784fc9c06be8.png)

こんな感じに表示されれば成功です！

## さいごに
Storybook 7ではこれらのハックは使えないと思われるので、7.xがリリースされたらまた対応が必要になると思われます。

ただ、 https://github.com/storybookjs/storybook/issues/19280 のissueによると部分的なSvelteKitサポートが入っているようなので、今よりかは若干楽に動かせるようになるかもしれません。