Setup Vue with Storybook using Vue CLI 3.0 (Typescript example)

Note: Although here I am setting up a project with Typescript, you can follow similar steps to make storybook work with any vue-cli setup

TL;DR

If you do not want to read, the code is here

Scaffold Vue project

Scaffold a vue project using vue-cli 3.0

$ mkdir typescript-vue-storybook && cd typescript-vue-storybook
$ npx -p @vue/cli vue create .
npx allows us to invoke vue-cli without installing it as a global dependency (if you do not have that command, please upgrade to the latest npm version)
The dot . in the end indicates that I want to scaffold the project in the current directory.
All terminal commands should be invoked from project root

Answer questions asked by vue-cli. Below are my responses, your could be different

I exclude PWA, Router, Vuex and e2e testing as I use storybook in a separate component library project and these features are not necessary for me. You can test with your setups and tell me whether that worked for you or not.
Also note that if you choose typescript I suggest picking eslint + something (prettier in my case) for linter / formatting config as tslint + vue has weak integration currently

After the project has been installed run $ npm run serve and check that the app runs in the browser

Add storybook

Storybook has a cli as well, so run it

$ npx -p @storybook/cli getstorybook
If you have yarn, then storybook cli will run yarn when installing dependencies, so if you have chosen npm when setting up vue project, then delete yarn.lock and run npm i

You should see storybook and build-storybook appear in scripts in your <project-root>/package.json. So run $ npm run storybook.

If you see the error similar to what I saw below

Module build failed: TypeError: /Users/almas/code/typescript-vue-storybook-tutorial/stories/index.stories.js: Duplicate declaration “h” (This is an error on an internal node. Probably an internal error.)

Then remove withJSX story in <project-root>/stories/index.stories.js:

...
// remove this block
.add('with JSX', () => ({
  components: { MyButton },
  render() {
    return <my-button onClick={this.action}>With JSX</my-button>;
  },
  methods: { action: linkTo('clicked') },
}))
...

Now go to http://localhost:6006/ and check that storybook has booted.

If you wish you can remove redundant preset added by storybook from <project-root>/.babelrc and the file should look like this

{
  "presets": ["@vue/app"]
}

Make storybook work with vue’s webpack.config.js

To make storybook work with vue’s webpack.config.js we need to create a custom webpack config for storybook. There is this solution to make it work with typescript, but I want to use webpack configuration generated by vue-cli, which would work for any vue-cli project.

Storybook asks to preserve the following fields in a custom webpack config:

So to make storybook work with vue’s webpack config we need to:

  • overwrite entry and output fields of vue’ webpack.config with storybook’s entry and output
  • concat plugins
  • remove duplicated plugins.
First loader in module.loaders in storybook’s config is jsx loader and vue’ webpack config handles jsx files as well, so no need to do anything here.
If you want to debug vue’s webpack config use this command
npx -p @vue/cli vue inspect > output.js

In order to merge webpack plugins we need to install webpack-merge package:

$ npm i -D webpack-merge

While that is installing create <project-root>/.storybook/webpack.config.js file and add the following contents:

const merge = require("webpack-merge");

const genStorybookDefaultConfig = require("@storybook/vue/dist/server/config/defaults/webpack.config.js");
const vueConfig = require("@vue/cli-service/webpack.config.js");
module.exports = (storybookBaseConfig, configType) => {
  const storybookConfig = genStorybookDefaultConfig(
    storybookBaseConfig,
    configType
  );
  return {
    ...vueConfig, // use vue's webpack configuration by default
    entry: storybookConfig.entry, // overwite entry
    output: storybookConfig.output, // overwrite output
    // remove duplicated plugins
plugins: merge({
      customizeArray: merge.unique(
        "plugins",
        [
          "HotModuleReplacementPlugin",
          "CaseSensitivePathsPlugin",
          "WatchMissingNodeModulesPlugin"
        ],
         plugin => plugin.constructor && plugin.constructor.name
      )
    })(vueConfig, storybookConfig).plugins
  };
};

change <project-root>/.storybook/config.js

import { configure } from '@storybook/vue'
// automatically import all files ending in *.stories.js
const req = require.context('../src/stories', true, /.stories.ts$/); <-------- THIS LINE
function loadStories() {
req.keys().forEach((filename) => req(filename));
}
configure(loadStories, module);

Move ./stories into ./src folder as vue’s webpack configuration only checks for src and test folders.

Then change extension of <project-root>/src/stories/index.stories.js to <project-root>/src/stories/index.stories.ts

Now change <project-root>/src/stories/MyButton.js to <project-root>/src/stories/MyButton.vue component and put the following contents into it

<template>
  <button :style="buttonStyles" @click="onClick">
  <slot></slot>
  </button>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
  name: "my-button",
  data() {
    return {
      buttonStyles: {
        border: "1px solid #eee",
        borderRadius: 3,
        backgroundColor: "#FFFFFF",
        cursor: "pointer",
        fontSize: 15,
        padding: "3px 10px",
        margin: 10
      }
    };
  },
  methods: {
    onClick() {
      this.$emit("click");
    }
  }
});
</script>

Now change the import statement of MyButton in <project-root>/src/stories/index.stories.ts to

import MyButton from "./MyButton.vue";

Rerun npm run storybook and check that everything still works.

Note that although everything renders we see the following error in the console

[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

It occurs because vue uses runtime build for some reason and compiles vue templates into render function beforehand.

To solve this replace vue’s alias in webpack.config.js by storybook’s vue alias:

...
return {
  ...vueConfig, // use vue's webpack configuration by default
  entry: storybookConfig.entry, // overwite entry
  output: storybookConfig.output, // overwrite output
  // remove duplicated plugins
  plugins: merge({
    customizeArray: merge.unique(
      "plugins",
      [
        "HotModuleReplacementPlugin",
        "CaseSensitivePathsPlugin",
        "WatchMissingNodeModulesPlugin"
      ],
      plugin => plugin.constructor && plugin.constructor.name
    )
  })(vueConfig, storybookConfig).plugins,
  resolve: { // <--------- This bit here
    ...vueConfig.resolve,
    alias: {
      ...vueConfig.resolve.alias,
      vue$: storybookConfig.resolve.alias.vue$
    }
  }
};

Rerun npm run storybook and check that everything works as expected

Although this works, but stories with render function do not work with this setup. I have a vague idea that it is related to not compiling templates into render function beforehand as happens when npm run serve command is called. I read the source code of how vue-cli-service serve works, but could not understand it… 😢. May be you can help me out here

Finally we can add typescript’s type definitions for storybook. Run

$ npm i -D @types/storybook__vue @types/storybook__addon-actions @types/storybook__addon-links

And rerun $ npm run storybook

Conclusion

So that’s it. I hope it helped someone. If something goes wrong compare to the final project here or leave a comment.

If you have any issues or want to discuss anything, please leave a comment and let’s talk 😄

You can follow me on twitter. Have a nice day!