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: add types condition to the front of the exports #155

Closed
wants to merge 1 commit into from

Conversation

frankie303
Copy link

I coincidentally saw that there is an issue with TypeScript which could cause some potential bugs in libraries. I also saw that @Andarist was opening some pr-s (example) in popular libraries to fix the bug. So I just moved types to the top in exports.

So I think this pr should fix 🐛 Used fallback condition issues here.

@Andarist
Copy link

This is a "correct" fix to get rid of the "🐛 Used fallback condition". There are other issues with this particular package.json#exports though but they are there without this fix too:

// @moduleResolution: node16
// @module: commonjs

// 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("ts-pattern")' call instead.
//   To convert this file to an ECMAScript module, create a local package.json file with `{ "type": "module" }`.(1479)
import { match } from 'ts-pattern' // types: 4.2.2

TS playground

@gvergnaud
Copy link
Owner

gvergnaud commented May 6, 2023

Thanks for the PR @frankie303, and for pointing out this additional issue @Andarist. I tried fixing most problems surfaced by "Are the types wrong?" and here is what I've got so far:

image

I wasn't able to find a way to fix the ESM (dynamic import only) error though. If I understand correctly, if my types are declared/exported using the ESM syntax BUT the imported file is the CJS version, then typescript mistakenly thinks ts-pattern doesn't support CJS?

@Andarist
Copy link

Andarist commented May 6, 2023

It's not exactly that it thinks that mistakenly - it has some good reasons to think that. If you provide different implementations (through import+require conditions) then it only makes sense to provide different types as well (even if their content is pretty much the same).

Consider for example this case:

const { SomeClass } = require('lib')

const instance = new SomeClass()
console.log('is it the same?', instance instanceof (await import('lib')).SomeClass)

This logs false because you have different ESM/CJS implementations and TypeScript has to account for such things. This particular example can impact the control flow analysis and thus type narrowing etc.

@Andarist
Copy link

Andarist commented May 6, 2023

A potential fix for this is:

  • copy-paste your declaration files
  • change their extension to .d.cts
  • remove types and rely on "sibling lookup" or change exports to smth like { import: { types, default }, require: { types, default } }

@gvergnaud
Copy link
Owner

gvergnaud commented May 6, 2023

Thanks @Andarist, that makes sense. The only issue is that I'm not bundling my declarations into a single d.ts file, I have one declaration file for each input file and I don't want to copy/paste all of them by hand every time I want to make a new release :/

microbundle doesn't seem to have an option to generate d.cts in addition to d.ts files. If you know a good tool that would deal with generating declaration files for each different module type, that would be amazing!

@gvergnaud
Copy link
Owner

I'm going to merge #158 as it's already an improvement. Tested it with a few project configurations and it seems to work well

@Andarist
Copy link

Andarist commented May 6, 2023

I certainly wasn't implying that you should copy-paste this stuff by hand :p I imagine that a simple script could do that for you and that you could just add it as a step to your build/prepare scripts. Smth like this could work:

find dist -name '*.d.ts' | while read NAME; do mv $NAME ${NAME%.d.ts}.d.cts; done

Note that it's also not a bullet-proof solution (see Andrew's comment here) but most of the time... it's good enough.

Another "good enough" solution (although, IMHO, way less correct than copy-pasting declaration files) is to remove package.json#type. If you are shipping a "dual package" then declaring your package as a module isn't really buying you a lot. Note that this would make arethetypeswrong to classify your package as "masquerading as CJS" (and rightfully so) - but many think that it's OK-ish. I'm not a fan of this solution but I mention this for completeness.

microbundle doesn't seem to have an option to generate d.cts in addition to d.ts files. If you know a good tool that would deal with generating declaration files for each different module type, that would be amazing!

I didn't yet see any of the lib builders to actually do that. I can recommend using https://github.com/preconstruct/preconstruct/ . I might be biased here (since I'm sort of a partial maintainer of that project) but we put a lot of effort into making it as seamless as possible for variety of use cases and edge cases. Note that, for important reasons, we do not emit dual packages there. node ends up loading a CJS package (but sometimes through an ESM wrapper). This ensures that you never end up with the instanceof problem that I outlines above (and it's not exclusive to instanceof, it's also important for other identity-based things, like React context and more). And even though node sees it loads it as a CJS package we use a module condition to allow bundlers to see the ESM implementation.

@gvergnaud
Copy link
Owner

gvergnaud commented May 6, 2023

Smth like this could work

Well, I believe you would also need to change all imports of all module to include the cts extension, right?

Thanks for these explainations though. I think I'm just going to leave the nodenext + commonjs combination unsupported for now. It doesn't seem very common, and people can easily fix this on their side by either switching to moduleResolution: bundler, or to es modules

@Andarist
Copy link

Andarist commented May 6, 2023

Well, I believe you would also need to change all imports of all module to include the cts extension, right?

Ah, ye - right, good catch. This is avoidable if you nest your CJS files in a directory with an extra package.json with this simple content:

{ "type": "commonjs" }

I think I'm just going to leave the nodenext + commonjs combination unsupported for now. It doesn't seem very common,

For what it's worth - I ran into this problem in the past, just with different packages than ts-pattern 😉

@gvergnaud
Copy link
Owner

gvergnaud commented May 7, 2023

I think I finally have got something that works well enough in PR #160

https://arethetypeswrong.github.io/?p=ts-pattern%404.2.4-test.0

I went with some sed magic to include .d.cts files to the build:

files=$(find dist -type f -name "*.d.ts");

# Loop through the declaration files
for file in $files; do
    # Update imports to include the '.cjs' extension
    sed -E "s/(.*)from '([^']*)'/\1from '\2.cjs'/g" "$file" > "${file%.d.ts}.d.cts"
done

And I updated the exports map to:

"exports": {
    ".": {
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      },
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.mjs"
      },
      "types": "./dist/index.d.ts",
      "default": "./dist/index.mjs"
    },
    "./package.json": "./package.json"
  },

I'm by no means a bash expert, so please let me know if you catch problems with this code :)

@Andarist
Copy link

Andarist commented May 7, 2023

Yeah, I'm not a bash expert either 😅 I would just rewrite this into a JS script for readability and maintainability.

Despite passing all checks on arethetypeswrong - this isn't currently consumable from ESM:

node_modules/ts-pattern/dist/index.d.ts:1:26 - error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Consider adding an extension to the import path.

1 import * as Pattern from './patterns';
                           ~~~~~~~~~~~~

node_modules/ts-pattern/dist/index.d.ts:2:23 - error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Consider adding an extension to the import path.

2 export { match } from './match';
                        ~~~~~~~~~

node_modules/ts-pattern/dist/index.d.ts:3:28 - error TS2834: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Consider adding an extension to the import path.

3 export { isMatching } from './is-matching';

To observe this you might want to disable skipLibCheck. Alternatively, you can verify this on this TS playground. You can play around with this to see that match is just an unresolved symbol and not a signature with types or just compare with the displayed value here.

I think that this should be reported by arethetypeswrong as a severe problem and I filled a bug report here: arethetypeswrong/arethetypeswrong.github.io#24

To fix this you can just include explicit extensions in both of those declaration "sets". It's not that .d.cts needs them but .d.ts doesn't.

Small nit: it probably doesn't create any problems in this setup but using non-matching extensions between implementation and declaration files really bugs me. It would be best to use either .js+.d.ts or .mjs+.d.mts 😉

@gvergnaud
Copy link
Owner

gvergnaud commented May 8, 2023

Thanks, I added explicit .js extensions to my .d.ts files. Also followed your advice to use .js+.d.ts instead of .mjs+.d.ts ;)

See https://arethetypeswrong.github.io/?p=ts-pattern%404.2.4-test.1

Do you know if explicit extensions in d.ts files is supported by relatively old typescript versions too, like say 4.5?

@Andarist
Copy link

Andarist commented May 8, 2023

Do you know if explicit extensions in d.ts files is supported by relatively old typescript versions too, like say 4.5?

I can't say that I'm like 100% sure - but I think they are supported just fine. You can verify here that it works even with 4.1: TS playground (I didn't bother testing other versions :P)

@gvergnaud
Copy link
Owner

closing in favor of #158 and #160

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 this pull request may close these issues.

3 participants