Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ts-node doesn't run ESM modules as expected, either refusingimport statements in ts file or not being able to run .ts files #2086

Open
GovindarajanNagarajan-TomTom opened this issue Nov 16, 2023 · 11 comments

Comments

@GovindarajanNagarajan-TomTom

Search Terms

ESM import paths

Expected Behavior

I expect ts-node to be able to run typescript code using ESM module imports. However , it either fails with

SyntaxError: Cannot use import statement outside a module  

or after adding a "type": "module" to package.json, fails with

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/MY_PROJECT_DIR/workers/master.ts

The documentation is rather insufficient on how ts-node operations with tsconfig.json and the type value in package.json.
Of all of target, module and moduleResolution in tsconfig.json:compilerOptions, what should be the first value that is set (as in, does target determine module and moduleResolution's values, or vice versa ?)

Please provide some guidance on how this works.

Actual Behavior

Steps to reproduce the problem

This file creates basically parses the env variables and then creates a bunch of worker processes.

run with npx ts-node workers/master.ts

// workers/master.ts
import Redis from 'ioredis';
import { fork } from 'child_process';
import dotenv from 'dotenv';

import { Worker } from './worker';
import { isValid } from './../shared/lib/utils';

dotenv.config();
const streamNames: string[] = ["stream-1", "stream-2"];
const numWorkersPerStream: number = 5;
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = isValid(process.env.REDIS_PORT) ? parseInt(process.env.REDIS_PORT!, 10) : null;

if (!isValid(REDIS_HOST)) {
  console.error('REDIS_HOST is not a valid environmental variable');
  process.exit(1);
}

if (!isValid(REDIS_PORT)) {
  console.error('REDIS_PORT is not a valid environmental variable');
  process.exit(1);
}


const redisClient = new Redis({
  host: REDIS_HOST!,
  port: REDIS_PORT!,
});

async function createConsumerGroupIfNotExists(streamName: string, consumerGroup: string) {
  try {
    await redisClient.xgroup('CREATE', streamName, consumerGroup, '$', 'MKSTREAM');
    console.log(`Consumer group "${consumerGroup}" created (if not already exists) for stream "${streamName}"`);
  } catch (error: any) {
    if (!error.message.includes('BUSYGROUP Consumer Group name already exists')) {
      console.error('Error creating consumer group:', error);
    }
  }
}

// Create consumer groups only once
for (let i = 0; i < streamNames.length; i++) {
  const streamName = streamNames[i];
  const consumerGroup = `${streamName}-group`;
  createConsumerGroupIfNotExists(streamName, consumerGroup);
}



// Create worker processes for each stream and group combination
for (let i = 0; i < streamNames.length; i++) {
  for (let j = 0; j < numWorkersPerStream; j++) {
    const workerName: string = `worker-${streamNames[i]}-${j}`;
    const streamName: string = streamNames[i];
    const consumerGroup: string = `${streamName}-group`;
    const args: string[] = [workerName, streamName, consumerGroup, REDIS_HOST!, REDIS_PORT!.toString()];
    const workerProcess = fork("./workers/worker.ts", args);
    workerProcess.send("start"); // Signal the child process to start
  }
}%

Minimal reproduction

Specifications

OS: macOS Sonoma 14.1.1
"ts-node": "^10.9.1"
node version: 20.6.1

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "ts-node": {
    "compilerOptions": {
       "target": "es2015",
      "module": "es2015",
      "moduleResolution": "nodenext"
    }
  }
}
@MGMehdi
Copy link

MGMehdi commented Nov 21, 2023

I've got the same issue.
I've tried to change the target and module in tsconfig.json and add type:module in package.json. Tried every combinaison but there is always something that prevents to run.

It looks like this problem has been around for a few years from the posts on github and stackoverlow.

@nikkehtine
Copy link

