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

Migrate to ESM-first #608

Closed
SmashingQuasar opened this issue Jun 28, 2023 · 16 comments · Fixed by #613
Closed

Migrate to ESM-first #608

SmashingQuasar opened this issue Jun 28, 2023 · 16 comments · Fixed by #613
Labels

Comments

@SmashingQuasar
Copy link

SmashingQuasar commented Jun 28, 2023

Hey!

So this may seem like a feature request, but it actually falls under the bug category in my opinion.

As of today, umzug is written and built in CJS.
The problem is, this breaks applications written in pure ESM because umzug is using a dynamic require.
This problem is currently affecting several ORMs (including sequelize).

I think migrating to ESM-first is necessity since ESM supports importing any kind of module unlike CJS.

I am willing to write a PR to that effect if reviewers are available to check it.
This will obviously create many changes within this dependency. It should not, however, create any breaking change.
Let me know if you are willing to migrate and if you want a PR implementing this change.

@ephys
Copy link
Member

ephys commented Jun 28, 2023

I'm personally very pro-esm, and umzug is one of those libraries I think could be fully converted to ESM (dropping support for CJS) without being a huge problem because it's often used indirectly.

The sequelize cli will be undergoing a complete rewrite in ESM, so umzug going ESM would not be a problem for the cli team.

That being said, umzug can be used with ESM, but you need a custom resolver to replace the default resolver: https://github.com/sequelize/umzug/blob/586675894c3b099bb50ad2f07e46d89277ee29bb/examples/2.es-modules/umzug.mjs

I would argue that, at a minimum, the default resolver should be changed to use a dynamic import() instead of a dynamic require().

@mmkal
Copy link
Contributor

mmkal commented Jun 30, 2023

I don't want to drop commonjs support, but yes, I agree that esm should work out of the box, although as @ephys mentioned it does work already with a little extra work. For the current major version, it would probably need to be limited to looking for a .mjs extension.

In general, I want to make sure umzug stays framework agnostic and unopinionated - sequelize is an important consumer but not the only one.

@ephys
Copy link
Member

ephys commented Jun 30, 2023

sequelize is an important consumer but not the only one.

Absolutely, please take my comment as coming from a user of umzug that pushes for ESM but is unrelated to umzug even if we're in the same org

@SmashingQuasar
Copy link
Author

I don't want to drop commonjs support, but yes, I agree that esm should work out of the box, although as @ephys mentioned it does work already with a little extra work. For the current major version, it would probably need to be limited to looking for a .mjs extension.

In general, I want to make sure umzug stays framework agnostic and unopinionated - sequelize is an important consumer but not the only one.

CJS is natively supported by ESM, the opposite is not true. This is due to the asynchronous nature of ESM.
If you want umzug to be agnostic and unopinionated, then it should be written in ESM.
Moving on to ESM will not drop CJS support, it will simply make ESM work so I believe that migrating to ESM would meet your expectations.

@ephys
Copy link
Member

ephys commented Jun 30, 2023

By "drop support for CJS", what we mean is that CJS codebases won't be able to require() this library anymore, not that this library won't be able to import CJS migration files.

Dynamic import is available to both CJS and ESM, so we can support ESM migration files without dropping support for being imported by CJS codebases by replacing the dynamic require() by a dynamic import(), like here

But doing this is indeed a breaking change, because hooking import (for things like typescript support) uses the loader API instead of hooking require

@SmashingQuasar
Copy link
Author

I see, you mean that umzug is actually using hooks on require to achieve it's purpose?

@mmkal
Copy link
Contributor

mmkal commented Jul 4, 2023

I would like umzug to take a side in the holy war of support-commonjs vs don't-support-commonjs, but not in the holy war of esm vs commonjs - esm is almost certainly the way of the future, but some OSS maintainers don't want to support developers who live in the present and are stuck with commonjs. I do want to support such developers, partly because I'm one of them in my day job.

It shouldn't be too hard to support everything. We can start using microbundle or similar to compile the package, so it has a commonjs and an esm import option. And we can pretty easily add support for .mjs in the current major version. If someone wants to open PR(s) for either of these things, feel free to. Otherwise I will aim to do it some time in July.

In the next major, we could default to assuming .js is esm instead of commonjs and look for .cjs for commonjs. But I'm in no particular rush to make that change.

@ephys
Copy link
Member

ephys commented Jul 5, 2023

I very, very strongly recommend that you do not provide separate builds for esm and commonjs. That inevitably leads to issues as both version end up loaded in the same application but are incompatible with each other. I opened yet another bug report about this today in Apollo server.

