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

Fix in 4.16.3 broke use of certain libraries when allowJs is true #640

Open
5 of 6 tasks
lawrenceong opened this issue Aug 21, 2024 · 6 comments
Open
5 of 6 tasks
Labels
bug Something isn't working

Comments

@lawrenceong
Copy link

lawrenceong commented Aug 21, 2024

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://stackblitz.com/edit/node-epykqv

Problem & expected behavior (under 200 words)

From and including version 4.16.3 of tsx, the bug caused the following problem:

If tsconfig.json contains a compiler option of allowJs set to true:

{
  "compilerOptions": {
    "allowJs": true
  }
}

Then certain packages will fail when used with tsx. From the minimal reproduction URL, using libphonenumber-js and tsx, a simple call will result in the following error:

> tsx index.ts

Error: [libphonenumber-js] `metadata` argument was passed but it's not a valid metadata. Must be an object having `.countries` child object property. Got an object of shape: { default }.
    at validateMetadata (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/build/metadata.js:591:11)
    at new Metadata (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/build/metadata.js:43:5)
    at parse (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/build/parse.js:89:14)
    at parsePhoneNumberWithError (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/build/parsePhoneNumberWithError_.js:19:32)
    at parsePhoneNumber (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/build/parsePhoneNumber_.js:32:55)
    at parsePhoneNumber (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/build/parsePhoneNumber.js:20:44)
    at call (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/min/index.cjs.js:14:14)
    at parsePhoneNumberFromString (/home/projects/node-epykqv/node_modules/.pnpm/[email protected]/node_modules/libphonenumber-js/min/index.cjs.js:18:9)
    at eval (/home/projects/node-epykqv/index.ts:2:864)
    at Object.eval (/home/projects/node-epykqv/index.ts:3:3)

index.ts:

import parsePhoneNumber from 'libphonenumber-js';

console.log(parsePhoneNumber('+61491570006').isValid());

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
@lawrenceong lawrenceong added bug Something isn't working pending triage labels Aug 21, 2024
@paul-b-manning

This comment was marked as off-topic.

@privatenumber
Copy link
Owner

Thanks for the issue and minimal reproduction.

Looks like the TS resolver is trying the .js extension to handle the implicit extension case, and it happened to match, which ends up loading the wrong file.

Will try to look into a fix later this week.

If anyone is interested in helping, feel free to contribute constructively by identifying the problem and proposing solutions.

@wrose504
Copy link

wrose504 commented Nov 13, 2024

While debugging this today, I found a difference between 4.16.2 and 4.16.3 in how the libphonenumber-js/min/index.cjs was processed. In particular, the var metadata = require('../metadata.min.json') sets metadata to the JSON in 4.16.2, but in 4.16.3, it sets it to {default: { /* the JSON */, "__esModule", { value: true } }. That then means that metadata.countries is not defined, because it is at metadata.default.countries instead.

I was able to work around this in my code by using the libphonenumber-js/core import and separately importing the minimal metadata:

import {parsePhoneNumber} from 'libphonenumber-js/core';
import minimalMetadata from 'libphonenumber-js/min/metadata';

console.log(parsePhoneNumber('+61491570006', minimalMetadata).isValid());

This could be because libphonenumber-js/min/metadata maps to a JS version of the JSON in package.json:

{
  /* snip */
  "exports": {
    /* snip */
    "./metadata.min": {
      "import": "./metadata.min.json.js",
      "require": "./metadata.min.json"
    },
  }
}

I haven't dug into this further as I don't use much from libphonenumber-js and the workaround seemed easy enough.

@wrose504
Copy link

wrose504 commented Nov 13, 2024

I dug a little further and I think that mapTsExtensions should try the originally requested path before trying variants where it adds extensions.

diff --git a/src/utils/map-ts-extensions.ts b/src/utils/map-ts-extensions.ts
index 34f3a68..6af9e3e 100644
--- a/src/utils/map-ts-extensions.ts
+++ b/src/utils/map-ts-extensions.ts
@@ -47,6 +47,9 @@ export const mapTsExtensions = (
                );
        }
 
+       // If we're guessing extensions, and the file _has_ an extension, start with it.
+       if (!tryExtensions && extension) tryPaths.push(filePath);
+
        const guessExtensions = (
                (
                        !(filePath.startsWith(fileUrlPrefix) || isFilePath(pathname))

Currently when resolving ./path/to/mymodule.js, mapTsExtensions will recognize the .js extension and try replacing it with .ts, .tsx, .js (i.e. here it tries the original request), then .jsx, and then it will try suffixing so that .js becomes .js.ts, .js.tsx, .js.js, and .js.jsx.

When resolving ./path/to/mydata.json, mapTsExtensions does not recognize the .json extension and goes straight to suffixing, so it tries .json.ts, .json.tsx, .json.js, and .json.jsx. Unlike previously, it never actually tries .json, and only tries it after .json.js. Since libphonenumber-js has .json.js files, these are what get imported, presumably then as ES modules with a default export, instead of just JSON as expected.

If I add in the line above to try the original file if it has an extension, then the .json is tried before all the suffixed options, and resolves correctly.

This might also explain a pretty significant slowdown I am seeing in how quickly tsx is able to load a large project: if it's having to stat a bunch more options before giving up, it's probably wasting a lot of time.

@privatenumber
Copy link
Owner

To resolve this, tsx will need a custom CJS resolver. The native CJS resolver has limitations, especially in extending the export map resolution behavior, which prevents a fix with the current setup. Additionally, there's a problematic discrepancy between tsx's CJS and ESM resolvers.

Creating a custom CJS resolver could also address other problems where passing in conditions is necessary.

One potential solution I'm considering—but haven't yet explored—is switching to the Oxc resolver.

@wrose504
Copy link

wrose504 commented Nov 14, 2024

I don't know the intricacies of module resolution, but it seemed like things were okay with 4.16.2. Were there specific use cases you were trying to fix during the 4.16.3 refactor? Would it be a backward step to try to change the logic like I suggested?

I was thinking I could make a small PR with the change above, but if it doesn't actually make sense I am ok to wait for an alternative solution based on Oxc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants