Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resolver prioritizes extension guessing over real path #617

Closed
5 of 6 tasks
nwalters512 opened this issue Jul 22, 2024 · 3 comments · Fixed by KaliforniaShell/docs#1 · 4 remaining pull requests
Closed
5 of 6 tasks

resolver prioritizes extension guessing over real path #617

nwalters512 opened this issue Jul 22, 2024 · 3 comments · Fixed by KaliforniaShell/docs#1 · 4 remaining pull requests
Labels
bug Something isn't working outdated released

Comments

@nwalters512
Copy link
Contributor

Acknowledgements

  • I read the documentation and searched existing issues to avoid duplicates
  • I understand this is a bug tracker and anything other than a proven bug will be closed
  • I understand this is a free project and relies on community contributions
  • I read and understood the Contribution guide

Minimal reproduction URL

https://github.com/nwalters512/tsx-dynamic-import-default-repro

Problem & expected behavior (under 200 words)

I would expect the exact same output when compiling the src/index.ts program to *.js with tsc and running the compiled file with Node, and when running src/index.ts directly with tsx. Instead, the behavior is different:

  • When compiling to JS and executing with Node, the plugin object has a default property.
  • When executing directly with tsx, the plugin object does not have a default property.

I'd be happy to further investigate and possibly fix this if you could point me in the direction of whatever part of tsx handles things like this (dynamic imports? ESM compat? I'm not sure exactly what tsx does that would be causing this, I had assumed that this would all be strictly Node's responsibility.

Bugs are expected to be fixed by those affected by it

  • I'm interested in working on this issue

Compensating engineering work will speed up resolution and support the project

  • I'm willing to offer $10 for financial support
@privatenumber
Copy link
Owner

privatenumber commented Jul 23, 2024

Looks like this is happening because tsx tries guessing extensions before trying the actual path. I was pretty sure this is how the TS resolver works but I'll have to evaluate the order again.

So instead of trying prettier/plugins/estree which resolves to prettier/plugins/estree.mjs (ESM) based on their exports map, it directly loads prettier/plugins/estree.js (CommonJS).

If you'd interested, feel free to investigate the correct behavior and implement it.

@nwalters512
Copy link
Contributor Author

From what I can tell, this issue is caused by tsx failing to correctly account for the "exports" field of package.json, at least in this particular case. In my repro, I was ultimately able to trace things down to this function:

const resolveExtensions = async (
url: string,
context: ResolveHookContext,
nextResolve: NextResolve,
throwError?: boolean,
) => {

When that's called, url is prettier/plugins/estree and context is the following:

{
  conditions: [ 'node', 'import', 'node-addons' ],
  importAttributes: {},
  parentURL: 'file:///Users/nathan/git/tsx-dynamic-import-default-repro/src/index.ts'
}

conditions contains import, which is correct since this is a dynamic import. However, that function does not take conditions into account, it just blindly tries files based on their extension, specifically the extensions listed here:

const noExtension = ['.js', '.json', '.ts', '.tsx', '.jsx'];

Normally, this would work, as we'd eventually fallback to asking Node to resolve prettier/plugin/estree, at which point the usual machinery would resolve things by taking "exports" into account. However, Prettier defined a catchall export:

{
  "exports": {
    "./*": "./*",
  }
}

This means that when tsx tries to resolve prettier/plugin/estree.js in resolveExtensions, that actually succeeds immediately before the fallback behavior can take over.

I find it strange that tsx is at all involved in resolving things inside node_modules, as that would fail during "actual" runtime in Node when tsx is not used. Is this intentional? If so, I think a fix could be to try to use nextResolve with the raw URL first, before trying files with specific extensions. I'll see if I can add some test cases, make that change, and see if all the other test cases are unaffected. Of course, let me know if you have any specific recommendations given the above information.

@privatenumber privatenumber changed the title Behavior difference between tsx and tsc+Node resolver prioritizes extension guessing over real path Jul 26, 2024
@privatenumber
Copy link
Owner

This issue is now resolved in v4.16.3.

If you're able to, your sponsorship would be very much appreciated.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.