Search code examples
node.jstypescriptwebpackes6-modules

Which typescript module settings for a library that is used in browsers and node.js


There is lots of resources about this, yet it is easily confusing and I can't find any recent resources about this:

Today in 2024, I want to create a library, written in tyescript. It will be used in React projects that use a webpack bundler. And the app will be rendered on the server (node.js) and in the browser.

how do I configure tsconfig.json? Which setting do I choose for module ?

Context:

The library already exists in CJS, but in a current big rewrite I am considering to upgrade to ESM, since we have issues in our apps that use this library (which are also CJS at the moment) with importing ESM only modules. And also, with CJS we can't make use of automatically split bundles when using dynamic imports in webpack.

The documentation

The typescript reference for module states that es2015,es2020,es2022,esnext should not be used for node.js

And it states that

node16 and nodenext are the only correct module options for all apps and libraries that are intended to run in Node.js v12 or later

But does not mention if nodenext or node16 can be used for browsers / bundlers like webpack as well.

What is it? Which should I chose?

I wonder if should I go for a split route of publishing a /lib/cjs and /lib/esm version, with require and module fields in the export settings of package.json?

That still leaves me with the question which module output setting to use in tsconfig so that both browser (through webpack) and node.js can use ESM. Or should I opt for one of these environments to keep using CJS?


Solution

  • Due to historical reasons, there are just too many combinations, but most are not required anymore. In summary, the following four rules are more than enough if you want to publish ESM-only package that can be used by both Node.js and browser:

    • You package should use "type": "module'.
    • If you are authoring library for Node.js only, then:
      • "module": "NodeNext"
      • "moduleResolution": "NodeNext"
    • If you are authoring library for Frontend only via bundler, then:
      • "module": "ESNext"
      • "moduleResolution": "Bundler"
    • Finally, if you are authoring library for both Node.js and Frontend then:
      • "module": "NodeNext"
      • "moduleResolution": "NodeNext"

    More explanation

    The combination of NodeNext + NodeNext is a subset of ESNext + Bundler. The alternative way of saying this is that NodeNext + NodeNext is ESM with constraints as set up by Node.js module resolution algorithm. The most common example being the mandatory requirement of having file extension in the import statements e.g. import {} from 'other.js' in NodeNext + NodeNext. Such restriction does not apply in ESNext + Bundler mode.

    So, if you are publishing for both Node.js (expecting to use without bundling) and for frontend (expecting to use import-maps or bundler), then you should use module: NodeNext + moduleResolution: NodeNext.

    This ensures that TypeScript (and, other modern bundlers) respects main, exports declarations as specified by package.json specification.

    Also, keep in mind that "type": "module" is important. In its absence, even with module: NodeNext, typescript compiler will compile *.ts files to *.js with CJS require statements and *.mts files to *.mjs with ESM import statements.

    Finally, there is alternative way of thinking about it too! If you are using TypeScript as a compiler to compile your library, then you should use NodeNext + NodeNext. And, if you are using bundler, then you have a choice of either combination (although I still recommend using NodeNext + NodeNext due to strict semantics it introduces).

    For more detailed, in-depth explanation of module and moduleResolution, this is a good read.