Skip to content

Commit

Permalink
Expose WASM bindings in Middleware (#34437)
Browse files Browse the repository at this point in the history
This PR introduces a way to use WASM in middlewares.
Next.js will find all `.wasm` imports in middlewares and load them as `WebAssembly.Module` objects, which then can be later instantiated.
The metadata will be stored in `middleware-manifest.json`

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
  • Loading branch information
Schniz committed Mar 2, 2022
1 parent b579a35 commit 7b2fb70
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 15 deletions.
9 changes: 9 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -1146,6 +1146,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)
Expand Down Expand Up @@ -1529,6 +1530,14 @@ export default async function getBaseWebpackConfig(

const webpack5Config = webpackConfig as webpack5.Configuration

webpack5Config.module?.rules?.unshift({
test: /\.wasm$/,
issuerLayer: 'middleware',
loader: 'next-middleware-wasm-loader',
type: 'javascript/auto',
resourceQuery: /module/i,
})

webpack5Config.experiments = {
layers: true,
cacheUnaffected: true,
Expand Down
21 changes: 21 additions & 0 deletions 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')
}
Expand Up @@ -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 {
Expand Down Expand Up @@ -52,15 +52,15 @@ export default class FunctionsManifestPlugin {
createAssets(
compilation: webpack5.Compilation,
assets: any,
envPerRoute: Map<string, string[]>,
perRoute: PerRoute,
isEdgeRuntime: boolean
) {
const functionsManifest: FunctionsManifest = {
version: 1,
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'
Expand Down
33 changes: 28 additions & 5 deletions packages/next/build/webpack/plugins/middleware-plugin.ts
Expand Up @@ -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$/
Expand All @@ -27,6 +28,7 @@ export interface MiddlewareManifest {
name: string
page: string
regexp: string
wasm?: WasmBinding[]
}
}
}
Expand All @@ -49,9 +51,14 @@ function getPageFromEntrypointName(pagePath: string) {
return page
}

export type PerRoute = {
envPerRoute: Map<string, string[]>
wasmPerRoute: Map<string, WasmBinding[]>
}

export function getEntrypointInfo(
compilation: webpack5.Compilation,
envPerRoute: Map<string, string[]>,
{ envPerRoute, wasmPerRoute }: PerRoute,
isEdgeRuntime: boolean
) {
const entrypoints = compilation.entrypoints
Expand Down Expand Up @@ -87,6 +94,7 @@ export function getEntrypointInfo(

infos.push({
env: envPerRoute.get(entrypoint.name) || [],
wasm: wasmPerRoute.get(entrypoint.name) || [],
files,
name: entrypoint.name,
page,
Expand Down Expand Up @@ -114,10 +122,14 @@ export default class MiddlewarePlugin {
createAssets(
compilation: webpack5.Compilation,
assets: any,
envPerRoute: Map<string, string[]>,
{ 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
})
Expand Down Expand Up @@ -152,7 +164,7 @@ export function collectAssets(
createAssets: (
compilation: webpack5.Compilation,
assets: any,
envPerRoute: Map<string, string[]>,
{ envPerRoute, wasmPerRoute }: PerRoute,
isEdgeRuntime: boolean
) => void,
options: {
Expand All @@ -175,6 +187,7 @@ export function collectAssets(
})

const envPerRoute = new Map<string, string[]>()
const wasmPerRoute = new Map<string, WasmBinding[]>()

compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => {
const { moduleGraph } = compilation as any
Expand All @@ -187,6 +200,7 @@ export function collectAssets(
) {
const middlewareEntries = new Set<webpack5.Module>()
const env = new Set<string>()
const wasm = new Set<WasmBinding>()

const addEntriesFromDependency = (dep: any) => {
const module = moduleGraph.getModule(dep)
Expand All @@ -203,6 +217,9 @@ export function collectAssets(
const queue = new Set(middlewareEntries)
for (const module of queue) {
const { buildInfo } = module
if (buildInfo.nextWasmMiddlewareBinding) {
wasm.add(buildInfo.nextWasmMiddlewareBinding)
}
if (
!options.dev &&
buildInfo &&
Expand Down Expand Up @@ -247,6 +264,7 @@ export function collectAssets(
}

envPerRoute.set(name, Array.from(env))
wasmPerRoute.set(name, Array.from(wasm))
}
}
})
Expand Down Expand Up @@ -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
)
}
)
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/next-server.ts
Expand Up @@ -1269,6 +1269,7 @@ export default class NextNodeServer extends BaseServer {
name: middlewareInfo.name,
paths: middlewareInfo.paths,
env: middlewareInfo.env,
wasm: middlewareInfo.wasm,
request: {
headers: params.request.headers,
method,
Expand Down
12 changes: 11 additions & 1 deletion packages/next/server/require.ts
Expand Up @@ -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}`)
Expand Down Expand Up @@ -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[]
wasm: WasmBinding[]
} {
const serverBuildPath = join(
params.distDir,
params.serverless && !params.dev ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
Expand Down Expand Up @@ -113,5 +119,9 @@ export function getMiddlewareInfo(params: {
name: pageInfo.name,
paths: pageInfo.files.map((file) => join(params.distDir, file)),
env: pageInfo.env ?? [],
wasm: (pageInfo.wasm ?? []).map((binding) => ({
...binding,
filePath: join(params.distDir, binding.filePath),
})),
}
}
32 changes: 27 additions & 5 deletions packages/next/server/web/sandbox/context.ts
@@ -1,6 +1,6 @@
import type { Context } from 'vm'
import { Blob, File, FormData } from 'next/dist/compiled/formdata-node'
import { readFileSync } from 'fs'
import { readFileSync, promises as fs } from 'fs'
import { requireDependencies } from './require'
import { TransformStream } from 'next/dist/compiled/web-streams-polyfill'
import cookie from 'next/dist/compiled/cookie'
Expand All @@ -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
Expand Down Expand Up @@ -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[]
wasm: 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)
}

Expand Down Expand Up @@ -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[]
wasm: WasmBinding[]
}) {
const requireCache = new Map([
[require.resolve('next/dist/compiled/cookie'), { exports: cookie }],
Expand Down Expand Up @@ -166,6 +169,8 @@ function createModuleContext(options: {
return fetch(String(input), init)
}

Object.assign(context, await loadWasm(options.wasm))

return moduleCache
}

Expand Down Expand Up @@ -260,3 +265,20 @@ function buildEnvironmentVariablesFrom(
const pairs = keys.map((key) => [key, process.env[key]])
return Object.fromEntries(pairs)
}

async function loadWasm(
wasm: WasmBinding[]
): Promise<Record<string, WebAssembly.Module>> {
const modules: Record<string, WebAssembly.Module> = {}

await Promise.all(
wasm.map(async (binding) => {
const module = await WebAssembly.compile(
await fs.readFile(binding.filePath)
)
modules[binding.name] = module
})
)

return modules
}
5 changes: 4 additions & 1 deletion 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'

Expand All @@ -8,12 +9,14 @@ export async function run(params: {
paths: string[]
request: RequestData
useCache: boolean
wasm: WasmBinding[]
}): Promise<FetchEventResult> {
const { runInContext, context } = getModuleContext({
const { runInContext, context } = await getModuleContext({
module: params.name,
onWarning: params.onWarning,
useCache: params.useCache !== false,
env: params.env,
wasm: params.wasm,
})

for (const paramPath of params.paths) {
Expand Down
Binary file added test/e2e/middleware-can-use-wasm-files/add.wasm
Binary file not shown.

0 comments on commit 7b2fb70

Please sign in to comment.