Originally published at shipshape.io
When we initially started on refreshing Shepherd, we wanted to modernize
the build process, and decided to switch from gulp to webpack. This worked well, and was a
step in the right direction, but with all the buzz around rollup 1.0, we decided to give it a try.
In some cases, things were a simple 1:1 conversion from a webpack plugin to a rollup plugin, but other things were much less straightforward.
We'll go through each conversion, step by step here, in the hopes that it will be helpful to others who may want to take rollup for a spin. If you just want
to see the entire webpack config and the entire rollup config, you can skip to the bottom and compare them yourself.
Table of Contents
Linting
eslint-loader -> rollup-plugin-eslint
ESLint is a linting tool for JavaScript, that allows us to enforce code style for all of our JS. We typically use it
in all our projects and we are used to it running automatically, while serving or building, since this is baked into
Ember.js, so naturally we wanted to get this same behavior with rollup.
We used eslint-loader with webpack, and passed all JS through it,
excluding node_modules
. We also had to make sure we ran it before babel transpilation.
// webpack.config.js
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
exclude: path.resolve(__dirname, 'node_modules'),
loader: 'eslint-loader'
},
{
test: /\.js$/,
exclude: path.resolve(__dirname, 'node_modules'),
include: [
path.resolve(__dirname, 'src/js')
],
loader: 'babel-loader'
}
]
}
For rollup, we installed rollup-plugin-eslint and added it to our
array of plugins.
// rollup.config.js
// Add eslint to plugins
eslint(),
babel({
exclude: 'node_modules/**'
})
This also needed to be added before babel still, to ensure it is run on the untranspiled code.
stylelint-webpack-plugin -> rollup-plugin-stylelint
Stylelint allows us to enforce linting rules for CSS and SCSS files.
We enforced this with stylelint-webpack-plugin previously,
but switched to rollup-plugin-stylelint for use with rollup.
First, we removed stylelint-webpack-plugin
from our package.json
and then added rollup-plugin-stylelint
by running:
yarn add rollup-plugin-stylelint --dev
The options for both webpack and rollup are options passed directly to stylelint, so we mostly just needed to copy and paste these.
// webpack.config.js
new StyleLintWebpackPlugin({
fix: false,
syntax: 'scss',
quiet: false
})
// rollup.config.js
stylelint({
fix: false,
include: ['src/**.scss'],
syntax: 'scss',
quiet: false
})
The one difference was we had to specify to only include scss
files, since the input for rollup is always the JS, and we did
not want to include imported CSS, just SCSS.
Local Development
browser-sync-webpack-plugin -> rollup-plugin-browsersync
We use browsersync for local development of the demo/docs site, so we can see everything updating in real time across browsers.
This one was a fairly simple conversion.
First, we removed browser-sync-webpack-plugin
from our package.json
and then added rollup-plugin-browsersync
by running:
yarn add rollup-plugin-browsersync --dev
The config for each plugin is basically identical, so we just copied from one to the other.
// webpack.config.js
new BrowserSyncPlugin(
{
host: 'localhost',
watch: true,
port: 3000,
notify: false,
open: true,
server: {
baseDir: 'docs/welcome',
routes: {
'/shepherd/dist/js/shepherd.js': 'dist/js/shepherd.js',
'/shepherd/docs/welcome/js/prism.js': 'docs/welcome/js/prism.js',
'/shepherd/docs/welcome/js/welcome.js': 'docs/welcome/js/welcome.js',
'/shepherd/docs/welcome/css/prism.css': 'docs/welcome/css/prism.css',
'/shepherd/docs/welcome/css/welcome.css': 'docs/welcome/css/welcome.css',
'/shepherd/docs/welcome/sheep.svg': 'docs/welcome/sheep.svg'
}
}
}, {
reload: true
}
)
// rollup.config.js
// Only add the browsersync plugin if we are in development
if (process.env.DEVELOPMENT) {
plugins.push(browsersync({
host: 'localhost',
watch: true,
port: 3000,
notify: false,
open: true,
server: {
baseDir: 'docs/welcome',
routes: {
'/shepherd/dist/js/shepherd.js': 'dist/js/shepherd.js',
'/shepherd/docs/welcome/js/prism.js': 'docs/welcome/js/prism.js',
'/shepherd/docs/welcome/js/welcome.js': 'docs/welcome/js/welcome.js',
'/shepherd/docs/welcome/css/prism.css': 'docs/welcome/css/prism.css',
'/shepherd/docs/welcome/css/welcome.css': 'docs/welcome/css/welcome.css',
'/shepherd/docs/welcome/sheep.svg': 'docs/welcome/sheep.svg'
}
}
}));
}
Styles
sass-loader -> rollup-plugin-sass
In webpack we used a combination of sass-loader,
css-loader, postcss-loader,
file-loader, and extract-loader
to consume our scss
files and output our various theme files.
// webpack.config.js
const glob = require('glob');
const sassArray = glob.sync('./src/scss/shepherd-*.scss');
const sassEntries = sassArray.reduce((acc, item) => {
const name = item.replace('.scss', '').replace('./src/', '');
acc[name] = item;
return acc;
}, {});
...
module.exports = [{
entry: sassEntries,
output: {
// This is necessary for webpack to compile
// But we never use removable-style-bundle.js
filename: 'removable-[id]-bundle.js'
},
module: {
rules: [
{
test: /\.s[c|a]ss$/,
include: [
path.resolve(__dirname, 'src/scss')
],
exclude: [
path.resolve(__dirname, 'docs/welcome/scss')
],
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css'
}
},
{ loader: 'extract-loader' },
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
plugins: () => [autoprefixer({ grid: false })]
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
}
}
]
}
]
}
}];
We were able to replace all of these loaders with just rollup-plugin-sass,
and postcss, when we switched to rollup. However, rollup has a hard time with
outputting multiple css files. It wants to consume all the styles and either bundle them as one file or just inject them into head
automatically for you. This made generating multiple theme files not very straightforward, but wasn't too bad, once we figured it out.
// rollup.config.js
const sassOptions = {
output(styles, styleNodes) {
fs.mkdirSync('dist/css', { recursive: true }, (err) => {
if (err) {
throw err;
}
});
// Loop through the style nodes and manually write css files
styleNodes.forEach(({ id, content }) => {
const scssName = id.substring(id.lastIndexOf('/') + 1, id.length);
const name = scssName.split('.')[0];
fs.writeFileSync(`dist/css/${name}.css`, content);
});
},
processor: css => postcss([
atImport(),
autoprefixer({
grid: false
})
])
.process(css)
.then(result => result.css)
};
...
plugins.push(sass(sassOptions));
Including tippy.js styles
In our webpack build, we aliased tippy.js
, so that when it was imported, it would import the styles as well.
// webpack.config.js
resolve: {
alias: {
'tippy.js': 'tippy.js/dist/tippy.all.min.js'
}
}
We initially tried to use an alias in rollup as well, but could not get it to work. We decided instead to use
rollup-plugin-css-only to handle CSS imports in the JS, and
we then injected those styles directly into the head
.
// css.js
import { isBrowserSupported } from './browser';
/**
* Injects a string of CSS styles to a style node in <head>
* @param {String} css
*/
export function injectCSS(css) {
if (isBrowserSupported) {
const style = document.createElement('style');
style.type = 'text/css';
style.textContent = css;
document.head.insertBefore(style, document.head.firstChild);
}
}
// tour.js
import { injectCSS } from './css';
import tippy from 'tippy.js';
import tippyStyles from 'tippy.js/dist/tippy.css';
export class Tour extends Evented {
constructor(){
...
injectCSS(tippyStyles);
}
}
Transpilation/Minification
babel-loader -> rollup-plugin-babel
Most modern web apps tend to use Babel, so they can use next generation JavaScript today. There isn't
a ton to configure with Babel, and it was mostly just switching packages, but we also did adjust our babel.config.js
.
Before
// babel.config.js
module.exports = function(api) {
api.cache(true);
return {
presets: [
['@babel/preset-env']
],
plugins: [
'add-module-exports',
'lodash',
'transform-es2015-modules-commonjs'
],
env: {
test: {
plugins: ['istanbul']
}
}
};
};
After
// babel.config.js
module.exports = function(api) {
api.cache(true);
return {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
],
plugins: [
'@babel/plugin-transform-object-assign'
],
env: {
test: {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
],
plugins: [
'transform-es2015-modules-commonjs'
]
}
}
};
};
The main differences are we no longer needed istanbul
because Jest
has code coverage built in, and we switched around
our module exports and transforms, so we could ship both UMD and ESM.
After the Babel config changes, we removed babel-loader
from our package.json
and installed rollup-plugin-babel
.
yarn add rollup-plugin-babel --dev
The usage in webpack and rollup is very similar, with the only option being to ignore node_modules
.
// webpack.config.js
{
test: /\.js$/,
exclude: path.resolve(__dirname, 'node_modules'),
include: [
path.resolve(__dirname, 'src/js')
],
loader: 'babel-loader'
}
// rollup.config.js
babel({
exclude: 'node_modules/**'
})
uglifyjs-webpack-plugin -> rollup-plugin-uglify
Uglify is the most common package used for minification of JavaScript, and we used it with both webpack
and rollup, we just needed to switch which package we used.
First we removed uglifyjs-webpack-plugin
from our package.json
and then we installed
rollup-plugin-uglify.
yarn add rollup-plugin-uglify --dev
This was one place where the webpack build was a lot simpler. We added the uglify plugin and only included the min
file, so we could use one build.
// webpack.config.js
optimization: {
minimizer: [
new UglifyJsPlugin({
include: /\.min\.js$/,
sourceMap: true
})
]
}
Then we added it to our rollup plugins, but to generate both a minified, and unminified version,
we were required to use two rollup builds, which was not required in webpack. We checked for the presence
of an environment variable DEVELOPMENT
, and generated the minified version when true.
// rollup.config.js
if (!process.env.DEVELOPMENT) {
rollupBuilds.push(
// Generate minifed bundle
{
input: './src/js/shepherd.js',
output: {
file: 'dist/js/shepherd.min.js',
format: 'umd',
name: 'Shepherd',
sourcemap: true
},
plugins: [
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**'
}),
license({
banner
}),
sass(sassOptions),
css({ output: false }),
uglify(),
filesize()
]
});
}
Config Files
For those of you who want to see the entire config for both webpack and rollup, to compare one to the other, here they are! It may also be helpful to
check out the PR where we converted from webpack to rollup, so you can see all the things involved.
Webpack
// webpack.config.js
/* global require, module, __dirname */
const webpack = require('webpack');
const path = require('path');
const autoprefixer = require('autoprefixer');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const StyleLintWebpackPlugin = require('stylelint-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const PACKAGE = require('./package.json');
const banner = ['/*!', PACKAGE.name, PACKAGE.version, '*/\n'].join(' ');
const glob = require('glob');
const sassArray = glob.sync('./src/scss/shepherd-*.scss');
const sassEntries = sassArray.reduce((acc, item) => {
const name = item.replace('.scss', '').replace('./src/', '');
acc[name] = item;
return acc;
}, {});
// Theme SCSS files
sassEntries['css/welcome'] = './docs/welcome/scss/welcome.scss';
module.exports = [{
entry: sassEntries,
output: {
// This is necessary for webpack to compile
// But we never use removable-style-bundle.js
filename: 'removable-[id]-bundle.js'
},
module: {
rules: [
{
test: /\.s[c|a]ss$/,
include: [
path.resolve(__dirname, 'src/scss')
],
exclude: [
path.resolve(__dirname, 'docs/welcome/scss')
],
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css'
}
},
{ loader: 'extract-loader' },
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
plugins: () => [autoprefixer({ grid: false })]
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
}
}
]
},
{
test: /welcome\.s[c|a]ss$/,
include: [
path.resolve(__dirname, 'docs/welcome/scss')
],
use: [
{
loader: 'file-loader',
options: {
outputPath: '../docs/welcome/',
name: 'css/[name].css'
}
},
{ loader: 'extract-loader' },
{
loader: 'css-loader',
options: {
sourceMap: true
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
plugins: () => [
autoprefixer({
grid: false,
browsers: [
'last 2 versions'
]
})]
}
},
{
loader: 'sass-loader',
options: {
outputStyle: 'expanded',
sourceMap: true
}
}
]
}
]
},
plugins: [
new StyleLintWebpackPlugin({
fix: false,
syntax: 'scss',
quiet: false
}),
new BrowserSyncPlugin(
{
host: 'localhost',
watch: true,
port: 3000,
notify: false,
open: true,
server: {
baseDir: 'docs/welcome',
routes: {
'/shepherd/dist/js/shepherd.js': 'dist/js/shepherd.js',
'/shepherd/docs/welcome/js/prism.js': 'docs/welcome/js/prism.js',
'/shepherd/docs/welcome/js/welcome.js': 'docs/welcome/js/welcome.js',
'/shepherd/docs/welcome/css/prism.css': 'docs/welcome/css/prism.css',
'/shepherd/docs/welcome/css/welcome.css': 'docs/welcome/css/welcome.css',
'/shepherd/docs/welcome/sheep.svg': 'docs/welcome/sheep.svg'
}
}
}, {
reload: true
}
),
new webpack.BannerPlugin(banner)
]
}];
// Library Shepherd files
module.exports.push({
entry: {
'js/shepherd': './src/js/shepherd.js',
'js/shepherd.min': './src/js/shepherd.js'
},
devtool: 'source-map',
target: 'web',
performance: {
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
library: 'Shepherd',
libraryTarget: 'umd',
globalObject: 'this',
},
resolve: {
alias: {
'tippy.js': 'tippy.js/dist/tippy.all.min.js'
}
},
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
exclude: path.resolve(__dirname, 'node_modules'),
loader: 'eslint-loader'
},
{
test: /\.js$/,
exclude: path.resolve(__dirname, 'node_modules'),
include: [
path.resolve(__dirname, 'src/js')
],
loader: 'babel-loader'
}
]
},
optimization: {
minimizer: [
new UglifyJsPlugin({
include: /\.min\.js$/,
sourceMap: true
})
]
},
plugins: [
new webpack.BannerPlugin(banner),
new LodashModuleReplacementPlugin
]
});
Rollup
// rollup.config.js
import autoprefixer from 'autoprefixer';
import babel from 'rollup-plugin-babel';
import browsersync from 'rollup-plugin-browsersync';
import commonjs from 'rollup-plugin-commonjs';
import css from 'rollup-plugin-css-only';
import cssnano from 'cssnano';
import { eslint } from 'rollup-plugin-eslint';
import fs from 'fs';
import license from 'rollup-plugin-license';
import postcss from 'postcss';
import filesize from 'rollup-plugin-filesize';
import resolve from 'rollup-plugin-node-resolve';
import sass from 'rollup-plugin-sass';
import stylelint from 'rollup-plugin-stylelint';
import { uglify } from 'rollup-plugin-uglify';
const pkg = require('./package.json');
const banner = ['/*!', pkg.name, pkg.version, '*/\n'].join(' ');
const sassOptions = {
output(styles, styleNodes) {
fs.mkdirSync('dist/css', { recursive: true }, (err) => {
if (err) {
throw err;
}
});
styleNodes.forEach(({ id, content }) => {
const scssName = id.substring(id.lastIndexOf('/') + 1, id.length);
const name = scssName.split('.')[0];
fs.writeFileSync(`dist/css/${name}.css`, content);
});
},
processor: css => postcss([
autoprefixer({
grid: false
}),
cssnano()
])
.process(css)
.then(result => result.css)
};
const plugins = [
resolve(),
commonjs(),
stylelint({
fix: false,
include: ['src/**.scss'],
syntax: 'scss',
quiet: false
}),
eslint(),
babel({
exclude: 'node_modules/**'
}),
css({ output: false })
];
if (!process.env.DEVELOPMENT) {
plugins.push(sass({
output: false
}));
}
// If we are running with --environment DEVELOPMENT, serve via browsersync for local development
if (process.env.DEVELOPMENT) {
plugins.push(sass(sassOptions));
plugins.push(browsersync({
host: 'localhost',
watch: true,
port: 3000,
notify: false,
open: true,
server: {
baseDir: 'docs/welcome',
routes: {
'/shepherd/dist/css/shepherd-theme-default.css': 'dist/css/shepherd-theme-default.css',
'/shepherd/dist/js/shepherd.js': 'dist/js/shepherd.js',
'/shepherd/docs/welcome/js/prism.js': 'docs/welcome/js/prism.js',
'/shepherd/docs/welcome/js/welcome.js': 'docs/welcome/js/welcome.js',
'/shepherd/docs/welcome/css/prism.css': 'docs/welcome/css/prism.css',
'/shepherd/docs/welcome/css/welcome.css': 'docs/welcome/css/welcome.css',
'/shepherd/docs/welcome/sheep.svg': 'docs/welcome/sheep.svg'
}
}
}));
}
plugins.push(license({ banner }));
plugins.push(filesize());
const rollupBuilds = [
// Generate unminifed bundle
{
input: './src/js/shepherd.js',
output: [
{
file: pkg.main,
format: 'umd',
name: 'Shepherd',
sourcemap: true
},
{
file: pkg.module,
format: 'esm',
sourcemap: true
}
],
plugins
}
];
if (!process.env.DEVELOPMENT) {
rollupBuilds.push(
// Generate minifed bundle
{
input: './src/js/shepherd.js',
output: {
file: 'dist/js/shepherd.min.js',
format: 'umd',
name: 'Shepherd',
sourcemap: true
},
plugins: [
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**'
}),
license({
banner
}),
sass(sassOptions),
css({ output: false }),
uglify(),
filesize()
]
});
}
export default rollupBuilds;
Summary
The webpack build of shepherd.min.js
was ~80 kb and the rollup build was ~25% smaller, at ~60 kb. Although getting rollup set up and working
is a lot more involved, and there are less examples than webpack, it is clearly worth the effort, for the bundle size savings alone.
Top comments (0)