How to write ESM modules that work in both the browser, and Node, with conditional imports

Viewed 544

I have a codebase that should run in the browser, and in node. It needs to switch some functionality based on if it is running in node, or the browser.

Previously I wrote modules in a CommonJS format. And I had a "shim" module, where I'd test the environment for features, and export things based on that. So all conditional behavior was contained to this shim file.

Now, I've tried to create "shim" ESM module. I can't figure out how. Some of the switching behavior depends on checking for the presence of other ESM modules. But you can only do a test like this inside a dynamic ESM import, which is asynchronous. And because export statements must be at the top level, I can't export the result, unless top level await is available. But this isn't available in the browser.

In my current hack, I export a deferred-like object, and await it where needed. But this has made a lot of my unnecessarily wrapped in (async()=>{...}).

So how do I do this?

I'd prefer to not use a bundler, my browser target is guaranteed to be fairly recent.

2 Answers

had a similar issue to get a module using performance.now() to work in both node and the browser.

This worked:

const P = typeof performance !== 'undefined' ? performance
  : (await import('perf_hooks')).performance

Top level await saved the day but:

  • as of now, only works in chrome, node and firefox canary
  • officially, still only a stage 3 proposal

Another answer that is more idiomatic is to set the main:main.js and browser:browser.js fields in package.json that set-up whatever environment-specific elements before importing and re-exporting the main module.

//main.js fix for nodejs
import {something} from 'nodejsSpecificModule'
global.something = something //or whatever else needed
export {...} from './index.js'
//browser.js fix for browser
import {something} from 'browserSpecificModule'
window.something = something //or whatever else needed
export {...} from './index.js'

this avoids dynamic imports with top-level-await

Related