privatenumber / tsx

⚡️ TypeScript Execute | The easiest way to run TypeScript in Node.js

Home Page:https://tsx.is

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`import.meta.url` mismatch

laverdet opened this issue · comments

Acknowledgements

  • I searched existing issues before opening this one to avoid duplicates
  • I understand this is not a place for seek help, but to report a bug
  • I understand that the bug must be proven first with a minimal reproduction
  • I will be polite, respectful, and considerate of people's time and effort

Minimal reproduction URL

https://stackblitz.com/edit/node-dhrkmk

Version

v4.7.0

Node.js version

v20.8.0

Package manager

npm

Operating system

macOS

Problem & Expected behavior

tsx forwards the TypeScript source filename as the value of import.meta.url, instead of the simulated JavaScript output filename. This causes an inconsistency at runtime between a tsc-compiled environment and a tsx-loaded environment. The mismatch here will cause complexity when deploying production builds which don't use tsx.

Output from the Stackblitz:

> tsc
> tsc -b && node dist/index.js

file:///home/projects/node-dhrkmk/dist/index.js

> tsx
> tsx index.ts

file:///home/projects/node-dhrkmk/index.ts

index.ts:

console.log(import.meta.url);

What you can do is set responseURL in the load hook to the name of what the output file would be under tsc. You should leave the resolved url to match the source file. In the case noEmit or emitDeclarationOnly is declared in tsconfig then you can leave the TypeScript source filename as-is because in that case you know the project will not be compiled.

I'm glad to see this project gaining steam because I feel ts-node is encouraging a lot of bad practices in the module ecosystem. I think the fix I mentioned here would be really helpful. Our team made a custom loader to replace ts-node which handles this case correctly: https://github.com/braidnetworks/loaderkit/blob/c7d8280d5434f4d81c68784a0a1910911048da73/packages/ts/loader.ts#L246-L254

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

I thought about this deeply, and I don't think I'll be moving forward with this. Particularly because I think this is less predictable behavior.

If someone derives a path from import.meta.url, it wouldn't exist if the dist has not been compiled yet:

await readFile(new URL('./file.json', import.meta.url)) // Might not exist or have outdated content if not recently compiled

This would technically also make it possible to import files relative from the path of the output destination which may not exist relative to the source path:

import '../relative/to/dist.js' // Might not exist relative to src

Sure, this is an environmental inconsistency between development vs production. But there's always plenty of other discrepancies (e.g. env vars).

I'm glad you gave it some thought, that's all I can ask! Something else to ponder: In TypeScript why do we import "./file.js" instead of import "./file.ts"? I think that the same runtime consistency argument applies to this case here.

I think that the vast majority of users don't do anything different between development and production. For example, if they use tsx in development then they will also use it in production. These users should set noEmit or emitDeclarationOnly which communicates to the transpiler that plain JavaScript files will never be written to the filesystem. I take the position that production servers should not need to transpile code, so multi-environment is important.

I think you make good points, and I agree these decisions aren't completely consistent—it's more about what's predictable.

tsx has gained its popularity for being beginner friendly and easy to jump start TS without knowing how to use tsconfig. With that in mind, making import.meta.url be the actual file path only when tsconfig.json#noEmit is set would be quite a curve ball for most people.