If you want to support being imported by both cjs and esm, write the library in cjs and provide an esm wrapper that declares the named exports. If you need a real life example, look at how Sequelize 7 handles this.

You also don't need to assume anything regarding whether .js is esm or cjs. Dynamic import() will import either correctly based on the extension and the package.json. All you need to do is to import the migration files with import instead of require, no extension check necessary.

I see, you mean that umzug is actually using hooks on require to achieve it's purpose?

Users could have require hooks to run the file through something like babel or ts-node at the time it gets loaded

@mmkal
Copy link
Contributor

mmkal commented Jul 5, 2023

If you want to support being imported by both cjs and esm, write the library in cjs and provide an esm wrapper that declares the named exports. If you need a real life example, look at how Sequelize 7 handles this.

This is pretty close to the status quo already. Despite it being mentioned earlier in this thread I forgot that we have an example of using esm, both for the Umzug instance and the migration files themselves. It requires only a tiny bit of extra work, and even supports mixed .cjs and .mjs migrations. https://github.com/sequelize/umzug/tree/main/examples/2.es-modules

I see this issue as a feature request, to make the above scenario boilerplate free (ideally, no need for createRequire, and no need to write a custom resolver).

I very, very strongly recommend that you do not provide separate builds for esm and commonjs. That inevitably leads to issues as both version end up loaded in the same application but are incompatible with each other. I opened yet another bug report about this today in Apollo server.

