How to easily create JS libraries with bundled dependencies compatible with ES/AMD/UMD/CJS module systems using Nx
Tags: javascript, webdev, programming, npm
Published: 2022-01-22 02:51:02

## General aspects

If you haven't yet checked out my [last post](https://dev.to/ipreda/how-to-easily-create-js-libraries-compatible-with-esamdumdcjs-module-systems-1kfd) it's high time to do so as we will need it for this article. 

Getting that out of the way, let's suppose we already have the workspace set up, we can build the libraries and publish them, but something is not exactly right, what if we want to ship a version of the library with all the dependencies already bundled so our users could use it directly from a CDN.

In this article, I will show you not only how to set up such a feature with the least possible setup, but I will also show you how to minimize the bundles to their possible best. 

## Concepts used this time around
- `@nrwl/web:webpack`
- `webpack config`

## Getting to action - Creating bundles 
- `npm install --save-dev @babel/preset-typescript`
- Adjust the babel.config.json file created last time to contain
```json
{ "presets": ["@babel/preset-typescript", "minify"] }
```
- Create a `webpack.config.js` in the root folder that should contain 
```javascript
// https://webpack.js.org/configuration/output/#outputlibrarytype
// possible libraryTargets in webpack 5: 'var', 'module', 'assign', 'assign-properties', 'this', 'window', 'self', 'global', 'commonjs', 'commonjs2', 'commonjs-module', 'amd', 'amd-require', 'umd', 'umd2', 'jsonp' and 'system'

// type:name collection used in file names
const libraryTypesWithNames = {
    var: 'var',
    module: 'esm',
    assign: 'assign',
    'assign-properties': 'assign-properties',
    this: 'this',
    window: 'window',
    self: 'self',
    global: 'global',
    commonjs: 'commonjs',
    commonjs2: 'commonjs2',
    'commonjs-module': 'commonjs-module',
    amd: 'amd',
    'amd-require': 'amd-require',
    umd: 'umd',
    umd2: 'umd2',
    jsonp: 'jsonp',
    system: 'system',
};

const getLibraryName = (type) => libraryTypesWithNames[type];

const getLibrary = (type, name) => {
    const unsetNameLibraries = ['module', 'amd-require']; // these libraries cannot have a name
    if (unsetNameLibraries.includes(type)) name = undefined;
    return { name, type, umdNamedDefine: true };
};

const modifyEntries = (config, libraryName, libraryTarget) => {
    const mainEntryPath = config.entry.main;
    try {
        delete config.entry.main;
    } catch (error) {
        console.warn(`Could not delete entry.main: ${error}`);
    }

    if (libraryTarget.includes('module')) {
        // https://webpack.js.org/configuration/output/#librarytarget-module
        // for esm library name must be unset and config.experiments.outputModule = true - This is experimental and might result in empty umd output
       config.experiments.outputModule = true
        config.experiments = {
            ...config.experiments,
            outputModule: true,
        };
    }

    libraryTarget.forEach((type) => {
        config.entry[`${libraryName}.${getLibraryName(type)}`] = {
            import: mainEntryPath,
            library: getLibrary(type, libraryName),
        };
    });

    // @nrwl/web:webpack runs webpack 2 times with es5 and esm configurations
    const outputFilename = config.output.filename.includes('es5') ? config.output.filename : '[name].js';
    config.output = {
        ...config.output,
        filename: outputFilename,
    };
};

module.exports = (config, { options }) => {
    const libraryTargets = options.libraryTargets ?? ['global', 'commonjs', 'amd', 'umd'];
    const libraryName = options.libraryName;

    config.optimization.runtimeChunk = false;
    modifyEntries(config, libraryName, libraryTargets);

    return config;
};

```
- go to `packages/LibraryName/project.json` and add this json property under the `package` property.
```json
 "bundle": {
            "executor": "@nrwl/web:webpack",
            "outputs": ["{options.outputPath}"],
            "options": {
                "libraryName": "LibraryName",
                "libraryTargets": ['global', 'commonjs', 'amd', 'umd'],
                "index": "",
                "tsConfig": "packages/LibraryName/tsconfig.lib.json",
                "main": "packages/LibraryName/src/index.ts",
                "outputPath": "dist/packages/LibraryName/bundles",
                "compiler": "babel",
                "optimization": true,
                "extractLicenses": true,
                "runtimeChunk": false,
                "vendorChunk": false,
                "generateIndexHtml": false,
                "commonChunk": false,
                "namedChunks": false,
                "webpackConfig": "webpack.config.js"
            }
        },
```
- Run `nx bundle:LibraryName` - this should create a `dist/packages/LibraryName/bundles` folder containing the `.umd` and `.umd.es5` bundled files.

## Inputs and configurations
### In `packages/LibraryName/project.json`
These variables are custom, as they are not internally used by Nx, and are just passed to the `webpack.config.js`.
- `libraryName` - String - This affects how webpack will export your library. For example `"libraryName": "LibraryName"` in UMD will export your library to an object called "LibraryName".
- `libraryTargets` - Array of any available webpack 5 library type (the keys of `libraryTypesWithNames` form the `webpack.config.js`)

