dividab/tsconfig-paths

Native ESM support, possibly requiring integration with ts-node

cspotcode opened this issue ยท 8 comments

In ts-node we released experimental support for native ECMAScript modules. To do this we needed to implement a custom resolve() loader hook. It is a copy-paste of node's built-in resolver, tweaked for our needs.

TypeStrong/ts-node#1007

Is native ESM support on your radar? Do you have thoughts on how best to implement it for tsconfig-paths? I expect we will need to coordinate, since node only supports a single loader hook.

I see that the compiler itself can perform these resolutions via ts.resolveModuleName. Do you use that internally, or do you do something else?

just posted a similar issue but I am not sure if this repo is still maintained...

@desmap you can be the one to do it!

You can also stop using path mappings in your project. Probably wouldn't be much work since it's some find-and-replace of the import statements.

What benefits do you hope to get from switching to native ESM? You'll be forcing your users to install a non-standard path mapping hook.

What benefits do you hope to get from switching to native ESM?

It'd def debatable but I'd like it, check yarnpkg/berry#638 (comment) and yarnpkg/berry#638 (comment) for examples

I had trouble getting module path mappings to work with ESM (using tsconfig-paths' register function -- pre-ESM I did it this way: #157 (comment)). What finally worked for me was using a custom loader (thanks for the tip/example @geigerzaehler TypeStrong/ts-node#1450 (comment))

To use the loader code below, pass it to node via --loader option, e.g. node --loader loader.js main.js.

Custom loader code
// (loader.ts)
// Uses tsconfig.json and createMatchPath (from tsconfig-paths lib) to implement a custom loader
// (resolve function) that applies path mappings like `@src/foo` --> `/path/to/app/build/src/foo.js`
// See also https://github.com/TypeStrong/ts-node/discussions/1450#discussion-3563207
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import { basename, dirname, resolve as pathResolve } from 'path';
import { fileURLToPath } from 'url';

import { createMatchPath } from 'tsconfig-paths';

const __dirname = dirname(fileURLToPath(import.meta.url));

async function getTSConfig() {
  const maxDepth = 32; // arbitrary
  let depth = 0;
  let tsConfigFile = pathResolve(__dirname, 'tsconfig.json');
  while(!existsSync(tsConfigFile)) {
    tsConfigFile = pathResolve(dirname(tsConfigFile), '..', basename(tsConfigFile));
    depth++;
    if (depth > maxDepth) {
      throw Error(`maxDepth (${maxDepth}) exceeded while searching for tsconfig.json`);
    }
  }
  return JSON.parse(await readFile(tsConfigFile, 'utf-8'));
}

const getMatchPathPromise = (async () => {
  const tsConfig = await getTSConfig();
  const baseUrl = tsConfig.compilerOptions.baseUrl || '.';
  const outDir = tsConfig.compilerOptions.outDir || '.';
  const absoluteBaseUrl = pathResolve(baseUrl, outDir);
  const paths = tsConfig.compilerOptions.paths;
  return createMatchPath(absoluteBaseUrl, paths);
})();

export async function resolve(specifier, context, defaultResolve) {
  const matchPath = await getMatchPathPromise;
  const mappedSpecifier = matchPath(specifier)
  if (mappedSpecifier) {
    specifier = `${mappedSpecifier}.js`
  }
  return defaultResolve(specifier, context, defaultResolve);
}

@jacobq Also have a look at this solution TypeStrong/ts-node#1450
It's rather simple because it extends native ts-node/esm loader.

@jacobq Also have a look at this solution TypeStrong/ts-node#1450 It's rather simple because it extends native ts-node/esm loader.

Yes, I saw it (hence why I mentioned it in my comment ๐Ÿ˜‰). I probably should've mentioned that I am not actually using ts-node though.

I wanted to mention new solution created by @charles-allen 4 days ago (TypeStrong/ts-node#1450 (comment))

Closing in favor of TypeStrong/ts-node#1585 which will add native path mapping support to ts-node.