Repository: TypeScript
Issue: Helping author dual CommonJS/ES module packages
Labels: Discussion

# Suggestion

## 🔍 Search Terms

Dual CommonJS / CJS + ES module / ESM packages

## ✅ Viability Checklist

My suggestion meets these guidelines:

* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] 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](https://github.com/Microsoft/TypeScript/wiki/TypeScript-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](https://nodejs.org/api/packages.html#dual-commonjses-module-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**
```ts
import b from './b.js';
export default function helloWorld() { return `${b()} world`; }
```
**hello.ts**
```ts
export default function hello() { return `hello`; }
```

The author wants to publish the package with both CJS and ESM:
**package.json**
```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.