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.
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:
server.js
and add them the req
via an express middlewareconst 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:
yarn install null-loader
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 :(
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:
yarn install null-loader
next.config.js