TypeStrong/ts-node

ts-node cannot run mixed ESM/CJS project

m-ronchi opened this issue · 2 comments

Search Terms

ESM CJS mixed project
SyntaxError: Named export not found. The requested module is a CommonJS module, which may not support all module.exports as named exports.

Expected Behavior

ts-node works and prints BAR

Actual Behavior

$ npx tsc && node dist/test.mjs
BAR
$ npx ts-node --esm src/test.mts
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".mts" for /***/ts-node-bug/src/test.mts
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:160:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:203:36)
    at defaultLoad (node:internal/modules/esm/load:143:22)
    at async nextLoad (node:internal/modules/esm/hooks:865:22)
    at async nextLoad (node:internal/modules/esm/hooks:865:22)
    at async Hooks.load (node:internal/modules/esm/hooks:448:20)
    at async MessagePort.handleMessage (node:internal/modules/esm/worker:196:18) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
$ node --loader ts-node/esm src/test.mjs
(node:65265) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
file:///****/ts-node-bug/src/test.mts:1
import { foo } from "./lib.js";
         ^^^
SyntaxError: Named export 'foo' not found. The requested module './lib.js' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from './lib.js';
const { foo } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:132:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:214:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:28:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)

Node.js v20.11.1
$ node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));' src/test.mts                   cluster: prod
file:///***/ts-node-bug/src/test.mts:1
import { foo } from "./lib.js";
         ^^^
SyntaxError: Named export 'foo' not found. The requested module './lib.js' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from './lib.js';
const { foo } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:132:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:214:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:28:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)

Node.js v20.11.1

Steps to reproduce the problem

run this:
ts-node-bug.zip

Minimal reproduction

lib.ts (inferred as CommonJS module)

export const foo = "BAR"

test.mts (ESM)

import { foo } from "./lib.js";

console.log(foo);

Specifications

ts-node v10.9.2
node v20.11.1
compiler v5.4.2

  • tsconfig.json, if you're using one:
{
  "compilerOptions": {
    "lib": [ "es2023" ],
    "module": "node16",
    "target": "es2022",

    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node16",
    "noEmit": false,
    "outDir": "dist/",
    "sourceMap": true,
    "strictNullChecks": true,
  },
  "include": [
    "src"
  ]
}

  • package.json:
{
  "name": "ts-node-bug",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^20.11.25",
    "ts-node": "^10.9.2"
  }
}

  • Operating system and version: macos
  • If Windows, are you using WSL or WSL2?:

I wanted to create a new issue targeting the same problem but then I found this one so I am commenting here. See this stackblitz.

// main.cts
(async () => {
  const dep = await import('./dep.mjs');
  console.log(dep);
})()
// dep.mts
export const dep = 'dependency';

I expected ts-node main.cts and tsc && node dist/main.cjs to behave the same but I get this error

Cannot find module '[..]/dep.mjs' imported from [..]/main.cts

What do I need to do to make this run with ts-node?
Edit: ts-node-esm main.cts works for me.

It appears this broke in Node 18.19 and versions released since have the issue.

There have been multiple issues tracking facets of it, in multiple repositories (node, typescript, ts-node, esbuild, tsx among others), over the last few months, but no resolution. In #2094 the common strategy is to work around the issue - either downgrade Node to 18.18, or use whatever alternative works in your scenario, for example tsx (in which the same issue is half-fixed). None of those workarounds represent an actual fix.

The scenario is very simple: it happens in mixed CJS/ESM repositories, using TS in development with tooling that isn't always new, and inevitably some dependencies that don't support a global switch to modules in package.json. Node and some tooling (tsx) defaults to CJS in the absence of type=module in package.json, and so import-s are now broken, especially in an .mjs file importing .ts, e.g. import { named } from './tsfile' (treated as CJS). The CJS-compatible import('./file').then(...) pattern works in the forced CJS context. Or you could rename your .ts file to .mts and hope that the ESM system wakes up.

I'd love if ts-node had updates about this, but I'm already having to test with alternatives. 🤷