Skip to content

Instantly share code, notes, and snippets.

@khalidx
Last active February 21, 2024 01:54
Star You must be signed in to star a gist
Save khalidx/1c670478427cc0691bda00a80208c8cc to your computer and use it in GitHub Desktop.
A Node + TypeScript + ts-node + ESM experience that works.

The experience of using Node.JS with TypeScript, ts-node, and ESM is horrible.

There are countless guides of how to integrate them, but none of them seem to work.

Here's what worked for me.

Just add the following files and run npm run dev. You'll be good to go!

package.json

{
  "private": true,
  "type": "module",
  "exports": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "sideEffects": false,
  "files": [
    "./dist/"
  ],
  "engines": {
    "node": "^20.9.0",
    "npm": "^10.1.0"
  },
  "scripts": {
    "dev": "node --no-warnings --enable-source-maps --loader ts-node/esm src/index.ts",
    "dev:watch": "nodemon --watch src/ -e ts --exec \"npm run dev\"",
    "test": "node --no-warnings --enable-source-maps --loader ts-node/esm --test src/**/*.test.ts",
    "test:watch": "node --no-warnings --enable-source-maps --loader ts-node/esm --test --watch src/**/*.test.ts"
  },
  "dependencies": {},
  "devDependencies": {
    "@sindresorhus/tsconfig": "^5.0.0",
    "nodemon": "^3.0.3",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  }
}

tsconfig.json

{
  "extends": "@sindresorhus/tsconfig",
  "compilerOptions": {
    "outDir": "./dist/",                              /* Specify an output folder for all emitted files. */
    "lib": ["ES2022"],
    "target": "ES2022",
    "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */    
    "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    "esModuleInterop": true
  },
  "include": [
    "./src/**/*.ts"
  ],
  "ts-node": {
    "esm": true,
    "transpileOnly": true,
    "files": true,
    "experimentalResolver": true
  }
}

src/utilities/node.ts

Some utilities for getting similar behavior as __filename, __dirname, and require.main === module in Node.JS CommonJS.

This file is optional.

import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
import { argv } from 'node:process'
import { createRequire } from 'node:module'

/**
 * This is an ESM replacement for `__filename`.
 * 
 * Use it like this: `__filename(import.meta)`.
 */
export const __filename = (meta: ImportMeta): string => fileURLToPath(meta.url)

/**
 * This is an ESM replacement for `__dirname`.
 * 
 * Use it like this: `__dirname(import.meta)`.
 */
export const __dirname = (meta: ImportMeta): string => dirname(__filename(meta))

/**
 * Indicates that the script was run directly.
 * This is an ESM replacement for `require.main === module`.
 * 
 * Use it like this: `isMain(import.meta)`.
 */
export const isMain = (meta: ImportMeta): boolean => {
  if (!meta || !argv[1]) return false
  const require = createRequire(meta.url)
  const scriptPath = require.resolve(argv[1])
  const modulePath = __filename(meta)
  return scriptPath === modulePath
}
@PandelisZ
Copy link

amen 🙏

I've read that "esModuleInterop": true is one to try to avoid as it forces every other dependant ts project to need to use it. Thoughts?

@Miserlou
Copy link

Miserlou commented Nov 21, 2023

Would love to see this updated with live code reloading and debugging.

@luastoned
Copy link

luastoned commented Nov 21, 2023

Add tsx and use it instead of node. Enjoy.

@petetnt
Copy link

petetnt commented Nov 21, 2023

import.meta.__dirname and import.meta.__filename are coming to NodeJS, reducing the need for the src/utilities/node.ts file

nodejs/node#48740

@bhouston
Copy link

I have a similar template project here: https://github.com/bhouston/template-typescript-monorepo. This is my starter for about a dozen in production projects now.

@xseman
Copy link

xseman commented Nov 21, 2023

Can you give an example of --enable-source-maps benefits?

@kristof-mattei
Copy link

Can you give an example of --enable-source-maps benefits?

Helps when you want to attach VS Code to the node process. Allows you to map it back to the TypeScript code.

@alexe-dev
Copy link

alexe-dev commented Nov 21, 2023

Add tsx and use it instead of node. Enjoy.

this

can be used as a nodejs loader as well https://github.com/privatenumber/tsx#nodejs-loader

@Aicirou
Copy link

Aicirou commented Nov 21, 2023

dev
node --no-warnings --enable-source-maps --loader ts-node/esm src/index.ts

node:internal/process/esm_loader:40
internalBinding('errors').triggerUncaughtException(
^
[Object: null prototype] {
[Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]]
}

help me with this error please?

@xseman
Copy link

xseman commented Nov 21, 2023

Add tsx and use it instead of node. Enjoy.

problem with tsx that it doesn't support decorators yet

@alexe-dev
Copy link

Add tsx and use it instead of node. Enjoy.

problem with tsx that it doesn't support decorators yet

yeah, indeed

@dhenson02
Copy link

no one uses tsc anymore?

@jessebond2
Copy link

Can you give an example of --enable-source-maps benefits?

Helps when you want to attach VS Code to the node process. Allows you to map it back to the TypeScript code.

Also if you use anything like Sentry it can help with debugging errors

@khaosdoctor
Copy link

khaosdoctor commented Nov 21, 2023

Add tsx and use it instead of node. Enjoy.

TSX is amazing, the only problem you get with it (besides the decorators) is that if you want to run your node process with the .env file loading you will eventually lose the ability to watch as you need to pass it to NODE_OPTIONS

