Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose WASM bindings in Middleware #34437

Merged
merged 11 commits into from Mar 2, 2022
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$/,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to prefix this? Otherwise its going to change all wasm imports instead of just middleware, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the secret sauce is issuerLayer which should only affect the middleware layer

issuerLayer: 'middleware',
loader: 'next-middleware-wasm-loader',
type: 'javascript/auto',
resourceQuery: /module/i,
})

webpack5Config.experiments = {
layers: true,
cacheUnaffected: true,
Expand Down
@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it better to use xxhash64 from webpack/lib/util/createHash ? /cc @sokra

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.