Migrate from @open-wc/demoing-storybook to @web/dev-server-storybook plugin

These are my steps to migrate from @open-wc/demoing-storybook to @web/dev-server-storybook plugin. As you may already seen, @open-wc/demoing-storybook package was sort of deprecated in 2020. Here you can find more details about the new recommended solution for Storybook.

My setup

For context, I maintain a library of web components based on Lit for my work. The library is a monorepo based on the recommendations from Open Web Components. You probably have a different setup, but the migration should be similar.

In my case, it all started with our pipeline that stoped working by throwing an error like this:

node:internal/crypto/hash:71
  this[kHandle] = new _Hash(algorithm, xofLen);
                  ^

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:71:19)
    at Object.createHash (node:crypto:133:10)
    ...
        at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read_file_context:68:3) {
  opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
  library: 'digital envelope routines',
  reason: 'unsupported',
  code: 'ERR_OSSL_EVP_UNSUPPORTED'
}

The error seems to be caused by the fact that our agents were upgraded to Node 18.x, while we were still on Node 16.x.

You can check a quick fix from StackOverflow, but on the long term you will have to migrate your Storybook setup.

I started by upgrading all my dependencies and leaving the @open-wc/demoing-storybook package at the end, knowing that I already have the last version. After doing all the upgrades, I still had the same error and was clearly that I had to drop that package.

The migration

First thing we need to do is to add a new npm package.

Update the packages

Uninstall @open-wc/demoing-storybook

npm uninstall @open-wc/demoing-storybook

Install the @web/dev-server-storybook plugin for web-dev-server

npm install -D @web/dev-server-storybook

This will also install @web/storybook-prebuilt which we will be using later.

Fix the local server

From now on, we'll be using a prebuilt version of Storybook and we'll be using the web-dev-server to also start the Storybook.

To do this, we need a new config file for web-dev-server.

// web-dev-storybook.config.mjs
import { storybookPlugin } from '@web/dev-server-storybook';

export default {
  plugins: [storybookPlugin({ type: 'web-components' })],
};
`

I put this file in the root of my project, but you can put it wherever you want.

Previously, I would start the local server for Storybook like this:

npm run storybook

where the storybook script was:

"storybook": "start-storybook --node-resolve --watch --open",

Now, we should change it to:

"storybook": "web-dev-server --node-resolve --open --config web-dev-storybook.config.mjs",

Fix the Storybook config

In your /.storybook folder you should have these two files: main.js and preview.js.

These configs are probably different than mine, so I'll just put my before and after for them.

main.js

// main.js before
module.exports = {
  stories: [
    '../packages/**/stories/*.stories.{js,mdx,md}',
  ],
  addons: [
    'storybook-prebuilt/addon-docs/register.js',
  ],
};
// main.js after
const { copy } = require('@web/rollup-plugin-copy');

module.exports = {
  stories: [
    '../packages/**/stories/*.stories.{js,mdx,md}',
  ],
  rollupConfig(config) {
    // we copy these files, because they are consumed at run time
    config.plugins.push(copy({ patterns: 'packages/**/custom-elements.json' }));

    return config;
  },
};

One mention here is that in my library, every component has a custom-elements.json file, so know I have to copy them at build time, using the @web/rollup-plugin-copy plugin.

To install this package, use:

npm install -D @web/rollup-plugin-copy

preview.js

// preview.js before
import {
  addParameters,
  addDecorator,
  setCustomElements,
  withA11y,
} from '@open-wc/demoing-storybook';
import { componentsList } from './components-list.js';

async function run() {
  // Custom Elements is used to generate API documentation on Docs tab in Storybook
  const customElementsJson = {
    version: 2,
    tags: [],
  };

  for (const [pckg, components] of Object.entries(componentsList)) {
    for (let component of components) {
      try {
        const customElement = await (
          await fetch(
            new URL(
              `../packages/${pckg}/components/${component}/custom-elements.json`,
              import.meta.url
            )
          )
        ).json();
        customElementsJson.tags.push(customElement.tags[0]);
      } catch (err) {}
    }
  }

  addDecorator(withA11y);

  addParameters({
    a11y: {
      config: {},
      options: {
        checks: { 'color-contrast': { options: { noScroll: true } } },
        restoreScroll: true,
        showRoots: true,
      },
    },
    docs: {
      iframeHeight: '200px',
    },
  });

  setCustomElements(customElementsJson);
}

run();
// preview.js after
import {
  addParameters,
  setCustomElements,
} from '@web/storybook-prebuilt/web-components.js';
import { componentsList } from './components-list.js';

async function run() {
  /**
   * Custom Elements is used to generate API documentation on Docs tab in Storybook
   * Minimal example for new scheme:
   * {
   *    schemaVersion: '2.0.0',
   *    modules: [
   *      {
   *        declarations: [
   *          {
   *            tagName: 'my-component',
   *            properties: [
   *              { name: 'data', type: 'array', 'description': "Foo.", default: 'null' }
   *            ],
   *            events: [],
   *            slots: [],
   *            cssProperties: []
   *          },
   *          ...
   *        ]
   *      }
   *    ]
   * }
   */
  const customElementsJson = {
    'schemaVersion': '2.0.0',
    'modules': [],
  };

  for (const [pckg, components] of Object.entries(componentsList)) {
    for (let component of components) {
      try {
        const customElement = await (
          await fetch(
            new URL(
              `../packages/${pckg}/components/${component}/custom-elements.json`,
              import.meta.url
            )
          )
        ).json();
        customElementsJson.modules.push({
          declarations: [
            {
              ...customElement.tags[0],
              tagName: customElement.tags[0].name,
            },
          ],
        });
      } catch (err) {}
    }
  }

  addParameters({
    a11y: {
      config: {},
      options: {
        checks: { 'color-contrast': { options: { noScroll: true } } },
        restoreScroll: true,
        showRoots: true,
      },
    },
    docs: {
      iframeHeight: '200px',
    },
  });

  setCustomElements(customElementsJson);
}

await run();

Please note that I have an older scheme version for custom-elements.json and that's why I build the customElementsJson object differently.

Fix the stories

Open all your stories and change the imports like this:

import { Story, Preview, Meta, Props, html } from '@open-wc/demoing-storybook';

to:

import { html } from 'lit';
import { Story, Preview, Meta, ArgsTable } from '@web/storybook-prebuilt/addon-docs/blocks.js';

Please note that Props was renamed to ArgsTable.

You also need to change <Props> to <ArgsTable> in your stories.

Fix the build

Here, I just changed the build script from:

"storybook:build": "build-storybook -o demo-storybook -s storybook-static",

to:

"storybook:build": "build-storybook --output-dir demo-storybook",

Done

Now we just need to test it. To start the Storybook locally, run:

npm run storybook

To build the Storybook, run:

npm run storybook:build