ts-node
ts-node copied to clipboard
Inconsistent behaviour with `moduleResolution: "node16"` and `swc: true` for CommonJS module output
I decided to file this issue here after looking at both the swc
and ts-node
issues - apologies if this is a better fit for swc
! I'm also fully expecting to be told that there is additional swc configuration required, in which case this issue could be reframed as "add additional info on configuring swc to https://typestrong.org/ts-node/docs/swc/".
Search Terms
moduleResolution
, swc
, CommonJS, ERR_REQUIRE_ESM
, import()
Expected Behavior
In a TypeScript project outputting CommonJS via "module": "CommonJS"
, ts-node
should compile and run files the same regardless of the swc
option. See below for an example where this surprisingly wasn't the case.
This came up when importing an ESM-only package into a TypeScript file targeting CommonJS. Node gives us the suggestion to Instead change the require of index.js in /Users/jbateson/code/ts-node-swc-module-resolution-repro/index.ts to a dynamic import() which is available in all CommonJS modules.
, but this doesn't seem to work with swc: true
.
Actual Behavior
Given the following tsconfig.json
:
{
"compilerOptions": {
"target": "es2021",
"module": "CommonJS",
"moduleResolution": "node16",
"esModuleInterop": true
}
}
the following code combining static ESM imports + dynamic imports of ESM-only code works fine:
import fs from 'fs';
(async function main() {
console.log('main');
// Import node-fetch dynamically as it's pure ESM, and our output target is CJS
// @ts-ignore
const fetch = await import('node-fetch');
console.log({ fetch, fs });
})();
but when adding
"ts-node": {
"swc": true
}
it outputs the following when invoked as ts-node index.ts
:
❯ ts-node index.ts
main
/Users/jbateson/code/ts-node-swc-module-resolution-repro/node_modules/ts-node/dist/index.js:851
return old(m, filename);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/jbateson/code/ts-node-swc-module-resolution-repro/node_modules/node-fetch/src/index.js from /Users/jbateson/code/ts-node-swc-module-resolution-repro/index.ts not supported.
Instead change the require of index.js in /Users/jbateson/code/ts-node-swc-module-resolution-repro/index.ts to a dynamic import() which is available in all CommonJS modules.
at Object.require.extensions.<computed> [as .js] (/Users/jbateson/code/ts-node-swc-module-resolution-repro/node_modules/ts-node/dist/index.js:851:20)
at /Users/jbateson/code/ts-node-swc-module-resolution-repro/index.ts:54:92
at async main (/Users/jbateson/code/ts-node-swc-module-resolution-repro/index.ts:54:19) {
code: 'ERR_REQUIRE_ESM'
}
which implies that swc
is transpiling the import()
differently from the default ts-node
/tsc
behaviour.
I've "verified" this (kinda) by looking at the swc repl, which turns import()
into require()
unconditionally when the target is CommonJS.
Steps to reproduce the problem
See above, also see a link to a runnable version of the same minimal repro below.
Minimal reproduction
Clone this repo, run yarn
to install dependencies, and then run ts-node index.ts
to reproduce the above: https://github.com/jdb8/ts-node-swc-module-resolution-repro
Commenting out swc: true
will show the expected behaviour.
Specifications
- ts-node version: v10.9.1
- node version: v16.16.0
- TypeScript version: v5.1.6
- tsconfig.json, if you're using one:
{
"compilerOptions": {
"target": "es2021",
"module": "CommonJS",
"moduleResolution": "node16",
"esModuleInterop": true
},
"ts-node": {
"swc": true
}
}
- package.json:
{
"name": "ts-node-swc-module-resolution-repro",
"version": "1.0.0",
"main": "index.js",
"author": "Joe Bateson <[email protected]>",
"license": "MIT",
"dependencies": {
"@swc/core": "^1.3.69",
"@swc/helpers": "^0.5.1",
"node-fetch": "^3.3.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"devDependencies": {
"@types/node": "^20.4.2"
}
}
- Operating system and version: mac osx 13.4
- If Windows, are you using WSL or WSL2?: n/a
Looking at the swc side of things, it seems like the "import() gets transpiled to require()" issue can be fixed by setting ignoreDynamic
in .swcrc
: https://swc.rs/docs/configuration/modules#ignoredynamic
But unfortunately due to https://github.com/TypeStrong/ts-node/issues/1856 it looks like that doesn't fix the issue when running the code via ts-node
.
Back here, looking at the internals of the swc transpiler, it seems like this case is accounted for: https://github.com/TypeStrong/ts-node/blob/47d4f45f35e824a2515e17383a563be7dba7d8ff/src/index.ts#L1469-L1474
But so far it's not obvious to me how to force ts-node
to enter the ignoreDynamic
codepath here: https://github.com/TypeStrong/ts-node/blob/47d4f45f35e824a2515e17383a563be7dba7d8ff/src/transpilers/swc.ts#L211
(i.e. a way to force ts-node
to recognise the nodeModuleEmitKind
as nodecjs
)
Final findings for today, which may indicate a fix for my project (and highlight my configuration mistakes):
- Looks like ts-node 10.8.0 added support for
module: 'node16'
to replacemodule: 'commonjs'
, which is also themodule
value set in the recommended base node 16 tsconfig - Switching from
commonjs
tonode16
for module (in addition to themoduleResolution
field which I'd already set atnode16
) seems to correctly trigger the codepath mentioned above - This also seems to be the recommended configuration for modern node esm/cjs interop (the alternative being
nodenext
which would presumably be for ESM -> CJS interop - the inverse)
My confusion was assuming that module: commonjs
was the recommended setting to emit commonjs, and I'd missed the fact that there was a separate "commonjs with ESM" flavour available.
I'm going to go back and update my non-toy project with these changes to see if that fixes things, but I assume it will. Assuming my understanding of the above is correct, this issue can probably be closed, but if possible it would be great if the docs could point to this behaviour somewhere, if others are using the stale commonjs
module target where they instead should be using node16
.
Happy to create a new issue for the doc updates if that's preferred!