Using webpack

How to build and distribute libraries via NPM using WebPack bundler.

Goal

Our goal is (in theory) to write cross-platform (Node + browser) code, otherwise known as universal JavaScript. In practice we will write CommonJS modules (or ES6 modules, if you want), that anyone can use on NodeJS platform. We will use Webpack to create bundles to be loaded in the browser. I like creating an extra bundle for each project that is intended to run in the browser, because I like creating demo pages with each project. Thus my typical project distributed via NPM has the following structure

1
2
3
4
5
6
7
8
9
10
11
./
package.json
webpack.config.js
index.js
src/
foo.js
bar.js
bar-spec.js
dist/
project-name.js
index.html

I build the file dist/project-name.js using Webpack, and I show how it should be used in the demo page dist/index.html. Any other NPM dependent project should require the main file index.js or whatever is listed as the main file inside package.json.

Here are my typical commands

Start an NPM package

npm init works great. If you want to keep the project private and not accidentally publish it to the NPM registry, set "private": true in the package.json right away

Decide what is the main source file for the package, for example ./index.js. Keep writing the code the same way as you would for Node - using CommonJS exports and require function.

Add and configure webpack (library case)

npm install --save-dev webpack then create a webpack.config.js file in the project's root folder. Here is sample contents for a project named "foo"

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
output: {
library: 'foo',
libraryTarget: 'umd',
path: './dist',
filename: 'foo.js'
},
entry: {
library: './index'
}
}

This will bundle everything reachable from the file "./index.js" into a file "dist/foo.js". The output file will have universal module definition boilerplate (UMD) code and can be used from other modules using 'require(...)', or as a stand alone bundle under the name 'foo'.

You can configure other library targets and export the code directly as a property of window for example

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
// will generate window.foo = ...
module.exports = {
output: {
library: 'foo',
libraryTarget: 'window',
path: './dist',
filename: 'foo.js'
},
entry: {
library: './index'
}
}

Set up the NPM script to run weback build step, for example

package.json
1
2
3
"scripts": {
"build": "webpack"
}

This should generate dist/foo.js file every time you run npm run build. You can include the bundle from the dist/index.html and use the library using window.foo (the name "foo", is the value of the "library" property)

dist/index.html
1
2
3
4
<script src="foo.js"></script>
<script>
foo.something('works')
</script>

Example project tiny-toast

Note

Do not forget to include the "dist" folder in the list of files published to NPM

Bundle JavaScript application

Sometimes you are writing a library to be reused by others, and in other cases you are writing an application. Webpack supports both types; bundling an application is even simpler than bundling a library - there is no UMD boilerplate. Instead specify an "entry" file

webpack.config.js
1
2
3
4
5
6
7
8
9
module.exports = {
output: {
path: './dist',
filename: 'app.js'
},
entry: {
app: './src/app.js'
}
}

This will create "dist/app.js" bundle that will include all the code reachable from "src/app.js". The "src/app.js" should have all the logic to start the application - this is what will run when the browser loads the page (if you include "dist/app.js" from the page, of course)

Example project instant-vdom-todo

Bundle application CSS

If your project includes CSS files, you can bundle them up using WebPack and even compile SaSS / Less / Stylus / etc into CSS using loaders. This applies more to the application bundles, not the library bundles.

To create a simple CSS bundle next to the corresponding JavaScript bundle in the "dist" folder, install the following webpack plugins

npm install --save-dev extract-text-webpack-plugin css-loader style-loader

Tell webpack to load every ".css" file using the style-loader and create a separate bundle

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
output: {
path: './dist',
filename: 'app.js'
},
entry: {
app: './src/app.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
}
]
}
}
module.exports.plugins = [
new ExtractTextPlugin('app.css', {
allChunks: true
})
]

In "src/app.js" use "require" to load CSS files to let webpack know which files to bundle to create "dist/app.css"

src/app.js
1
2
3
4
'use strict'
require('../node_modules/todomvc-common/base.css')
require('../node_modules/todomvc-app-css/index.css')
// the rest of JavaScript code

Example project instant-vdom-todo

Bundle library and application

