yarn workspace で各 workspace の storybook の依存を root で管理したい

現状と課題
現状
- yarn workspace を使った monorepo 環境で、各 workspace で storybook をセットアップしている。(= 全ての package.json に storybook 関連の依存が入った状態。)
- yarn v3 使ってる
課題
- storybook の version が workspace ごとに異なると、たびたびエラーが起きる。
- そのため、storybook の version は全ての workspace で固定。
- version を上げる際に全ての workspace で version を上げるのがめんどい。

project root でまとめてみる
とりあえず、何を project root にまとめたらいいかわからんので、storybook の初期化コマンド叩いて何が install されるのか調べる。
ちなみに、project 自体はシンプルな React の library。(CRA でも Vite でもない。)
npx storybook init
install されたもの一覧↓
"@babel/core": "^7.20.12",
"@mdx-js/react": "^1.6.22",
"@storybook/addon-actions": "^6.5.15",
"@storybook/addon-docs": "^6.5.15",
"@storybook/addon-essentials": "^6.5.15",
"@storybook/addon-interactions": "^6.5.15",
"@storybook/addon-links": "^6.5.15",
"@storybook/builder-webpack4": "^6.5.15",
"@storybook/manager-webpack4": "^6.5.15",
"@storybook/react": "^6.5.15",
"@storybook/testing-library": "^0.0.13",
"babel-loader": "^8.3.0"
これらを project root の package.json に加える。

workspace から storybook の依存を取り除いて実行してみる
試しに、storybook を使っている一つの workspace から storybook 関連の依存を取り除いて動くか確かめる。
=> 動かなかった....
storybook を実行しようとすると、
yarn storybook
command not found: start-storybook
start-storybook 見つかりません、と出てくる...

そもそも start-storybook ってなんだ...?
start-storybook
を叩いた時、何が起きてるのかわからんので調べる。

yarn bin
で、コマンド叩いた時に実際に実行されてる binary がわかるのでこれで調べる。
yarn bin
path/to/my-project-root/node_modules/@storybook/react/bin/index.js
どうやら、node_modules 下の @storybook/react/bin/index.js
が実行されてるみたいだな。
というか、start-storybook
を叩くと @storybook/react/bin/index.js
が実行されるのはなんでだ...?
どこかで設定してんのか...?
蛇足だけど調べるか...

@storybook/react
の package.json のここだな
package.json bin
bin
に binary を実行するためのコマンドを設定できるんだな
A lot of packages have one or more executable files that they'd like to install into the PATH. npm makes this pretty easy (in fact, it uses this feature to install the "npm" executable.)

command not found: start-storybook
の原因を考える
とりあえず現状整理だな
- workspace から storybook の依存を削除したことで、
start-storybook
コマンドを実行するための@storybook/react
が workspace の node_modules から削除された - project root の node_modules には
@storybook/react
が存在する
package.json bin に設定されているコマンドに関しては、project root まで見てくれてないっぽいな...
やっぱりそうみたい↓
This command will run a tool. The exact tool that will be executed will depend on the current state of your workspace:
If the scripts field from your local package.json contains a matching script name, its definition will get executed.
Otherwise, if one of the local workspace's dependencies exposes a binary with a matching name, this binary will get executed.
Otherwise, if the specified name contains a colon character and if one of the workspaces in the project contains exactly one script with a matching name, then this script will get executed.
ということは、各 workspace から project root の node_modules に install されてる @storybook/react
を利用するように指定できれば良さそう
できるのか...?

似たような issue あった

こちらを参照、とのこと
Little-known Yarn feature: any script with a colon in its name (build:foo) can be called from any workspace. Another little-known feature: $INIT_CWD will always point to the directory running the script. Put together, you can write scripts that can be reused this way:
:
使ってる script はどの workspace からも使えるんか...!
そして、$INIT_CWD
で実行中の directory を指定できると。
project root で、
{
"dependencies": {
"typescript": "^3.8.0"
},
"scripts": {
"g:tsc": "cd $INIT_CWD && tsc"
}
}
g:tsc
を定義していたら他の workspace で
{
"scripts": {
"build": "yarn g:tsc"
}
}
このように実行できると。
他にも run -T
で project root の node_modules を実行できるみたい。
{
"scripts": {
"build": "run -T tsc"
}
}

project root の package.json で storybook 起動用のコマンドを準備して対応してみる
{
"name": "root",
"scripts": {
"g:storybook": "cd $INIT_CWD && start-storybook"
}
}
これで、別の workspace で yarn g:storybook
とすれば、project root の package.json に記載した cd $INIT_CWD && start-storybook
が実行される。
実際に試してみる。
packageA で yarn g:storybook
を実行するコマンドを用意して実行。
{
"name": "packageA",
"scripts": {
"storybook": "yarn g:storybook",
}
}
yarn storybook
いけた!!!!

他の storybook を利用する workspace の package.json stoybook
コマンドを上記のものに差し替えたら完了!

