Open19

Deno / Fresh / preact向けのstorybook的なものを構築したい

hashrockhashrock

そもそもあんまりstorybookを運用した経験がない。まずはどのへんが嬉しいのかを調べていく。

hashrockhashrock

https://zenn.dev/sa2knight/books/storybook-7-with-vue-3/viewer/summary

Vueで運用する例。PropsをUI上から書き換えたり、自動テストをしたりしている。

操作を行った結果をプレビューできるのは良さそう。

Propsの書き換えは便利に見えるけど、でもいろんなpropsを入力済みにしたケースをたくさん並べたほうが、テストケースっぽくて良さそうに見える。インタラクティビティが本当に必要なのかちょっとわからないな。

hashrockhashrock

あまり気乗りしない(大きいツールが好きじゃない)のだが、とりあえずvite + preact + TSのプロジェクトをstorybook化してみよう。

npm create vite

その後、components/MyButton.tsxを作る。

import type { ComponentChildren } from "preact";

export interface MyButtonProps{
  children: ComponentChildren
}

export default function MyButton(props: MyButtonProps){
  return <button style={{
    backgroundColor: "black",
    color: "white"
  }}>
    {props.children}
  </button>
}

hashrockhashrock

というわけでstorybookをインストールする。

npx storybook@latest init

preactとviteが判別され、インストールは成功。

hashrockhashrock

MyButton.stories.tsxを作成する。

import type { Meta, StoryObj } from '@storybook/preact';

import MyButton  from './MyButton';

const meta: Meta<typeof MyButton> = {
  component: MyButton,
};

export default meta;
type Story = StoryObj<typeof MyButton>;

export const Primary: Story = {
  render: () => <MyButton>Hello</MyButton>,
};

無事認識された。いけそうか?

hashrockhashrock

ダメ元でdeno taskで動かしてみる。

hashrock@hashrocknoMac-mini study-preact-vite-storybook % deno task storybook
Task storybook storybook dev -p 6006
@storybook/cli v7.5.3

✔ Port 6006 is not available. Would you like to run Storybook on port 62211 instead? … yes
TypeError: worker.unref is not a function
    at startWorkerThreadService (file://./node_modules/.deno/esbuild@0.18.20/node_modules/esbuild/lib/main.js:2306:10)
    at transformSync (file://./node_modules/.deno/esbuild@0.18.20/node_modules/esbuild/lib/main.js:2054:29)
    at compile2 (file://./node_modules/.deno/esbuild-register@3.5.0/node_modules/esbuild-register/dist/node.js:4882:43)
    at Module._compile (file://./node_modules/.deno/esbuild-register@3.5.0/node_modules/esbuild-register/dist/node.js:2254:31)
    at Module._extensions..js (node:module:747:10)
    at Object.newLoader [as .ts] (file://./node_modules/.deno/esbuild-register@3.5.0/node_modules/esbuild-register/dist/node.js:2262:9)
    at Module.load (node:module:658:32)
    at Function.Module._load (node:module:539:12)
    at Module.require (node:module:677:19)
    at require (node:module:791:16)

WARN Broken build, fix the error above.
WARN You may need to refresh the browser.

esbuildのビルドでエラーになる。
(このレベルで依存が多いツールはまあ動かないだろうとは思っていた)

freshプロジェクトでstorybookを使いたいのだが、storybookのビルドはnodeでやらざるを得ないようだ。

hashrockhashrock

さてどうするか。

  • このままNode.jsをプロジェクト内に導入することを受け入れて、コンポーネントのみviteの中で作る。
    • そんなメリットあったか?
  • Histoire / Ladleのようなalternativeがpreactで使えるか試してみる
    • 見た感じは望み薄そうな…
  • 自分で書く

というかstorybook alternativeはいくらでもみんな作ってそして消えていったものなので、個人で作ったものが本家に太刀打ちできる可能性はゼロなんだよな、この中で「自分で書く」というのはあまり良い選択肢ではないんだろうな…

でも書いてみたい…

hashrockhashrock

結局書くんだからもう書いたらええんじゃ。

ツールをフルスクラッチで書く場合、自分の中で許容できるパターンはただ一つ。「100行で書く」みたいなミニマル実装を作る。絶対に大きくしない。

だからフレームワークべったりで良い。誰か使いたい人がいるなら勝手にフォークしてカスタマイズすればよい。

欲しい機能:

  • *.stories.tsxを見つけてリストする。
  • 特定のストーリーだけをserveする単体ページ。
  • リストからコンポーネントを選択すると、単体ページをiframeで表示する。

これだけ。

Storybookは、TSの型からpropsを取得して編集可能にすることができるようだ(どうやってるんだろう?ASTを読んでる?)。まあそれはやりたい人がやるだろう。

hashrockhashrock

割と難しいかもしれない。

まず動的にtsxを見つけて動かすというのができないはず。deno deployは動的importに対応しているが、静的解析が可能なものだけである(パスを動的に生成する場合はeszipにソースを含めてくれない=動かない)。

freshがfresh.gen.tsを生成するのも、動的importを不要にするためである。routesはどこからも繋がっていないのでfresh.gen.tsから繋げてあげる必要があるというわけ。

じゃあstories.tsxのパスを直指定してそれをiframe内にレンダリングするというのはできない。対処としてはソースコードを生成してしまうか、routesかislandsの自動リンクの仕組みを利用してしまうかだ。

hashrockhashrock

…というより、普通に「全部手で書け」というだけの話か。自動でstoriesを見つけるとかはそういうコマンドを用意すればいい。

hashrockhashrock

なんかComponentをIslandに変換するような仕組みがほしいのではないか?現状だと、islands/MyButtonWrapper.stories.tsxを作る必要がある気がする

hashrockhashrock

まあいいか。今なislandsがネスト可能になったから、インタラクティブなコンポーネントだったら気軽に全部islandsにしちゃえばいいんだよな。

hashrockhashrock

あと結局storiesは全部routesに書け、にした。でも外から見れちゃうのどうなんだろう(middlewareで制限できるとはいえ)。
どうだったらいい、が見えないなぁ…何度か書き直すしかないな。

hashrockhashrock

動き始めた。

https://hr-my-small-storybook.deno.dev/catalog

今のところislandsにstoriesというフォルダでストーリーを書いていく方式。

https://github.com/hashrock/fresh-stories/tree/main/islands/stories

なにげにislandsは動的import(ほんとに動的なやつ)も動くと判明してそれを使っている。というのも、islandsはfresh.gen.ts内に自動的にリストアップされるので、基本すべてがeszip内に含まれる。eszipにあればimportしてこれるのだ…(悪用に近いぞ)

本当は MyButton.stories.tsx みたいな拡張子でコンポーネント本体と区別できるべきだと思う(Ctrl + Pでの検索で両方引っかかるとだるいため)。しかしfreshが . を含むislandsをimportできないっぽい現象を見つけたため今のところはやっていない(バグっぽい)。

前述の通りprops関連の機能は全カット。自動テストはpuppeteerで書けばいいしアクセシビリティのテストは普通にchromeの機能でやればいいんじゃないでしょうか。darkmodeを確認できる機能はほしい気がするなぁ。あとはドキュメントを書く機能(markdownが書ければそれでよさそう)をつけるかちょっと悩むところ。

グルーピングの機能とかも全然ない。どこまでやったもんだかね。

hashrockhashrock

Darkmode switchを実装しようとしている。

知らなくてハマったんだけど、親要素に dark classをつけてダークモードの手動切替を行うには、
tailwindのconfigでdarkMode: 'class' を指定する必要があるらしい。

これはプラグインとして実装しようとしている現状では問題で、ユーザに上記設定を強制することになってしまう。設定の有無を取得しにいって、可能な場合のみスイッチを表示するか?なんか微妙だな〜〜