Saturday, 4 September 2021

Using Webpack to bundle an Isomorphic npm package which runs in both browsers and NodeJS

I recently tried to create a npm package which runs in both browsers and the Node environment. As a complete beginner to modern Javascript toolchains, I found this quite frustrating, time consuming, and the documentation not very approachable. I couldn't one place documenting how to do this, so here goes!

If you depend on APIs that aren't present in both NodeJS and browser environments (like HTML5 fetch for example), you likely want to be producing a multiple JS bundle files; one for Node, and one for browsers. To do that, you want to configure Webpack to produce multiple targets, i.e. have your webpack.config.json file's module.exports return a list of configs, one for target: node, and one for target: browser.  This will produce multiple bundle JS files in your output dir. Running with the NodeJS/browser example, you'd output an index.node.js and an index.js file.

Another option to consider here to retain a single file might be to use polyfills. Webpack was quite good at telling you which polyfills you need and how to add them. This might allow you to ship a single bundle for both browser and NodeJS, but for the packages I needed I found that bloated my package by 300kB, which was unacceptable. Your mileage may vary. 

Assuming you're going with the multiple output targets approach, you then need to configure your module's npm package.json to direct browsers to load the file produced for the browser target above, and for Node to load file produced for its target. By default the JS environment loading your package will load the entry point file specified in the main field in package.json, so NodeJS will use this, but you can specify which file is executed for browsers by setting the browser field in package.json. So when you have Webpack producing multiple bundle files for multiple targets, you just need to ensure the files specified in package.json line up. Easy... once you know!

You also probably want each configuration to have output.library.type: "umd", so that it works with a variety of Javascript import patterns. If you do that, then you'll also want to set output.globalObject: "this" as well, otherwise you'll get weird undefined errors when trying to touch globals in the Node environment. See also the Webpack output.globalObject docs for a tiny bit more details.