ts-node: `ts-node` fails when ES Modules are in the dependency graph in Node.js 13+

I basically detailed the issue in this comment: https://github.com/TypeStrong/ts-node/issues/155#issuecomment-570120923

It’s a chicken-and-egg-like problem:

  • If we use module: 'commonjs', then if any TS files import ES Modules (indirectly in their dependency graph), then Node throws an error because CommonJS modules can not import ES Modules.
  • If we change to module: 'esnext', then the errors from the previous point go away, but now the .js file that is loading ts-node and calling something like require('typescript-entry-point.ts') will have a similar issue, because the call to require('typescript-entry-point.ts') will try to load an ES Module.
  • Finally, if we convert the .js file into an ES Module, we can not convert require('typescript-entry-point.ts') into import 'typescript-entry-point.ts' because now ES Modules don’t handle .ts extensions (at least not out of the box, and it seems the old require hooks don’t operate on these new identifiers)

At the moment, I’m sort of stuck, because I have dependencies in my dependency tree that are ES Modules.

The only workaround I can think of is to compile everything to .js files (ES Modules) and avoid to use ts-node.

I wonder if a combination of allowJs and ignore so that it compiles JS files would help. I haven’t tried that yet.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 139
  • Comments: 80 (4 by maintainers)

Commits related to this issue

Most upvoted comments

It seems that ts-node fails to run with "module": "esnext" in the tsconfig.json and "type": "module" in the package.json with the following error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/user/test-project/index.ts
    at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
    at Loader.resolve (internal/modules/esm/loader.js:98:42)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at Loader.getModuleJob (internal/modules/esm/loader.js:188:29)
    at Loader.import (internal/modules/esm/loader.js:163:17)

Sorry to say this but the way ESM was added to Node is a total disaster and many thousands of hours wasted for developers worldwide. 😕

If you’re still facing the issue with module ESNext, this saved my day:

If you must keep “module”: “ESNext” for tsc, webpack, or another build tool, you can set an override for ts-node.

{
  "compilerOptions": {
    "module": "ESNext"
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}

Source: https://www.npmjs.com/package/ts-node?activeTab=readme#commonjs

It is amazing that 3 years after this was reported I am still facing this issue with no solutions tried would solve but hours spent and wasted.

Just add module: commonjs in tsconfig.json and remove type: moudle in package.json works for me.

tsconfig.node.json

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node"
  }
}

package.json

{
  "scripts": {
    "serve": "ts-node --project tsconfig.node.json server/index.ts"
  },
  "devDependencies": {
    "@types/node": "^18.0.0",
    "ts-node": "^10.8.1",
    "typescript": "^4.7.4"
  }
}

server/index.ts

import * as http from 'http'
import type { IncomingMessage, ServerResponse } from 'http';

const requestListener = function (request: IncomingMessage, response: ServerResponse) {
  response.writeHead(200);
  response.end('Hello, World!');
}

const server = http.createServer(requestListener);
server.listen(8080);

console.log('Server running at http://localhost:8080/');

If anyone is coming to this issue like I did I was able to get TS working inside a project with "type": "module" through nodemon by adding a nodemon.json with this in it:

{
  "execMap": {
    "ts": "node --loader ts-node/esm"
  }
}

Essentially replacing the default "execMap" of "ts": "ts-node" … in case it matters my tsconfig.json like:

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true,
    "noEmit": true,
    "strict": true,
    "lib": [
      "es2020"
    ]
  }
}

With that my "dev" script in package.json is just "nodemon src/index.ts" and it all seems to work!

Well, looks like @K-FOSS/TS-ESNode does work with Node 13.7. That’s amazing. I’ll get to work on porting this directly into ts-node hopefully sometime this weekend. Work has been busy, not enough time for personal coding.

I’m closing this issue because the feature has been implemented, and feedback is tracked by #1007.

same issues occurring right now

@menangaTech I was trying to use formadata-node, which in an ESM-only package. Here’s a snippet that causes problems:

import { FormData } from "formdata-node"
const form = new FormData()

I fixed it by having "type":"module" in my package.json file and the following tsconfig.json:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "nodenext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "ts-node": {
    "esm": true
  }
}

The important parts are the module and ts-node settings

@meabed are you sure it’s not working? I just tried and and it works correctly (must use node v14.3.0+ with tla flag)

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "module": "ESNext"
  }
}

package.json

{
  "scripts": {
    "start": "node --loader ts-node/esm.mjs --experimental-top-level-await index.ts"
  },
  "dependencies": {
    "@types/node": "^14.0.13",
    "ts-node": "^8.10.2",
    "typescript": "^3.9.5"
  },
  "type": "module"
}

index.ts

import { promisify } from "util";

