moduleResolution node16 conflicts with type "module" packages that export commonjs
markw65 opened this issue ยท 9 comments
Bug Report
I have a package with "type": "module"
which uses the "exports" field to indicate how to use it via require:
"type": "module",
"exports": {
".": {
"types": "./build/index.d.ts",
"require": "./build/index.cjs",
"import": "./build/index.cjs"
}
},
and I have a project targeting commonjs
that wants to use it. In order to do so, I have to set "moduleResolution":"node16"
(or nodenext
) in my project (the real package has multiple exports, so I can't just fall back to "main" and "types").
But now I get an error:
src/index.ts:1:21 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("package")' call instead.
To convert this file to an ECMAScript module, change its file extension to '.mts' or create a local package.json file with `{ "type": "module" }`.
even though the package explicitly exports a require friendly version.
In addition, if index.d.ts
in the package imports from another file eg:
import { foo } from "./util";
I get another error:
../package/build/index.d.ts:1:21 - error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './util.js'?
1 import { foo } from "./util";
I sort of get why this happens; but the .d.ts was generated by tsc in the first place, and is solely for tsc's consumption. And since I'm generating commonjs from a commonjs export, it seems like this really shouldn't apply.
If I change the type of the package to "commonjs" all the issues go away. But I don't necessarily have control over the package, and even if I do, it seems reasonable that I should be able to develop it as a module (ie have .js files treated as esm rather than commonjs) while providing suitable exports for both commonjs and esm. And I should add that this actually works as far as node is concerned.
๐ Search Terms
moduleresolution node16 commonjs
๐ Version & Regression Information
4.7 (when ts first added support for "exports" in package files).
- This is the behavior in every version I tried, from 4.7 through
@next
and I reviewed the FAQ for entries aboutmoduleResolution
โฏ Playground Link
I don't think its possible to setup the playground or workbench to demonstrate this, but here's a tiny project:
git clone https://github.com/markw65/moduleresolution-bug
cd moduleresolution-bug/package
npm install
npx tsc
cd ../project
npm install
npx tsc
results in
../package/build/index.d.ts:1:21 - error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './util.js'?
1 import { foo } from "./util";
~~~~~~~~
src/index.ts:1:21 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("package")' call instead.
To convert this file to an ECMAScript module, change its file extension to '.mts' or create a local package.json file with `{ "type": "module" }`.
1 import { baz } from "package";
๐ป Code
This is less of a code issue, and more related to the whole environment
๐ Actual behavior
tsc reports an error saying that commonjs code can't require a esmodule
๐ Expected behavior
tsc should recognize that the "exports" field explicitly exports a commonjs entry point, and use that.
Your entrypoints are .cjs
files - correspondingly, your declaration files should be .d.cts
files. Whatever transform you're doing on your js to make it into cjs, you need to do a corresponding transform on the declaration file.
Basically, the minimal edit to make this build is:
- Rename
package/build/index.d.ts
topackage/build/index.d.cts
Going above and beyond, you can write a nested export map, something like
"exports": {
".": {
"require": { "types": "./build/index.d.cts", "default": "./build/index.cjs"},
"import": { "types": "./build/index.d.ts", "default": "./build/index.js"},
}
},
though that isn't necessary in this case.
We're working on a new round of module docs and "How to ship a dual-mode package" will definitely be a topic there
Thanks - that makes sense.
But now I'm puzzled as to why tsc is producing .cjs files and .d.ts files from a single build? rather than .cjs and .d.cts files?
Whatever transform you're doing on your js to make it into cjs, you need to do a corresponding transform on the declaration file
That's the point. I'm not doing a transform, I'm just running tsc in the package directory. And it produces .cjs files, and .d.ts files. Which seems kind of odd? Shouldn't it be consistent?
You can see this in the referenced project. Here's the tsconfig for easy access. Is there some other setting to control the declaration files?
{
"compilerOptions": {
"baseUrl": "./",
"outDir": "build",
"module": "commonjs",
"target": "esnext",
"lib": ["esnext"],
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"declaration": true,
},
"include": ["src/**/*.ts"],
}
What you're doing now is writing ES-style imports and producing a CJS output, which really should have never been supported by TS in the first place, but here we are. Writing dual-mode packages requires a bit more precision.
What you "should" be doing is writing either 100% ES or 100% CJS, and at the last step transmogrifying the artifacts (note: not a functionality TS itself provides) into their other-module-system versions. For a package without any dependencies this can be done with some clever regexes + copying; for a package with dependencies it's much more complex, or even possibly impossible (e.g. if one of your dependencies doesn't provide the correct set of equivalent exports).
#52086 has some context and discussion (linking mostly for my own findability benefit)
I'm going to fold this into #52593
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
But now I'm puzzled as to why tsc is producing .cjs files and .d.ts files from a single build?
This would be news to me, and a definite bug IMO. Normally people only get themselves into situations like this by using external tools or hand-writing declaration files. But your example project doesnโt show any .cts
files. If you can give an example that shows tsc
compiling a .cts
file (or any input file, for that matter) into a .cjs
and .d.ts
file, please open a new bug specific to that behavior. Thanks!
Sorry, you're right. As I cut my project down to produce the test case, I failed to notice that I had (long ago) added a step to rename the output .js files to .cjs - but had never needed the same for the .d.ts files (since I had moduleResolution:node
in the consuming project.
So yes, the package/build directory in my sample project does indeed end up with .js and .d.ts files; not .cjs and .d.ts files.