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

improving esm support #102

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open

improving esm support #102

wants to merge 29 commits into from

Conversation

alexreardon
Copy link
Owner

@alexreardon alexreardon commented May 22, 2024

  • adding esm outputs (through "module" field and "exports")
  • still has cjs support through "main" field
  • moving from es5 target to es2022 target (will likely need a major for this)
  • now using esbuild for building
  • added "engines" to package.json

@alexreardon alexreardon requested a review from Andarist May 22, 2024 23:09
Copy link

github-actions bot commented May 22, 2024

size-limit report 📦

Path Size
dist/index.js 0 B (-100% 🔽)
./dist/bind.js 95 B (+100% 🔺)
./dist/bind-all.js 211 B (+100% 🔺)

@alexreardon alexreardon changed the title moving package to esm improving esm support May 23, 2024
"compilerOptions": {
"outDir": "dist",
"target": "es5",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shifting from es5 to es2022 (likely will be a major)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely broken. You mention in the description of the PR that it introduces exports but it has not been done. Even if it would, you are specifying type: "module" in this package.json and yet your "main" points to a CJS file with .js extension. That can't work without an additional package.json "in-between".

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Not adding the "exports" was an oversight. How is it looking now?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better, question @Andarist: what changes do you think I need to make? I find it's hard to know what is the right way to make sure things will work for folks

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No pressure for you to reply though. I'll keep researching :)

Copy link
Owner Author

@alexreardon alexreardon May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my reading of the node module documentation is that "type": "module" should mean that I can use esm everywhere.

But I don't know if that is correct, or if that is only correct from a particular node version

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I don't know if that is correct, or if that is only correct from a particular node version

it's correct since node 12.

if that is correct, I should be able to drop the cjs build and just have an esm build, and have the "main" field point to that

yes, if you don't want to maintain compatibility with the CJS consumers. It might still be good to use exports in such a case as that limits what can be imported from your package (exports is a strict allowlist for accessible files/specifiers). In fact, with that you don't even need main as exports can substitute it just fine

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say "cjs" consumers, do you mean folks on node using "cjs"? (require('bind-event-listener')

Would this package going esm prevent require('bind-event-listener') usage?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To take a step back: the intention of this PR is to give ESM consumers a better time. Right now esm consumers were relying on the bundler to wrap the cjs outputs with some lame code to get it working on esm.

Ideally folks using node or bundlers would just be able to have a good time 😊

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I have some reading to do: https://nodejs.org/api/packages.html#dual-commonjses-module-packages

Seems like I might be able to get away with just making this package esm only

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this package going esm prevent require('bind-event-listener') usage?

Yes. If you are going ESM-only then you make it impossible to require this package. At least until this experimental feature lands: https://nodejs.org/en/blog/announcements/v22-release-announce#support-requireing-synchronous-esm-graphs

package.json Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
"sideEffects": false,
"type": "module",
Copy link
Owner Author

@alexreardon alexreardon May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Andarist I have gone for the approach outlined in the Node conditional exports documentation.

So the package is listed as a "type: "module", but the when "require" is being used we provide CommonJS files by using the .cjs file extension

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am doing some testing now, but this seems really promising

package.json Outdated
"exports": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs",
"types": "./dist/esm/index.d.ts"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are two problems here:

  1. conditions are exports are matched from the top to the bottom. It doesn't make sense to have types last here. This condition should usually be first. TS will still try to match import and require conditions when appropriate and it might never reach types in a manifest like this (it would reach it here as a sibling .d.ts file of your import condition but that's coincidental)
  2. TS will still think that this package can't be required. If you are shipping .cjs file here then you should also consider shipping .d.cts file and create conditions structure like { import: { types, default }, require: { types, default } }

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome thanks. Do you know how I can get tsc to generate .d.cts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't 😂 Or rather, it generates its outputs based on your inputs. Your input is a .ts file (not an .cts file) so it won't guess that you want some extra .d.cts file here. The thing to understand here is that TS won't really understand that you are trying to ship a dual package from a single set of sources so it won't help you anyhow to generate multiple outputs based on a single input.

You can either cp && mv your .d.ts or use some tool that handles this for you (tsup or tshy)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i've done it! I am now using tsup. It looks like the declaration files for esm and tsc are the same (which is what I was getting caught on before - i thought the cjs ones needed to be formatted differently).

Changes:

  • now using tsup to build dist (and declaration files)
  • "exports" now pointing to the correct types for given bundles (might not be strictly needed, but seems helpful)
  • added @arethetypeswrong/cli to validate that the package is setup correctly

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keen to know what you think @Andarist

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bind-event-listener-3.0.0.tgz

Attached the npm pack for reference

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package.json Outdated
"check": "yarn check:typescript && yarn check:prettier && yarn check:dist",
"check:prettier": "prettier --debug-check $npm_package_config_prettier_target",
"check:typescript": "tsc --noEmit && tsc --noEmit --project ./test/tsconfig.json",
"check:dist": "attw --pack . --ignore-rules no-resolution",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to add --ignore-rules no-resolution as I haven't added a node10 mechanism. I could fix that by adding a "main" field that points to the CJS output - but I am not sure that I should

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added "main" for now - but not sure if I should keep it

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this PR has changed so much 🤭

// https://github.com/egoist/tsup/issues/953#issuecomment-2132576167
// ensuring that all local requires in `.cjs` files import from `.cjs` files.
// require('./path') → require('./path.cjs') in `.cjs` files
name: 'fix-cjs-require',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that u need any kind of fixup here. What would break if this was removed?

Copy link
Owner Author

@alexreardon alexreardon May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using bind-event-listener in a plugin for eslint@8 which was failing without this fix 😢

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. I'd probably stay away from type: module here since you are creating a dual package anyway. That would fix your issue but it would introduce the mirror issue in .mjs files (it just perhaps would be less surprising there that you need to add extensions in your imports)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang. I was keen to make cjs more of an after thought which I could drop. Maybe easiest to go the other way 😭

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, since u already have those workarounds in place - it's probably fine the way it is. I only have a vague recollection of some React Native versions having problems with non .js files in main but perhaps u can ignore it. Similarly, IIRC, old CRAs were loading .cjs as files and not javascript sources (as in, they were replacing them with URLs to the generated assets, like they do with .png files and more)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking of leaving it is as "it is an esm module, but it has some cjs fallbacks when we need it". If it doesn't work out, we can go full esm.

Aside: publishing packages that "just work" is a nightmare right now 😭

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any objections @Andarist to merging in it's current state? My open thought is whether I should update "target" to be "ES6" 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d bump it even higher although this lib is so small that it probably doesnt use any es2016+ features anyway ;p

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I have target set at es2022

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't test it at all but the package.json configuration looks fairly OK to me now

"esModuleInterop": true,
"allowSyntheticDefaultImports": true
"skipLibCheck": true,
"target": "es2022",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i'll update this to be "target": "es6"

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.

2 participants