Storybook での開発体験爆上げ VS Code 拡張機能を作った話
自己紹介
Kyoto.js は3回目の参加、初登壇です!
今年の春から京都
株式会社ゆめみ
storybook.icon Storybook 使ってますか?
storybook.icon Storybook のうれしさ
UI コンポーネントを単体でレンダーできる!
ページの適当な場所に置いて…… が無くなる
エッジケースのUIを直接表示できる
確認のために連打とかしなくて良い!
アクセシビリティテストも!
インタラクションテストも!
msw で API モックも!
React、Vue、Svelte、HTML!などいろいろサポート
どうやって?
便利!🤩
でも……
コンポーネントが増えたとき、目的の Story を開くのが面倒!
特に複数のコンポーネントにまたがる実装、レビューをしている時とか…
$ 🤔^💡
Storybook は Story のファイルから Storybook の URL を構築している
code:/src/components/ui/button.stories.tsx
const meta = {
component: Button,
args: {
children: "ボタン",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
↓
http://localhost:6006/?path=/story/components-ui-button--default
VS Code 拡張機能で同じことをやればファイルから直接 Storybook を開けるのでは……?
どうしたら実装できる?$ 🤔^{❓}
1. Storybook はどうやってファイルから URL を取っているか
2. どうやって VS Code で ↑ を実行するか
Storybook はどうやってファイルから URL を取っているか
Story ファイル
code:/src/components/ui/button.stories.tsx
const meta = {
component: Button,
args: {
children: "ボタン",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
↓
http://localhost:6006/?path=/story/components-ui-button--default
Storybook の奥地へ…
Indexer (旧 Story Indexer)
code:code/lib/core-server/src/presets/common-preset.ts
const indexer = {
test: /(stories|story)\.tjsx?$/, indexer: async (fileName, opts) => {
const code = readFileSync(fileName, { encoding: "utf-8" });
return loadCsf(code, { ...opts, fileName }).parse();
},
};
loadCsf
code:code/lib/csf-tools/src/CsfFile.ts
export const loadCsf = (code: string, options: CsfOptions) => {
const ast = babelParse(code);
return new CsfFile(ast, options);
};
Babel の AST を解析していた
indexer は設定で上書きできる
設定すればどんなファイルでも読み込める
例: addon-docs は indexer 内で MDX をコンパイルする
code:ccode/addons/docs/src/preset.ts
export const createStoriesMdxIndexer = (legacyMdx1?: boolean): Indexer => ({
test: /(stories|story)\.mdx$/,
createIndex: async (fileName, opts) => {
let code = (await fs.readFile(fileName, 'utf-8')).toString();
const { compile } = legacyMdx1
? await import('@storybook/mdx1-csf')
: await import('@storybook/mdx2-csf');
code = await compile(code, {});
const csf = loadCsf(code, { ...opts, fileName }).parse();
// 略
},
});
つまりVS Code 拡張機能でこれをやるには…… $ 🤔^{💡}
Storybook の config を読み込む
Storyboox と同様のアルゴリズムで Indexer を用いて Stories ファイルを読み込む
読み込んだ Stories の ID と URL を得る
VS Code 拡張機能でどうやって Storybook のモジュールを読み込む?
ユーザーがインストールした Storybook の Addon を使いたい
https://gyazo.com/22dc0ecb2766ff5fe3337d8a0bb6bf96
$ 🤔^{❓}
何か参考になるものは… $ 🤔^{💭}
なるべく大きくない(大変なので)
ユーザーの設定を読み込んでいる
プラグインのような機構を持っている
ユーザーのソースコードを解析する必要がある
https://gyazo.com/4e8fbeaf241f63be52975fb7ae87d9c4
$ 🤔^💡
https://gyazo.com/76d0ca38999d2dc3d89967cf72124ab8
https://gyazo.com/76d0ca38999d2dc3d89967cf72124ab8
https://gyazo.com/6b286a0f896aabd293bf22e0af92e172
https://gyazo.com/8f54c8ac081b92f74bf09253dc900cb8
code:src/utils/requireFrom.ts
export const requireFrom = function (id: string, dir: string) {
return require(require.resolve(id, { paths: dir })); };
code:ts
const { loadMainConfig } = requireFrom(
"@storybook/core-common",
workingDir,
) as typeof import("@storybook/core-common");
余談
Prettier 拡張機能と同じ問題にハマっていた
VS Code 拡張機能では dynamic import できない!
https://gyazo.com/3562f97a96e1eacd3f95723f17ee34d8
その後追加された機能
使っていって不便だと感じたことを直していった
Storybook が起動してなかったら起動してくれたらいいのに…
サーバーの自動起動
button.tsx や button.module.css から button.stories.tsx の Story が開けたらいいのに…
colocation のサポート
Monorepo で使いたい!
Muliti root workspace のサポート
PR が来た!
https://gyazo.com/b7d2f6d68e38a758f49c6826e40487b6
Storybook と VS Code に詳しくなれた
これまで謎技術だったStorybookをちょっとはわかってあげられるように
リリースノートとか
Storybook でトラブっても「あーはいはいアレね」と言えるようになった
Storybook のバグを修正して Contribute できた!
https://gyazo.com/4cf22f8f9ae3acae1b871268c31bc8f9
最後に
「VS Code 使ってないので…」?
/storybook フォルダ内は VS Code の API 非依存です!
あなたのエディタで作るときの参考にしてください!