Scott Nath

Front-end architect

Scott Nath picture
DEV logoDEV GitHub logoGitHub LinkedIn logoLinkedIn Mastodon logoMastodon

Running a local multi-framework composition Storybook


How to run a local multi-framework composition Storybook instance with one command

tl;dr

problem: we can’t compose multiple Storybooks in the same process without getting CORS errors

typical solution: run one storybook, open a new terminal tab, run the next, new tab, start the main composed storybook

one-tab solution: Use concurrently and wait-on to run multiple Storybooks in one terminal tab.

See this example of a multi-framework composition Storybook for working code.

Prerequisite knowledge

note: This article is specific to running Storybook locally.

Problem 1: Storybook Composition CORS bugs

The most common errors when using Storybook Composition seem to be related to CORS (Cross-Origin Resource Sharing). You can see how often CORS is mentioned when searching Storybook’s GitHub repo’s issues for CORS + Composition.

Basically, if the parent Storybook (the Storybook instance which references the child Storybooks) is started before the child Storybooks have completely launched and are viewable in browser, then the parent Storybook will throw CORS errors. The CORS errors will block the parent Storybook from loading the child Storybooks. No clear fix has been found for this issue - and I’ve tried a bunch.

The common solution is to use multiple terminal windows and wait for the child Storybooks to be fully launched before starting the parent Storybook.

Why do we have to do it that way?

See: ⬇️

NodeJS is single-threaded …

When running a script in a NodeJS process, it is the only script which can run in that process at that time. So, if you’re in a terminal window you cannot run another script in that window until the first script has finished.

… and Storybook composition requires waiting

When running Storybook locally in dev mode, it does not exit or otherwise programatically indicate that Storybook has started. A composed Storybook should not start until the child Storybooks are running. But using scripts like concurrently or npm-run-all has not consistently worked for me when the parent/composed Storybook instance is in the list of scripts running concurrently.

Of course, you can open a new tab/terminal window for each command, but that means all users of your repo need to know that trick too. Inefficient!

The following is a solution that works consistently with a single NPM script.

Example code

Astro’s Kitchen Sink

We’ll be using Astro’s Kitchen Sink: Microfrontends with Astro, which showcases Astro’s built-in support for multiple frameworks (React, Preact, Svelte, and Vue (v3.x)). This a multi-framework application, meaning it can use components from multiple JS frameworks in the UI - very cool!

StoryDocker-storybook

We’ll be adding StoryDocker’s module StoryDocker-storybook which injects the typical dependencies needed to set up Storybook.

Working codebase on GitHub

The code referenced is available on GitHub via the astro-framework-multiple example in the StoryDocker-examples repo.

Steps to compose Storybooks locally

Step 1: Start up the child Storybook(s) in parallel

We’ll be using concurrently, an NPM module which allows running multiple commands at the same time. (Concurrently on npmjs)

The child Storybooks don’t need to know each other exist, so they can be started at the same time.

Using concurrently, we’ll run each child-Storybook in parallel.

Step 1.a: Running one child Storybook

This command will run one child Storybook:

npm run storybook -- --config-dir .framework-storybooks/.storybook-preact --port 6001 --no-open

Let’s break down that command:

npm run storybook is the command to run Storybook via the NPM script storybook in package.json. That script is calling the Storybook CLI with the command storybook dev.

-- the extra double-dashes allow us to send flags-and-options directly to storybook dev

--config-dir .framework-storybooks/.storybook-preact is the command to run Storybook with the preact-specific configuration found in the local directory

--port 6001 is the command to run Storybook on port 6001

--no-open tells Storybook to not open a browser window when it starts.

So, all that means we’re starting a Storybook instance using the configuration found in .framework-storybooks/.storybook-preact and we’re running it on port 6001 and telling Storybook to skip it’s default behavior of opening a browser window.

Step 1.b: The other child Storybooks

The example code has 3 other child Storybooks, so we’ll need to run those too. They are:

React: npm run storybook -- --config-dir .framework-storybooks/.storybook-react --port 6002 --no-open

Svelte: npm run storybook -- --config-dir .framework-storybooks/.storybook-svelte --port 6003 --no-open

Vue: npm run storybook -- --config-dir .framework-storybooks/.storybook-vue --port 6004 --no-open

Step 1.c: Running all child Storybooks in parallel

Putting them together, we’ll run all the child Storybooks in parallel using concurrently. This is the NPM script we’ll use:

"sbook-children": "npx concurrently \"npm run storybook -- --config-dir .framework-storybooks/.storybook-preact --port 6001 --no-open\"  \"npm run storybook -- --config-dir .framework-storybooks/.storybook-react --port 6002 --no-open\" \"npm run storybook -- --config-dir .framework-storybooks/.storybook-svelte --port 6003 --no-open\" \"npm run storybook -- --config-dir .framework-storybooks/.storybook-vue --port 6004 --no-open\"",

Running npm run sbook-children will start all the child Storybooks in parallel. We use npx instead of npm so we can avoid adding concurrently as a dependency.

Step 2: Run the parent Storybook

We do not want to start the parent Storybook until all the child Storybooks are running and viewable in browser.

Step 2.a: The parent Storybook

The main composed Storybook is configured like a normal Storybook within the .storybook directory. The only addition is the refs.js file which is imported by main.js and contains the references to the child Storybooks. The contents of refs.js are:

export default {
  "astro_preact": {
    "title": "preact",
    "url": "http://localhost:6001",
  },
  "astro_react": {
    "title": "react",
    "url": "http://localhost:6002",
  },
  "astro_svelte": {
    "title": "svelte",
    "url": "http://localhost:6003",
  },
  "astro_vue": {
    "title": "vue",
    "url": "http://localhost:6004",
  },
}

Step 2.b: Waiting on one child Storybook

We’ll use wait-on, an NPM module which stops Node from continuing a process until a condition is met. In this case, wait-on is going to check for each localhost port to be open before continuing. (wait-on on npmjs)

This command:

npx wait-on http://localhost:6001

will wait until localhost:6001 is open and returns a 2xx response before continuing.

Port 6001 is the Storybook for Preact.

Step 2.c: Waiting on all child Storybooks

We’ll be adding a double-ampersand in between each localhost check, which means wait-on will wait for each port sequentially. After all the wait-ons, we’ll start Storybook with npm run storybook.

The following NPM script will wait for all the child Storybooks to be open before continuing to open the parent Storybook.

"sbook-parent": "npx wait-on http://localhost:6001 && npx wait-on http://localhost:6002 && npx wait-on http://localhost:6003  && npx wait-on http://localhost:6004 && npm run storybook",

Step 3: Single command to run all Storybooks

Using concurrently again, we’ll run both the child and parent NPM scripts:

npx concurrently "npm run sbook-children" "npm run sbook-parent"

and the NPM script in package.json:

"sbook": "npx concurrently \"npm run sbook-children\" \"npm run sbook-parent\""

Step 4: Profit!

Happy coding folks!