Helping author dual CommonJS/ES module packages
See original GitHub issueSuggestion
š 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:
- Created 2 years ago
- Reactions:4
- Comments:8 (3 by maintainers)
Top GitHub Comments
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/orbrowser
at the top, support varies quite wildly)@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.