### Webpack.config.js
- You can manually change the values of `libraryTypesWithNames` to alter the bundle suffix. E.g. changing `var:'var'` to `'var:'web'` will generate a bundle file ending in `.web.js` and `.web.es5.js`.
- You can manually change the default array for `libraryTargets`.


## Some code explanation for the configurable variables
```javascript
const libraryTypesWithNames = {
    var: 'var',
    module: 'esm',
    assign: 'assign',
    'assign-properties': 'assign-properties',
    this: 'this',
    window: 'window',
    self: 'self',
    global: 'global',
    commonjs: 'commonjs',
    commonjs2: 'commonjs2',
    'commonjs-module': 'commonjs-module',
    amd: 'amd',
    'amd-require': 'amd-require',
    umd: 'umd',
    umd2: 'umd2',
    jsonp: 'jsonp',
    system: 'system',
};
```
This contains all the available libraries in webpack 5 as stated on their [website]( https://webpack.js.org/configuration/output/#outputlibrarytype). 
We use the key in it to retrieve the name we want to use in our bundled filename. Feel free to change them as you would like. We also do not use them all at once as you will notice later on. 


```javascript
const libraryTargets = options.libraryTargets ?? ['global', 'commonjs', 'amd', 'umd'];
```
This line contains as default an array of options that can be used for most libraries, you can customize it directly or you can provide an array of library types to the `libraryTargets` property in `packages/LibraryName/project.json` using the keys of `libraryTypesWithNames`.
E.g. if you want to use all the available options you can simply change the variable to 
```javascript
const libraryTargets = Object.keys(libraryTypesWithNames);
```

## Minimizing the bundle size using gzip
- `npm install compression-webpack-plugin --save-dev`
- Depending on what you want to achieve change in `webpack.config.js` the following - This will help with AWS and some CDNs as the gzipped files have `.js` extension and can be embedded directly.

```javascript
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = (config, { options }) => {
    const libraryTargets = options.libraryTargets ?? ['global', 'commonjs', 'amd', 'umd'];
    const libraryName = options.libraryName;

    config.optimization.runtimeChunk = false;
    const terser = config.optimization.minimizer.find((minimizer) => minimizer.constructor.name === 'TerserPlugin');
    if (terser) {
        terser.options.exclude = /\.gz\.js$/;
    }
    config.plugins = [
        ...config.plugins,
        new CompressionPlugin({
            filename: `[name].gz[ext]`,
        }),
    ];
    modifyEntries(config, libraryName, libraryTargets);

    return config;
};

```

Otherwise, you might consider the simpler version that outputs `.gz` files.
```javascript

const CompressionPlugin = require('compression-webpack-plugin');

module.exports = (config, { options }) => {
    const libraryTargets = options.libraryTargets ?? ['global', 'commonjs', 'amd', 'umd'];
    const libraryName = options.libraryName;

    config.optimization.runtimeChunk = false;
    config.plugins = [
        ...config.plugins,
        new CompressionPlugin(),
    ];
    modifyEntries(config, libraryName, libraryTargets);

    return config;
};
```

## Final touches
If last time you created an executor to automate publishing, we can now automate even more. We will set up automatically running bundling and packaging before publishing whenever we run `nx publish:LibraryName`.
All you need to do is:
- Go to  `packages/LibraryName/project.json` and change the `bundle` property to:
```json

        "bundle": {
            "executor": "@nrwl/web:webpack",
            "outputs": ["{options.outputPath}"],
            "dependsOn": [
                {
                    "target": "package",
                    "projects": "dependencies"
                }
            ],
            "options": {
                "libraryName": "LibraryName",
                "libraryTargets": ["global", "commonjs", "amd", "umd"],
                "index": "",
                "tsConfig": "packages/LibraryName/tsconfig.lib.json",
                "main": "packages/LibraryName/src/index.ts",
                "outputPath": "dist/packages/LibraryName/bundles",
                "compiler": "babel",
                "optimization": true,
                "extractLicenses": true,
                "runtimeChunk": false,
                "vendorChunk": false,
                "generateIndexHtml": false,
                "commonChunk": false,
                "namedChunks": false,
                "webpackConfig": "webpack.config.js"
            }
        },
```
- Then go to nx.json and add in `targetDependencies` another option with
```json
 "publish": [
            {
                "target": "package",
                "projects": "self"
            }
        ]
```

Alternatively you can just add to `targetDependencies` these 2 options to affect all the future projects.
```json
    "bundle": [
            {
                "target": "package",
                "projects": "self"
            }
        ],
        "publish": [
            {
                "target": "bundle",
                "projects": "self"
            }
        ]
```


## Good to know
- The `module` option is still experimental and requires turning on `outputModule` flag, you should know this might result in an empty `umd` bundle. 
- For a full example you can visit https://github.com/IP-OpenSourceWeb/OpenSourceWeb