Next.js: exclude a module client side

Created on 26 May 2017  路  22Comments  路  Source: vercel/next.js

Hello,

I'd like to exclude a specific module that uses fs from the client side bundle. I've tried

  "browser": {
    "./lib/api.js": false
  }

in my package.json but I get Error: Cannot find module '../lib/api' so it seems to be excluded server side as well somehow.

My use case is that I want my getInitialProps function to use fetch client side and an other method from my ./lib/api.js file server side.

Most helpful comment

@nfantone's solution of using IgnorePlugin does not work on current Next.js canary.

So I've solved this using the following:

  1. yarn install null-loader
  2. Add the following to next.config.js
module.exports = {
  webpack(config, { isServer }) {
    if (!isServer) {
      // Ensure prisma client is not included in the client bundle
      config.module.rules.push({
        test: /@prisma\/client/,
        use: "null-loader",
      })
    }
    return config
  },
}

All 22 comments

EDIT: Following approach works but it's incorrect. Even though the module is loaded only in the server, it's bundled by webpack anyway. So, that's not a good solution.

In this case, the best way to use dynamic imports which comes with v3. See: https://zeit.co/blog/next3-preview

You can do something like this:

Comp.getInitialProps = async (ctx) => {
  if (isServer) {
    const api = await import('./lib/server-api.js')
    // do something
  } else {
    const api = await import('./lib/client-api.js')
    // do something
  }
}

Thanks.

It does sounds like a workaround though. Dynamic imports is a great feature but I don't feel they fit my use case, it's not like I need them on demand and I'd rather have my imports at the top of my file.

Does it means next.js is incompatible with libraries which depends on the webpack/browserify browser field of package.json?

related https://github.com/zeit/next.js/issues/1091

I just had a look at isomorphic-fetch package.json file

{
  "browser": "fetch-npm-browserify.js",
  "main": "fetch-npm-node.js",
}

so Next.js does honor the browser field for external packages (via webpack I guess). It apparently doesn't for the application own package.json (at least for paths).

Let me know if you think this is in scope for Next.js and I'll try to send a PR.

Cheers

@arunoda FYI this returns errors:

if (false) {
  await import('request');
}
These modules were not found:

* fs in ./~/request/lib/har.js
* net in ./~/forever-agent/index.js, ./~/tunnel-agent/index.js and 1 other
* tls in ./~/forever-agent/index.js, ./~/tunnel-agent/index.js

To install them, you can run: npm install --save fs net tls

So branching in the code is not really possible.

@fatfisz this a good one.
I think even if it's not downloaded at once. Webpack is looking to bundle it.
I hope we need to do something about this.

There are couple of ways we can fix this. Here's the easy way:

Comp.getInitialProps = async (ctx) => {
  if (isServer) {
    const api = eval(`require('./lib/server-api.js')`)
    // do something
  } else {
    const api = await import('./lib/client-api.js')
    // do something
  }
}

I wrote a blog post about this here - https://arunoda.me/blog/ssr-and-server-only-modules

Frankly, there's nothing we can do about this in Next.js other than educating everyone.

Have you used this webpack-node-externals to keep the webpack from bundling the specific libs only used on server? https://github.com/liady/webpack-node-externals

Using eval is hacky to webpack.

@haohcraft you can use solutions like that now, but I wouldn't recommend adding webpack-node-externals 馃憤

@arunoda thanks for your article on the matter.

Using the same technique, I was forced to change the require statement like this:

const repository = eval("require('../../../../repositories/place-repository')");

Even if folders repositories and pages are in the same lib folder.
But 'next' don't seems to look that way.

The error is:

Cannot find module '../repositories/place-repository'
    at ...
    at /Users/macbook/code/project/lib/.next/server/bundles/pages/places.js:1383:26

Like, instead of looking in the current server folder, lib, the eval took place in the generated next folder.

Is there a way to use a usual require path in those kind of situations?
Or is this a commun workaround?

Thanks.

@timneutkens is eval still the recommended approach for this situation?

I found an interesting solution:

  1. serve next using an express server as shown here
  2. require whatever server side modules you need inside your server.js and add them the req via an express middleware
const myModule = require('this-only-works-on-the-server');

// ... initialize express + next

server.use((req, res, next) => {
  // now I can access this module inside the getInitialProps function
  req.myModule = myModule;
  return next();
})

server.get('*', (req, res) => {
  return handle(req, res);
})

:point_up: this avoids webpack ever trying to include the server side module since the server.js file is never imported by webpack.

hi @codyzu have you found a way to make this work also on next export?

hello @knitkoder ... no, I haven't used the export functionality. However, my feeling is it would be ~strange~ impossible to use server side imports/requires in a exported static site. By definition, a static site doesn't have any server side JS running.

thanks @codyzu I ended up using gatsby to combine data sources from js files, in the end that was what I needed

@codyzu you made my day!!))

Using process.browser (from webpack) is the most effective way to deal with this:

let getData;
if (process.browser) {
  getData = () => fetch('https://....');
} else {
  const api = require('lib/api');
  getData = () => api.foo();
}

export default class extends React.Component {
  static async getInitialProps() {
    const { data } = await getData();
    return {
      data,
    };
  }
  // ...
}

The browser-side code then compiles down to something like:

var getData;

if (false) { var api; } else {
  getData = function getData() {
    return fetch('https://...');
  };
}

// ...

For a real-world example of this pattern, see: https://codesandbox.io/s/n3yj15mk7p

@arunoda Is the guide on SSR and Server Only Modules still relevant after almost three years?

I've been trying to follow instructions there but I'm unsure how they should actually work.

If I ignore a module (using IgnorePlugin) that's meant to be loaded server side, I -unsurprisingly- get a MODULE_NOT_FOUND error on the server console.

Error: Cannot find module 'jsonwebtoken'
    at webpackMissingModule (/my-project/.next/server/static/development/pages/index.js:1891:79)
# ...
  code: 'MODULE_NOT_FOUND'
}

I'm trying to validate a JWT token on the server with jsonwebtoken, BTW. Below is my current setup.

// next.config.js

module.exports = {
  // Do not show the X-Powered-By header in the responses
  poweredByHeader: false,
  webpack: (config, { webpack }) => {
    // Avoid including jsonwebtoken module in the client side bundle
    // See https://arunoda.me/blog/ssr-and-server-only-modules
    config.plugins.push(new webpack.IgnorePlugin(/jsonwebtoken/));

    return config;
  }
};
// auth-token.js (server side only module)

export async function verifyAuthToken(token) {
  // Import `jsonwebtoken` here using `require` as this
  // is meant to be a server-side only module
  // See https://arunoda.me/blog/ssr-and-server-only-modules
  const jwt = require('jsonwebtoken');

  return new Promise((resolve, reject) => {
    jwt.verify(token, JWT_SECRET, (err, decoded) => {
      return err ? reject(err) : resolve(decoded);
    });
  });
}
// private-page.js

// ...

  PrivatePage.getInitialProps = async ctx => {
    // Extract authorization token from cookies
    const token = getAuthToken(ctx);

    if (ctx.req) {
      // Server only
      try {
        const auth = await verifyAuthToken(token);
        return { auth };
      } catch (err) {
        console.log(err); // <-- this prints the MODULE_NOT_FOUND error server side

        // User is either not authenticated or the token verification
        // failed: we return `false` as the value for the `auth` prop;
        // this should trigger a redirect to the login page
        return { auth: false };
      }
    }

    // Client only
    return { auth: false };
  };

What am I missing?

EDIT: Found the solution. I tripped over the fact that I'm excluding the module for both client and server compilations. The isServer property, as documented here, worked for this.

const nextConfig = {
  // Do not show the X-Powered-By header in the responses
  poweredByHeader: false,
  webpack: (config, { webpack, isServer }) => {
    // Avoid including jsonwebtoken module in the client side bundle
    // See https://arunoda.me/blog/ssr-and-server-only-modules
    if (!isServer) {
      config.plugins.push(new webpack.IgnorePlugin(/jsonwebtoken/));
    }

    return config;
  }
};

@arunoda's 2017 blog post should probably be updated to reflect this - specially since it's linked directly on the official Next.js docs.

@nfantone's solution of using IgnorePlugin does not work on current Next.js canary.

So I've solved this using the following:

  1. yarn install null-loader
  2. Add the following to next.config.js
module.exports = {
  webpack(config, { isServer }) {
    if (!isServer) {
      // Ensure prisma client is not included in the client bundle
      config.module.rules.push({
        test: /@prisma\/client/,
        use: "null-loader",
      })
    }
    return config
  },
}

@flybayer

Do you - because you've obviously had a working configuration - possibly know, where my mistake could be:

const withTM = require('next-transpile-modules')(['nd-component-factory']);
module.exports = withTM({
    async redirects() {
        return [
             {
                source: '/',
                destination: '/projekte',
                permanent: true,
            },
            {
                source: '/projekte/:id',
                destination: '/projekte',
                permanent: true,
            },
        ];
    },
    webpack(config, { isServer, webpack }) {
        if (isServer) {
            config.module.rules.unshift({
                test: /@azure\/msal-browser/,
                use: 'null-loader',
            });
        }
        return config;
    },
});

I have to exclude @azure/msal-browser from being imported in the server render, because it uses the window object which causes everything to crash.

I also tried requiring the null-loader package manual and put it to the config. Same result, not working.

@azure/msal-browser still gets included on the server side. The packaged is imported in a wrapper for this library. I need to export a hook from there, so I can't use dynamic() because it only generates LoadableComponents...

Help would be highly appreciated!

@florianmatz I think for that you definitely need to use await import('dep') or dynamic(() => import('dep')).

Probably you need to use dynamic() on the parent component of where the azure/msal hook is used.

@flybayer Yes, I have that, getting

const MsalConsumer = dynamic(() => import('auth').then(mod => mod.MsalConsumer), {
    ssr: false,
});

and

const MsalProvider = dynamic(() => import('auth').then(mod => mod.MsalProvider), { ssr: false });

For the Provider, everything is fine. But with the Consumer it's difficult, because - of some reasons, that are hard to describe here - I actually need to use a useContext Hook, not the Consumer.

So out of my auth-Module I need to import

export const useMsal = (): any => useContext(MsalContext);

But then again, the SSR breaks due to the @azure-package...

So I basically would need something like

const msalHook = dynamicFUNCTION(() => import('auth').then(mod => mod.useMsal), {
    ssr: false,
});

But of course, I only get the LoadableComponent...

The consumer or the hook are basically provding tokens that I need to call some APIs.

I need either to fix next ignoring the package on SSR (prefered) or Microsoft to fix their libraries to support SSR, which is unlikely :(

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Vista1nik picture Vista1nik  路  55Comments

ematipico picture ematipico  路  66Comments

nvartolomei picture nvartolomei  路  78Comments

nickredmark picture nickredmark  路  60Comments

Timer picture Timer  路  60Comments