tsconfig-paths
tsconfig-paths copied to clipboard
Support for ts-node + tsconfig-paths + esm
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
hasts-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 torequire
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 theresolve
function used internally and there usetsconfig-paths
to map the specified to the real file.
In tsconfig-paths we could ship this maybe with two steps as a new feature:
- 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. - 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 atsconfig-paths/esm
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:
@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