VueJS

How to Create and Deploy a Vue Component Library to NPM

Adesoji Temitope
4 min read
How to Create and Deploy a Vue Component Library to NPM
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Introduction

When working across multiple Vue projects that share the same design system, it is more efficient and faster to have a component library that you can reference for all your components within the different projects. In this article, we will go through the steps needed to create and deploy a Vue component library to npm, so we can reuse them across various projects.

  • Create a vue component library
  • Register library components
  • Setup a build process
  • Test locally then publish our library to npm

Create a Vue component library

Setup our Project

Let’s start by creating our Vue project. We would be using yarn for package management.

To start, lets run

npm init

For this tutorial, we are going to create just a single component within our component library. One of the things to consider when building a component library at scale is to allow import of just single components to enable tree shaking.

We would be using this folder structure for our component library:

- src /
  - components /
    - button /
      - button.vue
      - index.ts

    - index.ts

  - styles /
    - components /
        - _button.scss

    - index.scss

- Package.json
- rollup.config.js

Create button component

Let's create a simple button component. We define the basic props our component can accept and compute the class based on the props.

Add this to the button.vue file.

<!-- src/components/button/button.vue -->
<template>
    <button
        v-bind="$attrs"
        :class="rootClasses"
        :type="type"
        :disabled="computedDisabled"
    >
      <slot></slot>
    </button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
    name: 'DSButton',
    inheritAttrs: false,
    props: {
        /**
         * disabled status
         * @values true, false
         */
        disabled: {
            type: Boolean,
        },
        /**
        * Color of button
        * @values primary, secondary
        */
        variant: {
            type: String,
            validator: (value: string) => {
                return [
                    'primary',
                    'secondary'
                ].indexOf(value) >= 0
            }
        },
        /**
         * type of button
         * @values button, submit
         */
        type: {
            type: String,
            default: 'button',
            validator: (value: string) => {
                return [
                    'button',
                    'submit',
                    'reset'
                ].indexOf(value) >= 0
            }
        },
        /**
         * Size of button
         * @values sm, md, lg
         */
        size: {
            type: String,
            validator: (value: string) => {
                return [
                    'sm',
                    'md',
                    'lg'
                ].indexOf(value) >= 0
            }
        }
    },
    computed: {
        rootClasses() {
            return [
                'ds-button',
                'ds-button--' + this.size,
                'ds-button--' + this.variant
            ]
        },
        computedDisabled() {
            if (this.disabled) return true
            return null
        }
    }
})
</script>

Let's style the button component, added our variant and size classes based on what is specified in the button component file.

Add this to the _button.scss file.

// src/styles/components/_button.scss
$primary: '#0e34cd';
$secondary: '#b9b9b9';
$white: '#ffffff';
$black: '#000000';
$small: '.75rem';
$medium: '1.25rem';
$large: '1.5rem';

.ds-button {
    position: relative;
    display: inline-flex;
    cursor: pointer;
    text-align: center;
    white-space: nowrap;
    align-items: center;
    justify-content: center;
    vertical-align: top;
    text-decoration: none;
    outline: none;

    // variant
    &--primary {
        background-color: $primary;
        color: $white;
    }
    &--secondary {
        background-color: $secondary;
        color: $black;
    }

    // size
    &--sm {
        min-width: $small;
    }
    &--md {
        min-width: $medium;
    }
    &--lg {
        min-width: $large;
    }
}

Register library components

Next we need to register our components. To do this, we import all our components into a single file and create our install method.

// src/components/button/index.ts This file exports the install methods as default. For cases where we need to import just this button component in our other projects, it also exports Button which we will use in a bit.

// src/components/button/index.ts
import { App, Plugin } from 'vue'

import Button from './button.vue'

export default {
    install(Vue: App) {
        Vue.component(Button.name, Button)
    }
} as Plugin

export {
    Button as DSButton
}

// src/components/index.ts Lets import all the components in our components folder here. Since we have only our button component, we will imort it.

// src/components/index.ts
import Button from './button'

export {
    Button
}

// src/styles/index.scss Lets import all the components styles into index.scss in our styles folder. This helps us with having one source of export for all of our styles.

// src/styles/index.scss
@import "components/_button";

// src/index.ts Lets import all the components into index.ts in our src folder. Here, we create our install method for all the component. We export DSLibrary as default, and also export all our components.

// src/index.ts
import { App } from 'vue'

import * as components from './components'

const DSLibrary = {
    install(app: App) {
        // Auto import all components
        for (const componentKey in components) {
            app.use((components as any)[componentKey])
        }
    }
}

