question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if youā€™re still stuck at the end, weā€™re happy to hop on a call to see how we can help out.

Helping author dual CommonJS/ES module packages

See original GitHub issue

Suggestion

šŸ” Search Terms

Dual CommonJS / CJS + ES module / ESM packages

āœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldnā€™t be a breaking change in existing TypeScript/JavaScript code
  • This wouldnā€™t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isnā€™t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScriptā€™s Design Goals.

The last checkbox is unticked because this feature request is related to extending tscā€™s role in a build pipeline and itā€™s about aligning with Nodeā€™s behavior, not ECMAScriptā€™s, which has no concept of packages. That said, Node is obviously one of the two main targets for TypeScript (the other being the web).

ā­ Suggestion

This suggestion is more abstract rather than a precise implementation proposal. The issue is that writing dual CJS/ESM packages for Node currently is relatively high-friction with or without TypeScript, and tsc could make this better. I understand if this issue is considered out of scope and closed.

Iā€™m writing this assuming that the people reading this understand how ESM work in Node 16 and their constraints.

With "module": "nodenext", TypeScript allows .ts files to import other .ts files by using import specifiers with .js extensions so that browsers and Node can run the emitted code. That is, when tsc sees import './x.js';, it knows to consider looking for ./x.ts to resolve the module. This way, tsc doesnā€™t need to rewrite the import specifier and can emit import './x.js'; in the compiled JS code.

Separately, dual CJS/ESM packages require some of their files to use either the .mjs or .cjs file extension. This is because the package declares either "type": "module" or "type": "commonjs" and files of the other type need to use .cjs or .mjs, respectively.

(Aside: this problem exists regardless of whether a package uses TypeScript. Even if they werenā€™t using TypeScript, authors writing ESM would need to duplicate or compile their ESM to CJS.)

This feature request is for tsc to be able to emit compiled JS with .js file extensions rewritten to .cjs (or .mjs) and import specifiers with .js similarly rewritten to .cjs (or .mjs). This way a packageā€™s source of truth would be its .ts files, and running e.g. tsc && tsc -p tsconfig.commonjs.json would produce ESM and CJS for the dual-mode package.

šŸ“ƒ Motivating Example

Consider a package that consists of more than one file with intrapackage imports.

hello-world.ts

import b from './b.js';
export default function helloWorld() { return `${b()} world`; }

hello.ts

export default function hello() { return `hello`; }

The author wants to publish the package with both CJS and ESM: package.json

{
  "type": "module",
  "//": "Using longhand notation below for clarity"
  "exports": {
    ".": {
      "import": "./build/esm/hello-world.js",
      "require": "./build/cjs/hello-world.cjs",
      "types": "./build/esm/hello-world.d.ts"
    }
  }
}

Even if it involved two tsconfig.json files (e.g. tsconfig.json and tsconfig.commonjs.json, the latter of which extends the former and overrides a few options), being able to run tsc twice to generate working ESM and CJS would be useful. Namely, in this example, the emitted CJS would need .cjs file extensions and .cjs import specifiers since package.json declares "type": "module".

šŸ’» Use Cases

I think this is described above mostly.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:4
  • Comments:8 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
segevfinercommented, Sep 5, 2022

More modern though is using the new ā€œexportsā€ property in package.json to publish dual ESM/CommonJS packages. (For broadest support you might also want to specify, module and/or browser at the top, support varies quite wildly)

1reaction
AlCalzonecommented, Nov 12, 2021

@ide I know you wrote that you want to have a single tool (tsc) to take care of this but maybe https://github.com/AlCalzone/esm2cjs would solve your immediate problem. Its a small wrapper around esbuild I created when I had the same issue - letting tsc output ESM and somehow turn it into a hybrid ESM & CJS package.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Support Dual CommonJS/ES Module Packages with isolated ...
I understand that rollup explicitly is an "ES Module" bundler. However I can imagine that other library authors will want to ship dual...
Read more >
How to Create a Hybrid NPM Module for ESM and CommonJS.
Author your code in ES6, ES-Next or Typescript using import and export. From this base, you can import either ES modules or CommonJS...
Read more >
Hybrid npm packages (ESM and CommonJS) - 2ality
In this blog post, we look at npm packages that contain both ES modules and CommonJS modules.
Read more >
Publish ESM and CJS in a single package - Anthony Fu
A short tutorial of shipping both ESM and CJS dual formats in a single NPM package.
Read more >
Node Modules at War: Why CommonJS and ES ... - Code Red
ESM scripts can import CJS scripts, but only by using the ā€œdefault importā€ syntax import _ from 'lodash' , not the ā€œnamed importā€...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found