dividab / tsconfig-paths

Load node modules according to tsconfig paths, in run-time or via API.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for ts-node + tsconfig-paths + esm

Danielku15 opened this issue · comments

I am currently trying to migrate the browser based tests (Rollup+Karma+Jasmine) of my TypeScript project to a node based setup (ts-node+mocha) but unfortunately it seems almost every package lacks some features, especially around ESM.

  • ts-node would have ESM support, but doesn't have paths support
  • tsconfig-paths has ts-node support but no ESM support.

So I attempted to get tsconfig-paths running with ESM and was successful by hooking into the resolve process. But it is still a bit hacky currently because ts-node doesn't export the relevant modules, and tsconfg-paths doesn't have a public API of resolving the actual file that was found to match a configured paths. First some code:

Usage via node
node --experimental-specifier-resolution=node --loader=ts-node-esm-paths.mjs ...
Usage via .mocharc.json
{
    "extension": [
        "ts"
    ],
    "node-option": [
        "experimental-specifier-resolution=node",
        "loader=./scripts/ts-node-esm-paths",
        "no-warnings"
    ],
    "spec": "test/**/*.test.ts"
}
ts-node-esm-paths.mjs
//
// Override default ESM resolver to map paths
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { join } from 'path';
import * as fs from 'fs';

const require = createRequire(fileURLToPath(import.meta.url));
const __dirname = fileURLToPath(new URL('.', import.meta.url));

import { createMatchPath, loadConfig, matchFromAbsolutePaths } from 'tsconfig-paths';

const configLoaderResult = loadConfig();
const matchPath = createMatchPath(
    configLoaderResult.absoluteBaseUrl,
    configLoaderResult.paths,
    configLoaderResult.mainFields,
    true
);

/** @type {import('ts-node/dist-raw/node-internal-modules-esm-resolve')} */
const esmResolver = require(join(
    __dirname,
    '..',
    'node_modules',
    'ts-node',
    'dist-raw',
    'node-internal-modules-esm-resolve.js'
));
const originalCreateResolve = esmResolver.createResolve;
esmResolver.createResolve = opts => {
    const resolve = originalCreateResolve(opts);

    const originalDefaultResolve = resolve.defaultResolve;
    resolve.defaultResolve = (specifier, context, defaultResolveUnused) => {
        const found = matchPath(specifier);
        if (found) {
            // NOTE: unfortunately matchPath doesn't give us the absolute path
            // therefore we have to cheat here a bit
            if (fs.existsSync(found + '.ts')) {
                specifier = new URL(`file:///${found}.ts`).href;
            } else if (fs.existsSync(join(found, 'index.ts'))) {
                specifier = new URL(`file:///${join(found, 'index.ts')}`).href;
            }
        }

        const result = originalDefaultResolve(specifier, context, defaultResolveUnused);
        return result;
    };

    return resolve;
};

//
// Adopted from ts-node/esm
/** @type {import('ts-node/dist/esm')} */
const esm = require(join(__dirname, '..', 'node_modules', 'ts-node', 'dist', 'esm.js'));
export const { resolve, load, getFormat, transformSource } = esm.registerAndCreateEsmHooks();

What I've done:

  • I created a custom node loader which does the same thing like ts-node/esm. This makes the usage easier because only one single loader has to be specified everywhere. But ts-node doesn't export some internal files, so we are required to require them via absolute path.
  • Before registering the default ts-node/esm I override some internal functions of it (like the tsconfig-paths/register does it for the CJS file loading). I override the resolve function used internally and there use tsconfig-paths to map the specified to the real file.

In tsconfig-paths we could ship this maybe with two steps as a new feature:

  1. The loaded could be adapted into tsconfig-paths. matchPath needs an extension to get back the final file path of the module which was found.
  2. We could ask the folks over at ts-node if they can expose some hook to do a custom resolving more officially than relying on the require hacks. (e.g. they could expose the registerAndCreateEsmHooks with a callback for resolving we can import in a tsconfig-paths/esm

I started the relevant changes here: master...Danielku15:tsconfig-paths:feature/ts-node-esm

The new ts-node-esm.mjs can be used in node --loader and will bootstrap tsconfig-paths together with ts-node in an ESM setup. The new example shows how it can be used. Beside that I needed an extension of the path resolving which returns me the matched file path instead of the trimmed variant.

I could prepare a full PR if there is a chance of getting it merged. Unit Tests are missing at this point.

After integrating a test build into my own project (TypeScript Codebase+Mocha+ESM+ts-node+tsconfig-paths), I got it even running with the Test Explorer VS Code Extensions:
image

@Danielku15 have you tried out tsx?
I found it the other day and it's simplified every TypeScript + Node + ESM project I work on, and it does the paths resolution for you

@effervescentia
I have tried it and encountered an issue with the decorator.

@effervescentia I have tried it and encountered an issue with the decorator.

Interesting... I use it on a project that runs a NestJS application and uses decorators heavily and haven'y had any issues
based on the limitations they say that the configuration setting emitDecoratorMetadata isn't supported, which you don't need to actually use decorators for route binding like NestJS does.
do you actually need it enabled for your usecase?

@8naf I was able to get it working by adding an explicit @Inject(AppService) decorator
https://stackblitz.com/edit/node-nxsjdb?file=src/app.controller.ts

@8naf I was able to get it working by adding an explicit @Inject(AppService) decorator https://stackblitz.com/edit/node-nxsjdb?file=src/app.controller.ts

Fantastic! Everything is working now. I had to struggle all day to find a way to solve this issue.
Thank you very much!

tsx still doesn't look like it can run typeorm

tsx still doesn't look like it can run typeorm

@mfts2048

I'm actually using typeorm and @nestjs/typeorm with my project and they're working properly
However, my ORM models are stored in a separate package so they have already been compiled with the appropriate metadata before being consumed in my NestJS app, so that might be why I'm not having the same issue.

Is there a way to get ESM supported in tsconfig-paths?

Any updated?

@Danielku15 any chance to use with ESM? The following does not work still:

NODE_ENV=development node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register ./src/index.ts

@damianobarbati I am currently moving over towards tsx which works fine for all my use cases.
https://github.com/esbuild-kit/tsx