Search code examples
typescriptaliastsconfigts-node

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


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


Solution

  • 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