Publishing Storybook: One main Storybook instance for all projects
This guide extends the Using Storybook in a Nx workspace - Best practices guide. In that guide, we discussed the best practices of using Storybook in a Nx workspace. We explained the main concepts and the mental model of how to best set up Storybook. In this guide, we are going to see how to put that into practice, by looking at a real-world example. We are going to see how you can publish one single Storybook for your workspace.
This case would work if all your projects (applications and libraries) containing stories that you want to use are using the same framework (Angular, React, Vue, etc). The reason is that you will be importing the stories in a central host Storybook's .storybook/main.ts
, and we will be using one specific builder to build that Storybook. Storybook does not support mixing frameworks in the same Storybook instance.
Let’s see how we can implement this solution:
Steps
Generate a new library that will host our Storybook instance
According to the framework you are using, use the corresponding generator to generate a new library. Let’s suppose that you are using React and all your stories are using the @storybook/react-vite
framework:
The command below uses the as-provided
directory flag behavior, which is the default in Nx 16.8.0. If you're on an earlier version of Nx or using the derived
option, omit the --directory
flag. See the workspace layout documentation for more details.
❯
nx g @nx/react:library storybook-host --directory=libs/storybook-host --bundler=none --unitTestRunner=none --projectNameAndRootFormat=as-provided
Now, you have a new library, which will act as a shell/host for all your stories.
Configure the new library to use Storybook
Now let’s configure our new library to use Storybook, using the @nx/storybook:configuration
generator. Run:
❯
nx g @nx/storybook:configuration storybook-host --interactionTests=true --uiFramework=@storybook/react-vite
This generator will only generate the storybook
, build-storybook
and test-storybook
targets in our new library's project.json
(libs/storybook-host/project.json
), and also the libs/storybook-host/.storybook
folder. This is all we care about. We don’t need any stories in this project, since we will be importing the stories from other projects in our workspace. So, if you want, you can delete the contents of the src/lib
folder. You may also delete the lint
and test
targets in libs/storybook-host/project.json
. We will not be needing those.
Import the stories in our library's main.ts
Now it’s time to import the stories of our other projects in our new library's ./storybook/main.ts
.
Here is a sample libs/storybook-host/.storybook/main.ts
file:
1import type { StorybookConfig } from '@storybook/react-vite';
2import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
3import { mergeConfig } from 'vite';
4
5const config: StorybookConfig = {
6 stories: ['../../**/ui/**/src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
7 addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
8 framework: {
9 name: '@storybook/react-vite',
10 options: {},
11 },
12
13 viteFinal: async (config) =>
14 mergeConfig(config, {
15 plugins: [nxViteTsPaths()],
16 }),
17};
18
19export default config;
20
Notice how we only link the stories matching a specific pattern. According to your workspace set-up, you can adjust the pattern, or add more patterns, so that you can match all the stories in all the projects you want.
For example:
1stories: [
2 '../../**/ui/**/src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)',
3 '../../**/src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)',
4 // etc...
5];
6
Import the stories in tsconfig.storybook.json
Here is a sample libs/storybook-host/tsconfig.storybook.json
file:
1{
2 "extends": "../tsconfig.json",
3 "compilerOptions": {
4 "emitDecoratorMetadata": true
5 },
6 ...
7 "include": [
8 "../../**/ui/**/src/**/*.stories.ts",
9 "../../**/ui/**/src/**/*.stories.js",
10 "../../**/ui/**/src/**/*.stories.jsx",
11 "../../**/ui/**/src/**/*.stories.tsx",
12 "../../**/ui/**/src/**/*.stories.mdx",
13 ".storybook/*.js",
14 ".storybook/*.ts"
15 ]
16}
17
Notice how in the include
array we are specifying the paths to our stories, using the same pattern we used in our .storybook/main.ts
.
For Angular, that file is in your project's .storybook
directory, so in this case it would be under libs/storybook-host/.storybook/tsconfig.json
.
Serve or build your Storybook
Now you can serve, test or build your Storybook as you would, normally. And then you can publish the bundled app!
❯
nx storybook storybook-host
or
❯
nx build-storybook storybook-host
or
❯
nx test-storybook storybook-host
Use cases that apply to this solution
Can be used for:
- Workspaces with multiple apps and libraries, all using a single framework
Ideal for:
- Workspaces with a single app and multiple libraries all using a single framework
Extras - Dependencies
Your new Storybook host, essentially, depends on all the projects from which it is importing stories. This means whenever one of these projects updates a component, or updates a story, our Storybook host would have to rebuild, to reflect these changes. It cannot rely on the cached result. However, Nx does not understand the imports in libs/storybook-host/.storybook/main.ts
, and the result is that Nx does not know which projects the Storybook host depends on, based solely on the main.ts
imports. The good thing is that there is a solution to this. You can manually add the projects your Storybook host depends on as implicit dependencies in your project’s project.json
, in the implicit dependencies array.
For example, libs/storybook-host/project.json
:
1{
2 "$schema": "../../node_modules/nx/schemas/project-schema.json",
3 "sourceRoot": "libs/storybook-host/src",
4 "projectType": "library",
5 "tags": ["type:storybook"],
6 "implicitDependencies": [
7 "admin-ui-footer",
8 "admin-ui-header",
9 "client-ui-footer",
10 "client-ui-header",
11 "shared-ui-button",
12 "shared-ui-main",
13 "shared-ui-notification"
14 ],
15