2

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.

Balázs Édes
  • 13,452
  • 6
  • 54
  • 89
  • The way I typically handle this is have a `foo.js` and `foo.web.js`, and the latter gets picked/preferred up automatically by webpack if it exists. – Evert Jul 14 '22 at 17:30
  • Why is `foo.web.js` picked up automatically? Is it a feature specific to webpack? Do other bundlers have it? – jackdbd Jul 15 '22 at 14:06
  • @Evert I'm not really sure what do you mean, this is a commonjs package, nothing would indicate foo.web.js should be picked up instead of foo.js. – Balázs Édes Jul 15 '22 at 15:00
  • For now I went with updating the docs to include the polyfills if someone wants to use the package in the browser, but I'll leave the question open in case there's something I missed. – Balázs Édes Jul 15 '22 at 15:01
  • @BalázsÉdes if you are using a bundler, it can do this. Just needs to be configured as such. – Evert Jul 15 '22 at 15:20

0 Answers0