ts-node with tsconfig-paths won't work when using esm
Asked Answered
O

3

21

I couldn't figure out why ts-node isn't resolving the alias when esm is enabled

I made a tiny project trying to isolate the issue as much as possible

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "module": "es2020",                                
    "baseUrl": "./",                                  
    "paths": {
      "$lib/*": [
        "src/lib/*"
      ]
    },
  },
  "ts-node": {
    "esm": true
  }
}

test.ts

import { testFn } from "$lib/module"

testFn()

lib/module.ts

export function testFn () {
  console.log("Test function")
}

command

ts-node -r tsconfig-paths/register src/test.ts

Here's a minimal repo

Obeded answered 22/3, 2022 at 11:56 Comment(5)
Any update on this? I'm having the same problemThunderbolt
@Thunderbolt Check the first line in my question, I linked to a solutionObeded
I tried it but unfortunately did not succeed. I wrote a comment there. If you can unblock me would really appreciate it github.com/TypeStrong/ts-node/discussions/…Thunderbolt
Just putting here OPs own answer https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115Innes
@Innes If you'd like to summarize or explain the solution, I will accept it as the solution. CheersObeded
G
11

Solution from: https://github.com/TypeStrong/ts-node/discussions/1450#discussion-3563207

At the moment, the ESM loader does not handle TypeScript path mappings. To make it work you can use the following custom loader:

// loader.js
import {
  resolve as resolveTs,
  getFormat,
  transformSource,
  load,
} from "ts-node/esm";
import * as tsConfigPaths from "tsconfig-paths"

export { getFormat, transformSource, load };

const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig()
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths)

export function resolve(specifier, context, defaultResolver) {
  const mappedSpecifier = matchPath(specifier)
  if (mappedSpecifier) {
    specifier = `${mappedSpecifier}.js`
  }
  return resolveTs(specifier, context, defaultResolver);
}

Then use the loader with: node --loader loader.js index.ts

Caveat: This only works for module specifiers without an extension. For example, import /foo/bar works, but import /foo/bar.js and import /foo/bar.ts do not.

Remember to install these packages as well:

"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2",

Updated: 09/11/2023

The new loader will automatically resolve the index.(js|ts):

import { resolve as resolveTs } from 'ts-node/esm'
import * as tsConfigPaths from 'tsconfig-paths'
import { pathToFileURL } from 'url'

const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig()
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths)

export function resolve (specifier, ctx, defaultResolve) {
  const match = matchPath(specifier)
  return match
    ? resolveTs(pathToFileURL(`${match}`).href, ctx, defaultResolve)
    : resolveTs(specifier, ctx, defaultResolve)
}

export { load, transformSource } from 'ts-node/esm'

Example:

Path: /src/modules/order/index.ts
Resolve: import orderModule from '@/modules/order';

Updated: 11/04/2024

For the node version 20, we need to set the arg: --experimental-specifier-resolution=node

node --experimental-specifier-resolution=node --loader ./loader.js src/main.ts
Gorgoneion answered 2/2, 2023 at 3:14 Comment(4)
don't forget to rexport load as well from ts-node/esm or else you'll get TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"Calebcaledonia
Doesn't work with node 20 anymore: Error [ERR_LOADER_CHAIN_INCOMPLETE]: "../loader.js 'resolve'" did not call the next hook in its chain and did not explicitly signal a short circuit. If this is intentional, include "shortCircuit: true" in the hook's return.Quartas
@MarcJ.Schmidt it works well with node v20 on my side. Did you check the last update?Gorgoneion
I'm getting TypeScript Errors for all imports from tsconfig paths. For example: error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'string' Does anyone have similar problems?Ariadne
F
4

I suggest to use esno esno src/test.ts

Farrier answered 16/6, 2023 at 13:26 Comment(1)
esno is another name for tsx, and tsx does not support emitting decorators metadata unfortunatelyBoettcher
V
2

You can use @bleed-believer/path-alias to execute your ESM project (ts-node is included as dependency):

npm i --save @bleed-believer/path-alias

To execute your source files with ts-node:

npx bb-path-alias ./src/test.ts

Assuming "outDir": "./dist", to execute your transpiled code directly with node (bypassing ts-node):

npx bb-path-alias ./dist/test.js

If you don't want to use the library to execute your transpiled project, you can use swc (this transpiler resolves the path aliases), or you can use tsc-alias after transpile your project with tsc.

Note

I updated the method of use the library according the last update.

Vanwinkle answered 15/3, 2023 at 14:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.