DEV Community

Daniel Imfeld
Daniel Imfeld

Posted on • Originally published at imfeld.dev on

Setting up SvelteKit with Storybook

I’ve become a big fan of using Storybook to develop components in an isolated context, and so it was a natural choice to use it for Ergo. In the process I found that some workarounds are needed to get things to work with SvelteKit, so this sums up everything I learned about getting it to actually work.

Once you have set up a SvelteKit project, the easiest way to add Storybook is with the npx sb init command. This installer will detect the project type and install all the necessary dependencies and configuration files.

Ok, let’s try it!

$ npm run storybook

> sveltekit-storybook@0.0.1 storybook sveltekit-storybook
> start-storybook -p 6006

info @storybook/svelte v6.3.4
info
ERR! Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: sveltekit-storybook/.storybook/main.js
ERR! require() of ES modules is not supported.

And more errors...

Enter fullscreen mode Exit fullscreen mode

This is a common issue when using development tools with projects that set "type": "module" in the package.json file, as SvelteKit does. Storybook uses require to include .storybook/main.js, but that file is an ES Module due to the project settings, so it can not be loaded via require.

Fortunately, there’s a solution. Rename the file to .storybook/main.cjs, and Node.js will force it to be treated as a traditional “CommonJS” style module, that can be loaded with require. Storybook is set up to look for this extension, so it all works.

Trying again…

$ mv .storybook/main.js .storybook/main.cjs
$ npm run storybook
~/projects/sveltekit-storybook ❯ npm run storybook

> svelte-storybook@0.0.1 storybook sveltekit-storybook
> start-storybook -p 6006

info @storybook/svelte v6.3.4
info
ERR! Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: sveltekit-storybook/svelte.config.js
ERR! require() of ES modules is not supported.

Enter fullscreen mode Exit fullscreen mode

Same issue, but slightly trickier. Storybook tries to be helpful and load the Svelte preprocessor configuration from your svelte.config.jsfile, but since we converted .storybook/main.js to be a CommonJS module, now it can’t require an ES module like svelte.config.js.

In this case we can’t just rename the file since SvelteKit complains loudly if the file is named svelte.config.cjs. The easiest solution I’ve found here is to just make the Storybook main.cjs file recreate the preprocessor configuration instead of pulling it in from svelte.config.js. Not ideal, but it’s a pretty small bit of configuration to duplicate, so not that big a deal.

const preprocess = require('svelte-preprocess');

module.exports = {
  // The rest of the config here...
  svelteOptions: {
    // Same options that you pass to preprocess in svelte.config.js
    preprocess: preprocess(),
  },
};

Enter fullscreen mode Exit fullscreen mode

Ok, let’s try again.

$ pnpm storybook

> svelte-storybook@0.0.1 storybook sveltekit-storybook
> start-storybook -p 6006

info @storybook/svelte v6.3.4
info
info => Loading presets
WARN Unable to find main.js: sveltekit-storybook/.storybook/main
info => Loading 1 config file in "sveltekit-storybook/.storybook"
info => Loading 9 other files in "sveltekit-storybook/.storybook"
info => Adding stories defined in "sveltekit-storybook/.storybook/main.js"
WARN Unable to find main.js: sveltekit-storybook/.storybook/main
info => Using implicit CSS loaders
info => Using default Webpack4 setup

And more output...

Enter fullscreen mode Exit fullscreen mode

Despite the “Unable to find main.js” warning, it works! Well, mostly. You can develop just fine for a while this way, but as soon as you use optional chaining or nullish coalescing, it falls apart. (That is, the .? or ?? operators.)

You can sort of get away with it by using Typescript to convert these features into older equivalents, but Svelte’s TypeScript support doesn’t currently process the template, so a component like this one will still have trouble.

<script lang="ts">
  export let value;
  export let defaultValue = 'N/A';
</script>

{value ?? defaultValue}

Enter fullscreen mode Exit fullscreen mode

This is because Storybook uses Webpack 4 by default, which doesn’t support these newer JavaScript syntax features. A common solution here is to force Webpack to use a newer version of the acorn dependency, which it uses for parsing JavaScript. For Storybook, this causes very strange issues that mostly prevent Storybook from working at all.

A better solution is to use Webpack 5. Storybook recently gained full support for Webpack 5, so this can be enabled with just a few commands.

$ npm install --save-dev @storybook/builder-webpack5 @storybook/manager-webpack5

Enter fullscreen mode Exit fullscreen mode

Once the dependencies are installed, a small update to our main.cjs will enable it.

module.exports = {
  core: {
    builder: 'webpack5',
  },
  svelteOptions: {
    preprocess: preprocess(),
  },
  // Rest of the configuration here
};

Enter fullscreen mode Exit fullscreen mode

This doesn’t quite work though. I’ll skip all the errors but there are two things in the Webpack 5 configuration that need fixing:

  1. All files must reference the same copy of the svelte library to avoid the dreaded “function called outside component initialization” error.
  2. With "type": "module", the Webpack resolver must be set to fullySpecified: false, so that import calls don’t need to have the full file extension to work properly.

Storybook allows us to add a webpackFinal function to our configuration to make these changes.

const path = require('path');
const preprocess = require('svelte-preprocess');

module.exports = {
  core: {
    builder: 'webpack5',
  },
  svelteOptions: {
    preprocess: preprocess(),
  },
  webpackFinal: async (config) => {
    config.resolve = {
      ...config.resolve,
      alias: {
        ...config.resolve.alias,
        svelte: path.resolve(__dirname, '..', 'node_modules', 'svelte'),
      },
      mainFields: ['svelte', 'browser', 'module', 'main'],
    };

    config.module.rules.push({
      resolve: {
        fullySpecified: false,
        extensions: ['.js', '.ts'],
      },
    });

    return config;
  },
  // Rest of the config...
};

Enter fullscreen mode Exit fullscreen mode

With all these changes, our Storybook compiles once again! But there’s one last error that shows up when loading Storybook in the browser. The devtools reveal the problem:

Uncaught ReferenceError: require is not defined
    at Object../.storybook/generated-stories-entry.js (generated-stories-entry.js:3)
    at __webpack_require__ (bootstrap:24)
    at __webpack_exec__ (main.iframe.bundle.js:221)
    at main.iframe.bundle.js:222
    at Function. __webpack_require__.O (chunk loaded:23)
    at main.iframe.bundle.js:223
    at webpackJsonpCallback (jsonp chunk loading:557)
    at main.iframe.bundle.js:1

Enter fullscreen mode Exit fullscreen mode

Some code inside Storybook’s client also uses require and module, but when Webpack 5 sees a “module” type package it doesn’t provide all that CommonJS functionality. Fortunately, we can trick Webpack a bit here.

By adding the file .storybook/package.json with just the contents {} (yes, an empty object), Webpack 5 won’t see "type": "module" in the package.json at the root of your project, and will instead run in a module-agnostic mode. This allows files to use ES Module style import and export, but also provides require, module, and so on for CommonJS code that might need it.

Finally, we have a Storybook configuration where everything works. I’m sure that a lot of these workarounds will no longer be needed as time goes on, but for now I hope this helps you get a working setup.

I’ve created a GitHub repository with all these changes so that you can pull them into your own project.

Discussion (0)