msw: "Cannot find module 'msw/node'" in Jest JSDOM environment

Prerequisites

Environment check

  • I’m using the latest msw version
  • I’m using Node.js version 18 or higher

Node.js version

v18.18.2

Reproduction repository

https://github.com/textbook/msw2-mre

Reproduction steps

npm ci && npm test

Current behavior

Error message

Cannot find module ‘msw/node’ from ‘path/to/file’

$ npm test

> msw2@0.1.0 test
> jest

 FAIL  ./index.test.js
  ● Test suite failed to run

    Cannot find module 'msw/node' from 'index.test.js'

      1 | const { http, HttpResponse } = require("msw");
    > 2 | const { setupServer } = require("msw/node");
        |                         ^
      3 |
      4 | const server = setupServer();
      5 |

      at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:427:11)
      at Object.require (index.test.js:2:25)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.961 s
Ran all test suites.
Error: Process completed with exit code 1.

🔴 Failing Actions run

Expected behavior

The test should pass, as it does if you switch to the Node environment by updating jest.config.js as follows:

diff --git a/jest.config.js b/jest.config.js
index f343a5c..4e07134 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -13,5 +13,5 @@ module.exports = {
                ReadableStream,
                TextEncoder,
        },
-       testEnvironment: "jsdom",
+       testEnvironment: "node",
 };

Outcome:

$  npm test