v8 では project root に react, react-dom ないとエラーになる
project root の storybook を利用して workspace 内で start-storybook すると "Cannot find module 'react-dom/client'" のエラーが出る。
project root に react と react-dom を install してあげると治る。
"devDependencies": {
//...
"react": "^18.3.1",
"react-dom": "^18.3.1",
//...

workspace の react version が project root と異なるとエラーになる
"Cannot read properties of null (reading 'useMemo')"
のようなエラーが起こる。hooks を使ってる story が軒並みエラーになる。
project root で install した react と同一 version にすれば解決する。
共通の ui package みたいな dependencies に react を持たず peerDependencies に react を指定しているような package では、開発時は project root の react が参照されるためこの問題は起きないはず。

別の方法
要は、project root の node_module 内の binary を叩ければいいので、以下のように直接叩いてあげてもいけた。
"name": "packageA",
"scripts": {
"storybook": "$(yarn workspace root bin start-storybook)",
}
yarn workspace ${workspace name (package.json の name)}
で root の workspace を指定して、yarn bin start-storybook
で start-storybook の binary の path を取得している。

v7 ではよりやりやすくなるみたいなので期待

V7 に upgrade してみる

monorepo root の package.json に最新版の @storybook 関連の dependencies を追加する。
react + webpack5 の project で npx storybook@latest init
を実行し、install されたものを参考にした。
"@storybook/addon-essentials": "^7.0.24",
"@storybook/addon-interactions": "^7.0.24",
"@storybook/addon-links": "^7.0.24",
"@storybook/blocks": "^7.0.24",
"@storybook/react": "^7.0.24",
"@storybook/react-webpack5": "^7.0.24",
"@storybook/testing-library": "^0.0.14-next.2",
"storybook": "^7.0.24",
storybook
は storybook の start/dev を実行するための cli みたい。
今までは、@storybook/react
の中に start-storybook
, build-storybook
用の bin が含まれていたが、v7 からはこれらが削除され代わりに cli 用の package が用意された模様
詳しくはこちら↓
start-storybook
command も置き換える。
{
"name": "root",
"scripts": {
- "g:storybook": "cd $INIT_CWD && start-storybook"
+ "g:storybook": "cd $INIT_CWD && storybook dev"
}
}

実行してみる
エラーでた
ERR! Error: Could not find a 'framework' field in Storybook config.
ERR!
ERR! Please run 'npx storybook@next automigrate' to automatically fix your config.
ERR!
ERR! See the migration guide for more information:
ERR! https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-framework-api

In SB7.0, we've removed these binaries and replaced them with new commands in Storybook's CLI: storybook dev and storybook build. These commands will look for the framework field in your .storybook/main.js config--which is now required--and use that to determine how to start/build your Storybook. The benefit of this change is that it is now possible to install multiple frameworks in a project without having to worry about hoisting issues.
Storybook 7 introduces the concept of frameworks, which abstracts configuration for renderers (e.g. React, Vue), builders (e.g. Webpack, Vite) and defaults to make integrations easier. This requires quite a few changes, depending on what your project is using. We recommend you to use the automigrations, but in case the command fails or you'd like to do the changes manually, here's a guide:
- 今までは framework ごとの package (e.g.
@storybook/react
) に start と build の binary が梱包されていた - v7 から start とbuild を実行する binary は分離された
=> 別途 framework ごとの package を install して、config でどの framework を利用するのか指定してあげる必要がある
てな感じか

framework を設定する
いつの間にか storybook の config が ts で書けるようになっていたので、main.ts
に設定していく。
import type { StorybookConfig } from '@storybook/react-webpack5';
const config: StorybookConfig = {
framework: '@storybook/react-webpack5',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials'],
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
};
export default config;
@storybook/react-webpack5
から import した StorybookConfig 使うと、framework
がちゃんと型付けされる↓
type FrameworkName = '@storybook/react-webpack5';
type BuilderName = '@storybook/builder-webpack5';

再度実行
実行自体はできた!
が、babel-loader の SyntaxError でたぞ
TypeScript を parse できてな感あるな...

That's correct. We changed the way Babel is handled. During the 7.0 automigration if you don't have a babelrc, the migration tool can automatically create one for you with Typescript support built in. If you don't choose that option, we don't support TS.
v7 から storybook デフォの babelrc がなくなって、自前のものを参照してくれるようになったらしい。
そのかわり自前の babelrc で TS 用の setup をしておく必要がある。
ただし、babelrc がない状態で automigration をしてれば、自動で v6 系時の babelrc と同等のものを作成してくれるみたい。

automigrate command 実行
npx storybook@next automigrate
prompt が立ち上がって、いくつかの質問に答える必要がある。
今回は、babelrc の作成だけ行う。
✔ Do you want to run the 'missing-babelrc' migration on your project? … yes
これで、.babelrc.json
が生成される↓
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": 100
}
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": []
}
ついでに、babel 関連の以下の dependencies が install される↓
"@babel/preset-env": "^7.22.5",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",