const sleep = promisify(setTimeout);

console.log("start");
await sleep(2000);
console.log("finish");

Any update? “type”: “module” in package.json still failing

You better stop using ts-node and start using tsx unless you really need to use the emit decorators metadata option of typescript if you’re using something like TypeORM (you can still use tsx though).

Same here:

tsconfig.json -> compilerOptions:

"target": "es2017",
"module": "esnext"

package.json:

"type": "module"

Reason for the entries above:

Top-level ‘await’ expressions are only allowed when the ‘module’ option is set to ‘esnext’ or ‘system’, and the ‘target’ option is set to ‘es2017’ or higher. ts(1378)

@cspotcode thanks for a voice of reason 😃 I think the blame is spread. Firstly, I think Node made it look like ESM is ready for prime time and then some library authors jumped the gun by deprecating Common.JS builds. More reasonable library authors ship both ESM and Common.JS.

Not sure if this was a general expression of frustration, or was directed specifically at the nodejs team.

If the former, I tend to think the blame also rests largely on the developer community for moving libraries to ESM before it was ready.

If the latter, I’m not a part of the node team, but you can reach them here:

https://github.com/nodejs/node/discussions https://github.com/nodejs/loaders

Same error, none of the above solutions worked.

typescript: 4.9.5 ts-node: 10.9.1

Yes, this issue happened right after I added “type”: “module” to the package.json in an attempt to not create the .mjs files. I am on node 14

Any update on this? ts-node will always fail when the "type": "module" is in package.json

I was trying to setup this typescript for apollo-server and I was getting the same error, this is how my tsconfig.json file end up:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "lib": ["es2020"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "sourceMap": true
  },
  "ts-node": {
    "esm": true
  }
}

For some people who get to this point, if you add to your tsconfig.json, it allows the .js files to resolve.

{
  "ts-node": {
    "experimentalResolver": true
  }
}

file.ts

export default function foo() {}

another.ts

import foo from "./file.js";

This is released as an experimental feature in ts-node v8.10.0. Please test and share your feedback in #1007.

You will probably want to enable “transpileOnly” in your tsconfig for better performance. However, we do support typechecking if you want that.

I’d love to give that a shot. I’ll take a crack at it sometime this weekend.

@KristianFJones If you’re interested I’d love to land a PR in ts-node with this functionality.

I created a proof of concept for using the current version of the --experimental-loader feature to transpile typescript files right before node loads it and then give node the now ESNext Javascript code with the SyntheticModule VM feature. It’s fairly hacky right now but it made for a fun 6 hour session: https://github.com/KristianFJones/TS-ES-Node

Please, can somebody explain for dummy how to use ts-node imports. Whatever combination I’m trying to use resulting in some errors either Cannot use import statement outside a module or any other import problem…

Trivial setup: package.json

{
  "name": "items",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "ts-node app/translate.ts"
  },
  "dependencies": {
  },
  "devDependencies": {
    "sass": "^1.63.2",
    "ts-node": "^10.9.1",
    "@types/node": "20.2.5",
    "globby": "^13.1.4",
    "typescript": "5.1.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "lib": ["es2020"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "sourceMap": true
  },
  "include": ["app/*.ts"],
  "exclude": ["node_modules"],
  "ts-node": {
    "esm": true
  }
}

code app/translate.ts

import sass from "sass"
async function main() {
  const style = await sass.compileAsync('template/style.scss')
  console.log(style)
}
Promise.all([main()])

Error

> ts-node app/translate.ts

(node:27364) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
E:\Development\Items\node_modules\sass\sass.node.js:1
import * as util from "util"
^^^^^^

SyntaxError: Cannot use import statement outside a module

Will really appreciate for help.

This issue still happens :

try gulpfile.ts

#!/usr/bin/env node
import {globby} from "globby";

export default async () => {
    console.log(globby(["./lib/**/*.js"]));
};

tsconfig.json

{
    "ts-node": {
        "compilerOptions": {
            "module": "CommonJS"
        }
    },
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "declaration": true,
        "outDir": "./lib",
        "strict": true,
        "baseUrl": "./src"
    },
    "include": ["src"],
    "exclude": ["node_modules", "**/__tests__/*"]
}

That globby package has

	"type": "module",

in its package.json.

And importing it cause

[21:40:40] Requiring external module ts-node/register
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Volumes/HDD/Users/steve/IdeaProjects/yoohoo-js/gulpfile.ts
    at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
    at Loader.getFormat (internal/modules/esm/loader.js:104:42)
    at Loader.getModuleJob (internal/modules/esm/loader.js:242:31)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async Loader.import (internal/modules/esm/loader.js:176:17) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

If I don’t import it, I get no errors.

@cspotcode Thanks for your feedback 😃. microbundle which uses autoprefixer which uses colorette works with plain node. The error only happens if ts-node is involved, so to me it looks like ts-node obviously has to be “involved” ^^.

This is the trace: at Module._extensions…js (internal/modules/cjs/loader.js:1217:13) at Object.require.extensions.<computed> [as .js] (XYZ/node_modules/ts-node/src/index.ts:851:44) at Module.load (internal/modules/cjs/loader.js:1050:32) at Function.Module._load (internal/modules/cjs/loader.js:938:14) at Module.require (internal/modules/cjs/loader.js:1090:19) at require (internal/modules/cjs/helpers.js:75:18) at Object.<anonymous> (XYZ/node_modules/autoprefixer/lib/autoprefixer.js:5:17) at Module._compile (internal/modules/cjs/loader.js:1201:30) at Module._extensions…js (internal/modules/cjs/loader.js:1221:10) at Object.require.extensions.<computed> [as .js] (XYZ/node_modules/ts-node/src/index.ts:851:44)

Look’s like I know what I’m doing this weekend.

@achlubek Almost always when I hit this message if you look very carefully at the error you receive it will allow you to track down which specific module is causing the problem.

Yeah I managed to track it down to Chai v5 becoming ESM only. Too bad eh!

I was trying to setup this typescript for apollo-server and I was getting the same error, this is how my tsconfig.json file end up:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "lib": ["es2020"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "sourceMap": true
  },
  "ts-node": {
    "esm": true
  }
}

Thanks this actually worked @erick2014

still having the same issue. @mysterycommand’s solution doesnt work for me either. trying the same thing as the original author of this thread. Attempting to import a library with es6 modules in a ts project and trying to run it with ts-node <path-to-entry-point.ts> my tsconfig.json is as follows

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "lib": [
      "es2020"
    ],

    "allowJs": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "composite": true,
    "strict": true,
    "moduleResolution": "node",
    "rootDirs": [
      "."
    ],

    "esModuleInterop": true,
   
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "references": [],
  "watchOptions": {
    "fallbackPolling": "dynamicPriority",
    "excludeDirectories": [
      "**/node_modules",
      "build"
    ]
  }
}```

@katywings Thanks for the details. Do you have allowJs turned on? What’s the node version? Can you send a minimal reproducible example?

Colorette declares that require() should be loading index.cjs, not index.js. https://unpkg.com/browse/colorette@1.2.1/package.json

But the stack trace shows that it’s trying to load index.js.

That leads me to suspect a node bug that manifests when a custom require.extensions['.js'] handler is installed. But without a reproduction, there are too many unknowns: I don’t know the versions of libraries or of node and I don’t know the ts-node nor ts configurations.

I think your issue is subtley different than the ones described in this ticket, because ESM should not come into play. Colorette is telling node that it can be loaded as CommonJS but somehow that’s being ignored.

Feel free to file this as a separate ticket if you want.

@meabed that is also explained in the “Usage” section of #1007 and in node’s ESM documentation. Remember that a lot of things are controlled by node and TypeScript. At the end of the day, we’re using TypeScript to convert your code into JS that’s run by node, and we need to remain compatible with tsc && node. A great way to answer these questions for yourself is to try using tsc && node and see what is and is not allowed.

Thanks @cspotcode - it works perfectly fine 😃 Thanks alot! the missing part was importing the file with .js extension

import {car} from './test-export.js';

thats awesome it works 😃 a follow up question - do you think it would be doable without the extension import soon 👍 just trying to see should we all imports to .js to use the top level await or we hold a bit if its coming soon. Best regards.

@meabed This is a problem with your code. The solution is explained in the “Usage” section of the documentation here: #1007. Please read it fully, because it also explains some other details you need to be aware of.

yea please - this need a quick fix - no one can use top-level-await with ts-node 😦

Removed the globby requirement. Module has been published on NPM https://www.npmjs.com/package/@k-foss/ts-esnode zero dependencies other then peer dependency on TypeScript. NPM reports unpacked size of 8.66 kB. Should hopefully be a drop in replacement for TS-Node for now.

Okay, made a few small changes and fixes. It’s now able to handle external modules that have the TSLib import helper without requiring refactoring existing imports. The TS-ESNode loader hook can be dropped into existing projects and just work. Hopefully! I just tested it on my main Application template that uses TypeORM & TypeGraphQL with tsconfig.json set to use ESNext as target and modules and it all just works. Transpiled code is all using imports and exports with Node V14 in Module mode.

Been busy, here is a proof of concept using the transformSource hook. Supports relative imports of .ts and .tsx without requiring extensions. https://github.com/K-FOSS/TS-ESNode