You can have multiple parallel configurations inside a single config file. This is useful if you want to build unrelated bundles. For example a project might build a library and a demo application. Just export an array of configuraton objects

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = [{
output: {
library: 'fakeTodos',
libraryTarget: 'umd',
path: './dist',
filename: 'fake-todos.js'
},
entry: {
'fake-todos': './src/index'
}
}, {
output: {
path: './dist',
filename: 'demo-app.js'
},
entry: {
'demo-app': './src/demo-app'
}
}]

The above example is taken from fake-todos build. It builds a universal library 'dist/fake-todos.js' and a complete stand alone demo application dist/demo-app.js

Split code across several bundles

If you want to split a large code base across multiple bundles, with possible code sharing, for example, read the multiple entrypoints section.

As an example, consider a small Vue.js application. The only vendor libraries it imports are 'vue' and 'vue-router'. Thus we can mark these imports as "vendor" and they will be placed into separate bundle by the webpack.optimize.CommonsChunkPlugin.

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var path = require('path')
var webpack = require('webpack')

module.exports = {
entry: {
app: './public/main.js',
vendor: ['vue', 'vue-router']
},
output: {
path: path.resolve(__dirname, './public'),
filename: 'bundle.js'
},
plugins: [
// put all imports in the "vendor" entry list above
// into separate bundle file.
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
filename: "vendor.bundle.js"
})
]
}

This will produce in DEV mode files public/bundle.js and public/vendor.bundle.js, which is exactly what we need.

Add object spread

Often in the code examples we see features like Object spread. For example, if we want to quickly extend an object with additional properties to create a new object to use:

1
2
3
4
5
6
7
8
const p = {
name: 'John',
age: 21
}
const result = {
...foo,
job: 'writer'
}

The object result will have all properties of the object p plus additional property job. Without ...foo we could have used ES6 Object.assign to achieve the same but it looks clunky.

1
2
3
4
5
const p = {
name: 'John',
age: 21
}
const result = Object.assign({}, p, {job: 'writer'})

To allow us to use ...object notation we need to transpile code using Babel (or some other transpiler) plus add a special plugin to support "object spread and rest" feature.

In our "loaders" object we should specify Babel as the loader for ".js" files.

1
2
3
4
5
{
"loaders": {
"js": "babel"
}
}

In the babel options file .babelrc add this transform to the list

1
2
3
4
5
6
{
"presets": [
["es2015", { "modules": false }]
],
"plugins": ["transform-object-rest-spread"]
}

Do not forget to actually add the transform to the dev dependencies

1
npm i -D babel-plugin-transform-object-rest-spread

Enjoy the short object spread notation.

Inject environment variables during build

We might want to inject environment-specific variables when building the bundle. The easiest way to do this is by using "EnvironmentPlugin" which is included with the webpack.

Let us put all settings into file "dev-config.js". We will right away use environment variables in this file, for example process.env.USER

dev-config.js
1
2
3
module.exports = {
username: process.env.USER
}

Elsewhere in our code, we will access this file NOT by its filename, but by an alias, for example require('config') to dynamically select it. We could for example inject completely different settings file when building in production environment for example. In simple example, let us just print the loaded username from index.js

index.js
1
2
3
4
5
const config = require('config')
function foo() {
console.log('user', config.username)
}
foo()

We should configure the webpack.config.js like this

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.EnvironmentPlugin(['USER'])
],
entry: './index.js',
output: {
filename: 'bundle.js',
path: __dirname
},
resolve: {
alias: {
config: './dev-config.js'
}
}
}

Note the following points

  • We tell webpack to inject the current value (at build time) of the environment variable USER, thus it will literally replace every reference to string process.env.USER in our code.
  • We alias every request to "config" module to point at file "./dev-config.js". This alias can be very flexible and depend on the target environment for example.

The produced bundle shows the result of the build-time substitution

bundle.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
// dev-config.js contents with replaced `process.env.USER`
module.exports = {
username: "gleb"
}

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
// index.js loads "config"
const config = __webpack_require__(0)

function foo() {
console.log('user', config.username)
}
foo()

This produces the desired effect.

Futher reading

What I have shown is just the beginning. To learn more, read the following tutorials