dividab / tsconfig-paths

Load node modules according to tsconfig paths, in run-time or via API.
MIT License
1.8k stars 100 forks source link

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

Open Danielku15 opened 1 year ago

Danielku15 commented 1 year ago

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.

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 ```bash node --experimental-specifier-resolution=node --loader=ts-node-esm-paths.mjs ... ```
Usage via .mocharc.json ```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 ```js // // 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:

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 commented 1 year ago

I started the relevant changes here: https://github.com/dividab/tsconfig-paths/compare/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 commented 1 year ago

@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 commented 1 year ago

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

effervescentia commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

tsx still doesn't look like it can run typeorm

effervescentia commented 1 year ago

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 commented 1 year ago

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

MoKhajavi75 commented 1 year ago

Any updated?

damianobarbati commented 11 months ago

@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 commented 11 months ago

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