You are probably right - there may be no need for microbundle (partly because umzug in general doesn't need "bundling" anyway), and a wrapper.mjs will work. I will also test out just using import(...) in either case.

(Re the issue you opened, I'm guessing you're referring to this one? This is helpful, thank you!)

@SmashingQuasar
Copy link
Author

I very, very strongly recommend that you do not provide separate builds for esm and commonjs. That inevitably leads to issues as both version end up loaded in the same application but are incompatible with each other. I opened yet another bug report about this today in Apollo server.

I have been bundling my code and libraries at work for both ESM and CJS for years even on projects with hundreds of files without having any issue. May I suggest that the libraries you are having issues with may not know how to bundle ESM and CJS in different exports?

Users could have require hooks to run the file through something like babel or ts-node at the time it gets loaded

It is impossible to support every niche use case. Production should be the target and I sincerely, deeply, profundly hope that nobody is running ts-node in production. I know people are unfortunately.

If you want to support being imported by both cjs and esm, write the library in cjs and provide an esm wrapper that declares the named exports. If you need a real life example, look at how Sequelize 7 handles this.

I don't believe that to be a proper solution. This is how we end up having so many libraries still written in CJS in 2023 because people do not want to move forward.

I'll let you people decide how you want to handle this situation as we seem do have relatively opposite views on the matter. If you want help to migrate to ESM, I'll gladly help you. 👍

@ephys
Copy link
Member

ephys commented Jul 28, 2023

I have been bundling my code and libraries at work for both ESM and CJS for years even on projects with hundreds of files without having any issue. May I suggest that the libraries you are having issues with may not know how to bundle ESM and CJS in different exports?

Please see https://nodejs.org/api/packages.html#dual-commonjses-module-packages, and apollographql/apollo-server#7625. This is a very real issue, you just haven't encountered it yet.

It is impossible to support every niche use case.

That's still a breaking change, and should require a major release

I don't believe that to be a proper solution. This is how we end up having so many libraries still written in CJS in 2023 because people do not want to move forward.

If we want to support both CJS and ESM, it is our only option. I don't understand why it bothers you that the library uses CJS internally if it has no observable impact on you as an ESM user. The only people it impacts are the ones maintaining the library

@JSteunou
Copy link

Searching on current status of ESM support at umzug I found this discussion. Just dropping my 2 cents: in a full ESM project, using top level await, running migrations with umzug fails because it is CJS.

ERROR: Top-level await is currently not supported with the "cjs" output format

That is a bummer in 2023. One vote for ESM.

@SmashingQuasar
Copy link
Author

I have been bundling my code and libraries at work for both ESM and CJS for years even on projects with hundreds of files without having any issue. May I suggest that the libraries you are having issues with may not know how to bundle ESM and CJS in different exports?

Please see https://nodejs.org/api/packages.html#dual-commonjses-module-packages, and apollographql/apollo-server#7625. This is a very real issue, you just haven't encountered it yet.

Sorry, I forgot to answer you when I read your response.
To me, this problem is a consequence of having way too many dependencies. Nowadays people will install 80+ dependencies for their project. It's no wonder there are issues. If people do that, then in my opinion, it's not really a Node.JS issue but a development and applicative architecture issue.

It is impossible to support every niche use case.

That's still a breaking change, and should require a major release

I see no issue with major releases.

I don't believe that to be a proper solution. This is how we end up having so many libraries still written in CJS in 2023 because people do not want to move forward.

If we want to support both CJS and ESM, it is our only option. I don't understand why it bothers you that the library uses CJS internally if it has no observable impact on you as an ESM user. The only people it impacts are the ones maintaining the library

CJS is more a reflection of a refusal of change. Every Node.JS developers knows (or is lying to themselves) that CJS will eventually disappear. Maybe not in the following years but it is bound to disappear. I will never understand engineers or anyone involved in science that simply pushes back against progress. Aren't we supposed to be the one pushing progress? To me, by hard-rooted in CJS and refusing to change is not a good sign for the future of the library. It's not like ESM was released yesterday, it's been over 7 years now. I just want to emphasize that I am not meaning any harm or offense here. I am just voicing my disappointment in the JS community overall that happens to be somewhat reflected here.

@mmkal mmkal pinned this issue Nov 6, 2023
This was referenced Nov 6, 2023
mmkal added a commit that referenced this issue Nov 6, 2023
Switching from jest to vitest. This _might_ make the move to
[first-class-ESM](#608) easier,
since there'll be less dependency hell to worry about (jest, ts-jest,
...)

---------

Co-authored-by: Misha Kaletsky <mmkal@users.noreply.github.com>
Copy link

github-actions bot commented Nov 6, 2023

Released (as beta) in v3.5.0-0.

@mmkal
Copy link
Contributor

mmkal commented Nov 15, 2023

@SmashingQuasar I am hopeful that #613 is a satisfactory solution to this.

It's worth noting that it's only possible for #613 to solve the use-case here without breaking others' use-cases by not migrating to ESM-first. (Hence the title "ESM support" rather than "ESM migration"). It's worth reading @ljharb's comments in eslint/rfcs#88 which IMO articulate why well. As the maintainer of umzug (a package) - I think sticking with CJS is the right call. Nothing about that decision prevents you, a user of umzug (and presumably maintainer of an application) from modernizing to ESM. That eslint RFC also covers the (non-)benefit of applying "type": "module" to the package.json which I opted not to do either.

@ljharb sorry for the @ mention out of the blue, no need to respond!

@mmkal mmkal closed this as completed in #613 Dec 8, 2023
mmkal added a commit that referenced this issue Dec 8, 2023
Closes #608

This adds some esm-vs-commonjs "smartness" to umzug:

1. use `require` for `.cjs` migrations and `import` for `.mjs`
migrations (and their typescript equivalents)
2. use `require` for `.js` migrations _if_ `typeof require.main ===
'object'`, and `import` to `.js` migrations otherwise
3. use the same criteria to create (c)js vs mjs templates when creating
migration files
4. add `"moduleResolution": "Node16"` to tsconfig.lib.json to make sure
`import(filepath)` doesn't get transpiled into
`__importStar(require(filepath))` (see
[here](microsoft/TypeScript#43329 (comment))
and
[here](microsoft/TypeScript#43329 (comment)))

Tests/examples:

- add a `vanilla-esm` example to make sure using `import` / top-level
await works
- add a step to the `test_pkg` job to make sure vitest isn't hiding
gnarly import problems - this is installing the compiled library as a
`.tgz`, and with no other dev/prod dependencies like vitest or ts-node
having been installed, so should be very close to what end users will do

Didn't:

- add a wrapper.mjs file in the compiled folder as suggested in
#608 (comment),
mostly just because it didn't seem to be necessary? It seems to work
fine when imported from an ES-module, using top-level await, etc., even
though umzug is itself a commonjs module.

<details>
<summary>original body</summary>

~Related to #608 - although does not close it.~

~This adds built-in support for `.mjs` and `.mts` files. `.mjs` should
"just work" - write migrations as ESM modules and they'll be imported in
the right way. For the current major version, `.js` will continue to be
assumed commonjs. ESM-fans will just have to type that extra `m` in
their filenames.~

~This PR _doesn't_ add a wrapper file so that the umzug library itself
can be imported as an ES module. That can be done in a follow-up PR. In
the meantime, ESM users can use `createRequire` as in the [existing ESM
example](https://github.com/sequelize/umzug/tree/main/examples/2.es-modules).~
</details>

---------

Co-authored-by: Misha Kaletsky <mmkal@users.noreply.github.com>
Copy link

Released in v3.5.0.

@mmkal mmkal unpinned this issue Mar 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants