Skip to content

fix(middleware): Support wasm in bundled middleware #802

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

Merged
merged 3 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/perfect-cobras-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": patch
---

fix(middleware): copy wasm files for bundled middleware
31 changes: 22 additions & 9 deletions packages/open-next/src/build/createServerBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "node:path";

import type { FunctionOptions, SplittedFunctionOptions } from "types/open-next";

import { loadMiddlewareManifest } from "config/util.js";
import type { Plugin } from "esbuild";
import logger from "../logger.js";
import { minifyAll } from "../minimize-js.js";
Expand All @@ -13,7 +14,10 @@ import { getCrossPlatformPathRegex } from "../utils/regex.js";
import { bundleNextServer } from "./bundleNextServer.js";
import { compileCache } from "./compileCache.js";
import { copyTracedFiles } from "./copyTracedFiles.js";
import { generateEdgeBundle } from "./edge/createEdgeBundle.js";
import {
copyMiddlewareResources,
generateEdgeBundle,
} from "./edge/createEdgeBundle.js";
import * as buildHelper from "./helper.js";
import { installDependencies } from "./installDeps.js";
import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js";
Expand Down Expand Up @@ -135,12 +139,14 @@ async function generateBundle(
// `.next/standalone/package/path` (ie. `.next`, `server.js`).
// We need to output the handler file inside the package path.
const packagePath = buildHelper.getPackagePath(options);
fs.mkdirSync(path.join(outputPath, packagePath), { recursive: true });
const outPackagePath = path.join(outputPath, packagePath);

fs.mkdirSync(outPackagePath, { recursive: true });

const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs";
fs.copyFileSync(
path.join(options.buildDir, `cache.${ext}`),
path.join(outputPath, packagePath, "cache.cjs"),
path.join(outPackagePath, "cache.cjs"),
);

if (fnOptions.runtime === "deno") {
Expand All @@ -150,7 +156,7 @@ async function generateBundle(
// Bundle next server if necessary
const isBundled = fnOptions.experimentalBundledNextServer ?? false;
if (isBundled) {
await bundleNextServer(path.join(outputPath, packagePath), appPath, {
await bundleNextServer(outPackagePath, appPath, {
minify: options.minify,
});
}
Expand All @@ -159,15 +165,22 @@ async function generateBundle(
if (!config.middleware?.external) {
fs.copyFileSync(
path.join(options.buildDir, "middleware.mjs"),
path.join(outputPath, packagePath, "middleware.mjs"),
path.join(outPackagePath, "middleware.mjs"),
Copy link
Contributor

Choose a reason for hiding this comment

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

All this copy logic should also be in a function on this file so that everything middleware related is there https://github.com/opennextjs/opennextjs-aws/blob/main/packages/open-next/src/build/createMiddleware.ts.

Pretty sure we need to copy the wasm/asset files for the external middleware as well

);

const middlewareManifest = loadMiddlewareManifest(
Copy link
Contributor

Choose a reason for hiding this comment

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

We should create a generic function for this and use it both here and there

//Copy wasm files
const wasmFiles = middlewareInfo.wasm;
mkdirSync(path.join(outputDir, "wasm"), { recursive: true });
for (const wasmFile of wasmFiles) {
fs.copyFileSync(
path.join(buildOutputDotNextDir, wasmFile.filePath),
path.join(outputDir, `wasm/${wasmFile.name}.wasm`),
);
}
// Copy assets
const assets = middlewareInfo.assets;
mkdirSync(path.join(outputDir, "assets"), { recursive: true });
for (const asset of assets) {
fs.copyFileSync(
path.join(buildOutputDotNextDir, asset.filePath),
path.join(outputDir, `assets/${asset.name}`),
);
}

We also need to copy the assets

path.join(options.appBuildOutputPath, ".next"),
);

copyMiddlewareResources(
options,
middlewareManifest.middleware["/"],
outPackagePath,
);
}

// Copy open-next.config.mjs
buildHelper.copyOpenNextConfig(
options.buildDir,
path.join(outputPath, packagePath),
);
buildHelper.copyOpenNextConfig(options.buildDir, outPackagePath);

// Copy env files
buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath);
Expand Down
51 changes: 29 additions & 22 deletions packages/open-next/src/build/edge/createEdgeBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function buildEdgeBundle({
name,
additionalPlugins: additionalPluginsFn,
}: BuildEdgeBundleOptions) {
const isInCloudfare = await isEdgeRuntime(overrides);
const isInCloudflare = await isEdgeRuntime(overrides);
function override<T extends keyof Override>(target: T) {
return typeof overrides?.[target] === "string"
? overrides[target]
Expand Down Expand Up @@ -103,7 +103,7 @@ export async function buildEdgeBundle({
openNextEdgePlugins({
middlewareInfo,
nextDir: path.join(options.appBuildOutputPath, ".next"),
isInCloudfare,
isInCloudflare,
}),
...additionalPlugins,
// The content updater plugin must be the last plugin
Expand Down Expand Up @@ -146,7 +146,7 @@ Object.defineProperty = function(o, p, a) {
};

${
isInCloudfare
isInCloudflare
? ""
: `
const require = (await import("node:module")).createRequire(import.meta.url);
Expand Down Expand Up @@ -209,25 +209,7 @@ export async function generateEdgeBundle(
}
const middlewareInfo = functions[0];

//Copy wasm files
const wasmFiles = middlewareInfo.wasm;
mkdirSync(path.join(outputDir, "wasm"), { recursive: true });
for (const wasmFile of wasmFiles) {
fs.copyFileSync(
path.join(buildOutputDotNextDir, wasmFile.filePath),
path.join(outputDir, `wasm/${wasmFile.name}.wasm`),
);
}

// Copy assets
const assets = middlewareInfo.assets;
mkdirSync(path.join(outputDir, "assets"), { recursive: true });
for (const asset of assets) {
fs.copyFileSync(
path.join(buildOutputDotNextDir, asset.filePath),
path.join(outputDir, `assets/${asset.name}`),
);
}
copyMiddlewareResources(options, middlewareInfo, outputDir);

await buildEdgeBundle({
middlewareInfo,
Expand All @@ -240,3 +222,28 @@ export async function generateEdgeBundle(
additionalPlugins,
});
}

/**
* Copy wasm files and assets into the destDir.
*/
export function copyMiddlewareResources(
options: BuildOptions,
middlewareInfo: MiddlewareInfo | undefined,
destDir: string,
) {
mkdirSync(path.join(destDir, "wasm"), { recursive: true });
for (const file of middlewareInfo?.wasm ?? []) {
fs.copyFileSync(
path.join(options.appBuildOutputPath, ".next", file.filePath),
path.join(destDir, `wasm/${file.name}.wasm`),
);
}

mkdirSync(path.join(destDir, "assets"), { recursive: true });
for (const file of middlewareInfo?.assets ?? []) {
fs.copyFileSync(
path.join(options.appBuildOutputPath, ".next", file.filePath),
path.join(destDir, `assets/${file.name}`),
);
}
}
36 changes: 25 additions & 11 deletions packages/open-next/src/plugins/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ import { getCrossPlatformPathRegex } from "../utils/regex.js";
export interface IPluginSettings {
nextDir: string;
middlewareInfo?: MiddlewareInfo;
isInCloudfare?: boolean;
isInCloudflare?: boolean;
}

/**
* @param opts.nextDir - The path to the .next directory
* @param opts.middlewareInfo - Information about the middleware
* @param opts.isInCloudfare - Whether the code runs on the cloudflare runtime
* @param opts.isInCloudflare - Whether the code runs on the cloudflare runtime
* @returns
*/
export function openNextEdgePlugins({
nextDir,
middlewareInfo,
isInCloudfare,
isInCloudflare,
}: IPluginSettings): Plugin {
const entryFiles =
middlewareInfo?.files.map((file: string) => path.join(nextDir, file)) ?? [];
Expand Down Expand Up @@ -94,7 +94,7 @@ globalThis.self = globalThis;
globalThis._ROUTES = ${JSON.stringify(routes)};

${
isInCloudfare
isInCloudflare
? ""
: `
import {readFileSync} from "node:fs";
Expand Down Expand Up @@ -138,16 +138,11 @@ if (!globalThis.URLPattern) {
}
`
}
${wasmFiles
.map((file) =>
isInCloudfare
? `import ${file.name} from './wasm/${file.name}.wasm';`
: `const ${file.name} = readFileSync(path.join(__dirname,'/wasm/${file.name}.wasm'));`,
)
.join("\n")}
${importWasm(wasmFiles, { isInCloudflare })}
${entryFiles.map((file) => `require("${file}");`).join("\n")}
${contents}
`;

return {
contents,
};
Expand Down Expand Up @@ -202,3 +197,22 @@ ${contents}
},
};
}

function importWasm(
files: MiddlewareInfo["wasm"],
{ isInCloudflare }: { isInCloudflare?: boolean },
) {
return files
.map(({ name }) => {
if (isInCloudflare) {
// As `.next/server/src/middleware.js` references the name,
// using `import ${name} from '...'` would cause ESBuild to rename the import.
// We use `globalThis.${name}` to make sure `middleware.js` reference name will match.
return `import __onw_${name}__ from './wasm/${name}.wasm'
globalThis.${name} = __onw_${name}__`;
}

return `const ${name} = readFileSync(path.join(__dirname,'/wasm/${name}.wasm'));`;
})
.join("\n");
}