> msw2@0.1.0 test
> jest

 PASS  ./index.test.js
  ✓ works (10 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.203 s
Ran all test suites.

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Reactions: 34
  • Comments: 24 (6 by maintainers)

Commits related to this issue

Most upvoted comments

may I please stress this one last and final time: You need all this because you are using an OLD testing framework. Whether you decide to ignore the browser export condition in Node, which is what you should be doing, to begin with, or introduce a custom resolver for Jest—nothing of this has anything to do with MSW. Hope we get this straight in the discussions to come.

Sorry to drag this out more, but this is just not accurate @kettanaito. I understand the technical details of your explanation and the differences between jest, jsdom, vitest, etc. perfectly, so don’t mistake this for lack of understanding. There’s no need for reiteration of your position, rather there’s a need for listening and understanding your users’ (valid) choices and circumstances.

Jest and jsdom are not “OLD” or outdated. As @joshkel mentioned, they provide a far more accurate testing environment for browser code. They are actively maintained and updated. They support the bleeding edge of browser changes and many wide-sweeping libraries and tools.

I’ve already spent far too much time trying to work around this issue including your suggestion, using other DOM implementations that fall short (such as happy-dom), polyfills, resolvers, etc. None of these are fully acceptable and have side effects that impact other correct code outside of msw. Please note that just because I was able to get my tests to run and pass, that does not mean that the overall outcome is acceptable.

As such, this is a total blocker for us. We will not be sacrificing the accuracy of our tests in relation to the production environment they’ll be running in for one library. Somehow all the thousands of other libraries we use haven’t had this issue.

This could be fixed for your users with some action on your part and you’re choosing not to. That said, I understand if this is the direction you’re taking the library. I’m not asking for anything and I expect nothing in return. I simply wanted to share feedback with you about the entrenched position you’ve taken that is very clearly not cut and dry like you’ve painted it.

You spend your time making this awesome tool and I appreciate that, it’s just that I’ll be unable to continue using it going forward. Thanks for all of your hard work and maybe I’ll still be able to use it on some other non-browser project because I really do like it a lot ❤️

Why does this error happen?

Because JSDOM forces the browser export condition. In other words, JSDOM says “if a third-party package exports a browser field, use that.” That’s the default and a rather dangerous default at that. Why? Because JSDOM still runs in Node.js. More to that, JSDOM cannot have 100% browser compatibility by design, so forcing the browser export condition will subject your tests to failures more than necessary when working with packages that ship different code for different environments, like MSW does.

The msw/node import must never happen in the browser code. Thus, MSW has the following export condition to ensure that on the bundler level:

https://github.com/mswjs/msw/blob/1d1fbca6a39b0d89c7e18bc50cab73ddac7ea5f0/package.json#L23-L24

The same is true for the msw/browser import in Node.js. This is the right setup to achieve publishing to multiple environments at once. The problem is not the package but how your test environment is configured. Export conditions is a relatively new feature is Node.js, and some tools are either lacking behind, don’t understand it entirely, or, like JSDOM, assume dangerous defaults that put the users in confusing situations.

How to solve this?

Opt out of the browser export condition by adding this to your jest.config.js:

// jest.config.js 
module.exports = {
  testEnvironmentOptions: {
    customExportConditions: [''],
  }
}

This will force JSDOM to use the node (or default) export condition, which is the correct behavior.

If you encounter other import-related issued after this change, they aren’t related and have to be addressed separately. Adding this recommendation to the migration guide as well so everyone could follow.

I was hoping to change just the interaction between Jest and MSW, instead of changing everything within Jest to make MSW happy.

JSDOM is broken export conditions-wise, I believe I explained it at length. It’s not about making MSW happy, it’s about resolving third-party modules using the node export while running in Node.js. This is how module resolution is supposed to work, and by using old tools you are not getting the modern JavaScript. If you don’t believe me, try Vitest. It works with the correct module resolution out of the box.

@joshkel, your custom resolver implementation looks interesting. I will leave it up to the people to decide which approach works best for them, but may I please stress this one last and final time: You need all this because you are using an OLD testing framework. Whether you decide to ignore the browser export condition in Node, which is what you should be doing, to begin with, or introduce a custom resolver for Jest—nothing of this has anything to do with MSW. Hope we get this straight in the discussions to come. Thanks.

Thanks for the reply, @kettanaito (and for your patience).

If I’m understanding correctly, it’s not a matter of Jest being old or not supporting modern module resolution. (Jest fully supports modern modules and module resolution, as far as I know - somewhat held back by issues in Node.js itself.) Instead, it’s a legitimate trade-off:

  • There are advantages to emulating the browser as closely as possible when testing browser code. (It’s the same testing philosophy as Kent Dodds, I guess.)
  • There are advantages to being honest and accepting that the code runs in Node.js. The resulting code can be much simpler and can leverage pre-existing functionality (although that functionality may not behave the same as it does in the browser).

Jest and jsdom seem to default to the first approach. jsdom won’t use implementations if they’re not W3C-spec-compliant, and Jest uses the VM API (I think) to exclude Node.js globals (so code under test won’t mistakenly reference them), and Jest decides to emulate the browser as closely as possible when using jsdom, including honoring the "browser" export conditions of Node modules. (And, since Jest’s jsdom environment doesn’t expose Node.js globals, some packages need to have their "browser" export conditions honored, so they’ll reference browser globals rather than Node.)

Vitest takes the second approach (e.g., it exposes Buffer because “TODO a lot of dependencies use it”) - but that means I can have production browser-side code that mistakenly references Buffer and passes tests and fails at runtime. MSW 2 picks the second approach (for reasons that you explain in depth), and I’m certain those are the correct tradeoffs for MSW - but now I have test failures because new FormData(form) no longer works. (This is not a complaint and not your problem.)

It wouldn’t surprise me if Jest’s tradeoffs are worse in general. (They’ve certainly caused a lot of complexity.) But it doesn’t seem quite fair to say that picking a worse set of tradeoffs is broken. (I’m interested in switching to Vitest, but parts of the ecosystem still aren’t there for my project.)

I’m happy to accept MSW’s decision as to appropriate tradeoffs when using it. (That’s what I meant by “making MSW happy”; I apologize if I contributed to any confusion.)

We are unfortunately seeing the same issue when upgrading to v2. This is blocking us from moving to the new version. It is a Nextjs project and works without issue in v1

Here’s an improved version of my custom Jest resolver that replaces customExportConditions just for MSW, while leaving Jest’s browser export for other code that expects it. This allows tests of code that’s intended to run in the browser to continue to use the browser versions of other packages, while following MSW’s expectation and recommendation of using MSW’s Node code within tests.

module.exports = (path, options) => {
  // Jest + jsdom acts like a browser (i.e., it looks for "browser" imports
  // under pkg.exports), but msw knows that you're operating in a Node
  // environment:
  //
  // https://github.com/mswjs/msw/issues/1786#issuecomment-1782559851
  //
  // The MSW project's recommended workaround is to disable Jest's
  // customExportConditions completely, so no packages use their browser's
  // versions.  We'll instead clear export conditions only for MSW.
  if (/^(msw|@mswjs\/interceptors)(\/|$)/.test(path)) {
    return options.defaultResolver(path, { ...options, conditions: [] });
  }

  return options.defaultResolver(path, options);
};

If this works for others, it may be worth adding as an option in the docs.

This is what resolved it for me from the examples repository. I don’t pretend to know what this does, but it works.

https://github.com/mswjs/examples/blob/main/examples/with-jest/jest.config.ts#L20

For me, this works only partially. As it breaks other imports.

This is what resolved it for me from the examples repository. I don’t pretend to know what this does, but it works.

https://github.com/mswjs/examples/blob/main/examples/with-jest/jest.config.ts#L20

Disabling Jest’s customExportConditions everywhere seemed somewhat drastic to me; I was hoping to change just the interaction between Jest and MSW, instead of changing everything within Jest to make MSW happy. (And, in a brief test, I ran into problems with other packages not working correctly when I cleared customExportConditions.)

Instead, I’m using a custom Jest resolver to modify MSW’s imports.

Please note: This is a hack. It’s deliberately doing something not intended by the MSW project. Subsequent MSW updates may break it. If you do this and it causes problems, please do not report any resulting issues to this project.

To do this: In the Jest config, define your resolver:

  resolver: `${__dirname}/config/jest/resolver.js`,

And create it with the following contents:

// Based on https://github.com/microsoft/accessibility-insights-web/pull/5421#issuecomment-1109168149
module.exports = (path, options) => {
  // Call the defaultResolver, so we leverage its cache, error handling, etc.
  return options.defaultResolver(path, {
    ...options,
    // Use packageFilter to process parsed `package.json` before the resolution
    // (see https://www.npmjs.com/package/resolve#resolveid-opts-cb)
    packageFilter: (pkg) => {
      if (pkg.name === 'msw') {
        delete pkg.exports['./node'].browser;
      }
      if (pkg.name === '@mswjs/interceptors') {
        delete pkg.exports;
      }

      return pkg;
    },
  });
};

I have the same issue using react, vite and vitest.

"react": "^18.2.0",
"react-dom": "^18.2.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"vite": "^4.5.0",
"vitest": "^0.34.6",

System: NodeJS : v18.18.2 npm : 9.8.1 OS : macOS 13.5.2

Here’s an improved version of my custom Jest resolver that replaces customExportConditions just for MSW, while leaving Jest’s browser export for other code that expects it. This allows tests of code that’s intended to run in the browser to continue to use the browser versions of other packages, while following MSW’s expectation and recommendation of using MSW’s Node code within tests.

module.exports = (path, options) => {
  // Jest + jsdom acts like a browser (i.e., it looks for "browser" imports
  // under pkg.exports), but msw knows that you're operating in a Node
  // environment:
  //
  // https://github.com/mswjs/msw/issues/1786#issuecomment-1782559851
  //
  // The MSW project's recommended workaround is to disable Jest's
  // customExportConditions completely, so no packages use their browser's
  // versions.  We'll instead clear export conditions only for MSW.
  if (/^(msw|@mswjs\/interceptors)(\/|$)/.test(path)) {
    return options.defaultResolver(path, { ...options, conditions: [] });
  }

  return options.defaultResolver(path, options);
};

If this works for others, it may be worth adding as an option in the docs.

@joshkel thanks so much for this! Saved me a ton of time. I made one small tweak to this:

if (/^(msw|@mswjs\/interceptors)(\/|$)/.test(path)) {
    return options.defaultResolver(path, {
      ...options,
      conditions: options.conditions.filter(
        (condition) => condition !== "browser"
      ),
    })
}

Namely, I left the conditions intact aside from filtering out browser. I’m still early in my testing, but this seems to be working for me and preventing another issue I was running into when I overwrote conditions to be empty.

Still having the issue with vite, vitest and react…

I have the same issue using react, vite and vitest.

"react": "^18.2.0",
"react-dom": "^18.2.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"vite": "^4.5.0",
"vitest": "^0.34.6",

System: NodeJS : v18.18.2 npm : 9.8.1 OS : macOS 13.5.2

React Vite Vitest & MSW v2 still doesnt work 😵

I have same problem on React 18, Vite 5.1.0 , Vitest 1.2.2 and MSW 2.0

See no resolution for that on Vite and Vitest config.

I have checked example in Vitest from @kettanaito, but I cannot see the solution there…

Like @mkalvas said, there should be separated solution in documentation below Jest solution, as Vitest is huge communitt as well.

https://mswjs.io/docs/migrations/1.x-to-2.x/#cannot-find-module-mswnode-jsdom

Waiting for solution guide for Vitest 🧪

I got a similar error but running ESLINT

error: Resolve error: Package subpath './browser' is not defined by "exports" in /Users/cesar.santos/app/node_modules/msw/package.json
    at new NodeError (node:internal/errors:393:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:358:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:612:7)
    at resolveExports (node:internal/modules/cjs/loader:522:36)
    at Module._findPath (node:internal/modules/cjs/loader:562:31)
    at findModulePath (/Users/cesar.santos/app/node_modules/eslint-import-resolver-alias/index.js:99:27)
    at exports.resolve (/Users/cesar.santos/app/node_modules/eslint-import-resolver-alias/index.js:75:10)
    at v2 (/Users/cesar.santos/app/node_modules/eslint-module-utils/resolve.js:116:23)
    at withResolver (/Users/cesar.santos/app/node_modules/eslint-module-utils/resolve.js:121:14)
    at fullResolve (/Users/cesar.santos/app/node_modules/eslint-module-utils/resolve.js:138:22)
    at relative (/Users/cesar.santos/app/node_modules/eslint-module-utils/resolve.js:83:10)
    at resolve (/Users/cesar.santos/app/node_modules/eslint-module-utils/resolve.js:219:12)
    at isImportingSelf (/Users/cesar.santos/app/node_modules/eslint-plugin-import/lib/rules/no-self-import.js:14:70)
    at commonjs (/Users/cesar.santos/app/node_modules/eslint-plugin-import/lib/rules/no-self-import.js:35:9)
    at checkSourceValue (/Users/cesar.santos/app/node_modules/eslint-module-utils/moduleVisitor.js:29:5)
    at checkCommon (/Users/cesar.santos/app/node_modules/eslint-module-utils/moduleVisitor.js:67:5)
    at visitors.CallExpression (/Users/cesar.santos/app/node_modules/eslint-module-utils/moduleVisitor.js:105:29)
    at ruleErrorHandler (/Users/cesar.santos/app/node_modules/eslint/lib/linter/linter.js:1115:28)
    ...

I’m not sure why this “node: null” is needed 👇 https://github.com/mswjs/msw/blob/a54138a62a7e89bd5f768d369e6b5bc4c46686ae/package.json#L16-L17

but removing it fixes my issue above

Please refer to the usage examples with Vitest that feature both ESM and CJS tests, all functional with MSW v2.0:

I’ve also just migrated the entire internal test suite of MSW to Vitest yesterday without any issues. You don’t need to configure JSDOM in any way for Vitest+MSW to work.

Why does this error happen?

Because JSDOM forces the browser export condition. In other words, JSDOM says “if a third-party package exports a browser field, use that.” That’s the default and a rather dangerous default at that. Why? Because JSDOM still runs in Node.js. More to that, JSDOM cannot have 100% browser compatibility by design, so forcing the browser export condition will subject your tests to failures more than necessary when working with packages that ship different code for different environments, like MSW does.

The msw/node import must never happen in the browser code. Thus, MSW has the following export condition to ensure that on the bundler level:

https://github.com/mswjs/msw/blob/1d1fbca6a39b0d89c7e18bc50cab73ddac7ea5f0/package.json#L23-L24

The same is true for the msw/browser import in Node.js. This is the right setup to achieve publishing to multiple environments at once. The problem is not the package but how your test environment is configured. Export conditions is a relatively new feature is Node.js, and some tools are either lacking behind, don’t understand it entirely, or, like JSDOM, assume dangerous defaults that put the users in confusing situations.

How to solve this?

Opt out of the browser export condition by adding this to your jest.config.js:

// jest.config.js 
module.exports = {
  testEnvironmentOptions: {
    customExportConditions: [''],
  }
}

This will force JSDOM to use the node (or default) export condition, which is the correct behavior.

If you encounter other import-related issued after this change, they aren’t related and have to be addressed separately. Adding this recommendation to the migration guide as well so everyone could follow.

I have a similar situation

Inside the test environment (using vitest), I get the same error, because with ./browser.

If I follow the instruction to remove the conditional node, or put an empty string in the array, I end up breaking other tests that need the conditional node

Does anyone have any suggestions?

Confirmed, now 🟢 passes in both testEnvironments - thanks!