再度実行する
いけた!
v6 と比べると dev server の立ち上がりが段違いに早いな
╭──────────────────────────────────────────────────╮
│ │
│ Storybook 7.0.24 for react-webpack5 started │
│ 1.09 s for manager and 1.7 min for preview │
│ │
│ Local: http://localhost:6006/ │
│ On your network: http://100.64.1.25:6006/ │
│ │
╰──────────────────────────────────────────────────╯

まとめ
- project root の @storybook 関連の dependencies を更新
- 各 workspace の storybook config (main.js or main.ts) に framework を追加
-
.babelrc.json
を作成 - babel 関連の dependencies がない場合は install

v8 に upgrade してみる

init してみる
今回も、新規の react project 作って、npx storybook@latest init
して様子を見る。
npx storybook@latest init
install された packages↓
"@chromatic-com/storybook": "^1.6.1",
"@storybook/addon-essentials": "^8.2.7",
"@storybook/addon-interactions": "^8.2.7",
"@storybook/addon-links": "^8.2.7",
"@storybook/addon-onboarding": "^8.2.7",
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
"@storybook/blocks": "^8.2.7",
"@storybook/react": "^8.2.7",
"@storybook/react-webpack5": "^8.2.7",
"@storybook/test": "^8.2.7",
"eslint-plugin-storybook": "^0.8.0",
"storybook": "^8.2.7",

v8 から新しく追加されてる packages
"@chromatic-com/storybook": "^1.6.1",
"@storybook/addon-onboarding": "^8.2.7",
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
"@storybook/test": "^8.2.7",
v8 からなくなっている packages
"@storybook/testing-library": "^0.0.14-next.2",

Removed packages
@storybook/testing-library は deprecated で、代わりに、@storybook/test を使えばいいみたい。
@storybook/test への migration guide ↓

v8 から追加された新規 packages について
v8 init で新規追加されてた以下 packages についてみていく。
"@chromatic-com/storybook": "^1.6.1",
"@storybook/addon-onboarding": "^8.2.7",
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
"@storybook/test": "^8.2.7",

@chromatic-com/storybook
storybook で chromatic を利用するための addon。
"Visual Test" の addon panel が追加される↓
利用には chromatic でのアカウントが必要。
必須ではないので、chromatic を利用しないなら決して問題ない。

@storybook/addon-onboarding
onboarding 用のツアーが始まる
こんなの↓
すでに使い方わかってるなら必要ない。Uninstall してOK。

@storybook/addon-webpack5-compiler-swc
This addon adds SWC support to Storybook's webpack5 compiler. It adds the swc-loader to the webpack config and sets the swc-loader as the default loader for JavaScript and TypeScript files.
webpack5 で swc loader 使うための addon

@storybook/test
The @storybook/test package contains utilities for testing your stories inside play functions.
storybook の play function による interaction test のための package。
In Storybook 8, @storybook/testing-library has been integrated to a new package called @storybook/test, which uses Vitest APIs for an improved experience. When upgrading to Storybook 8 with 'npx storybook@latest upgrade', you will get prompted and will get an automigration for the new package. Please migrate when you can.
@storybook/testing-library の後釜。Vitest ベースになって experience 向上したとのこと。

方向性一旦整理
今回自分の project (webpack ベース) で行う v8 への migration でやること整理しておく。
- @chromatic-com/storybook, @storybook/addon-onboarding は必須ではないので今回は無視
- @storybook/testing-library は削除
- @storybook/test 追加
- @storybook/addon-webpack5-compiler-swc 追加
- その他の storybook 関連の package は latest に upgrade
- storybook config addons に "@storybook/addon-webpack5-compiler-swc"追加

"React is not defined" エラー
一通り上記の点を行ったのち、storybook を実行すると "React is not defined" エラー出た
利用している React は v18 で babel で transpile している。
storybook では @storybook/addon-webpack5-compiler-swc 利用しているので、swc で babel 相当の設定が必要そう。
babel では、@babel/preset-react で runtime "automatic" に設定してる。
{
"presets": [
"@babel/preset-typescript",
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-env"
],
//...
}
jsc.transform.react.runtime
swc Possible values: automatic, classic. This affects how JSX source code will be compiled.
Defauts to classic.
Use runtime: automatic to use a JSX runtime module (e.g. react/jsx-runtime introduced in React 17).
Use runtime: classic to use React.createElement instead - with this option, you must ensure that React is in scope when using JSX.
デフォが classic。"automatic" に設定すれば良さげ。
storybook config で swc の設定を変更
import type { Options } from '@swc/core';
import type { StorybookConfig } from '@storybook/your-framework';
//...
const config: StorybookConfig = {
//...
swc: (config: Options): Options => {
return {
...config,
jsc: {
...config.jsc,
transform: {
...config.jsc?.transform,
react: {
runtime: 'automatic',
},
},
},
};
},
};
export default config;