Skip to content

docs: add migration to esm post #54

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

Merged
merged 9 commits into from
Mar 20, 2025
Merged

docs: add migration to esm post #54

merged 9 commits into from
Mar 20, 2025

Conversation

43081j
Copy link
Contributor

@43081j 43081j commented Mar 19, 2025

No description provided.

@43081j
Copy link
Contributor Author

43081j commented Mar 19, 2025

@patak-dev could you help me with a cover image? 👀

@joyeecheung could you double check that the require(esm) section isn't nonsense? and if you have a useful link of where most of it is explained, i could link to it

Copy link

netlify bot commented Mar 19, 2025

Deploy Preview for shiny-salamander-b16dd2 ready!

Name Link
🔨 Latest commit 9fa4956
🔍 Latest deploy log https://app.netlify.com/sites/shiny-salamander-b16dd2/deploys/67dbec2fb95996000830691a
😎 Deploy Preview https://deploy-preview-54--shiny-salamander-b16dd2.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@43081j 43081j merged commit a754aaf into main Mar 20, 2025
4 checks passed
@43081j 43081j deleted the esm-mig branch March 20, 2025 10:24
Copy link

@joyeecheung joyeecheung left a comment

Choose a reason for hiding this comment

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

Sorry for the late reply, was swamped by meetings this week...

import './foo'

// after
import './foo.js'

Choose a reason for hiding this comment

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

Since the title says

Migrating a CommonJS package

Then I assume foo.js is a CJS module? I think in that case, it must be either renamed to foo.cjs if "type": "module", or the content of foo.js itself must be migrated to be authored in ESM.

If foo.js still contains CJS code, I am not sure if this really counts as migration - it just adds an extra ESM wrapper around the code authored in CJS, which isn't necessarily in the first place because import cjs already works, and this is just import esm-wrapper where esm-wrapper imports cjs again?

If it means foo.js must also be rewritten to be in ESM format, then from my reading this is missing a sentence to clarify that "first, rewrite foo.js to be in ESM format".

Copy link
Contributor Author

@43081j 43081j Mar 20, 2025

Choose a reason for hiding this comment

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

It is an es module

This snippet is assuming you already converted your syntax to esm

I'll try make that clearer

Choose a reason for hiding this comment

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

That makes sense now. Not sure if it's just me not reading thoroughly enough, it does feel that this assumption is missing somewhere...


Those shipping JavaScript often just use a bundler like [esbuild](https://github.com/evanw/esbuild) to create two bundles (or one and the sources).

To migrate from these setups, we mostly need to do the same steps as migrating a CommonJS package from above.
Copy link

@joyeecheung joyeecheung Mar 20, 2025

Choose a reason for hiding this comment

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

Not sure how tsup actually manages the exports, but from my understanding, usually migrating a dual module means adding module-sync to the exports nodejs/node#54648 if it's not yet ready to drop support of older Node.js versions.

So for example, if the exports map originally looks like this (it uses node to provide additional fallback for non-node environments, like what babel is doing):

{
  "type": "module",
  "exports": {
    // On Node.js, provide a CJS version of the package transpiled from the original
    // ESM version, so that both the ESM and the CJS consumers in the same graph get
    // the same version to avoid the having two versions of the same package
    // conflicting with each other a.k.a. package hazard.
    "node": "./dist/index.cjs",
    // On any other environment, use the ESM version.
    "default": "./index.js"
  }
}

To prepare to drop dual shipping (say v1 is still dual-shipping because it is supporting older versions of Node.js, but it still wishes to tell Node.js > 20 to always pick the ESM version, no matter it's required or imported)

{
  "version": "1.4.0",
  "type": "module",
  "exports": {
    "node": { // Packages can drop this special case as they drop support for older Node.js
      // On new version of Node.js, both require() and import get the ESM version
      "module-sync": "./index.js",
      // Supply ESM to bundlers for better generated code
      "module": "./index.js",
      // On older version of Node.js, where "module" and require(esm) are not supported,
      // use the transpiled CJS version to avoid dual-module hazard.
      "default": "./dist/index.cjs"
    },
    // On any other environment, use the ESM version.
    "default": "./index.js"
  }
}

To actually drop dual shipping (say on v2 which only supports Node.js v20.x and above):

{
  "version": "2.0.0",
  "type": "module",
  "exports": {
    ".": "./index.js"
  }
}

There are more complex cases where people define both require and import exports, which needs extra wrapping to avoid the dual package hazard. But in those cases the migration (without bumping major to avoid changing Node.js support matrix) would still boil down to: add a module-sync exports (which must preceed require, if there is one) that provides the ESM exports, so that both require and import picks the ESM version. And when it bumps major to drop support for Node.js <20, simply remove all these mess and just point the exports to the ESM, without caring how it's loaded.

Copy link
Contributor Author

@43081j 43081j Mar 20, 2025

Choose a reason for hiding this comment

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

This is written under the assumption you just go all in on esm. Which presumably means you have one very simple export (your last example), which works in most node versions as esm but only 20.x and above as cjs

We don't need to care about "module sync" then, right?

Copy link

@joyeecheung joyeecheung Mar 20, 2025

Choose a reason for hiding this comment

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

I think module-sync primarily concerns whether the package is still supporting Node.js < 20. If they already decide to bump major and drop support for Node.js < 20 then yes they do not need to care about "module-sync" (though they might still want "module-sync" to be the last patch they add to the release that still supports < v20, so that when the older release is loaded by Node.js > 20, it's actually the ESM that will be required) . Maybe a note about Node.js version support would help clarifying that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

node < 20 would still support it without module-sync would it not? as an ES module

it just means you can't require(esm) it. which is fine in this all-or-nothing strategy

Copy link

@joyeecheung joyeecheung Mar 22, 2025

Choose a reason for hiding this comment

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

node < 20 would still support it without module-sync would it not? as an ES module

Yes, it can be imported, just not required. Though for packages that adhere to the "when dropping support for older Node.js versions, bump major" principle, it affects what they could do with the last major release before the migration. For example:

  • The package is dual-shipped in v1, which supports Node.js 18 and above
  • The package wants to become ESM-only, so it just discards the CJS distribution, ditch the transpilation step, and ship in ESM form as-is, this is now v2 of the package, since it will no longer be require-able on Node.js 18.

The article has covered the two points above, though there is a remaining question:

  • Is the package still maintaining v1 for Node.js > 20 for some time, or are they completely abandoning it and moving on to v2?

If they are still maintaining v1 (e.g. if v2 is not just about shipping ESM - which is likely not observable for users - but also has some other significant and observable major changes, which means users can still cling on to v1 for a while), then "module-sync" allows them to specify that Node.js > 20 is always loading the ESM version to reduce dual-package hazard on v1. Or, even if they are abandoning v1, this addition of "module-sync" might be the last thing they'll release to v1 to reduce the issues that may come from the dual-package hazard when v1 is loaded in Node.js > 20.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense. In the packages that migrated so far as part of this coordinated effort, they did all just release new majors which contained nothing else. Just a switch to esm only

So none needed module-sync since it was generally understood that anyone who wants to require it from now on needs node 20 and above

Choose a reason for hiding this comment

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

Oh I see, is this article just about "what have been done", instead of "what should be done if you are going to migrate to ESM"? If it's the former, then it makes sense 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's about what the community is doing to help while also briefly explaining how you can also migrate your own packages

You would only use module-sync if you're continuing to ship a dual package, right? This is mostly pushing people to ship only esm. So what use does it have then? If you're shipping esm only, you're accepting that anything below 20 can't require it from what I understood

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.

4 participants