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

Typescript with module resolution "NodeNext" + type: "commonjs" fails to import the commonJS module #255

Closed
ItamarGronich opened this issue Mar 20, 2024 · 13 comments · Fixed by #258 · May be fixed by #256
Closed

Typescript with module resolution "NodeNext" + type: "commonjs" fails to import the commonJS module #255

ItamarGronich opened this issue Mar 20, 2024 · 13 comments · Fixed by #258 · May be fixed by #256

Comments

@ItamarGronich
Copy link
Contributor

Description

When using with packageJson.type = 'commonjs' (the default) and typescript with the following config:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true
  }
}

tsc fails to load the commonjs module.
After some research apparently tsc loads the ./dist/types/index.d.ts file from the packageJson.exports.type which makes typescript thing that the imported module is a esmodule and not a commonjs module.

That's because that in the index.d.ts file the way the code is exported is in esm format

export { type AsPlainObject, type AutoVacuumOptions, type BM25Params, type MatchInfo, type Options, type Query, type QueryCombination, type SearchOptions, type SearchResult, type Suggestion, type VacuumConditions, type VacuumOptions, type Wildcard, MiniSearch as default };

Expected behavior

tsc gets types that are applicable for both esm and commonjs.

Reproduction:

  1. download and unzip minisearch-types.tar.gz
  2. npm ci
  3. npm start
@ItamarGronich
Copy link
Contributor Author

I've implemented a PR in #256.
Let me know if there's anything you'd like me to change.

@lucaong
Copy link
Owner

lucaong commented Apr 3, 2024

Hi @ItamarGronich , thanks a lot for opening this issue and for sending a pull request. Allow me the time to reproduce the bug and fully understand the implications (I will be quite busy in the next few days, and I admit that tsconfig and module resolution in JS/TS sometimes evolves faster than I can follow), and I will review and hopefully merge it.

@lucaong
Copy link
Owner

lucaong commented Apr 3, 2024

@ItamarGronich I followed the steps for reproduction (thanks a lot for providing them, very helpful 👍 ). I indeed get an error when running npm run startrepor:

error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("minisearch")' call instead.
  To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to '/Users/lucaongaro/Downloads/minisearch-types/package.json'.

1 import Minisearch from "minisearch";
                         ~~~~~~~~~~~~


Found 1 error in src/index.ts:1

This error, though, seems more related to the reproduction file src/index.ts and its package.json, rather than to MiniSearch types. Indeed, if I follow the recommendation in the error, and either change the extension to src/index.mts, or set "type": "module" in the package.json, the issue is solved without any change to MiniSearch, and npm run startrepor runs without errors.

Is that the same error you are getting?

@ItamarGronich
Copy link
Contributor Author

Hi @lucaong, yes that's the same error.

However, I think this should work in all casses:

  1. "type": "commonjs" (default if type not supplied)
  2. "type": "module"
  3. index.mts
  4. index.cts
  5. index.ts (will use the value of "type" from package.json)

So this error shouldn't happen as tsc should know to select the cjs version of the code via the exports field in package.json.

So I think this error means that for some reason tsc fails to acknowledge that there's also a cjs version to load.

I think something is wrong with the way types are defined in package.json or maybe even a bug in tsc.

The expected behavior (the way i understand it) is like so:

1. Source code index.ts

import Minisearch from "minisearch";

// ... Do stuff with minisearch

2. Run tsc and it transpiles to cjs:

I want it to compile to cjs since my ecosystem runs on cjs yet.

const Minisearch = require('minisearch')

// ... Do stuff with minisearch

3. tsc should take cjs version

Now tsc should look at the package.json -> "exports" -> "." -> "require" -> "./dist/cjs/index.cjs" variant.
And work as expected.

Note:

This should also work when you use *.mjs or "type": "module" since then the flow is:
source -> tsc transpiles to esm -> package.json -> "exports" -> "." -> "require" -> "./dist/es/index.js" variant.

@ItamarGronich
Copy link
Contributor Author

Hi @ItamarGronich , thanks a lot for opening this issue and for sending a pull request. Allow me the time to reproduce the bug and fully understand the implications (I will be quite busy in the next few days, and I admit that tsconfig and module resolution in JS/TS sometimes evolves faster than I can follow), and I will review and hopefully merge it.

Sadly, also i think that my PR doesn't solve the problem - although i thought it did.
So hold off of the merge for now.

@lucaong
Copy link
Owner

lucaong commented Apr 3, 2024

@ItamarGronich I believe the issue is that when "type": "commonjs" in package.json, TypeScript expects all source files with extension .ts in the application to be CommonJS (and ES module files to use the .mts extension instead). This mirrors the same behavior from Node (expecting .js files to be CommonJS, and requiring .mjs for ES modules).

This only affects the application files (the src/index.ts file in the reproduction example), not the library files, which have their own package.json. The error is triggered when tsc encounters an unexpected import inside src/index.ts. Therefore, I believe it's not an issue with MiniSearch, and should instead be solved in the application code by applying one of the following alternatives:

  • Using only the CommonJS require and module.exports in application files
  • Using the .ts (or .js) extension for CommonJS files, and .mts (or .mjs) for ES module files
  • Using only ES module syntax (import and export) and set "type": "module" in the application's package.json

At least this is my understanding of the issue, but feel free to comment further if you think there is actually an issue with MiniSearch.

@ItamarGronich
Copy link
Contributor Author

ItamarGronich commented Apr 3, 2024

I think that with tsc you can always use import / export, regardless of "type" field in the package.json.

