Open bluelovers opened 2 years ago
What are exact use cases and what problem it should resolve? From first look TS would also need to rewrite extension in generated imports that in no go at this moment #49083 (if I haven't missed anything).
Major use-case is building dual-package with tsc
without any postbuild script, I think.
You shouldn't be dual building packages anyways (use a wrapper), so I prefer the .cts
/.mts
constraint that TS indirectly enforces.
i don't wanna make files of .cts and .mts they are same context so i wanna one .ts can be .cjs and .mjs
I've met same issue when I building dual package.
e.g. https://github.com/azu/check-ends-with-period/tree/v2.0.0 (It is invalid example as dual package)
TypeScript source code is insrc/*.ts
and package.json
has "type": "module"
field.
Also, this repository has two tsconfig files.
tsconfig.json
: generates esm to module/*.js
from src/*.ts
tsconfig.cjs.json
: generates cjs to lib/*.js
from src/*.ts
I've defined exports
field as follows, but this package was treated as ESM because "type": "module"
is defined.
As a result, This package can not be requred from CJS without dynamic import.
(Node.js treats *.js
file as ESM by "type": "module"
)
"exports": {
".": {
"require": "./lib/check-ends-with-period.js",
"import": "./module/check-ends-with-period.js"
}
},
I could not found just works solution without using bundler/post scripts.
If targetExtension
option exists, I can resolve this issue by tsconfig file.
tsconfig.json
+ "targetExtension": ".mjs"
,: generates esm to module/*.mjs
from src/*.ts
tsconfig.cjs.json
+ "targetExtension": ".cjs"
,: generates cjs to lib/*.cjs
from src/*.ts
However, this option will need to rewrite import path of source code. It oppsite TypeScript's design goal.
...
Edit(2023-01-14): I've created tsconfig-to-dual-package for avoiding this issue.
This tool add package.json which is { "type": "module" }
or { "type": "commonjs" }
based on tsconfig's module and outDir option.
In other words, publish *.js
as CJS and ESM in a single package.
This mechanism based on following:
Might I suggest packemon: https://packemon.dev/
Also, why exactly are you dual building? You run the risk of the dual package hazard: https://nodejs.org/api/packages.html#dual-package-hazard It's better to use an ES module wrapper.
i think this only work on nodejs does browser support it?
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;
Browsers don't support .cjs/.mjs natively, unless it gets bundled through webpack or a similar tool to .js, and at that point, why even use .cjs/.mjs for browsers?
The idea is to use native modules when targeting browsers without any transpilation/bundling, and optionally CommonJS when targeting Node.
Yes of course, but not if you're using .mjs. At least in @azu's example, their ESM code should be shipped to the browser with .js, and CJS code to Node.js with .cjs (or even just .js too).
We also just need more information, as we're making many assumptions here. The original post doesn't contain much.
Wouldn't we be emitting .cjs
+ .js
(+ type: module) when building dual-package purely for Node anyway? It's usually unnecessary to have explicit extensions for both sets of module types. Also, browsers can handle .mjs as well, as long as the MIME type is JavaScript. But to author dual-package of any kind, whether targeting Node or browser, we need at least one subset of the output to have an extension different from the other, and that would require TS to be configurable about this. But I agree we lack some context here.
I'm pro @bluelovers's suggestion. The use case for me is publishing dual ESM/CommonJS packages for nodejs.
There are many reasons why we need to publish dual ESM/CommonJS packages. For instance, not until 4.7, TS doesn't even allow a node application in commonjs to consume a pure ESM library due to the lack of support on await import(...)
. So I really find it annoying some package authors publish pure ESM packages.
@milesj the use of an ESM wrapper around CommonJS defeats tree-shaking, doesn't it? Given ESM has broken the whole ecosystem to try and achieve results like tree-shaking, having the recommended way to align with ESM being to throw away its core features is disappointing. Correct me if there is a reasonable way to get both. My expectation is to follow the principle that modules should be stateless - a good practice I don't ever find the need to violate. Then I understand there are no concerns with dual building.
If you have a dual package, and some other package in CJS context requires your package, and another package in ESM context imports your package, you'll end up with 2 copies of your package. For node this doesn't matter too much unless there's some kind of global/shared state, but for bundlers this is bad.
I've met same issue when I building dual package.
e.g. https://github.com/azu/check-ends-with-period/tree/v2.0.0 (It is invalid example as dual package) TypeScript source code is in
src/*.ts
andpackage.json
has"type": "module"
field. Also, this repository has two tsconfig files.
tsconfig.json
: generates esm tomodule/*.js
fromsrc/*.ts
tsconfig.cjs.json
: generates cjs tolib/*.js
fromsrc/*.ts
I've defined
exports
field as follows, but this package was treated as ESM because"type": "module"
is defined. As a result, This package can not be requred from CJS without dynamic import. (Node.js treats*.js
file as ESM by"type": "module"
)"exports": { ".": { "require": "./lib/check-ends-with-period.js", "import": "./module/check-ends-with-period.js" } },
I could not found just works solution without using bundler/post scripts.
If
targetExtension
option exists, I can resolve this issue by tsconfig file.
tsconfig.json
+"targetExtension": ".mjs"
,: generates esm tomodule/*.mjs
fromsrc/*.ts
tsconfig.cjs.json
+"targetExtension": ".cjs"
,: generates cjs tolib/*.cjs
fromsrc/*.ts
However, this option will need to rewrite import path of source code. It oppsite TypeScript's design goal.
@azu @milesj It looks like that tsc-multi might be worth exploring cc @tommy351 https://github.com/microsoft/TypeScript/issues/18442#issuecomment-1369110873
I've created tsconfig-to-dual-package for avoiding my issue that is described in https://github.com/microsoft/TypeScript/issues/49462#issuecomment-1152860276
This tool add package.json which is { "type": "module" }
or { "type": "commonjs" }
based on tsconfig's module
and outDir
options.
It resolve my issue by adding each type pacakge.json
to lib/
(CJS) and module/
(ESM) instead of use .cjs
and .mjs
This behavior is described in following:
In other words, Both(CJS and ESM) are *.js
.
The result is much closer to what I wanted to do.
$ tsc -p . && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# \ I want to remove it!
This approch pros is that no require addtional build/transpiler tool(no modify output code of tsc
).
Cons is that need to copy package.json
to outDir.
(Of course, dual package hazard is in here, but this hazard is also in browser's iframe/realm or multiple versions of the same library. Not ideal, I believe the actual harm is limited.)
📝 Note
TypeScript may change the moduleResolution, but did not likely change the output.
I feel like there is confusion everywhere about ESM support. Therefore, I thought that this is not an issue of TypeScript configuration, but rather an ecosystem-wide issue that needs to move forward.
I've created tsconfig-to-dual-package for avoiding my issue that is described in #49462 (comment) This tool add package.json which is
{ "type": "module" }
or{ "type": "commonjs" }
based on tsconfig'smodule
andoutDir
options. It resolve my issue by adding each typepacakge.json
tolib/
(CJS) andmodule/
(ESM) instead of use.cjs
and.mjs
This behavior is described in following:
- Improve documentation on Dual Module Packages nodejs/node#34515 (comment)
- https://nodejs.org/api/packages.html#type
In other words, Both(CJS and ESM) are
*.js
. The result is much closer to what I wanted to do.$ tsc -p . && tsc -p ./tsconfig.cjs.json && tsconfig-to-dual-package # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # \ I want to remove it!
This approch pros is that no require addtional build/transpiler tool(no modify output code of
tsc
). Cons is that need to copypackage.json
to outDir. (Of course, dual package hazard is in here, but this hazard is also in browser's iframe/realm or multiple versions of the same library. Not ideal, I believe the actual harm is limited.)📝 Note
TypeScript may change the moduleResolution, but did not likely change the output.
I feel like there is confusion everywhere about ESM support. Therefore, I thought that this is not an issue of TypeScript configuration, but rather an ecosystem-wide issue that needs to move forward.
@mobsense @jwalton @owenallenaz
I don't get the "you shouldn't be dual building" opinion, clearly there's a lot of fragmentation in the ecosystem currently and while everyone's moving in the direction of using ESM, there's still a lot of CJS projects and that's not going to change overnight. Because of this reason if you're building a library, it's important to output both in CJS and MJS.
Additionally, "type": "module"
and "moduleResolution": "nodenext"/"node16"
are the future, and in a project with these set any CJS build results outputted with a .js extension simply won't work.
Thus, it makes perfect sense to me that instead of outputting results with a vague ".js" extension which causes issues and headaches, you'd want to have explicit .cjs and .mjs extensions for CommonJS and ESModule modules respectively. On top of that, outputted declaration files should have d.cjs and .d.mjs extensions respectively, cause another issue I've ran into multiple times recently is library authors outputting results in .mjs and .cjs, but leaving the declaration files with the .d.ts extension which TypeScript then won't be able to pick up on.
What are people currently using to work around this? Separate build steps with manual file extension rewriting?
I believe that Dual Package can be achieved broadly as follows.
I stumbled upon this issue recently while wanting to build a dual package using babel and typescript. This file extension proliferation in the JS ecosystem is, needless to say, frustrating.
I just started a project babel-dual-package
that takes "type": "module"
packages and creates separate ESM and CJS builds with file and import/export extensions correctly updated. This includes declaration .d.ts
files as well. All you need to do is add your exports
field in package.json to match the build output. If you use babel and typescript together it might be helpful.
Running babel-dual-package --out-dir dist --extensions .ts,.mts,.cts src
will get you an ESM build in dist
and a CJS build in dist/cjs
. Then define your exports
accordingly.
What we ended up doing is simply... not supporting ESM. In my eyes, the ESM ecosystem is simply not robust enough and we were spending more time trying to fight through it than actually solving real problems. Maybe once the tooling is there, but it's simply not. I want to be able to import using barrels like import Foo from "./Foo"
where that references a file that is /foo/index.ts or /foo/index.tsx. It works in typescript natively, but when we compile for ESM it's busted. If developers want us to use ESM it needs to be ESM and no mandate that your entire toolset be built in ESM. If TS only worked with TS, it never would have got off the ground.
To add my two cents, regardless of whether you want to emit dual variants, module: "commonjs"
should output files with either a cjs
extension, or a js
extension.
At the moment, if you write your source code as mts
, the compiler emits commonjs code but in mjs
files which to me seems incorrect and broken. I don't think there should be a question of "why would you want to do that" - the compiler offers a module
option, and so the options should output spec-compliant code, or should produce an error if the inputs don't comply with the config or output format.
If you want to update your ESM/CJS specifiers pre/post build, check out @knighted/specifier. It will parse a file and update specifiers using a provided callback or regex map. Then write the updated source to disk using whatever file extension you want.
You can also try @knighted/duel (which uses @knighted/specifier) to easily create a dual CJS build. Here's an example repo, which is using the default args of -p tsconfig.json -x .cjs
, so the build command as an npm run script amounts to duel
.
The fact that .mts
files are always converted to the CJS module system anytime --module commonjs
is used (despite the --moduleResolution
used, or the type
defined in package.json) needs to be addressed first. This breaks things, and makes building dual packages with tsc
exclusively nearly impossible (ok, probably strictly impossible). This is clearly antithetical to how Node determines module systems.
See microsoft/TypeScript#54573 and all the related issues mentioned in this comment.
The file extension nonsense is annoying, but that's how we let the two module systems that Node currently supports coexist.
Today I also had the need for compiling .ts
files to javascript files with a different extension. Instead of the proposed "targetExtension": ".cjs"
, or "targetExtension": ".mjs"
I suggest something like "ensureQualifiedExtension" : "true"
that behaves like this:
"module": "commonjs"
then the extension is always .cjs
;"module": "es6"
, or es2015
, es2020
, es2022
, esnext
then the extension is always .mjs
;"module": "node16"
or nodenext
then the extension respects the rules for module detection as documented and the final extension can be either .cjs
or .mjs
.I need this nowadays, but we may not need it in the future, so keep it silence. Let time forget it.
Suggestion
🔍 Search Terms
List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.
✅ Viability Checklist
My suggestion meets these guidelines:
⭐ Suggestion
add something like
📃 Motivating Example
targetExtension
is.cjs
, all.ts
will emit as.cjs
, but.mts
still is emit as.mjs
targetExtension
is.mjs
, all.ts
will emit as.mjs
, but.cts
still is emit as.cjs
💻 Use Cases