When I run ts-node-esm ./src/index.ts or ts-node --esm ./src/index.ts, I get this error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for D:\Code\simple_node_server\src\index.ts
    at new NodeError (node:internal/errors:406:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:99:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:142:36)
    at defaultLoad (node:internal/modules/esm/load:120:20)
    at nextLoad (node:internal/modules/esm/hooks:833:28)
    at load (D:\Biblioteka\pnpm-store\5\.pnpm\ts-node@10.9.1_@types+node@14.18.33_typescript@5.2.2\node_modules\ts-node\dist\child\child-loader.js:19:122)
    at nextLoad (node:internal/modules/esm/hooks:833:28)
    at Hooks.load (node:internal/modules/esm/hooks:416:26)
    at MessagePort.handleMessage (node:internal/modules/esm/worker:168:24)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:807:20) {      
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

tsconfig:

"compilerOptions": {
    "target": "es2016",
    "module": "esnext",
    "esModuleInterop": true,
  }

@AitorMelero
Copy link

I had the same issue on a computer with macOS, another with Windows, and another with Ubuntu, all of them running Node version 20. When testing the previous version of Node (18.18.2) on a machine with Windows and another with Ubuntu, it worked on both.

It appears that the issue arises when using version 10 of ts-node with Node version 20.

@ethanhalko
Copy link

I was finally able to get this working with Node 20 and ts-node 10 by setting module to commonjs and target to es6.

Provided your project can support your Typescript compiling to CommonJS, it works well.

@arbassic
Copy link

arbassic commented Nov 22, 2023

In the latest node.js weekly newsletter they sent an interesting gist / project stub addressing the issues - it may be helpful!

https://gist.github.com/khalidx/1c670478427cc0691bda00a80208c8cc

It is really weird that ts-node in conjuction with Node 20 is making such problems!

Also.. have you tried tsx?

@janpapenbrock
Copy link

This is probably a duplicate of #1997?

@Zen-cronic
Copy link

Zen-cronic commented May 11, 2024

Hey, I had the same issue for my use case that resembles your "Steps to reproduce problem".

My case: Configuring ts-jest for my tests. And jest uses ts-node to compile the typescript test files. In those files, I am using fork from child_process (like in your code) to create processes and execute functions. But it was throwing this error

SyntaxError: Cannot use import statement outside a module 

by pointing at the ESM import statement declared in those processes.

Solution: register ts-node in the environments where the processes are executed.

My setup:

  1. package.json

type: module is NOT included. Just CommonJS module.

  1. tsconfig.json

module set to commonjs and target set as es2020.
These two properties doesn't have to be the same: the former is responsible for the type of import statements; the latter modifies the compiled JS version.

{
  "compileOnSave": true,
  "compilerOptions": {
    "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
     "module": "commonjs" /* Specify what module code is generated. */,
    "rootDir": "./src" /* Specify the root folder within your source files. */,
     "outDir": "./dist" /* Specify an output folder for all emitted files. */,
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
    "strict": true /* Enable all strict type-checking options. */,
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  },
  "exclude": ["node_modules", ...]
}
  1. source file: testHelper.ts

There are 2 approaches you can take:

A. Set the args to the child proc as -r ts-node/register which tells the node environment of the proc to be compiled with ts-node, because the proc doesn't know that by default.

    workerProcess = fork(testProcessPath, {
      stdio: [0, "pipe", "pipe", "ipc"],
      execArgv: ["-r", "ts-node/register"],
    ...
    });

B. Add those arguments in the test script in package.json depending on the platform. For linux, use export NODE_OPTIONS='-r ts-node/register'

"scripts": {
   ...
    "test": "set NODE_OPTIONS=-r ts-node/register && jest --watchAll --no-cache",
  },

Solutions found from this thread #619 and the docs.

Hope that helps!

@crazyjat
Copy link

Why are we still doing commonjs? Is ESM not the standard at this point?

@jishi
Copy link

jishi commented Nov 6, 2024

I came across this very issue a few days ago, but it was incoherent over different repositories. I finally concluded that this error started with Typescript 5.6.x, and doesn't happen in 5.5.x and below (this is without any tsconfig.json in place, which I expect should work out of the box).

Not sure what changed in typescript 5.6 which makes the inferred tsconfig to start failing like this (this happens in both Node 18 and Node 20 btw, which I know have other issues).

@aaarichter
Copy link

aaarichter commented Nov 22, 2024

having the same problem. The solutions from the readme doesn't work :(

@brunocapdevila
Copy link

Same issue on Mac with node 20.15.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests