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!
{
"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"
}
}
{
"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
}
}
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
}
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
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