Bundling for Node and the Browser

// #browser#bundler#dependency#node#package.json // Comment on DEV

I stumbled upon this feature just yesterday, and it's one of those things that I'd like to never forget again. When sharing code between the client (running in the browser) and the server (running in Node.js), I usually place these shared functions in an internal package (internal means I don't publish it to npm), which I install in both the client and server projects.

For example, I needed to parse an HTML string into a DOM Document. The browser already supports the DOMParser class, which is, unfortunately, not available in Node.js. Here, I have to fall back on JSDOM, which also exposes this class.

So, a simple conditional check on window should be all we need:

import jsdom from 'jsdom'; let document: Document; if (typeof window === 'undefined') { // we're running in Node.js const dom = new jsdom.JSDOM(); const parser = new dom.window.DOMParser(); document = parser.parseFromString(html, 'text/html'); } else { // we're running in the browser const parser = new DOMParser(); document = parser.parseFromString(html, 'text/html'); }

Unfortunately, when bundling the client app (in my case with Vite), it will pull in all dependencies, including JSDOM. However, JSDOM uses Node.js internal packages like vm, which are not available in the browser, and therefore, the bundler will warn or even error on bundling.

Fortunately, as I learned yesterday, there is a browser field you can add to your package.json that lets you exclude or replace certain dependencies from the browser bundle:

"browser": { "jsdom": false }

When Vite (or rather esbuild) bundles the client app, it will not pull in the jsdom dependency into the browser bundle. Great!

There's one caveat to be aware of: the jsdom dependency is not available in the browser bundle, but the import statement is still in our code. During runtime in the browser, I assume the import statement will resolve to undefined. That means we cannot use a named import for jsdom because it will result in an error in the browser. We should use a default import and access the component when we are sure we're running in Node.js:

// Named imports like this will cause an error in the browser because jsdom is undefined. import { JSDOM } from 'jsdom'; // Use a default import and access only when running in Node.js. import jsdom from 'jsdom'; if (typeof window === 'undefined') { const dom = new jsdom.JSDOM(); }

The browser field is an official package.json field, but its structure is not documented by npm. I found the following unofficial spec on GitHub: browser field specification. Furthermore, esbuild also mentions this field in their docs for bundling for Node.