tsx: import in referenced typescript project fail to load

Bug description

When working with composite projects, our tests failed because it seems that tsx is not taking the reference project’s baseUrl or path into consideration.

Reproduction

Test project:

https://github.com/steabert/esbuild-kit-tsx-composite.git

To reproduce the issue:

yarn install
yarn fail

Running yarn bundle and yarn types shows the result with e.g. esbuild or tsc.

It seems this does not work with ts-node either, but I don’t know how they handle composite typescript projects.

Environment

npx envinfo --system --npmPackages tsx --binaries
System:
  OS: Linux 5.4 Ubuntu 20.04.4 LTS (Focal Fossa)
  CPU: (8) x64 Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
  Memory: 23.58 GB / 31.21 GB
  Container: Yes
  Shell: 5.0.17 - /bin/bash
Binaries:
  Node: 16.14.2 - ~/.node/bin/node
  Yarn: 3.2.3 - ~/.npm-global/bin/yarn
  npm: 8.5.0 - ~/.node/bin/npm
npmPackages:
  tsx: 3.9.0 => 3.9.0

Can you contribute a fix?

  • I’m interested in opening a pull request for this issue.

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 5
  • Comments: 20 (10 by maintainers)

Most upvoted comments

I was able to get this working using a custom exports field on all my monorepo package.json files, and then passing --conditions to tsx:

  "exports": {
    ".": {
      "myOrganizationName:bundler": "./src/index.ts",
      "default": "./out/index.js"
    }
  }

Then

tsx --conditions=myOrganizationName:bundler ...

If people are stuck on this, I had success with vite-node combined with vite-tsconfig-paths.

It also plays nice with vitest as it’s reusing the same pipeline. 👍

I’m very sorry for this triple post, but I played some more with my TypeScript monorepo and I think I gained interesting insights for this issue.

Big picture

tldr; TypeScript project references are the way to hint about which tsconfig.json file to apply to parts of the dependency chain

First, the “one true way” of connecting TypeScript projects together is project references. This is the only mechanism that enables TypeScript to split the dependency chain between multiple TypeScript projects and use each project’s tsconfig.json.

Custom tsconfig.json files are handled by this mechanism as well:

{
    "compilerOptions": {
        // The usual
    },
    "references": [
        { "path": "../api/tsconfig.build.json" },
        { "path": "../sdk/tsconfig.whatever.json" },
    ]
}

Without references, if you require a TypeScript file from an external package (through either monorepo workspaces or paths alias, I personally recommend avoiding paths alias as you break your package isolation), only the tsconfig.json file of the root package will be used, this is why external packages paths alias break, because the root package is not aware of their internals (and it should not be 😄).

How to have this implemented in tsx

Project references were discussed in esbuild repository, and the conclusion was that it’s beyond the scope of esbuild, but could be implemented as a plugin!

A plugin exists: esbuild-plugin-ts-references but it’s not working on paths alias, it’s only rewriting the import statements to link directly to the references. But it shows that it’s possible with esbuild API to walk through each package’s tsconfig.json by following references and build some kind of map.

So the solution would be to write an esbuild plugin similar to esbuild-plugin-ts-references that can rewrite imports based on the file’s local tsconfig.json gathered through references analysis. The main blocker is that tsx is using esbuild’s Transform API and not the Build API, so it can’t consume a plugin.

@privatenumber How hard of a requirement is it to use the Transform API vs the Build API? Reading esbuild doc it seems like Transform is just a small subset of Build?

Another solution would be to have tsx do the references analysis work outside of esbuild to build the map and then apply the rewrites somehow with the Transform API, but I’m not familiar with it enough to know if it’s possible. 😅

edit: It seems like if we can construct the map <tsconfigPath, filePaths>, we could be able to pass the right tsconfig.json content here: https://github.com/esbuild-kit/cjs-loader/blob/94a69f9e6d41225cb8a132938a3b9b077c15f966/src/index.ts#L74

edit 2: I gave my “projects map” idea a try, I think I’m pretty close to making it works but I have a nasty recursive bug somewhere that I can’t figure out. 🤔

But I’m able to :

Please note that it’s not “performance” oriented, right now I’m just trying to understand the code and make it works. 😅

Here are the forks :

And I test against this project:

edit 3: My recursion bug is fixed and the code for https://github.com/jgoux/esm-loader/tree/feat-project-references is fully functional! 🎊