Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ts-node + tsconfig-paths + esm #243

Open
Danielku15 opened this issue Mar 5, 2023 · 12 comments
Open

Support for ts-node + tsconfig-paths + esm #243

Danielku15 opened this issue Mar 5, 2023 · 12 comments

Comments

@Danielku15
Copy link

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
@Danielku15
Copy link
Author

Danielku15 commented Mar 5, 2023

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

@effervescentia
Copy link

effervescentia commented Mar 10, 2023

@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

@8NAF
Copy link

8NAF commented Mar 14, 2023

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

@effervescentia
Copy link

@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?

@effervescentia
Copy link

@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
Copy link

8NAF commented Mar 15, 2023

@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!

@mfts2048
Copy link

tsx still doesn't look like it can run typeorm

@effervescentia
Copy link

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.

@CMCDragonkai
Copy link

CMCDragonkai commented Jul 7, 2023

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

@MoKhajavi75
Copy link

Any updated?

@damianobarbati
Copy link

damianobarbati commented Oct 5, 2023

@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

@Danielku15
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants