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.
If you don’t want to read the whole process, feel free to skip to the end, where I sum it up and provide a sample Github repository with all the fixes.
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.
First Run 🔗
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...
The .cjs File Extension 🔗
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.
Same issue, but slightly trickier. Storybook tries to be helpful and load the Svelte preprocessor configuration from your svelte.config.js
file, but since we converted .storybook/main.js
to be a CommonJS module, now it can’t require
an ES module like svelte.config.js
.
Duplicate svelte-preprocess Configuration 🔗
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(),
},
};
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...
Despite the “Unable to find main.js” warning, it works!
Modern JS Syntax Problems 🔗
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}
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
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
};
Updating the Webpack Configuration 🔗
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:
- All files must reference the same copy of the
svelte
library to avoid the dreaded “function called outside component initialization” error. - With
"type": "module"
, the Webpack resolver must be set tofullySpecified: false
, so thatimport
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...
};
One Last Error 🔗
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
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.
Summing It Up 🔗
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.
To recap:
- Rename
.storybook/main.js
to.storybook/main.cjs
. - Remove references in
main.cjs
tosvelte.config.js
. - Use Storybook’s webpack 5 builder.
- Update the webpack configuration for Svelte.
- Set
fullySpecified: false
in the resolver configuration. - Create
.storybook/package.json
just containing an empty object.
I’ve created a GitHub repository with all these changes so that you can pull them into your own project.