diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 4dc5b5b3e7a8..f67d52974583 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1147,6 +1147,7 @@ export default async function getBaseWebpackConfig( 'noop-loader', 'next-middleware-loader', 'next-middleware-ssr-loader', + 'next-middleware-wasm-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1535,6 +1536,12 @@ export default async function getBaseWebpackConfig( const webpack5Config = webpackConfig as webpack5.Configuration + webpack5Config.module!.rules!.unshift({ + test: /\.wasm$/, + issuerLayer: 'middleware', + loader: 'next-middleware-wasm-loader', + }) + webpack5Config.experiments = { layers: true, cacheUnaffected: true, diff --git a/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts new file mode 100644 index 000000000000..c57095f56341 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts @@ -0,0 +1,21 @@ +import crypto from 'crypto' + +export type WasmBinding = { + filePath: string + name: string +} + +export default function MiddlewareWasmLoader(this: any, source: Buffer) { + const name = `wasm_${sha1(source)}` + const filePath = `server/middleware-chunks/${name}.wasm` + const binding: WasmBinding = { filePath, name } + this._module.buildInfo.nextWasmMiddlewareBinding = binding + this.emitFile(`/${filePath}`, source, null) + return `module.exports = name;` +} + +export const raw = true + +function sha1(source: string | Buffer) { + return crypto.createHash('sha1').update(source).digest('hex') +} diff --git a/packages/next/build/webpack/plugins/functions-manifest-plugin.ts b/packages/next/build/webpack/plugins/functions-manifest-plugin.ts index 4a5cf366e147..c8a04a2f34b9 100644 --- a/packages/next/build/webpack/plugins/functions-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/functions-manifest-plugin.ts @@ -3,7 +3,7 @@ import { sources, webpack5 } from 'next/dist/compiled/webpack/webpack' import { normalizePagePath } from '../../../server/normalize-page-path' import { FUNCTIONS_MANIFEST } from '../../../shared/lib/constants' import { getPageFromPath } from '../../entries' -import { collectAssets, getEntrypointInfo } from './middleware-plugin' +import { collectAssets, getEntrypointInfo, PerRoute } from './middleware-plugin' const PLUGIN_NAME = 'FunctionsManifestPlugin' export interface FunctionsManifest { @@ -52,7 +52,7 @@ export default class FunctionsManifestPlugin { createAssets( compilation: webpack5.Compilation, assets: any, - envPerRoute: Map, + perRoute: PerRoute, isEdgeRuntime: boolean ) { const functionsManifest: FunctionsManifest = { @@ -60,7 +60,7 @@ export default class FunctionsManifestPlugin { pages: {}, } - const infos = getEntrypointInfo(compilation, envPerRoute, isEdgeRuntime) + const infos = getEntrypointInfo(compilation, perRoute, isEdgeRuntime) infos.forEach((info) => { const { page } = info // TODO: use global default runtime instead of 'web' diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 7ada45ac32d4..54195245aebe 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -10,6 +10,7 @@ import { MIDDLEWARE_SSR_RUNTIME_WEBPACK, } from '../../../shared/lib/constants' import { nonNullable } from '../../../lib/non-nullable' +import type { WasmBinding } from '../loaders/next-middleware-wasm-loader' const PLUGIN_NAME = 'MiddlewarePlugin' const MIDDLEWARE_FULL_ROUTE_REGEX = /^pages[/\\]?(.*)\/_middleware$/ @@ -27,6 +28,7 @@ export interface MiddlewareManifest { name: string page: string regexp: string + wasmBindings?: WasmBinding[] } } } @@ -49,9 +51,14 @@ function getPageFromEntrypointName(pagePath: string) { return page } +export type PerRoute = { + envPerRoute: Map + wasmPerRoute: Map +} + export function getEntrypointInfo( compilation: webpack5.Compilation, - envPerRoute: Map, + { envPerRoute, wasmPerRoute }: PerRoute, isEdgeRuntime: boolean ) { const entrypoints = compilation.entrypoints @@ -87,6 +94,7 @@ export function getEntrypointInfo( infos.push({ env: envPerRoute.get(entrypoint.name) || [], + wasmBindings: wasmPerRoute.get(entrypoint.name) || [], files, name: entrypoint.name, page, @@ -114,10 +122,14 @@ export default class MiddlewarePlugin { createAssets( compilation: webpack5.Compilation, assets: any, - envPerRoute: Map, + { envPerRoute, wasmPerRoute }: PerRoute, isEdgeRuntime: boolean ) { - const infos = getEntrypointInfo(compilation, envPerRoute, isEdgeRuntime) + const infos = getEntrypointInfo( + compilation, + { envPerRoute, wasmPerRoute }, + isEdgeRuntime + ) infos.forEach((info) => { middlewareManifest.middleware[info.page] = info }) @@ -152,7 +164,7 @@ export function collectAssets( createAssets: ( compilation: webpack5.Compilation, assets: any, - envPerRoute: Map, + { envPerRoute, wasmPerRoute }: PerRoute, isEdgeRuntime: boolean ) => void, options: { @@ -175,6 +187,7 @@ export function collectAssets( }) const envPerRoute = new Map() + const wasmPerRoute = new Map() compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => { const { moduleGraph } = compilation as any @@ -187,6 +200,7 @@ export function collectAssets( ) { const middlewareEntries = new Set() const env = new Set() + const wasmBindings = new Set() const addEntriesFromDependency = (dep: any) => { const module = moduleGraph.getModule(dep) @@ -203,6 +217,9 @@ export function collectAssets( const queue = new Set(middlewareEntries) for (const module of queue) { const { buildInfo } = module + if (buildInfo.nextWasmMiddlewareBinding) { + wasmBindings.add(buildInfo.nextWasmMiddlewareBinding) + } if ( !options.dev && buildInfo && @@ -247,6 +264,7 @@ export function collectAssets( } envPerRoute.set(name, Array.from(env)) + wasmPerRoute.set(name, Array.from(wasmBindings)) } } }) @@ -375,7 +393,12 @@ export function collectAssets( stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets: any) => { - createAssets(compilation, assets, envPerRoute, options.isEdgeRuntime) + createAssets( + compilation, + assets, + { envPerRoute, wasmPerRoute }, + options.isEdgeRuntime + ) } ) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 69d30e7357d4..921a1f7d4806 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1269,6 +1269,7 @@ export default class NextNodeServer extends BaseServer { name: middlewareInfo.name, paths: middlewareInfo.paths, env: middlewareInfo.env, + wasmBindings: middlewareInfo.wasmBindings, request: { headers: params.request.headers, method, diff --git a/packages/next/server/require.ts b/packages/next/server/require.ts index 9c34b8538300..df3460b7060c 100644 --- a/packages/next/server/require.ts +++ b/packages/next/server/require.ts @@ -11,6 +11,7 @@ import { normalizePagePath, denormalizePagePath } from './normalize-page-path' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' +import type { WasmBinding } from '../build/webpack/loaders/next-middleware-wasm-loader' export function pageNotFoundError(page: string): Error { const err: any = new Error(`Cannot find module for page: ${page}`) @@ -85,7 +86,12 @@ export function getMiddlewareInfo(params: { distDir: string page: string serverless: boolean -}): { name: string; paths: string[]; env: string[] } { +}): { + name: string + paths: string[] + env: string[] + wasmBindings: WasmBinding[] +} { const serverBuildPath = join( params.distDir, params.serverless && !params.dev ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY @@ -113,5 +119,9 @@ export function getMiddlewareInfo(params: { name: pageInfo.name, paths: pageInfo.files.map((file) => join(params.distDir, file)), env: pageInfo.env ?? [], + wasmBindings: (pageInfo.wasmBindings ?? []).map((binding) => ({ + ...binding, + filePath: join(params.distDir, binding.filePath), + })), } } diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 42f480bb6a31..2c56aa841a60 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -10,6 +10,7 @@ import { AbortSignal, } from 'next/dist/compiled/abort-controller' import vm from 'vm' +import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader' const WEBPACK_HASH_REGEX = /__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g @@ -52,18 +53,19 @@ const caches = new Map< * run in within the context. It may or may not use a cache depending on * the parameters. */ -export function getModuleContext(options: { +export async function getModuleContext(options: { module: string onWarning: (warn: Error) => void useCache: boolean env: string[] + wasmBindings: WasmBinding[] }) { let moduleCache = options.useCache ? caches.get(options.module) - : createModuleContext(options) + : await createModuleContext(options) if (!moduleCache) { - moduleCache = createModuleContext(options) + moduleCache = await createModuleContext(options) caches.set(options.module, moduleCache) } @@ -95,10 +97,11 @@ export function getModuleContext(options: { * 2. Dependencies that require runtime globals such as Blob. * 3. Dependencies that are scoped for the provided parameters. */ -function createModuleContext(options: { +async function createModuleContext(options: { onWarning: (warn: Error) => void module: string env: string[] + wasmBindings: WasmBinding[] }) { const requireCache = new Map([ [require.resolve('next/dist/compiled/cookie'), { exports: cookie }], @@ -166,6 +169,8 @@ function createModuleContext(options: { return fetch(String(input), init) } + Object.assign(context, await loadWasmBindings(options.wasmBindings)) + return moduleCache } @@ -260,3 +265,14 @@ function buildEnvironmentVariablesFrom( const pairs = keys.map((key) => [key, process.env[key]]) return Object.fromEntries(pairs) } + +async function loadWasmBindings( + wasmBindings: WasmBinding[] +): Promise> { + const modules: Record = {} + for (const binding of wasmBindings) { + const module = await WebAssembly.compile(readFileSync(binding.filePath)) + modules[binding.name] = module + } + return modules +} diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 576e8be7580c..543421d7ef36 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -1,3 +1,4 @@ +import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader' import type { RequestData, FetchEventResult } from '../types' import { getModuleContext } from './context' @@ -8,12 +9,14 @@ export async function run(params: { paths: string[] request: RequestData useCache: boolean + wasmBindings: WasmBinding[] }): Promise { - const { runInContext, context } = getModuleContext({ + const { runInContext, context } = await getModuleContext({ module: params.name, onWarning: params.onWarning, useCache: params.useCache !== false, env: params.env, + wasmBindings: params.wasmBindings, }) for (const paramPath of params.paths) {