General aspects
If you haven't yet checked out my last post 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
{ "presets": ["@babel/preset-typescript", "minify"] }
- Create a
webpack.config.js
in the root folder that should contain
// 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 thepackage
property.
"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 adist/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 oflibraryTypesWithNames
form thewebpack.config.js
)
Webpack.config.js
- You can manually change the values of
libraryTypesWithNames
to alter the bundle suffix. E.g. changingvar:'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
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.
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.
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
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.
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.
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 thebundle
property to:
"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
"publish": [
{
"target": "package",
"projects": "self"
}
]
Alternatively you can just add to targetDependencies
these 2 options to affect all the future projects.
"bundle": [
{
"target": "package",
"projects": "self"
}
],
"publish": [
{
"target": "bundle",
"projects": "self"
}
]
Good to know
- The
module
option is still experimental and requires turning onoutputModule
flag, you should know this might result in an emptyumd
bundle. - For a full example you can visit https://github.com/IP-OpenSourceWeb/OpenSourceWeb
Top comments (11)
Thank you for this post, it saved me a lot of time!
You are welcome! I am glad i could help someone.
Whenever I have time I'll continue this series demonstrating how to
That would be awesome. Also, a nice thing we have done here at my team is also bundling a css for a library, so we can also publish it to a cdn on the same build pipeline.
By the way, we went back to use rollup, I though the configuration overall it's simpler.
How did you get d.ts files into your build folder with rollup? I had to make another command that calls tsc "manually" (i.e. not with @nrwl/js:tsc) after the rollup. Did you find a better way?
D.ts files should be created automatically. Please check your ts-vonfig files to have them emitted. That should be done using the
"declaration":true
flagI have declarations on, and they are being generated when I call
tsc
with the same tsconfig. We are talking about the@nrwl/web:rollup
executor, right?Maybe this is only an issue when using
swc
, but I'm pretty sure it didn't work for me with babel either.Swc is nice, but until now I didn't have the best experience with it, maybe it's just my luck of experience, at least with babel with the version stated in the article I can guarantee that the declarations are emitted using the
@nrwl/web:rollup
executorI use
@nrwl/web:rollup
to build my library and it's very powerful. However, I'm having an issue now because I want to add font files in it and I can't find a way to compile it because I need webpack.Have you already faced this situation?
I think that is outside it's scope. Personally I would use
@nrwl/web:webpack
with a custom webpack config as presented in the article, you would need some more code to handle font import. Another solution would be to use some custom scripts that would be run after everything is bundled.you're right!
For people being in the same situation that I was and ending up in this article, I fixed it by pointing to a customized rollup config file and using
"@rollup/plugin-url"
for the fonts:I think it also works with jpg
Thank you for your contribution!