export default DSLibrary

// export all components as vue plugin
export * from './components'

Lets create a file called shim-vue.d.ts to help us with importing Vue files into our TypeScript files, and remove any linting errors caused by it.

// src/shim-vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

Setup a build process

In building the library, we need to create a bundled and minidifed of the library that will be shared to npm, for this we would use Rollup. Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger.

  • build component (vue code)
  • build styling (scss code)

Install rollup, and all of the rollup modules we would need.

$ npm i -D rollup rollup-plugin-vue rollup-plugin-terser rollup-plugin-typescript2 @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve

Build component (vue code)

we need two major build types for our component library to support various projects.

  • ES module - It has no requirements or exports
  • CommonJS module

Lets create a rollup.config.js file in the root folder and paste this there.

import { text } from './build/banner.json'
import packageInfo from './package.json'

import vue from 'rollup-plugin-vue'
import node from '@rollup/plugin-node-resolve'
import cjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2';

import fs from 'fs'
import path from 'path'

const baseFolderPath = './src/components/'
const banner = text.replace('${version}', packageInfo.version)

const components = fs
    .readdirSync(baseFolderPath)
    .filter((f) =>
        fs.statSync(path.join(baseFolderPath, f)).isDirectory()
    )

const entries = {
    'index': './src/index.ts',
    ...components.reduce((obj, name) => {
        obj[name] = (baseFolderPath + name)
        return obj
    }, {})
}

const babelOptions = {
    babelHelpers: 'bundled'
}

const vuePluginConfig = {
    template: {
        isProduction: true,
        compilerOptions: {
            whitespace: 'condense'
        }
    }
}

const capitalize = (s) => {
    if (typeof s !== 'string') return ''
    return s.charAt(0).toUpperCase() + s.slice(1)
}

export default () => {
    let config = []

    if (process.env.MINIFY === 'true') {
        config = config.filter((c) => !!c.output.file)
        config.forEach((c) => {
            c.output.file = c.output.file.replace(/\.m?js/g, r => `.min${r}`)
            c.plugins.push(terser({
                output: {
                    comments: '/^!/'
                }
            }))
        })
    }
    return config
}

Next, we create a build folder in the root of our project, and add a file called banner.json to it. We want our builds to contain the current app version everytime we build. This file is already imported into the rollup.config.js file, and we use the package version from our package.json to update the version.

{
    "text": "/*! DS Library v${version} */\n"
}

Currently, our config is an empty array. Next, we will add the different builds we want.

entries: path to files we want rollup to bundle external: the external package needed output.format: the bundled file format output.dir: the bundled files directory output.banner: text added to the begining of the file plugins: specify methods used to customize rollup behaviour

  • First we create an esm build for each component in our library:

        config = [{
             input: entries,
             external: ['vue'],
             output: {
                 format: 'esm',
                 dir: `dist/esm`,
                 entryFileNames: '[name].mjs',
                 chunkFileNames: '[name]-[hash].mjs',
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }],
    
  • Next we create a single esm build for all the components:

        config = [
          ...,
         {
             input: 'src/index.ts',
             external: ['vue'],
             output: {
                 format: 'esm',
                 file: 'dist/ds-library.mjs',
                 banner: banner
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }
       ],
    
  • Then we create an cjs build for each component in our library:

        config = [
          ...,
          ...,
         {
             input: entries,
             external: ['vue'],
             output: {
                 format: 'cjs',
                 dir: 'dist/cjs',
                 exports: 'named'
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }
       ],
    
  • After that, we create a single cjs build for each component in our library

        config = [
          ...,
          ...,
          ...,
         {
             input: 'src/index.ts',
             external: ['vue'],
             output: {
                 format: 'umd',
                 name: capitalize('ds-library'),
                 file: 'dist/ds-library.js',
                 exports: 'named',
                 banner: banner,
                 globals: {
                     vue: 'Vue'
                 }
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }
    
       ],
    

Finally, we update our package.json with our script command. We need both rimraf (to remove our old dist folder before we create a new bundle) and clean-css (to minify our bundled css file), so lets install:

$ npm i -D rimraf clean-css-cli

now lets update our package.json script

build:vue = rollup and minify build:style = bundled our style from scss to css, add our banner text(version number like we did above) and save in dist/ds-library.css then create a minified version.

For the banner text in our css file, we need to create a print-banner.js file inside the build folder. It takes our banner text, and writes it to the file.


// build/print-banner.js
const packageInfo = require('../package.json')
const { text } = require('./banner.json')

process.stdout.write(text.replace('${version}', packageInfo.version))
process.stdin.pipe(process.stdout)

build:lib = delete dist folder, build vue and style code publish:lib = run our build lib command then publish to npm

    "scripts": {
        "build:vue": "rollup -c && rollup -c --environment MINIFY",
        "build:vue:watch": "rollup -c --watch",
        "build:style": "sass --no-charset ./src/styles/index.scss | node ./build/print-banner.js > dist/ds-library.css && cleancss -o dist/ds-library.min.css dist/ds-library.css",
        "build:lib": "rimraf dist && npm run build:vue && npm run build:style",
        "publish:lib": "npm run build:lib && npm publish"
    },
    "peerDependencies": {
        "vue": "^3.0.0"
    },

Test locally then publish our library to npm

Now that we are done, we can test locally by running npm link in the root directory of this repo and also running npm link "package name" in the root directory of our test project. After this, the package will be available for use in our test package.

Once tested, you can run npm publish:lib to build and deploy to npm.

If you have any questions or run into any trouble, feel free to reach out on Twitter or Github.

About the author

Adesoji Temitope

Adesoji Temitope

Software Engineer

Keep reading

View all posts →

Awesome 3D experience with VueJS and TresJS: a beginner's guide

Unleash the power of 3D in your Vue.js projects with Tres.js! The future of immersive web experiences is here. #Vuejs #3Ddevelopment...

Mattia Magi5 mins
Vue3D

3 VueJS Component Libraries Perfect for Beginners

For developers checking out VueJS for the first time, the initial steps are overwhelming, particularly when setting up projects from square one. But don’t worry! The VueJS ecosystem offers a plethora of remarkable component libraries, easing this early obstacle. These three libraries are pre built toolkits, providing beginners with the means to kickstart their VueJS projects effortlessly. Let’s take a look! Quasar Quasar is among the most popular open source component libraries for Vue.js, offering a comprehensive set of ready to use UI components and tools for building responsive web applications and websites. Designed with performance, flexibility, and ease of use in mind, Quasar provides developers with a wide range of customizable components, such as buttons, forms, dialogs, and layouts, along with built in support for themes, internationalization, and accessibility. With its extensive documentation, active community support, and seamless integration with Vue CLI and Vuex, Quasar empowers developers to rapidly prototype and develop high quality Vue.js applications for various platforms, including desktop, mobile, and PWA (Progressive Web Apps). PrimeVue PrimeVue is a popular Vue.js component library offering a wide range of customizable UI components designed for modern web applications. Developed by PrimeTek, it follows Material Design guidelines, ensuring responsiveness and accessibility across devices. With features like theming, internationalization, and advanced functionalities such as lazy loading and drag and drop, PrimeVue provides developers with the tools to create elegant and high performing Vue.js applications efficiently. Supported by clear documentation, demos, and an active community, PrimeVue is an excellent choice for developers seeking to streamline their development process and deliver polished user experiences. Vuetify Vuetify is a powerful Vue.js component library that empowers developers to create elegant and responsive user interfaces with ease. Built according to Google's Material Design guidelines, Vuetify offers a vast collection of customizable UI components, ranging from buttons and cards to navigation bars and data tables. Its comprehensive set of features includes themes, typography, layout grids, and advanced components like dialogues and sliders, enabling developers to quickly build modern web applications that look and feel polished. With extensive documentation, active community support, and ongoing development, Vuetify remains a top choice for Vue.js developers seeking to streamline their workflow and deliver visually stunning user experiences. For newcomers venturing into Vue.js, the initial setup might seem daunting. Thankfully, Vue.js offers a variety of component libraries to simplify this process. Quasar, PrimeVue, and Vuetify are standout options, each providing pre built tools to kickstart projects smoothly. Whether you prefer Quasar's extensive UI components, PrimeVue's Material Design inspired features, or Vuetify's responsive interfaces, these libraries cater to diverse preferences and project requirements. With their clear documentation and active communities, these libraries empower developers to start Vue.js projects confidently and efficiently, enabling Vue developers to create polished user experiences....

2 mins
VuetifyVueQuasar

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how <Suspense> and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly...

Jesús Padrón6 mins
VueWeb PerformanceUXJavaScript

Getting started with Vitepress

Create your static blog site using Vitepress. Setup and configure your site in minutes by following this step-by-step tutorial....

Simone Cuomo12 mins
VueViteVitepress