chai: ERR_REQUIRE_ESM in v5

Hi there,

I don’t have all the details, as I don’t have access to my workstation right now, but our CI is failing with this error for all our projects using chai when trying an upgrade to chai@5. These projects are commonjs modules using require('chai') everywhere in tests, running under Node 20.

Is chai now ESM-only?

I can gather more details if required, feel free to ask.

Thanks, David

About this issue

  • Original URL
  • State: closed
  • Created 6 months ago
  • Reactions: 5
  • Comments: 41 (16 by maintainers)

Commits related to this issue

Most upvoted comments

chai could have waited until Node 22 released. Because it’s going to get the support of interoperability between CJS and ESM at runtime. (I ranted about it here.)

Otherwise, chai v5 is going to break a lot of CI/CD pipelines out there.

To clarify some points:

  • Chai 4 is still available and supports older non-standard module formats like cjs.
  • Chai 4 has been very stable and has been available for over 5 years now.
  • Chai is adhering to the semver contract; version 5 is a breaking change which drops support for non-ESM.
  • The headline change in Chai 5 is ESM. Most other important changes have been backported to 4.x so the only motivation to move to 5 today is to get native support for ESM.
  • If you’re using a transpiler or bundler system that transpiles native ESM to something else - for example babel-plugin-transform-modules-commonjs then this won’t work with Chai 5 as it is converting standard ESM to non-standard formats.
  • To summarise the above points, if you do not want to leverage native ESM there is little point in upgrading to Chai 5.
  • If your CI systems are automatically upgrading packages across major versions, or your package.json is not using ^ or ~ prefixes, you may want to address that as you’ll likely encounter issues from many packages in the ecosystem which adhere to the semver contract.

To talk about the motivation for this change, especially from the lens of the ecosystem of available build tools: there is a complexity trade off for libraries like Chai to support each of these build tools, and it becomes somewhat of an N*M problem. Standards allow us to target a single syntax to reach the broadest support, and will be an inevitable shift in the JS ecosystem, it is just a matter of “when”. Version 5 is chai’s “when”. Certainly if we are the first to support ESM it would cause undue friction, but if we were last it would also cause undue friction. Packages like @esm-bundle/chai already exist which demonstrate the friction is there today. There is no time we could have made this change that would satisfy all users. The Chai team is very confident we’ve made the right decision so support ESM only, and Chai >5 will continue to support only ESM (or whatever module system the EcmaScript standardises) indefinitely.

OK.

