Joshua's Docs - Node / NPM Cross-Environment Isomorphic Packages - Dev Tips


Creating Cross-Environment NPM Packages

Splitting your code into separate packages is always an option, but what if you want to bundle and publish a single JS package that can be used for both Node.JS (backend) and the browser (frontend)? This is possible, but a little complicated and there are a few important things to note:

Cross-Environment NPM Packages - Environment Detection

One way to make sure that your code only uses what is available within its executing environment is to physically separate the code into logical files and bundle them separately for distribution - e.g. src/helpers_node.js & src/helpers_web.js.

If this is not possible, or you have files that you want shared across environments, you need to build in environment detection code, so you don't try to do something like access document.location inside Node.JS, which would result in a fatal error.

Cross-Environment NPM Packages - How to Detect NodeJS vs Browser

You could roll your own method to detect if your code is executing inside Node vs the browser, or even grab something off Stack Overflow, but the easiest solution is to just use something like the process package - this exposes the process global to browser code, and sets process.browser to true in the browser, but undefined in Node.

It does this by hooking into the browser field in the package.json file, so in reality, the use of this package is usually not left to you as a package developer, but to the developers of the bundler you are using:

  • If you are using Browserify or Webpack, both of these will automatically set process.browser based on environment
  • Parcel also supports this to some degree, although documented to a lesser extent; looks like it replaces specific keys with the value itself, so depending on process.browser works, but only when interacted with normally
    • Doing something like console.log(process) will fail for the browser export, and so will eval('process.browser')
    • Normal usage, such as if (process.browser) { ... } works just fine though
  • If you are using Rollup, you need to do some manual work to get process.browser working
    • Using @rollup/plugin-node-resolve, with browser: true, and rollup-plugin-node-builtins + rollup-plugin-node-globals, should accomplish the task of exposing process as a cross-env global, and setting process.browser appropriately
    • You could also (or alternatively), use rollup-plugin-replace to replace the string literal 'process.browser' with a boolean value based on a config (see this guide for details).

Cross-Environment NPM Packages - Distribution Files

The easiest way (for both you and your users) is to distribute both browser JS and Node.JS code in the same package is to automatically bundle the browser code together into a distributable file(s), and upload it to the NPM registry along with your regular NodeJS files.

Pre-compiling the final browser-ready and node-ready code just takes a little time and processing power on your end, but can save a bunch of headache for your end-users.

Using this strategy usually involves at least a few of the following steps:

  • Create an automated build process step that creates the JS files that can be consumed by the browser
    • Use a logical module pattern (probably UMD)
      • Unless your code was written as 100% vanilla JS, with no modules, you need to convert the code to a module pattern that the browser can understand. UMD is a great fit for this case, as it works for both Node.JS and the browser.
      • ES6 Modules (aka ES Modules) are gaining browser support, but still leave out ~10% of users (2020).
      • You could technically use an IIFE and global scope for cross-compatibility, but UMD uses that anyways as a last-resort fallback
    • This can be via a standard script entry calling a standard bundler like Webpack (example - Vuetify), or even something more advanced, like calling and executing a Makefile (example - chaijs)
    • Ideally, if this is a true distribution file, it should be minified / optimized
      • You might want to include source-maps
    • You can trigger the build step automatically (⚡) before NPM publishing by using the prepublish script entry in package.json!
  • Make sure the distributable files are including in the NPM release, but optionally (although highly recommended), exclude the compiled files from being tracked in version control (e.g. Git)
    • The easiest way to include dist files in a NPM release, but exclude from git, is by adding the dist files to your .gitignore, and then using the files entry in package.json to include them in the NPM release. See the publishing section for more details.
    • Please don't require your end users to transpile code themselves...
  • Provide instructions to your users on how to pull the bundled browser-ready JS into a webpage
    • You might give multiple options, such as pulling directly from ./node_modules/{...} folder in a <script> tag, to pulling from a file distributed via a CDN, such as jsdelivr

Targeting UMD Output From Bundlers

  • Browserify
    • Use the -s {myVarName} option
      • aka standalone
  • Webpack
    • Use the libraryTarget option
  • Rollup
    • Use the --format umd --name "{myVarName}" option
  • Parcel
    • Use the --global {myVarName} option

Cross-Environment NPM Packages - Real World Examples

To understand how to write, package, and distribute a cross-environment NPM Package, it might help to see real example repositories. Here are some popular projects as examples:

Examples of PRs / Commits that add various cross-browser stuff

For finding more real-world examples, try searching "node browser" on Github and sorting by star count.

Markdown Source Last Updated:
Sun Jun 27 2021 05:05:22 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Wed Jul 15 2020 03:20:37 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby