Component Library( Vue 3 + Rollup)

shubhadip

shubhadip

Posted on October 23, 2020

Component Library( Vue 3  + Rollup)

This is part2 of creating a component library using vue 3 and rollup.we will be building a rollup configuration so that we can build our library.

Lets install few rollup libraries that we require
yarn add @babel/preset-env@7.12.1 @rollup/plugin-alias@3.1.1 @rollup/plugin-babel@5.2.1 @rollup/plugin-commonjs@14.0.0 @rollup/plugin-image@2.0.5 @rollup/plugin-node-resolve@9.0.0 @rollup/plugin-replace@2.3.3 @rollup/plugin-url@5.0.1
rollup@2.30.0 rollup-plugin-postcss@3.1.8 rollup-plugin-terser@7.0.2 rollup-plugin-vue@6.0.0-beta.10 rimraf@3.0.2 cross-env@7.0.2

lets update our babel.config.js

module.exports = {
  presets: [
    "@babel/preset-env"
  ]
}
Enter fullscreen mode Exit fullscreen mode

rollup-plugin-vue will be used to process vue templates and rollup-plugin-postcss with handle our postcss.

Now that we have all our dependencies we can write our config.Lets create a rollup.config.js, first we will create a baseconfig which can be reused for different module system builds

Lets import all dependencies that we require

import fs from 'fs';
import path from 'path';
import vue from 'rollup-plugin-vue';
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import babel from '@rollup/plugin-babel';
import PostCSS from 'rollup-plugin-postcss';
import simplevars from 'postcss-simple-vars'
import postcssImport from 'postcss-import'
import minimist from 'minimist';
import postcssUrl from 'postcss-url'
import url from '@rollup/plugin-url'
import nested from 'postcss-nested'
import { terser } from 'rollup-plugin-terser'
import  autoprefixer  from 'autoprefixer

Enter fullscreen mode Exit fullscreen mode

lets add a variable which we can use to identify which module are we going to build and our project root path:

const argv = minimist(process.argv.slice(2));
const projectRoot = path.resolve(__dirname, '.');
Enter fullscreen mode Exit fullscreen mode

we will be adding a script like this in package.json
"build:es": "rimraf dist && cross-env NODE_ENV=production rollup --config rollup.config.js --format es"

Lets create baseConfig now, baseconfig will have configuration realted to vue, it will deal with preVue, Vue, postVue and babelConfig.

const baseConfig = {
  plugins: {
    preVue: [
      alias({
        entries: [
          {
            find: '@',
            replacement: `${path.resolve(projectRoot, 'src')}`,
          },
        ],
        customResolver: resolve({
          extensions: ['.js', '.jsx', '.vue'],
        }),
      }),
    ],
    replace: {
      'process.env.NODE_ENV': JSON.stringify('production'),
      __VUE_OPTIONS_API__: JSON.stringify(true),
      __VUE_PROD_DEVTOOLS__: JSON.stringify(false),
    },
    vue: {
      target: 'browser',
      preprocessStyles: true,
      postcssPlugins:[
       ...postcssConfigList
      ]
    },
    postVue: [
      // Process only `<style module>` blocks.
      PostCSS({
        modules: {
          generateScopedName: '[local]___[hash:base64:5]',
        },
        include: /&module=.*\.css$/,
      }),
      // Process all `<style>` blocks except `<style module>`.
      PostCSS({ include: /(?<!&module=.*)\.css$/,
        plugins:[
          ...postcssConfigList
        ]
       }),
        url({
          include: [
            '**/*.svg',
            '**/*.png',
            '**/*.gif',
            '**/*.jpg',
            '**/*.jpeg'
          ]
        }),
    ],
    babel: {
      exclude: 'node_modules/**',
      extensions: ['.js', '.jsx', '.vue'],
      babelHelpers: 'bundled',
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

above config will be used to different builds we also have postconfig which is used in different places.

baseConfig.vue is the part which is utilized by rollup-plugin-vue to compiler our codebase and then different plugins act accordingly.

Before we proceed further we will declare some globals and externals which is used by rollup to identify external dependency and global outputs.
const external = ['vue'];
const globals = { vue: 'Vue' };

Lets create a entry points for our projects, there will be one default entry point as src/index.js and different with each components index.js e.g components/helloworld/index.js

const baseFolder = './src/'
const componentsFolder = 'components/'

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

const entriespath = {
  index: './src/index.js',
  ...components.reduce((obj, name) => {
    obj[name] = (baseFolder + componentsFolder + name + '/index.js')
    return obj
  }, {})
}

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

Now that entry points are ready lets write the crux of the module bundlers.We will be using above mentioned argv to identify which module build are we supposed to build.

Lets start with esm build

(argv === 'es')
configuration will be as follows :

// Customize configs for individual targets
let buildFormats = [];
// this will hold our whole configuration object 

if (!argv.format || argv.format === 'es') {
  const esConfig = {
    input: entriespath,
    external,
    output: {
        format: 'esm',
        dir: 'dist/esm'
    },
    plugins: [
      commonjs(),
      replace(baseConfig.plugins.replace),
      ...baseConfig.plugins.preVue,
      vue(baseConfig.plugins.vue),
      ...baseConfig.plugins.postVue,
      babel({
        ...baseConfig.plugins.babel,
        presets: [
          [
            '@babel/preset-env',
            { modules: false }
          ],
        ],
      }),
    ],
  };

  const merged = {
    input: 'src/index.js',
    external,
    output: {
      format: 'esm',
      file: 'dist/vuelib.esm.js'
    },
    plugins: [
      commonjs(),
      replace(baseConfig.plugins.replace),
      ...baseConfig.plugins.preVue,
      vue(baseConfig.plugins.vue),
      ...baseConfig.plugins.postVue,
      babel({
        ...baseConfig.plugins.babel,
        presets: [
          [
            '@babel/preset-env',
            { modules: false }
          ],
        ],
      }),
    ]
  }
  buildFormats.push(esConfig);
  buildFormats.push(merged);
}

Enter fullscreen mode Exit fullscreen mode

This sets up our configuration for esm builds. module (rollup, webpack) bundlers will pick this builds.

ESM Build output

With this we have a single build which has all our code and others are splitted chunks from esm/index.js.

Also with this we can have treeshaking on project which uses library.

with both components
With All Components

This is with only one component.
With Single Components

Only the component which is included comes in the build.

Now lets add other module configs:


if (!argv.format || argv.format === 'iife') {
  const unpkgConfig = {
    ...baseConfig,
    input: './src/index.js',
    external,
    output: {
      compact: true,
      file: 'dist/vuelib-browser.min.js',
      format: 'iife',
      name: 'vuelib',
      exports: 'named',
      globals,
    },
    plugins: [
      commonjs(),
      replace(baseConfig.plugins.replace),
      ...baseConfig.plugins.preVue,
      vue(baseConfig.plugins.vue),
      ...baseConfig.plugins.postVue,
      babel(baseConfig.plugins.babel),
      terser({
        output: {
          ecma: 5,
        },
      }),
    ],
  };
  buildFormats.push(unpkgConfig);
}

if (!argv.format || argv.format === 'cjs') {
  const cjsConfig = {
    ...baseConfig,
    input: entriespath,
    external,
    output: {
      compact: true,
      format: 'cjs',
      dir: 'dist/cjs',
      exports: 'named',
      globals,
    },
    plugins: [
      commonjs(),
      replace(baseConfig.plugins.replace),
      ...baseConfig.plugins.preVue,
      vue({
        ...baseConfig.plugins.vue,
        template: {
          ...baseConfig.plugins.vue.template,
          optimizeSSR: true,
        },
      }),
      ...baseConfig.plugins.postVue,
      babel(baseConfig.plugins.babel),
    ],
  };
  buildFormats.push(cjsConfig);
}
Enter fullscreen mode Exit fullscreen mode

Lets create individual build of each components i.e umd Builds

const mapComponent = (name) => {
    return [
      {
        input: baseFolder + componentsFolder + `${name}/index.js`,
        external,
        output: {
          format: 'umd',
          name: capitalize(name),
          file: `dist/components/${name}/index.js`,
          exports: 'named',
          globals,
        },
        plugins: [
          ...baseConfig.plugins.preVue,
          vue({}),
          ...baseConfig.plugins.postVue,
          babel({
            ...baseConfig.plugins.babel,
            presets: [
              [
                '@babel/preset-env',
                { modules: false }
              ],
            ],
          }),
          commonjs(),
        ]
      }
    ]
  }

const ind = [...components.map((f) => mapComponent(f)).reduce((r, a) => r.concat(a), [])]

buildFormats = [...buildFormats, ...ind]
Enter fullscreen mode Exit fullscreen mode

Now with all build formats done we can export our whole config

export default buildFormats;

Enter fullscreen mode Exit fullscreen mode

Lets make changes to our package.json

...
"main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "unpkg": "dist/vuelib.min.js",
  "files": [
    "dist",
    "src"
  ],
  "sideEffects": [
    "*.css",
    "*.scss"
  ],
 "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "build:es": "rimraf dist && cross-env NODE_ENV=production rollup --config rollup.config.js --format es",
    "build:js": "rimraf dist && cross-env NODE_ENV=production rollup --config rollup.config.js"
  },

...
Enter fullscreen mode Exit fullscreen mode

with this each project will be correctly pick formats they need. commonjs projects will pick cjs folder and webpack or rollup ones will pic esm folder.

dist folder after all builds

With this configuration we can build our library. We have added treeshaking and postcss preprocessors to our library.

Note: we have inlined the css on the components directly in case we need css in different files e.g bundle.css we need to use plugins like rollup-plugin-scss, rollup-plugin-css-only.

So, we have created vue 3 components library with rollup and postcss and it has treeshaking capability.

Code related to this article is available on Github

💖 💪 🙅 🚩
shubhadip
shubhadip

Posted on October 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related