That’s clearly sad news and something I personally don’t understand considering the plethora of build tools (from the top of my mind https://github.com/egoist/tsup and https://github.com/unjs/unbuild at least) that can double-build at near 0 cost… Lots of very large projects are still commonjs, Node still has first class support for commonjs, etc.

Anyway, thanks for confirming and long live to Chai 👍

Cheers, David

Probably should have removed all the references to require('chai') from the documentation as part of this change!

It is esm only in 5.x

You can still use a dynamic import to import it in node at least afaik, but should probably just stick to 4.x until you can migrate to esm (feature set shouldn’t be much different)

In case anyone else comes to this thread trying to use mocha and chai together, using cjs modules;

For me, await import('chai') broke tests due to its asynchronous nature - none of the tests ran. This was regardless of whether I used async/await on my describe() and it() calls. I had to make use of mocha’s before() for this:

let expect;
before(async () => {
    expect = (await import('chai')).expect;
});

I hope from this issue it’s apparent how this change may have potentially cost many hours of human effort on part of all the developers who use this module in their unit tests. The issues surrounding ESM and CJS are known, plentiful, and widespread; IMO all of the very popular node.js modules should be responsible for ensuring things go smoothly with both systems, regardless of the opinions of the maintainers on ESM vs CJS.

This is an open source project and so it is up to the contributors we have to resolve issues and add features. The core maintainers (which is a group that changes over time) may raise PRs and make changes, but they can also mentor and guide contributors to do the same. This is to say if you are aware of a security fix that needs to be back-ported to 4.x.x you can raise a PR for it, against the 4.x.x branch, and a core maintainer will steer it in the right direction.

All the while the 4.x.x branch is open I’m happy to approve/merge PRs and cut releases for Chai 4. I can’t give a definitive answer on when we’ll delete that branch because ultimately it’ll be “when it feels right”. Certainly the majority of feature development will be done in Chai@5.

We typically get about 9MM downloads a week. Last week was particularly low because of the holidays but we saw about 4MM downloads. Of that 4MM, 100k of that is chai@5 (2.5%), about 300k for chai@3 (7.5%), and 3.6MM for chai@4 (87.5%). We do not get issues for Chai@3 and I don’t think we’d cut a new release for the 3.x series, so one heuristic you could look at is this. Should Chai@4 reach 7.5% of our version distribution, I could foresee it being EOL then, but that’s not the case today.

you are right, i’ll close this.

for anyone bumping into this error

As of 5.0.0, chai only ships with ES modules going forward (no longer commonjs).

For those of you with ES module packages

Everything should work as you’d expect. You’re probably in this category if:

  • you have "type": "module" in your package.json (i.e. it is an ESM project)
  • you use a bundler which understands ES modules (e.g. esbuild)

For those of you with CJS packages

You’re in this category if you don’t follow any of the previous structures, and/or you use typescript with "module": "commonjs".

In this case, you have a couple of options:

  • Use a bundler like esbuild, which will bundle your project into one big commonjs module (therefore making chai imports work in commonjs)
  • Migrate to es modules
  • Use a dynamic import (e.g. const chai = await import('chai'))
  • Stay on 4.x for now

If you can’t use a bundler at that point in your project, it is probably best to stay on chai 4.x until you move to ES modules one day (if ever).

ts-node

If you’re using ts-node, it has support for ES modules and more information here.

Keep in mind, you would have to migrate your own project to use ES modules to use this. So please just refer to the previous 2 sections above (CJS, ESM) for whichever you’ve chosen to use right now.

notes

Migrating to ESM is no simple task, so i’m sure many of us will still be stuck with commonjs for the near future at least.

In those cases, my personal opinion (not speaking for chai), is that i’d use 4.x with the aim of one day performing the ESM migration.

the feature sets between 4.x and 5.x are near identical. it is perfectly fine to stay on 4.x for now

discussion still needs to happen around security fixes etc, but i’m sure we won’t be leaving those of you on 4.x still behind.

Amazing answer @keithamus This great communication skill is the reason why I am one of your followers on Twitter.

My questions are all sorted. I am happy to see much professionalism and community focused project governance.

Cheers people!

Have a great year. 😃 🎄

ah i see now

yeah unfortunately just because of how standard import syntax works in JS, we can’t really chain it that way anymore. so it means you’d just have to do something like this:

import {should as loadShould} from 'chai';

const should = loadShould();

how did you use it before?

i suspect you need to do something like this:

import {should} from 'chai';

should(); // this will patch `Object` so you can chain things like foo.bar.should

// tests

or

import 'chai/register-should.js'; // will patch `Object` _and_ make `globalThis.should` available

you can find a reproduction here: RigoBlock/v3-contracts#413

Not really a minimal reproduction 😅

I can’t install the dependencies and get it up and running. A minimal reproduction should be the least amount of code and build setup in order to reproduce the error in question.

As far as I can tell with the information given this seems to be some sort of configuration issue in TypeScript? The code is using native JavaScript modules but maybe the tsconfig.json is set to output CommonJS? The quick solve here would be to dynamically import chai as that should work in a CommonJS environment.

Might be worth looking into your TypeScript setup but I don’t think this is a issue with Chai.

They don’t build at zero cost, nothing does. Inevitably, you need to publish a CJS entry point and an ESM entry point.

On top of that, to work in browsers, you need to purge cjs entirely from the tree - this means publishing an entire cjs version and an entire esm version (no wrappers).

There’s so many reasons to do this, that have been discussed.

People on very old node can continue to use 4.x, the feature set is near enough the same.

I agree, I wasn’t aware a 5.0 was being published today. @koddsson if you’re already digging around the repo, maybe you could update the docs too?

we still haven’t had chance to discuss it as a group yet, i’ll try get hold of the others soon so we can answer you

my personal preference (not speaking for chai) would be to do security fixes for some time, until more people move (which could be a long time)

Another option is to put it into mocha global setup so its only there and you don’t need to worry about it in any other test file

You should avoid using wildcard dependencies versions like "chai": "*". If the tutorial told you to do that then the tutorial is giving you bad advice and it should be corrected to say something like "chai": "^4.3.10".

The reason for this is exactly what you’re discovering: a new major version of chai can contain breaking changes.

why do you think this?

It doesn’t work with ts-node, a big point I think for me, for example, I run mocha with ts-node/register, this stops working now if chai is on v5. If there is a solution to that I’m all ears, I could really use some help if it can be resolved.

presumably your project is commonjs? if that’s the case, you’d have to use a dynamic import to import chai 5.x. but in those cases, i’d just stick to 4.x until you can migrate your own project to use ESM.

if you can show a reproduction somewhere, i can take a look.

in ESM projects, mocha needs a loader instead of a require hook ("loader": "ts-node/esm").

i’m happy to help as it is important for all of us and many other packages to move the ecosystem forward to a standardised module system like this. i do understand it isn’t a quick job to move to ESM, though, so i have no doubt we will still be supporting 4.x in some way.

Generally this is a signal to switch to something else

why do you think this?

v5 is ESM only, following the standard module system node and browsers are aligning on going forward

v4 still exists for those who, for whatever reason, can’t use dynamic import or can’t migrate to ESM.

i’m sure in terms of security, we could continue updating a v4 branch meanwhile. needs some discussion but that seems to be the sole reason we need to keep 4.x around, since we’re not changing feature set.

I feel like this is some misconfiguration issue and can be resolved

no, it was an active decision to move towards using the standard module system all platforms are aligning to. it isn’t a misconfiguration

it would be good to understand what your concerns drill down to, as there’s absolutely no reason to be moving off chai just because you can’t yet upgrade to 5.x.