Component Library( Vue 3 + Rollup)

shubhadip maity
6 min readOct 23, 2020

This is part2 of creating a component library using vue3 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"
]
}

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

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, '.');

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',
},
},
};

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)
}

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);
}

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

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

This is with only one component.

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);
}

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]

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

export default buildFormats;

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"
},

...

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.

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

--

--