What's changed with the "type" field is what format tsc compiles your code to.

  • if you have source files written in esm + "type": "commonjs" + "module": "NodeNext" -> tsc compiles your code to nodejs compliant cjs format.
  • if you have source files written in esm + "type": "module" + "module": "NodeNext" -> tsc comiles your code to nodejs compliant esm format.

@lucaong
Copy link
Owner

lucaong commented Apr 3, 2024

@ItamarGronich
Copy link
Contributor Author

ItamarGronich commented Apr 3, 2024

Yeah it's more complicated.

Actually, i was doing a bit of research and i found this really cute package: https://www.npmjs.com/package/@arethetypeswrong/cli

I was skeptical of how it would work at first but wow it's awesome.

itamar.gronich minisearch $ attw --pack .

minisearch v6.3.0

Build tools:
- typescript@^5.2.2
- rollup@^4.1.0
- @rollup/plugin-typescript@^11.0.0

❗️ The resolved types use export default where the JavaScript file appears to use module.exports =. This will cause TypeScript under the node16 module mode to think an extra .default property access is required, but that will likely fail at runtime. These types should use export = instead of export default. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseExportDefault.md

👺 Import resolved to an ESM type declaration file, but a CommonJS JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md

💀 Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md


┌───────────────────┬──────────────────────────────┬────────────────────────────┐
│                   │ "minisearch""minisearch/SearchableMap" │
├───────────────────┼──────────────────────────────┼────────────────────────────┤
│ node10            │ ❗️ Incorrect default export  │ 💀 Resolution failed       │
├───────────────────┼──────────────────────────────┼────────────────────────────┤
│ node16 (from CJS) │ 👺 Masquerading as ESM       │ 👺 Masquerading as ESM     │
├───────────────────┼──────────────────────────────┼────────────────────────────┤
│ node16 (from ESM) │ 🟢 (ESM)                     │ 🟢 (ESM)                   │
├───────────────────┼──────────────────────────────┼────────────────────────────┤
│ bundler           │ 🟢                           │ 🟢                         │
└───────────────────┴──────────────────────────────┴────────────────────────────┘

So this read was actually really helpful and i think i know how to solve this now.

@lucaong
Copy link
Owner

lucaong commented Apr 3, 2024

This seems to be a solution: #258

It seems that the actual content of the declaration file does not matter, but its extension does: a .d.ts file assumes the presence of a .js file, and same for .d.mts (assumes .d.mjs) and .d.cts (assumes .d.cjs). Before, the type declaration was .d.ts, which matched the ES file dist/es/index.js but not the CJS file dist/cjs/index.cjs. Therefore, I now generate separate declaration files for CommonJS and ES, with identical content, but different extensions, and properly list them in the exports section of the package.json.

Could you please try it out too, and possibly test out with the package you found? To test it out, build it with yarn build or npm run build, then copy the package.json and dist folder into the node_modules/minisearch folder in the reproduction example.

@ItamarGronich
Copy link
Contributor Author

Well the package doesn't like it.

itamar.gronich@ItamarGronich minisearch % attw --pack .

minisearch v6.3.1

Build tools:
- typescript@^5.2.2
- rollup@^4.1.0
- @rollup/plugin-typescript@^11.0.0

❌ Import resolved to JavaScript files, but no type declarations were found. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md

❗️ The resolved types use export default where the JavaScript file appears to use module.exports =. This will cause TypeScript under the node16 module mode to think an extra .default property access is required, but that will likely fail at runtime. These types should use export = instead of export default. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseExportDefault.md

💀 Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md


┌───────────────────┬──────────────────────────────┬──────────────────────────────┐
│                   │ "minisearch"                 │ "minisearch/SearchableMap"   │
├───────────────────┼──────────────────────────────┼──────────────────────────────┤
│ node10            │ ❌ No types                  │ 💀 Resolution failed         │
├───────────────────┼──────────────────────────────┼──────────────────────────────┤
│ node16 (from CJS) │ ❗️ Incorrect default export │ ❗️ Incorrect default export │
├───────────────────┼──────────────────────────────┼──────────────────────────────┤
│ node16 (from ESM) │ 🟢 (ESM)                     │ 🟢 (ESM)                     │
├───────────────────┼──────────────────────────────┼──────────────────────────────┤
│ bundler           │ 🟢                           │ 🟢                           │
└───────────────────┴──────────────────────────────┴──────────────────────────────┘

the reason is that export { ......... MiniSearch as default }; at the index.d.cts file.

This is because it's generated by a third party dts tool and not tsc.

@lucaong
Copy link
Owner

lucaong commented Apr 3, 2024

Unfortunately, I cannot seem to get the @rollup/plugin-typescript to generate a single .d.ts declaration file: it creates separate files for each source file instead. That's why I use rollup-plugin-dts, which on the other hand does not seem to generate the correct export syntax depending on the module type.

I wonder if Rollup is even needed for this. Maybe it could be used only for the UMD bundle, while the ES and CJS versions could be created with tsc alone.

@ItamarGronich
Copy link
Contributor Author

Unfortunately, I cannot seem to get the @rollup/plugin-typescript to generate a single .d.ts declaration file: it creates separate files for each source file instead. That's why I use rollup-plugin-dts, which on the other hand does not seem to generate the correct export syntax depending on the module type.

Why do you need a single dts file? why can't it be many?
Alothough, i think if you maybe use outFile instead of outDir you can maybe create a signle dts file? not sure. i know you can do that with tsc so maybe it can apply for @rollup/plugin-typescript.

I wonder if Rollup is even needed for this. Maybe it could be used only for the UMD bundle, while the ES and CJS versions could be created with tsc alone.

Yeah thought about it too. You don't necessarily have to bundle when building for node/bundlers.
Only for the browser if you're targeting the option to making it usable as a script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants