How to deal with node modules in the browser?

773 views Asked by At

My title is a bit vague, here is what I'm trying to do:

  • I have a typescript npm package
  • I want it to be useable on both node and browser.
  • I'm building it using a simple tsc command (no bundling), in order to get proper typings
  • My module has 1 entry point, an index.ts file, which exposes (re-exports) everything.

Some functions in this module are meant to be used on node-only, so there are node imports in some files, like:

import { fileURLToPath } from 'url'
import { readFile } from 'fs/promises'
import { resolve } from 'path'
// ...

I would like to find a way to:

  • Not trip-up bundlers with this
  • Not force users of this package to add "hacks" to their bundler config, like mentioned here: Node cannot find module "fs" when using webpack
  • Throw sensible Errors in case they are trying to use node-only features
  • Use proper typings inside my module, utilizing @types/node in my code

My main problem is, that no matter what, I have to import or require the node-only modules, which breaks requirement 1 (trips up bundlers, or forces the user to add some polyfill).

The only way I found that's working, is what isomorphic packages use, which is to have 2 different entry points, and mark it in my package.json like so:

{
  // The entry point for node modules
  "main": "lib/index.node.js",
  // The entry point for bundlers
  "browser": "lib/index.browser.js",
  // Common typings  
  "typings": "lib/index.browser.d.ts"
}

This is however very impractical, and forces me to do a lots of repetition, as I don't have 2 different versions of the package, just some code that should throw in the browser when used.

Is there a way to make something like this work?

// create safe-fs.ts locally and use it instead of the real "fs" module
import * as fs from 'fs'

function createModuleProxy(moduleName: string): any {
  return new Proxy(
    {},
    {
      get(target, property) {
        return () => {
          throw new Error(`Function "${String(property)}" from module "${moduleName}" should only be used on node.js`)
        }
      },
    },
  )
}

const isNode = typeof window === undefined && typeof process === 'object'
const safeFs: typeof fs = isNode ? fs : createModuleProxy('fs')
export default safeFs

As it stands, this trips up bundlers, as I'm still importing fs.

0

There are 0 answers