Authoring Libraries

Aside from applications, webpack can also be used to bundle JavaScript libraries. The following guide is meant for library authors looking to streamline their bundling strategy.

Authoring a Library

Let's assume that you are writing a small library ,webpack-numbers, that allows users to convert the numbers 1 through 5 from their numeric representation to a textual one and vice-versa, e.g. 2 to 'two'.

The basic project structure may look like this:

project

+  |- webpack.config.js
+  |- package.json
+  |- /src
+    |- index.js
+    |- ref.json

Initialize npm, install webpack and lodash:

npm init -y
npm install --save-dev webpack lodash

src/ref.json

[
  {
    "num": 1,
    "word": "One"
  },
  {
    "num": 2,
    "word": "Two"
  },
  {
    "num": 3,
    "word": "Three"
  },
  {
    "num": 4,
    "word": "Four"
  },
  {
    "num": 5,
    "word": "Five"
  },
  {
    "num": 0,
    "word": "Zero"
  }
]

src/index.js

import _ from 'lodash';
import numRef from './ref.json';

export function numToWord(num) {
  return _.reduce(numRef, (accum, ref) => {
    return ref.num === num ? ref.word : accum;
  }, '');
}

export function wordToNum(word) {
  return _.reduce(numRef, (accum, ref) => {
    return ref.word === word && word.toLowerCase() ? ref.num : accum;
  }, -1);
}

The usage specification for the library use will be as follows:

  • ES2015 module import:
import * as webpackNumbers from 'webpack-numbers';
// ...
webpackNumbers.wordToNum('Two');
  • CommonJS module require:
const webpackNumbers = require('webpack-numbers');
// ...
webpackNumbers.wordToNum('Two');
  • AMD module require:
require(['webpackNumbers'], function (webpackNumbers) {
  // ...
  webpackNumbers.wordToNum('Two');
});

The consumer also can use the library by loading it via a script tag:

<!doctype html>
<html>
  ...
  <script src="https://unpkg.com/webpack-numbers"></script>
  <script>
    // ...
    // Global variable
    webpackNumbers.wordToNum('Five')
    // Property in the window object
    window.webpackNumbers.wordToNum('Five')
    // ...
  </script>
</html>

Note that we can also configure it to expose the library in the following ways:

  • Property in the global object, for node.
  • Property in the this object.

For full library configuration and code please refer to webpack-library-example.

Base Configuration

Now let's bundle this library in a way that will achieve the following goals:

  • Using externals to avoid bundling lodash, so the consumer is required to load it.
  • Setting the library name as webpack-numbers.
  • Exposing the library as a variable called webpackNumbers.
  • Being able to access the library inside Node.js.

Also, the consumer should be able to access the library in the following ways:

  • ES2015 module. i.e. import webpackNumbers from 'webpack-numbers'.
  • CommonJS module. i.e. require('webpack-numbers').
  • Global variable when included through script tag.

We can start with this basic webpack configuration:

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js',
  },
};

Base Configuration with source map

Source maps is a useful debugging tool that allows you to view where the minified code originated from.

webpack.config.js

const path = require('path');

module.exports = [
  'source-map'
].map(devtool => ({
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js'
  },
  devtool,
  optimization: {
    runtimeChunk: true
  }
}));

For more information about getting source maps setup and available options please refer to Devtool configuration

To see code examples please refer to webpack repository

Externalize Lodash

Now, if you run webpack, you will find that a largish bundle is created. If you inspect the file, you'll see that lodash has been bundled along with your code. In this case, we'd prefer to treat lodash as a peerDependency. Meaning that the consumer should already have lodash installed. Hence you would want to give up control of this external library to the consumer of your library.

This can be done using the externals configuration:

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js',
    },
+   externals: {
+     lodash: {
+       commonjs: 'lodash',
+       commonjs2: 'lodash',
+       amd: 'lodash',
+       root: '_',
+     },
+   },
  };

This means that your library expects a dependency named lodash to be available in the consumer's environment.

External Limitations

For libraries that use several files from a dependency:

import A from 'library/one';
import B from 'library/two';

// ...

You won't be able to exclude them from the bundle by specifying library in the externals. You'll either need to exclude them one by one or by using a regular expression.

module.exports = {
  //...
  externals: [
    'library/one',
    'library/two',
    // Everything that starts with "library/"
    /^library\/.+$/,
  ],
};

Expose the Library

For widespread use of the library, we would like it to be compatible in different environments, i.e. CommonJS, AMD, Node.js and as a global variable. To make your library available for consumption, add the library property inside output:

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js',
+     library: 'webpackNumbers',
    },
    externals: {
      lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_',
      },
    },
  };

This exposes your library bundle available as a global variable named webpackNumbers when imported. To make the library compatible with other environments, add libraryTarget property to the config. This will add various options about how the library can be exposed.

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js',
      library: 'webpackNumbers',
+     libraryTarget: 'umd',
    },
    externals: {
      lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_',
      },
    },
  };

You can expose the library in the following ways:

  • Variable: as a global variable made available by a script tag (libraryTarget:'var').
  • This: available through the this object (libraryTarget:'this').
  • Window: available through the window object, in the browser (libraryTarget:'window').
  • UMD: available after AMD or CommonJS require (libraryTarget:'umd').

If library is set and libraryTarget is not, libraryTarget defaults to var as specified in the output configuration documentation. See output.libraryTarget there for a detailed list of all available options.

Final Steps

Optimize your output for production by following the steps mentioned in the production guide. Let's also add the path to your generated bundle as the package's main field in with the package.json

package.json

{
  ...
  "main": "dist/webpack-numbers.js",
  ...
}

Or, to add it as a standard module as per this guide:

{
  ...
  "module": "src/index.js",
  ...
}

The key main refers to the standard from package.json, and module to a proposal to allow the JavaScript ecosystem upgrade to use ES2015 modules without breaking backwards compatibility.

Now you can publish it as an npm package and find it at unpkg.com to distribute it to your users.