DEV Community

Raffaele Pizzari
Raffaele Pizzari

Posted on • Updated on

Multiple VueJs Apps in a Lerna monorepo, sharing a Storybook component library.

(check out my blog)

Hello! This is work-in-progress prototype but actually it already works.
I have this scenario: multiple VueJs Apps which are sharing a component library. I'd like to place all of them in a monorepo managed by Lerna.
The component library is based on Storybook.

Feel free to help me / send me your suggestions.

What I want to achieve

Simplicity and maintainability.
In my scenario one or more teams are working on the components and updating them using semantic versioning.
All the VueJs Apps are using the shared components and the change-log is automatically created based on commit messages and tags.
The commit messages and the tags are automatically managed by Lerna.

The "frame" is already working, but I still have to refine some steps and add features.

This is the GitHub repo: https://github.com/pixari/component-library-monorepo.

And here the "How to":

Getting Started

Install Lerna

Let's start by installing Lerna globally with npm:

$ npm install --global lerna
Enter fullscreen mode Exit fullscreen mode

Next we have to create a new git repository:

$ git init component-library-monorepo && cd component-library-monorepo 
Enter fullscreen mode Exit fullscreen mode

And then, following Lerna's official documentation, will turn it into a Lerna repo:

lerna init
Enter fullscreen mode Exit fullscreen mode

The repository should look lihe this:

component-library-monorepo/
  packages/
  lerna.json
  package.json
Enter fullscreen mode Exit fullscreen mode

If you'd like to learn something more about this process, you can check the official Lerna documentation.

Install Storybook

Let's start by installing Lerna globally with npm:

$ npm install @storybook/vue --save-dev
Enter fullscreen mode Exit fullscreen mode

Add peer dependencies

$ npm install vue --save
$ npm install vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue --save-dev 
Enter fullscreen mode Exit fullscreen mode

Add a npm script

{
  "scripts": {
    "storybook": "start-storybook"
  }
}
Enter fullscreen mode Exit fullscreen mode

For a basic Storybook configuration, the only thing you need to do is tell Storybook where to find stories.

To do that, create a file at .storybook/config.js with the following content:

import { configure } from '@storybook/vue';

const req = require.context('../packages', true, /.stories.js$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);
Enter fullscreen mode Exit fullscreen mode

Add the first component to the component library

We create in the root a packages/index.stories.js file and write our first story:

import Vue from 'vue';
import { storiesOf } from '@storybook/vue';
import MyButton from './Button/src/Button.vue';

storiesOf('Button', module)
  .add('as a component', () => ({
    components: { MyButton },
    template: '<my-button>with text</my-button>'
  }))
  .add('with emoji', () => ({
    components: { MyButton },
    template: '<my-button>๐Ÿ˜€ ๐Ÿ˜Ž ๐Ÿ‘ ๐Ÿ’ฏ</my-button>'
  }))
  .add('with text', () => ({
    components: { MyButton },
    template: '<my-button :rounded="true">rounded</my-button>'
  }));
Enter fullscreen mode Exit fullscreen mode

Now we create the real "Button" component:

/packages/Button
  /src
    Button.vue
Enter fullscreen mode Exit fullscreen mode
<template>
  <button type="button"><slot /></button>
</template>

<script>
export default {
  name: 'MyButton',
}
</script>
Enter fullscreen mode Exit fullscreen mode

The index.js

/packages/Button
  src/index.js
Enter fullscreen mode Exit fullscreen mode
import MyButton from './Button.vue';
export default MyButton;
Enter fullscreen mode Exit fullscreen mode

And the package.json:

{
  "name": "@mylibrary/my-button",
  "version": "0.2.0",
  "description": "Just a simple button component",
  "main": "dist/index.js",
  "module": "src/index.js",
  "scripts": {
    "transpile": "vue-cli-service build --target lib ./src/index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Start Storybook

Now you are ready to start Storybook and play with your first component:

$ npm run storybook
Enter fullscreen mode Exit fullscreen mode

And you should see it running here:

http://localhost:51368
Enter fullscreen mode Exit fullscreen mode

Create a VueJs App

Installation

To install the Vue CLI, use this command:

$ npm install -g @vue/cli
$ npm install --save-dev @vue/cli-service
Enter fullscreen mode Exit fullscreen mode

Create a new project

To create a new project, run:

$ cd packages && vue create my-app
Enter fullscreen mode Exit fullscreen mode

And please choose the easiest option:

> default (babel, eslint)
Enter fullscreen mode Exit fullscreen mode

In this tutorial we don't want to build the best VueJs App possible, but just show how to share a component library between VueJs Apps.

Add eslint configuration

Create ./packages/my-app/.eslintrc.js

module.exports = {
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:vue/essential"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "vue"
    ],
    "rules": {
    }
};
Enter fullscreen mode Exit fullscreen mode

Run the App

Let's run our new app:

$ cd my-app && npm run serve
Enter fullscreen mode Exit fullscreen mode

And now you should see here your app, up&running:

http://localhost:8080/
Enter fullscreen mode Exit fullscreen mode

Using Lerna to link dependencies

Add the following dependency to your packages/my-app/package.json:

{
  "dependencies": {
    "@mylibrary/my-button": "*"
  }
}
Enter fullscreen mode Exit fullscreen mode

Fix eslint

const path = require('path');
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('eslint')
      .use('eslint-loader')
      .tap(options => {
        options.configFile = path.resolve(__dirname, ".eslintrc.js");
        return options;
      })
  },
  css: {
    loaderOptions: {
      postcss: {
        config:{
          path:__dirname
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And now we can "bootstrap" the packages in the current Lerna repo, install all of their dependencies and links any cross-dependencies:

In the root:

$ lerna bootstrap
Enter fullscreen mode Exit fullscreen mode

Update the Vue App

Change the content of ./packages/my-app/src/main.js:

import Vue from 'vue'
import App from './App.vue'
import MyButton from '@mylibrary/my-button';

Vue.config.productionTip = false
Vue.component('my-button', MyButton);
new Vue({
  render: h => h(App),
}).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

and change the content of our HelloWorld component (./packages/my-app/src/components/HelloWorld.vue):

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <my-button>It Works!</my-button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

We now transpile our components:

$ lerna run transpile
Enter fullscreen mode Exit fullscreen mode

run again..

$ cd packages/my-app && npm run serve
Enter fullscreen mode Exit fullscreen mode

Go to http://localhost:8080 and you should se the button in the middle of the HelloWorld page :)

Top comments (3)

Collapse
 
stefanovualto profile image
stefanovualto

The content seems nice, I will give it a try.

I have few questions:

  • Does then your webapp published when you run lerna publish or you do not publish your components on npm?
  • Does your transpile script (cf. "transpile": "vue-cli-service build --target lib ./src/index.js") use the babel behind the scene? What I mean by that is is it possible to have a generic babel config for the components and then a more specific one in any packages that need it (like the webapp)?

Thank you for helping to try to understand the coupling that lerna create between actual packages and any webapp/storybook ui.

Collapse
 
zleight1 profile image
Zachary Leighton • Edited

For babel you can put a babel.config.js at the root and then have package specific .babelrcs modify the default. Examples are on the babel documentation for this exact scenario.

Collapse
 
sgobotta profile image
Santiago Botta

Thank you for this post! I'm currently migrating a repository using this tutorial and lerna documentation and I'm wondering if there's a way to properly handle webpack aliases from a vue.config.js file inside a source package. Image a scenario where this package is required by the client package. Have you ever tried something similar?