next.js: Webpack 5 breaks dynamic wasm import for SSR

What version of Next.js are you using?

10.2.3

What version of Node.js are you using?

14.16.0

What browser are you using?

Chrome

What operating system are you using?

Windows

How are you deploying your application?

Other

Describe the Bug

Using Webpack 5 breaks dynamic import for WASM modules when using SSR.

ENOENT: no such file or directory, open '...\.next\server\static\wasm

I’ve provided a minimal reproducible example here: https://github.com/TimoWilhelm/mre-next-with-webassembly-webpack-5

Expected Behavior

Dynamic import of WASM modules should work for SSR when using Webpack 5.

To Reproduce

Run npm run build

info  - Using webpack 5. Reason: future.webpack5 option enabled https://nextjs.org/docs/messages/webpack5
info  - Checking validity of types
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
[=== ] info  - Generating static pages (0/3)
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
Error: ENOENT: no such file or directory, open 'C:\...\with-webassembly\.next\server\static\wasm\1f565eb157746630b627.wasm'
info  - Generating static pages (3/3)

Disabling SSR for the dynamic import fixes the issue.

 const RustComponent = dynamic({
   loader: async () => {
     // Import the wasm module
     const rustModule = await import('../add.wasm')
     // Return a React component that calls the add_one method on the wasm module
     return (props) => <div>{rustModule.add_one(props.number)}</div>
   },
+  ssr: false,
 });

Downgrading the Webpack version to use v4 also fixes the issue.

 module.exports = {
   future: {
-    webpack5: true,
+    webpack5: false,
   },
   webpack(config) {
-    config.experiments = { syncWebAssembly: true };
     config.output.webassemblyModuleFilename = "static/wasm/[modulehash].wasm";
     return config;
   },
...

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 9
  • Comments: 17 (8 by maintainers)

Commits related to this issue

Most upvoted comments

`const CopyPlugin = require(“copy-webpack-plugin”); /** @type {import(‘next’).NextConfig} */ const nextConfig = { webpack: function (config, options) { config.plugins.push(new CopyPlugin({ patterns: [ { from: “public/wasm”, to: “./static/wasm” }, ], })) return config; } }

module.exports = nextConfig `

It resolved my issue related to including wasm file in bundle

Nice idea! I made an webpack plugin to create the symlink, so it doesn’t depend on cleanDistDir.

config.plugins.push(
  new (class {
    apply(compiler) {
      compiler.hooks.afterEmit.tapPromise(
        'SymlinkWebpackPlugin',
        async (compiler) => {
          if (isServer) {
            const from = join(compiler.options.output.path, '../static');
            const to = join(compiler.options.output.path, 'static');

            try {
              await access(from);
              console.log(`${from} already exists`);
              return;
            } catch (error) {
              if (error.code === 'ENOENT') {
                // No link exists
              } else {
                throw error;
              }
            }

            await symlink(to, from, 'junction');
            console.log(`created symlink ${from} -> ${to}`);
          }
        },
      );
    }
  })(),
);

It seems like the issue is the following line part of the webpack config:

https://github.com/vercel/next.js/blob/1ebf26af784637a27fe422090418126c474353f4/packages/next/build/webpack-config.ts#L918-L921

The server output gets prefixed with 'chunks' which can’t be resolved by the static site generation.

image

I’ve managed to make it work by using the following webassemblyModuleFilename config in my next.config.js but that doesn’t seem ideal.

webpack: (config, { isServer }) => {
  config.experiments = { asyncWebAssembly: true };

  if (isServer) {
    config.output.webassemblyModuleFilename =
      './../static/wasm/[modulehash].wasm';
  } else {
    config.output.webassemblyModuleFilename =
      'static/wasm/[modulehash].wasm';
  }

  return config;
},

Didn’t worked without config.optimization.moduleIds = 'named';.

So the result workaround for me is:

webpack: (config, options) => {
    patchWasmModuleImport(config, options.isServer);
    return config;
},

+

function patchWasmModuleImport(config, isServer) {
    config.experiments = Object.assign(config.experiments || {}, {
        asyncWebAssembly: true,
    });

    config.optimization.moduleIds = 'named';

    config.module.rules.push({
        test: /\.wasm$/,
        type: 'webassembly/async',
    });

    // TODO: improve this function -> track https://github.com/vercel/next.js/issues/25852
    if (isServer) {
        config.output.webassemblyModuleFilename = './../static/wasm/[modulehash].wasm';
    } else {
        config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm';
    }
}

@michalzalobny these are from node.js bult-in modules - join is from node:path while access and symlink are from node:fs/promises.

Nice idea! I made an webpack plugin to create the symlink, so it doesn’t depend on cleanDistDir.

config.plugins.push(
  new (class {
    apply(compiler) {
      compiler.hooks.afterEmit.tapPromise(
        'SymlinkWebpackPlugin',
        async (compiler) => {
          if (isServer) {
            const from = join(compiler.options.output.path, '../static');
            const to = join(compiler.options.output.path, 'static');

            try {
              await access(from);
              console.log(`${from} already exists`);
              return;
            } catch (error) {
              if (error.code === 'ENOENT') {
                // No link exists
              } else {
                throw error;
              }
            }

            await symlink(to, from, 'junction');
            console.log(`created symlink ${from} -> ${to}`);
          }
        },
      );
    }
  })(),
);

When I try to push this plugin it breaks and says that It could not find the join, access, and symlink functions. Where are they declared in your webpack code?

Vercel being serverless might be peculiar about what Node.js APIs you can use (here node:fs/promises and node:path).

Usually there’s a solution in configuring Vercel for runtime filesystem emulation, but since this is a build-time issue, it won’t be of any use here.