mswjs / msw

Industry standard API mocking for JavaScript.
https://mswjs.io
MIT License
16.04k stars 525 forks source link

Package path ./browser is not exported from package (Next.js) #1877

Closed xereda closed 10 months ago

xereda commented 1 year ago

Prerequisites

Environment check

Browsers

Chromium (Chrome, Brave, etc.)

Reproduction repository

https://github.com/xereda/nextjs-msw-example

Reproduction steps

Current behavior

yarn dev                             yarn dev                                                                                                                 17.8m  ter 21 nov 2023 14:52:22
yarn run v1.22.19
warning ../package.json: No license field
$ next dev
   ▲ Next.js 14.0.3
   - Local:        http://localhost:3000

warning ../package.json: No license field
 ✓ Ready in 2.4s
 ○ Compiling / ...
 ⨯ ./src/mocks/browser.js:2:0
Module not found: Package path ./browser is not exported from package /home/xereda/repositories/nextjs-msw-example/node_modules/msw (see exports field in /home/xereda/repositories/nextjs-msw-example/node_modules/msw/package.json)
  1 | // mocks/browser.js
> 2 | import { setupWorker } from "msw/browser";
  3 | import { handlers } from "./handlers";
  4 |
  5 | if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {

https://nextjs.org/docs/messages/module-not-found

Import trace for requested module:
./src/pages/_app.js

Expected behavior

Service mock enabled.

Alex-apostolo commented 1 year ago

im having the same issue and cannot migrate because of it

Alex-apostolo commented 1 year ago

fixed by changing the import from ES Module to CommonJS const { setupWorker } = require("msw/browser");. Im still having problems on my dev server since im using Vite which directly injects ESM without polyfills so it would be much appreciated if the default export for msw/browser is the mjs file

kdn0325 commented 1 year ago

i'm having the same issue

Step yarn add -msw --dev

//broswer.ts

"use client";

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
image
lgenzelis commented 1 year ago

I'm having the same issue. It's easy to reproduce:

  1. Create a new next application

    $ npx create-next-app@latest
  2. Reply "no" to everything (to reduce e.g. typescript as a possible culprit). So:

    ✔ What is your project named? … my-app
    ✔ Would you like to use TypeScript? … No
    ✔ Would you like to use ESLint? … No
    ✔ Would you like to use Tailwind CSS? … No
    ✔ Would you like to use `src/` directory? … No
    ✔ Would you like to use App Router? (recommended) … No
    ✔ Would you like to customize the default import alias (@/*)? … No
  3. Run

    $ cd my-app
    $ npm i -D msw
  4. Edit ./pages/_app.js and add import { setupWorker } from 'msw/browser' on top.

  5. Run npm run build and you'll see the error

    Module not found: Package path ./browser is not exported from package /Users/my.user/my-app/node_modules/msw (see exports field in /Users/my.user/my-app/node_modules/msw/package.json)

It makes no sense, because the "exports field in /Users/my.user/my-app/node_modules/msw/package.json" clearly shows:

    "./browser": {
      "node": null,
      "types": "./lib/browser/index.d.ts",
      "require": "./lib/browser/index.js",
      "import": "./lib/browser/index.mjs",
      "default": "./lib/browser/index.js"
    },

😖😖

skopz356 commented 1 year ago

Same thing happens when you run dependency-cruiser: error not-to-unresolvable: src/app/mock/worker.mock.ts → msw/browser

tstewart-klaudhaus commented 12 months ago

Same issue here, when trying to access setupWorker from within a dynamically imported module. It works when the import is static but my build process involves some dynamic module loading like:

const myModule = await import(jsModulePath)

And in turn the module at jsModulePath has a transitive dependency on another module that imports setupWorker. Exact same error message. I can see that the export is indeed defined in package.json.

tstewart-klaudhaus commented 12 months ago

My workaround is to create the api mocks as a separate stand-alone module which is all statically linked and then load it in a separate script tag at runtime with the async attribute so mocking is set up before my main app script loads. This way I can drop it into a page without any impact on my main app when I need mocking, so I'm happier with this than having it linked with the main app anyway.

There do seem to be a few issues around package exports with dynamic imports, maybe related to the problems other people are having.

https://github.com/webpack/webpack/issues/13865 https://github.com/highlightjs/highlight.js/issues/3384

deminoth commented 11 months ago

This issue appears to be due to NextJS attempting to import /browser from the Node environment for SSR. I resolved this issue by using dynamic import only in the browser environment.

if (typeof window !== 'undefined') {
  const { setupWorker } = await import('msw/browser');
  …
RaflyLesmana3003 commented 11 months ago

I found duplicate issue here

https://github.com/mswjs/msw/issues/1801

try this in your next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
   webpack: (config, { isServer }) => {
      if (isServer) {
         // next server build => ignore msw/browser
         if (Array.isArray(config.resolve.alias)) { // in Next the type is always object, so this branch isn't necessary. But to keep TS happy, avoid @ts-ignore and prevent possible future breaking changes it's good to have it
            config.resolve.alias.push({ name: "msw/browser", alias: false })
            } else {
            config.resolve.alias["msw/browser"] = false
            }
         } else {
            // browser => ignore msw/node
            if (Array.isArray(config.resolve.alias)) {
            config.resolve.alias.push({ name: "msw/node", alias: false })
            } else {
            config.resolve.alias["msw/node"] = false
            }
         }
      return config;
   }};

module.exports = nextConfig
filiphazardous commented 10 months ago

I can add that the same problem is present in Nuxt as well. Will try the workarounds in this thread. Edit: The simpler workarounds don't seem to work. To be more specific, I'm working with Nuxt in combination with Histoire. The setup is run server side in a thread with Jsdom. For some reason require doesn't solve the problem - I still get: Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './browser' is not defined by "exports" in /app/frontend/node_modules/msw/package.json

kettanaito commented 10 months ago

I suspect this error is happening because Next.js runs client-only components in Node.js for SSR. This effectively means that a JavaScript module is evaluated in a Node.js environment. This causes webpack to treat the export conditions as those of "node", which fails "msw/browser" imports, which are completely legal imports in the client.

I'm a bit perplexed by this because even if I put a dynamic import into useEffect, Next.js still moves that import statement to the top of the module. Afaik, that's not how imports behave in ESM. This is the root cause of this issue.

The suggestion from @RaflyLesmana3003 does help but it's rather a hack and I don't recommend it. What it does it tells webpack to ignore some of MSW's exports based on the environment so webpack wouldn't error. Suppressing the error, though, doesn't mean things are working properly.

I will close this issue because it cannot be addressed on MSW's side.

kettanaito commented 10 months ago

Also keep track of the official Next.js + MSW example (https://github.com/mswjs/examples/pull/101). It's not ready yet but once it is, you will have a reference point.

filiphazardous commented 10 months ago

For the record, this also happens with Nuxt.js and Vite, in conjunction with Histoire (a Storybook replacement).

SSR is disabled in our configuration. This is the only package displaying this problem, so I still figure it may be connected to MSW. I'll post a comment/PR if I can pinpoint exactly how.

kettanaito commented 10 months ago

@filiphazardous, it's a combination of MSW using export conditions and whichever compiler you have not respecting those export conditions correctly. In hindsight, export conditions seem to be extremely new and their support differs vastly across tooling.

Can you please share a reproduction repository? I would love to have one to look into the problem.

anusreesubash commented 8 months ago

This worked for me on Next13

import { handlers } from './handlers'

export const setUpMocks = async () => {
  if (typeof window !== 'undefined') {
    const { setupWorker } = require("msw/browser");

    const worker = setupWorker(...handlers)
  }
}
filiphazardous commented 8 months ago

I'm sorry, but we moved on to another solution - this was a show stopper for us.

My platform is Vue + Nuxt 3 + Vite 5. (Vite may have been at version 4 when I encountered the problem.)

NateWilliams2 commented 7 months ago

I had this same issue in a Docusaurus project (ridiculous, I know, I promise I had a real use case). I was able to solve it with a hack similar to the one listed above (and at https://github.com/mswjs/msw/issues/1801), creating a custom Docusaurus plugin in order to modify the webpack config with the following strategy:

plugins/webpack-loader/index.js

module.exports = function (context, options) {
  return {
    name: 'webpack-loader',
    configureWebpack(config, isServer) {
      if (isServer) {
        return {
          resolve: {alias: {'msw/browser': false}},
        };
      }
    },
  };
};

This hacky solution differs from the one above because I found that adding a !isServer clause to alias 'msw/node' removed the actual msw functionality from the client. Note that I am using msw in production, not just development.

Leaving this comment in case it helps anyone in a similar situation.

Spacylion commented 6 months ago

Works for me with this declaration:

import {http} from 'msw';
import {generatePayloadHandler} from "./handlers/generate-payload-handler";

let setupWorker: any;
if (typeof window !== 'undefined') {
    setupWorker = require('msw/browser').setupWorker;
}

const baseUrl = document.baseURI.replace(/\/$/, '');
Abdullah-Sajjad026 commented 6 months ago

This is how I got it in my Next.js app.

// helper_functions/setup-msw-browser.js

let mswWorker;
const mockHandlers = [];

export const setupMswWorker = async () => {
    if (typeof window !== 'undefined' && !mswWorker) {
        const { setupWorker } = await import('msw/browser');
        mswWorker = setupWorker(...mockHandlers);
        return mswWorker;
    }
};

Then in the _app.js file:


import { setupMswWorker } from "helper_functions/setup-msw-browser";
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

export default function App({ Component, pageProps }) {
    const getLayout = Component.getLayout || (page => page);

    useEffect(() => {
        let mswWorker;
        const startMswWorker = async () => {
            mswWorker = await setupMswWorker();
            await mswWorker?.start();
        }
        if (process.env.NODE_ENV === "development") {
                startMswWorker();
        }
        return () => {
            mswWorker?.stop();
        }
    }, []);

    return (
        <QueryClientProvider client={queryClient}>
                        {getLayout(<Component {...pageProps} />)}
        </QueryClientProvider>
    );
}
cheryl-c-tirerack commented 5 months ago

This worked for me on Next13

import { handlers } from './handlers'

export const setUpMocks = async () => {
  if (typeof window !== 'undefined') {
    const { setupWorker } = require("msw/browser");

    const worker = setupWorker(...handlers)
  }
}

where does this live? in the Next js app directory structure...

hlohrenz commented 2 months ago

The solutions here to me aren't really solutions though. It's only using the browser version of msw.. My goal is to intercept requests made on the server side (within server components in NextJS) so I can tell Cypress to wait for them to resolve before running any assertions. You can't do this when only starting the browser worker and not the node server in msw. Unless my understanding of this package is not correct? And there is no way to intercept/wait for server-side requests within Cypress. I thought msw might be the answer, but maybe it's not.

DharmilJarsaniya1711 commented 1 month ago

### Add below code in next.config.mjs file and then run it's working fine.

/* @type {import('next').NextConfig} / const nextConfig = { reactStrictMode: true, webpack: (config, { isServer }) => { if (isServer) { // next server build => ignore msw/browser if (Array.isArray(config.resolve.alias)) { // in Next the type is always object, so this branch isn't necessary. But to keep TS happy, avoid @ts-ignore and prevent possible future breaking changes it's good to have it config.resolve.alias.push({ name: "msw/browser", alias: false }) } else { config.resolve.alias["msw/browser"] = false } } else { // browser => ignore msw/node if (Array.isArray(config.resolve.alias)) { config.resolve.alias.push({ name: "msw/node", alias: false }) } else { config.resolve.alias["msw/node"] = false } } return config; } };

export default nextConfig;