NODE_OPTIONS='--import tsx' node --env-file .env script.js

It's not THAT big of a dealbreaker, but it can be annoying.

Also, very important, --loader is deprecated on Node 20(ish?) and will be replaced by --import

@bogeeee
Copy link

bogeeee commented Nov 21, 2023

Does it properly halt on your breakpoints in .ts files ?

@khaosdoctor
Copy link

Does it properly halt on your breakpoints in .ts files ?

It should yes.

Another option is to always ditch Jest and use the native Node.js test runner. You can run tests with TS easier with glob -c "node --import tsx --test --experimental-code-coverage" "some/**/test/folder/*.test.ts"

as far as I know the glob support is coming in node 22

@o-az
Copy link

o-az commented Nov 22, 2023

Add tsx and use it instead of node. Enjoy.

TSX is amazing, the only problem you get with it (besides the decorators) is that if you want to run your node process with the .env file loading you will eventually lose the ability to watch as you need to pass it to NODE_OPTIONS

NODE_OPTIONS='--import tsx' node --env-file .env script.js

It's not THAT big of a dealbreaker, but it can be annoying.

Also, very important, --loader is deprecated on Node 20(ish?) and will be replaced by --import

you could do this and get watch/live reload and env file

node --import tsx --env-file .env --watch index.ts

@khaosdoctor
Copy link

Add tsx and use it instead of node. Enjoy.

TSX is amazing, the only problem you get with it (besides the decorators) is that if you want to run your node process with the .env file loading you will eventually lose the ability to watch as you need to pass it to NODE_OPTIONS

NODE_OPTIONS='--import tsx' node --env-file .env script.js

It's not THAT big of a dealbreaker, but it can be annoying.
Also, very important, --loader is deprecated on Node 20(ish?) and will be replaced by --import

you could do this and get watch/live reload and env file

node --import tsx --env-file .env --watch index.ts

Weird, my test here kept reloading the file with --watch

@sleep-written
Copy link

Now using Typescript with ts-node in ESM projects it's a mess, however sometime ago I made a library called @bleed-believer/path-alias to resolve path aliases (uses ts-node as dependency), and handle the troubles asociated with this development environment. This library it's capable to run ESM projects with Node 20, even if you don't uses path alias in your project.

If you want to try it:

  • Set your project as ESM in ./package.json file:

    {
      "name": "my-project",
      "version": "0.0.0",
      "type": "module"
    }
  • Set your tsconfig.json:

    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",
        "moduleResolution": "Node16",
    
        /* More configuration options... here */
    }
  • Install the package:

npm i --save @bleed-believer/path-alias
  • Finally run your project:
node --import @bleed-believer/path-alias ./src/index.ts

@o-az
Copy link

o-az commented Nov 25, 2023

Add tsx and use it instead of node. Enjoy.

TSX is amazing, the only problem you get with it (besides the decorators) is that if you want to run your node process with the .env file loading you will eventually lose the ability to watch as you need to pass it to NODE_OPTIONS

NODE_OPTIONS='--import tsx' node --env-file .env script.js

It's not THAT big of a dealbreaker, but it can be annoying.
Also, very important, --loader is deprecated on Node 20(ish?) and will be replaced by --import

you could do this and get watch/live reload and env file

node --import tsx --env-file .env --watch index.ts

Weird, my test here kept reloading the file with --watch

If you want the file/s to not reload you could try tsup build with --onSuccess:

https://github.com/o-az/typescript-template/blob/d60b0ddffdfc34b5412260b30ec6cf9511eece9c/package.json#L26

@malixsys
Copy link

Would love to see this updated with live code reloading and debugging.

Use vavite…

https://github.com/cyco130/vavite

@habtamu
Copy link

habtamu commented Nov 26, 2023

Do we have a fix to resolve this issue?

➜ node-typescript-esm npm run dev

dev
node --no-warnings --enable-source-maps --loader ts-node/esm src/index.ts

node:internal/process/esm_loader:40
internalBinding('errors').triggerUncaughtException(
^
[Object: null prototype] {
[Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]]
}

Node.js v20.9.0

@Aicirou
Copy link

Aicirou commented Dec 6, 2023

Do we have a fix to resolve this issue?

instead of

"node --no-warnings --enable-source-maps --loader ts-node/esm src/index.ts"

try tsx,

"node --watch --no-warnings --enable-source-maps --import tsx ./src/index.ts"

@a0viedo
Copy link

a0viedo commented Dec 11, 2023

just wrote about my experience setting up ESM, TypeScript and Node.js v20 here. I ended up using SWC for transpilation, tsc for type-checking and tsx for running TypeScript files

@bogeeee
Copy link

bogeeee commented Jan 30, 2024

Sure, that --enable-source-maps is needed ? It outputs stacks and debugs fine (with Webstorm) without that flag, just with node --import tsx

@c0bra
Copy link

c0bra commented Feb 8, 2024

Do we have a fix to resolve this issue?

instead of

"node --no-warnings --enable-source-maps --loader ts-node/esm src/index.ts"

try tsx,

"node --watch --no-warnings --enable-source-maps --import tsx ./src/index.ts"

When I try tsx, I can run from the cli, but inside of a Docker container, I get TypeError [Error]: undefined is not iterable (cannot read property Symbol(Symbol.iterator)) at some place in the internals of tsx. Nothing in their github issues that seems to correspond.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment