From a732739629e9205d73e01b23b6c06c538b64a390 Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Mon, 28 Jul 2025 11:08:29 +0200
Subject: [PATCH 1/9] fix: fail build/deploy when using unsupported Node.js
 Midleware

---
 src/build/content/server.ts                   | 18 +++++++++++++++++
 tests/fixtures/middleware-node/app/layout.js  | 12 +++++++++++
 tests/fixtures/middleware-node/app/page.js    |  7 +++++++
 tests/fixtures/middleware-node/middleware.ts  |  9 +++++++++
 tests/fixtures/middleware-node/next.config.js | 12 +++++++++++
 tests/fixtures/middleware-node/package.json   | 20 +++++++++++++++++++
 tests/integration/edge-handler.test.ts        | 11 ++++++++++
 7 files changed, 89 insertions(+)
 create mode 100644 tests/fixtures/middleware-node/app/layout.js
 create mode 100644 tests/fixtures/middleware-node/app/page.js
 create mode 100644 tests/fixtures/middleware-node/middleware.ts
 create mode 100644 tests/fixtures/middleware-node/next.config.js
 create mode 100644 tests/fixtures/middleware-node/package.json

diff --git a/src/build/content/server.ts b/src/build/content/server.ts
index 40e15663f3..3959dc6dc9 100644
--- a/src/build/content/server.ts
+++ b/src/build/content/server.ts
@@ -17,6 +17,7 @@ import { trace } from '@opentelemetry/api'
 import { wrapTracer } from '@opentelemetry/api/experimental'
 import glob from 'fast-glob'
 import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
+import type { FunctionsConfigManifest } from 'next-with-cache-handler-v2/dist/build/index.js'
 import { prerelease, satisfies, lt as semverLowerThan, lte as semverLowerThanOrEqual } from 'semver'
 
 import type { RunConfig } from '../../run/config.js'
@@ -131,6 +132,10 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
           return
         }
 
+        if (path === 'server/functions-config-manifest.json') {
+          await verifyFunctionsConfigManifest(join(srcDir, path))
+        }
+
         await cp(srcPath, destPath, { recursive: true, force: true })
       }),
     )
@@ -376,6 +381,19 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) =
   await writeFile(destPath, newData)
 }
 
+const verifyFunctionsConfigManifest = async (sourcePath: string) => {
+  const data = await readFile(sourcePath, 'utf8')
+  const manifest = JSON.parse(data) as FunctionsConfigManifest
+
+  // https://github.com/vercel/next.js/blob/8367faedd61501025299e92d43a28393c7bb50e2/packages/next/src/build/index.ts#L2465
+  // Node.js Middleware has hardcoded /_middleware path
+  if (manifest.functions['/_middleware']) {
+    throw new Error(
+      'Only Edge Runtime Middleware is supported. Node.js Middleware is not supported.',
+    )
+  }
+}
+
 export const verifyHandlerDirStructure = async (ctx: PluginContext) => {
   const { nextConfig } = JSON.parse(
     await readFile(join(ctx.serverHandlerDir, RUN_CONFIG_FILE), 'utf-8'),
diff --git a/tests/fixtures/middleware-node/app/layout.js b/tests/fixtures/middleware-node/app/layout.js
new file mode 100644
index 0000000000..6565e7bafd
--- /dev/null
+++ b/tests/fixtures/middleware-node/app/layout.js
@@ -0,0 +1,12 @@
+export const metadata = {
+  title: 'Simple Next App',
+  description: 'Description for Simple Next App',
+}
+
+export default function RootLayout({ children }) {
+  return (
+    <html lang="en">
+      <body>{children}</body>
+    </html>
+  )
+}
diff --git a/tests/fixtures/middleware-node/app/page.js b/tests/fixtures/middleware-node/app/page.js
new file mode 100644
index 0000000000..1a9fe06903
--- /dev/null
+++ b/tests/fixtures/middleware-node/app/page.js
@@ -0,0 +1,7 @@
+export default function Home() {
+  return (
+    <main>
+      <h1>Home</h1>
+    </main>
+  )
+}
diff --git a/tests/fixtures/middleware-node/middleware.ts b/tests/fixtures/middleware-node/middleware.ts
new file mode 100644
index 0000000000..064f5bb6c3
--- /dev/null
+++ b/tests/fixtures/middleware-node/middleware.ts
@@ -0,0 +1,9 @@
+import type { NextRequest } from 'next/server'
+
+export async function middleware(request: NextRequest) {
+  console.log('Node.js Middleware request:', request.method, request.nextUrl.pathname)
+}
+
+export const config = {
+  runtime: 'nodejs',
+}
diff --git a/tests/fixtures/middleware-node/next.config.js b/tests/fixtures/middleware-node/next.config.js
new file mode 100644
index 0000000000..24a4bdfa44
--- /dev/null
+++ b/tests/fixtures/middleware-node/next.config.js
@@ -0,0 +1,12 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  output: 'standalone',
+  eslint: {
+    ignoreDuringBuilds: true,
+  },
+  experimental: {
+    nodeMiddleware: true,
+  },
+}
+
+module.exports = nextConfig
diff --git a/tests/fixtures/middleware-node/package.json b/tests/fixtures/middleware-node/package.json
new file mode 100644
index 0000000000..735b637ecc
--- /dev/null
+++ b/tests/fixtures/middleware-node/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "middleware-node",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "postinstall": "next build",
+    "dev": "next dev",
+    "build": "next build"
+  },
+  "dependencies": {
+    "next": "canary",
+    "react": "18.2.0",
+    "react-dom": "18.2.0"
+  },
+  "test": {
+    "dependencies": {
+      "next": ">=15.2.0"
+    }
+  }
+}
diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts
index 825ed6fac1..b3632a279c 100644
--- a/tests/integration/edge-handler.test.ts
+++ b/tests/integration/edge-handler.test.ts
@@ -4,6 +4,7 @@ import { type FixtureTestContext } from '../utils/contexts.js'
 import { createFixture, invokeEdgeFunction, runPlugin } from '../utils/fixture.js'
 import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
 import { LocalServer } from '../utils/local-server.js'
+import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
 
 beforeEach<FixtureTestContext>(async (ctx) => {
   // set for each test a new deployID and siteID
@@ -626,3 +627,13 @@ describe('page router', () => {
     expect(bodyFr.nextUrlLocale).toBe('fr')
   })
 })
+
+test.skipIf(!nextVersionSatisfies('>=15.2.0'))<FixtureTestContext>(
+  'should throw an Not Supported error when node middleware is used',
+  async (ctx) => {
+    await createFixture('middleware-node', ctx)
+    await expect(runPlugin(ctx)).rejects.toThrow(
+      'Only Edge Runtime Middleware is supported. Node.js Middleware is not supported.',
+    )
+  },
+)

From fc55e715871edf9f17d414d473b00b9903a0435a Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Wed, 30 Jul 2025 13:12:25 +0200
Subject: [PATCH 2/9] [wip] feat: support node middleware

---
 src/build/content/server.ts                   |  43 ++++-
 src/build/functions/edge.ts                   | 159 ++++++++++++++++++
 src/build/plugin-context.ts                   |  18 ++
 tests/fixtures/middleware-node/middleware.ts  |   7 +-
 tests/fixtures/middleware-node/next.config.js |   5 +
 5 files changed, 223 insertions(+), 9 deletions(-)

diff --git a/src/build/content/server.ts b/src/build/content/server.ts
index 3959dc6dc9..fbd289533c 100644
--- a/src/build/content/server.ts
+++ b/src/build/content/server.ts
@@ -133,7 +133,13 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
         }
 
         if (path === 'server/functions-config-manifest.json') {
-          await verifyFunctionsConfigManifest(join(srcDir, path))
+          try {
+            await replaceFunctionsConfigManifest(srcPath, destPath)
+          } catch (error) {
+            throw new Error('Could not patch functions config manifest file', { cause: error })
+          }
+
+          return
         }
 
         await cp(srcPath, destPath, { recursive: true, force: true })
@@ -381,16 +387,41 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) =
   await writeFile(destPath, newData)
 }
 
-const verifyFunctionsConfigManifest = async (sourcePath: string) => {
+// similar to the middleware manifest, we need to patch the functions config manifest to disable
+// the middleware that is defined in the functions config manifest. This is needed to avoid running
+// the middleware in the server handler, while still allowing next server to enable some middleware
+// specific handling such as _next/data normalization ( https://github.com/vercel/next.js/blob/7bb72e508572237fe0d4aac5418546d4b4b3a363/packages/next/src/server/lib/router-utils/resolve-routes.ts#L395 )
+const replaceFunctionsConfigManifest = async (sourcePath: string, destPath: string) => {
   const data = await readFile(sourcePath, 'utf8')
   const manifest = JSON.parse(data) as FunctionsConfigManifest
 
   // https://github.com/vercel/next.js/blob/8367faedd61501025299e92d43a28393c7bb50e2/packages/next/src/build/index.ts#L2465
   // Node.js Middleware has hardcoded /_middleware path
-  if (manifest.functions['/_middleware']) {
-    throw new Error(
-      'Only Edge Runtime Middleware is supported. Node.js Middleware is not supported.',
-    )
+  if (manifest?.functions?.['/_middleware']?.matchers) {
+    const newManifest = {
+      ...manifest,
+      functions: {
+        ...manifest.functions,
+        '/_middleware': {
+          ...manifest.functions['/_middleware'],
+          matchers: manifest.functions['/_middleware'].matchers.map((matcher) => {
+            return {
+              ...matcher,
+              // matcher that won't match on anything
+              // this is meant to disable actually running middleware in the server handler,
+              // while still allowing next server to enable some middleware specific handling
+              // such as _next/data normalization ( https://github.com/vercel/next.js/blob/7bb72e508572237fe0d4aac5418546d4b4b3a363/packages/next/src/server/lib/router-utils/resolve-routes.ts#L395 )
+              regexp: '(?!.*)',
+            }
+          }),
+        },
+      },
+    }
+    const newData = JSON.stringify(newManifest)
+
+    await writeFile(destPath, newData)
+  } else {
+    await cp(sourcePath, destPath, { recursive: true, force: true })
   }
 }
 
diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts
index af6405b57c..49fa1fd2c0 100644
--- a/src/build/functions/edge.ts
+++ b/src/build/functions/edge.ts
@@ -194,11 +194,170 @@ export const clearStaleEdgeHandlers = async (ctx: PluginContext) => {
 }
 
 export const createEdgeHandlers = async (ctx: PluginContext) => {
+  // Edge middleware
   const nextManifest = await ctx.getMiddlewareManifest()
+  // Node middleware
+  const functionsConfigManifest = await ctx.getFunctionsConfigManifest()
+
   const nextDefinitions = [...Object.values(nextManifest.middleware)]
   await Promise.all(nextDefinitions.map((def) => createEdgeHandler(ctx, def)))
 
   const netlifyDefinitions = nextDefinitions.flatMap((def) => buildHandlerDefinition(ctx, def))
+
+  if (functionsConfigManifest?.functions?.['/_middleware']) {
+    const middlewareDefinition = functionsConfigManifest?.functions?.['/_middleware']
+    const entry = 'server/middleware.js'
+    const nft = `${entry}.nft.json`
+    const name = 'node-middleware'
+
+    // await copyHandlerDependencies(ctx, definition)
+    const srcDir = join(ctx.standaloneDir, ctx.nextDistDir)
+    // const destDir = join(ctx.edgeFunctionsDir, getHandlerName({ name }))
+
+    const fakeNodeModuleName = 'fake-module-with-middleware'
+
+    const fakeNodeModulePath = ctx.resolveFromPackagePath(join('node_modules', fakeNodeModuleName))
+
+    const nftFilesPath = join(ctx.nextDistDir, nft)
+    const nftManifest = JSON.parse(await readFile(nftFilesPath, 'utf8'))
+
+    const files: string[] = nftManifest.files.map((file: string) => join('server', file))
+    files.push(entry)
+
+    // files are relative to location of middleware entrypoint
+    // we need to capture all of them
+    // they might be going to parent directories, so first we check how many directories we need to go up
+    const maxDirsUp = files.reduce((max, file) => {
+      let dirsUp = 0
+      for (const part of file.split('/')) {
+        if (part === '..') {
+          dirsUp += 1
+        } else {
+          break
+        }
+      }
+      return Math.max(max, dirsUp)
+    }, 0)
+
+    let prefixPath = ''
+    for (let nestedIndex = 1; nestedIndex <= maxDirsUp; nestedIndex++) {
+      // TODO: ideally we preserve the original directory structure
+      // this is just hack to use arbitrary computed names to speed up hooking things up
+      prefixPath += `nested-${nestedIndex}/`
+    }
+
+    for (const file of files) {
+      const srcPath = join(srcDir, file)
+      const destPath = join(fakeNodeModulePath, prefixPath, file)
+
+      await mkdir(dirname(destPath), { recursive: true })
+
+      if (file === entry) {
+        const content = await readFile(srcPath, 'utf8')
+        await writeFile(
+          destPath,
+          // Next.js needs to be set on global even if it's possible to just require it
+          // so somewhat similar to existing shim we have for edge runtime
+          `globalThis.AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage;\n${content}`,
+        )
+      } else {
+        await cp(srcPath, destPath, { force: true })
+      }
+    }
+
+    await writeFile(join(fakeNodeModulePath, 'package.json'), JSON.stringify({ type: 'commonjs' }))
+
+    // there is `/chunks/**/*` require coming from webpack-runtime that fails esbuild due to nothing matching,
+    // so this ensure something does
+    const dummyChunkPath = join(fakeNodeModulePath, prefixPath, 'server', 'chunks', 'dummy.js')
+    await mkdir(dirname(dummyChunkPath), { recursive: true })
+    await writeFile(dummyChunkPath, '')
+
+    // await writeHandlerFile(ctx, definition)
+
+    const nextConfig = ctx.buildConfig
+    const handlerName = getHandlerName({ name })
+    const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName)
+    const handlerRuntimeDirectory = join(handlerDirectory, 'edge-runtime')
+
+    // Copying the runtime files. These are the compatibility layer between
+    // Netlify Edge Functions and the Next.js edge runtime.
+    await copyRuntime(ctx, handlerDirectory)
+
+    // Writing a file with the matchers that should trigger this function. We'll
+    // read this file from the function at runtime.
+    await writeFile(
+      join(handlerRuntimeDirectory, 'matchers.json'),
+      JSON.stringify(middlewareDefinition.matchers ?? []),
+    )
+
+    // The config is needed by the edge function to match and normalize URLs. To
+    // avoid shipping and parsing a large file at runtime, let's strip it down to
+    // just the properties that the edge function actually needs.
+    const minimalNextConfig = {
+      basePath: nextConfig.basePath,
+      i18n: nextConfig.i18n,
+      trailingSlash: nextConfig.trailingSlash,
+      skipMiddlewareUrlNormalize: nextConfig.skipMiddlewareUrlNormalize,
+    }
+
+    await writeFile(
+      join(handlerRuntimeDirectory, 'next.config.json'),
+      JSON.stringify(minimalNextConfig),
+    )
+
+    const htmlRewriterWasm = await readFile(
+      join(
+        ctx.pluginDir,
+        'edge-runtime/vendor/deno.land/x/htmlrewriter@v1.0.0/pkg/htmlrewriter_bg.wasm',
+      ),
+    )
+
+    // Writing the function entry file. It wraps the middleware code with the
+    // compatibility layer mentioned above.
+    await writeFile(
+      join(handlerDirectory, `${handlerName}.js`),
+      `
+    import { init as htmlRewriterInit } from './edge-runtime/vendor/deno.land/x/htmlrewriter@v1.0.0/src/index.ts'
+    import { handleMiddleware } from './edge-runtime/middleware.ts';
+
+    import * as handlerMod from '${fakeNodeModuleName}/${prefixPath}${entry}';
+
+    const handler = handlerMod.default || handlerMod;
+
+    await htmlRewriterInit({ module_or_path: Uint8Array.from(${JSON.stringify([
+      ...htmlRewriterWasm,
+    ])}) });
+
+    export default (req, context) => {
+      return handleMiddleware(req, context, handler);
+    };
+    `,
+    )
+
+    // buildHandlerDefinition(ctx, def)
+    const netlifyDefinitions: Manifest['functions'] = augmentMatchers(
+      middlewareDefinition.matchers ?? [],
+      ctx,
+    ).map((matcher) => {
+      return {
+        function: getHandlerName({ name }),
+        name: `Next.js Node Middleware Handler`,
+        pattern: matcher.regexp,
+        cache: undefined,
+        generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
+      }
+    })
+
+    const netlifyManifest: Manifest = {
+      version: 1,
+      functions: netlifyDefinitions,
+    }
+    await writeEdgeManifest(ctx, netlifyManifest)
+
+    return
+  }
+
   const netlifyManifest: Manifest = {
     version: 1,
     functions: netlifyDefinitions,
diff --git a/src/build/plugin-context.ts b/src/build/plugin-context.ts
index 9148d0dd56..7ecc24e16c 100644
--- a/src/build/plugin-context.ts
+++ b/src/build/plugin-context.ts
@@ -14,6 +14,7 @@ import type { PrerenderManifest, RoutesManifest } from 'next/dist/build/index.js
 import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
 import type { PagesManifest } from 'next/dist/build/webpack/plugins/pages-manifest-plugin.js'
 import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
+import type { FunctionsConfigManifest } from 'next-with-cache-handler-v2/dist/build/index.js'
 import { satisfies } from 'semver'
 
 const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
@@ -259,6 +260,23 @@ export class PluginContext {
     )
   }
 
+  /**
+   * Get Next.js Functions Config Manifest config if it exists from the build output
+   */
+  async getFunctionsConfigManifest(): Promise<FunctionsConfigManifest | null> {
+    const functionsConfigManifestPath = join(
+      this.publishDir,
+      'server/functions-config-manifest.json',
+    )
+
+    if (existsSync(functionsConfigManifestPath)) {
+      return JSON.parse(await readFile(functionsConfigManifestPath, 'utf-8'))
+    }
+
+    // this file might not have been produced
+    return null
+  }
+
   // don't make private as it is handy inside testing to override the config
   _requiredServerFiles: RequiredServerFilesManifest | null = null
 
diff --git a/tests/fixtures/middleware-node/middleware.ts b/tests/fixtures/middleware-node/middleware.ts
index 064f5bb6c3..0ccf3aa76c 100644
--- a/tests/fixtures/middleware-node/middleware.ts
+++ b/tests/fixtures/middleware-node/middleware.ts
@@ -1,7 +1,8 @@
-import type { NextRequest } from 'next/server'
+import { type NextRequest, NextResponse } from 'next/server'
+import { join } from 'path'
 
-export async function middleware(request: NextRequest) {
-  console.log('Node.js Middleware request:', request.method, request.nextUrl.pathname)
+export default async function middleware(req: NextRequest) {
+  return NextResponse.json({ message: 'Hello, world!', joined: join('a', 'b') })
 }
 
 export const config = {
diff --git a/tests/fixtures/middleware-node/next.config.js b/tests/fixtures/middleware-node/next.config.js
index 24a4bdfa44..94c39a2d81 100644
--- a/tests/fixtures/middleware-node/next.config.js
+++ b/tests/fixtures/middleware-node/next.config.js
@@ -7,6 +7,11 @@ const nextConfig = {
   experimental: {
     nodeMiddleware: true,
   },
+  webpack: (config) => {
+    // disable minification for easier inspection of produced build output
+    config.optimization.minimize = false
+    return config
+  },
 }
 
 module.exports = nextConfig

From 2e1f69817f8919774d8fd672a6040e0875bacd43 Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Wed, 30 Jul 2025 14:02:34 +0200
Subject: [PATCH 3/9] shim otel, so things work when deploying from outside of
 this repo

---
 src/build/functions/edge.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts
index 49fa1fd2c0..b7232dcd21 100644
--- a/src/build/functions/edge.ts
+++ b/src/build/functions/edge.ts
@@ -273,6 +273,21 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
     await mkdir(dirname(dummyChunkPath), { recursive: true })
     await writeFile(dummyChunkPath, '')
 
+    // there is also `@opentelemetry/api` require that fails esbuild due to nothing matching,
+    // next is try/catching it and fallback to bundled version of otel package in case of errors
+    const otelApiPath = join(
+      fakeNodeModulePath,
+      'node_modules',
+      '@opentelemetry',
+      'api',
+      'index.js',
+    )
+    await mkdir(dirname(otelApiPath), { recursive: true })
+    await writeFile(
+      otelApiPath,
+      `throw new Error('this is dummy to satisfy esbuild used for npm compat using fake module')`,
+    )
+
     // await writeHandlerFile(ctx, definition)
 
     const nextConfig = ctx.buildConfig

From 661496bbc0efca403b9eb3a2ff537520701bec52 Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Wed, 30 Jul 2025 14:03:57 +0200
Subject: [PATCH 4/9] test: deploy no longer fail, so ignore this test

---
 tests/integration/edge-handler.test.ts | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts
index b3632a279c..9782160135 100644
--- a/tests/integration/edge-handler.test.ts
+++ b/tests/integration/edge-handler.test.ts
@@ -628,12 +628,13 @@ describe('page router', () => {
   })
 })
 
-test.skipIf(!nextVersionSatisfies('>=15.2.0'))<FixtureTestContext>(
-  'should throw an Not Supported error when node middleware is used',
-  async (ctx) => {
-    await createFixture('middleware-node', ctx)
-    await expect(runPlugin(ctx)).rejects.toThrow(
-      'Only Edge Runtime Middleware is supported. Node.js Middleware is not supported.',
-    )
-  },
-)
+// this is now actually deploying
+// test.skipIf(!nextVersionSatisfies('>=15.2.0'))<FixtureTestContext>(
+//   'should throw an Not Supported error when node middleware is used',
+//   async (ctx) => {
+//     await createFixture('middleware-node', ctx)
+//     await expect(runPlugin(ctx)).rejects.toThrow(
+//       'Only Edge Runtime Middleware is supported. Node.js Middleware is not supported.',
+//     )
+//   },
+// )

From 3c2789b486e6743d048ca7359cf85f9473edb7c6 Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Wed, 30 Jul 2025 15:15:35 +0200
Subject: [PATCH 5/9] test: try running existing tests against node middleware

---
 .github/workflows/run-tests.yml                              | 2 +-
 tests/fixtures/middleware-conditions/middleware.ts           | 1 +
 tests/fixtures/middleware-i18n-excluded-paths/middleware.ts  | 1 +
 tests/fixtures/middleware-i18n-skip-normalize/middleware.js  | 4 ++++
 tests/fixtures/middleware-i18n/middleware.js                 | 4 ++++
 tests/fixtures/middleware-node/middleware.ts                 | 4 +++-
 tests/fixtures/middleware-pages/middleware.js                | 4 ++++
 tests/fixtures/middleware-src/src/middleware.ts              | 1 +
 tests/fixtures/middleware-static-asset-matcher/middleware.ts | 1 +
 tests/fixtures/middleware-subrequest-vuln/middleware.ts      | 4 ++++
 tests/fixtures/middleware-trailing-slash/middleware.ts       | 4 ++++
 tests/fixtures/middleware/middleware.ts                      | 1 +
 12 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 96bce88839..0536c6388d 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -38,7 +38,7 @@ jobs:
           elif [ "${{ github.event_name }}" = "schedule" ] || [ "${{ steps.check-labels.outputs.result }}" = "true" ]; then
             echo "matrix=[\"latest\", \"canary\", \"14.2.15\", \"13.5.1\"]" >> $GITHUB_OUTPUT
           else
-            echo "matrix=[\"latest\"]" >> $GITHUB_OUTPUT
+            echo "matrix=[\"canary\"]" >> $GITHUB_OUTPUT
           fi
 
   e2e:
diff --git a/tests/fixtures/middleware-conditions/middleware.ts b/tests/fixtures/middleware-conditions/middleware.ts
index fdb332cf8e..ae6c50afc5 100644
--- a/tests/fixtures/middleware-conditions/middleware.ts
+++ b/tests/fixtures/middleware-conditions/middleware.ts
@@ -23,4 +23,5 @@ export const config = {
       locale: false,
     },
   ],
+  runtime: 'nodejs',
 }
diff --git a/tests/fixtures/middleware-i18n-excluded-paths/middleware.ts b/tests/fixtures/middleware-i18n-excluded-paths/middleware.ts
index 712f3648b7..7f5c235d6f 100644
--- a/tests/fixtures/middleware-i18n-excluded-paths/middleware.ts
+++ b/tests/fixtures/middleware-i18n-excluded-paths/middleware.ts
@@ -33,4 +33,5 @@ export const config = {
      */
     '/((?!api|excluded|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
   ],
+  runtime: 'nodejs',
 }
diff --git a/tests/fixtures/middleware-i18n-skip-normalize/middleware.js b/tests/fixtures/middleware-i18n-skip-normalize/middleware.js
index 24517d72de..0c39b3f66b 100644
--- a/tests/fixtures/middleware-i18n-skip-normalize/middleware.js
+++ b/tests/fixtures/middleware-i18n-skip-normalize/middleware.js
@@ -89,3 +89,7 @@ export async function middleware(request) {
     })
   }
 }
+
+export const config = {
+  runtime: 'nodejs',
+}
diff --git a/tests/fixtures/middleware-i18n/middleware.js b/tests/fixtures/middleware-i18n/middleware.js
index 3462214f1d..72da32c5fc 100644
--- a/tests/fixtures/middleware-i18n/middleware.js
+++ b/tests/fixtures/middleware-i18n/middleware.js
@@ -114,3 +114,7 @@ export async function middleware(request) {
     })
   }
 }
+
+export const config = {
+  runtime: 'nodejs',
+}
diff --git a/tests/fixtures/middleware-node/middleware.ts b/tests/fixtures/middleware-node/middleware.ts
index 0ccf3aa76c..33328de8d5 100644
--- a/tests/fixtures/middleware-node/middleware.ts
+++ b/tests/fixtures/middleware-node/middleware.ts
@@ -2,7 +2,9 @@ import { type NextRequest, NextResponse } from 'next/server'
 import { join } from 'path'
 
 export default async function middleware(req: NextRequest) {
-  return NextResponse.json({ message: 'Hello, world!', joined: join('a', 'b') })
+  const response = NextResponse.next()
+  response.headers.set('x-added-middleware-headers-join', join('a', 'b'))
+  return response
 }
 
 export const config = {
diff --git a/tests/fixtures/middleware-pages/middleware.js b/tests/fixtures/middleware-pages/middleware.js
index a89a491a8c..6e689b7bc7 100644
--- a/tests/fixtures/middleware-pages/middleware.js
+++ b/tests/fixtures/middleware-pages/middleware.js
@@ -123,3 +123,7 @@ const params = (url) => {
   }
   return result
 }
+
+export const config = {
+  runtime: 'nodejs',
+}
diff --git a/tests/fixtures/middleware-src/src/middleware.ts b/tests/fixtures/middleware-src/src/middleware.ts
index 247e7755c3..79963f7e9a 100644
--- a/tests/fixtures/middleware-src/src/middleware.ts
+++ b/tests/fixtures/middleware-src/src/middleware.ts
@@ -28,4 +28,5 @@ const getResponse = (request: NextRequest) => {
 
 export const config = {
   matcher: '/test/:path*',
+  runtime: 'nodejs',
 }
diff --git a/tests/fixtures/middleware-static-asset-matcher/middleware.ts b/tests/fixtures/middleware-static-asset-matcher/middleware.ts
index 26924f826d..3ea6d1362a 100644
--- a/tests/fixtures/middleware-static-asset-matcher/middleware.ts
+++ b/tests/fixtures/middleware-static-asset-matcher/middleware.ts
@@ -4,4 +4,5 @@ export default function middleware() {
 
 export const config = {
   matcher: '/hello/world.txt',
+  runtime: 'nodejs',
 }
diff --git a/tests/fixtures/middleware-subrequest-vuln/middleware.ts b/tests/fixtures/middleware-subrequest-vuln/middleware.ts
index c91447b69a..2b8cdea2b7 100644
--- a/tests/fixtures/middleware-subrequest-vuln/middleware.ts
+++ b/tests/fixtures/middleware-subrequest-vuln/middleware.ts
@@ -11,3 +11,7 @@ export async function middleware(request: NextRequest) {
 
   return response
 }
+
+export const config = {
+  runtime: 'nodejs',
+}
diff --git a/tests/fixtures/middleware-trailing-slash/middleware.ts b/tests/fixtures/middleware-trailing-slash/middleware.ts
index f4b2ae6390..0a34a67b90 100644
--- a/tests/fixtures/middleware-trailing-slash/middleware.ts
+++ b/tests/fixtures/middleware-trailing-slash/middleware.ts
@@ -56,3 +56,7 @@ const getResponse = (request: NextRequest) => {
 
   return NextResponse.json({ error: 'Error' }, { status: 500 })
 }
+
+export const config = {
+  runtime: 'nodejs',
+}
diff --git a/tests/fixtures/middleware/middleware.ts b/tests/fixtures/middleware/middleware.ts
index 735f3a8488..6280e410cd 100644
--- a/tests/fixtures/middleware/middleware.ts
+++ b/tests/fixtures/middleware/middleware.ts
@@ -92,4 +92,5 @@ const getResponse = (request: NextRequest) => {
 
 export const config = {
   matcher: '/test/:path*',
+  runtime: 'nodejs',
 }

From 22036403820694f1068bf9f476dd413d674422cd Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Wed, 30 Jul 2025 15:36:28 +0200
Subject: [PATCH 6/9] test: skip force chunking in middleware fixture

---
 tests/fixtures/middleware/next.config.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/fixtures/middleware/next.config.js b/tests/fixtures/middleware/next.config.js
index 28875fd694..4de8b236d0 100644
--- a/tests/fixtures/middleware/next.config.js
+++ b/tests/fixtures/middleware/next.config.js
@@ -7,7 +7,7 @@ const nextConfig = {
   webpack: (config) => {
     // this is a trigger to generate multiple `.next/server/middleware-[hash].js` files instead of
     // single `.next/server/middleware.js` file
-    config.optimization.splitChunks.maxSize = 100_000
+    // config.optimization.splitChunks.maxSize = 100_000
 
     return config
   },

From b5a32698c52959d1545a41128638d51fee8944d4 Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Thu, 31 Jul 2025 19:30:56 +0200
Subject: [PATCH 7/9] fix: nft reading to work in integration tests

---
 src/build/functions/edge.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts
index b7232dcd21..1c61c40067 100644
--- a/src/build/functions/edge.ts
+++ b/src/build/functions/edge.ts
@@ -218,7 +218,7 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
 
     const fakeNodeModulePath = ctx.resolveFromPackagePath(join('node_modules', fakeNodeModuleName))
 
-    const nftFilesPath = join(ctx.nextDistDir, nft)
+    const nftFilesPath = join(process.cwd(), ctx.nextDistDir, nft)
     const nftManifest = JSON.parse(await readFile(nftFilesPath, 'utf8'))
 
     const files: string[] = nftManifest.files.map((file: string) => join('server', file))

From 20310775a5ed6b335be07f756ae7c2c6f63d8d49 Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Thu, 31 Jul 2025 21:07:56 +0200
Subject: [PATCH 8/9] test: make sure to use appropiate edge handler name

---
 tests/integration/edge-handler.test.ts        | 60 ++++++++++---------
 .../integration/hello-world-turbopack.test.ts | 10 +++-
 tests/integration/wasm.test.ts                | 13 +++-
 tests/utils/fixture.ts                        |  6 ++
 4 files changed, 57 insertions(+), 32 deletions(-)

diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts
index 9782160135..23e009079c 100644
--- a/tests/integration/edge-handler.test.ts
+++ b/tests/integration/edge-handler.test.ts
@@ -1,7 +1,13 @@
 import { v4 } from 'uuid'
 import { beforeEach, describe, expect, test, vi } from 'vitest'
 import { type FixtureTestContext } from '../utils/contexts.js'
-import { createFixture, invokeEdgeFunction, runPlugin } from '../utils/fixture.js'
+import {
+  createFixture,
+  EDGE_MIDDLEWARE_FUNCTION_NAME,
+  EDGE_MIDDLEWARE_SRC_FUNCTION_NAME,
+  invokeEdgeFunction,
+  runPlugin,
+} from '../utils/fixture.js'
 import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
 import { LocalServer } from '../utils/local-server.js'
 import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
@@ -30,7 +36,7 @@ test<FixtureTestContext>('should add request/response headers', async (ctx) => {
   ctx.cleanup?.push(() => origin.stop())
 
   const response = await invokeEdgeFunction(ctx, {
-    functions: ['___netlify-edge-handler-middleware'],
+    functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
     origin,
     url: '/test/next',
   })
@@ -58,7 +64,7 @@ test<FixtureTestContext>('should add request/response headers when using src dir
   ctx.cleanup?.push(() => origin.stop())
 
   const response = await invokeEdgeFunction(ctx, {
-    functions: ['___netlify-edge-handler-src-middleware'],
+    functions: [EDGE_MIDDLEWARE_SRC_FUNCTION_NAME],
     origin,
     url: '/test/next',
   })
@@ -78,7 +84,7 @@ describe('redirect', () => {
 
     const origin = new LocalServer()
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       redirect: 'manual',
       url: '/test/redirect',
@@ -101,7 +107,7 @@ describe('redirect', () => {
 
     const origin = new LocalServer()
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       redirect: 'manual',
       url: '/test/redirect-with-headers',
@@ -140,7 +146,7 @@ describe('rewrite', () => {
     ctx.cleanup?.push(() => origin.stop())
 
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/test/rewrite-external?external-url=http://localhost:${external.port}/some-path`,
     })
@@ -167,7 +173,7 @@ describe('rewrite', () => {
     ctx.cleanup?.push(() => origin.stop())
 
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/test/rewrite-external?external-url=http://localhost:${external.port}/some-path`,
       redirect: 'manual',
@@ -196,7 +202,7 @@ describe("aborts middleware execution when the matcher conditions don't match th
     ctx.cleanup?.push(() => origin.stop())
 
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: '/_next/data',
     })
@@ -223,7 +229,7 @@ describe("aborts middleware execution when the matcher conditions don't match th
 
     // Request 1: Middleware should run because we're not sending the header.
     const response1 = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: '/foo',
     })
@@ -238,7 +244,7 @@ describe("aborts middleware execution when the matcher conditions don't match th
       headers: {
         'x-custom-header': 'custom-value',
       },
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: '/foo',
     })
@@ -264,7 +270,7 @@ describe("aborts middleware execution when the matcher conditions don't match th
 
     for (const path of ['/hello', '/en/hello', '/nl/hello', '/nl/about']) {
       const response = await invokeEdgeFunction(ctx, {
-        functions: ['___netlify-edge-handler-middleware'],
+        functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
         origin,
         url: path,
       })
@@ -278,7 +284,7 @@ describe("aborts middleware execution when the matcher conditions don't match th
 
     for (const path of ['/invalid/hello', '/hello/invalid', '/about', '/en/about']) {
       const response = await invokeEdgeFunction(ctx, {
-        functions: ['___netlify-edge-handler-middleware'],
+        functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
         origin,
         url: path,
       })
@@ -299,7 +305,7 @@ describe('should run middleware on data requests', () => {
 
     const origin = new LocalServer()
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       redirect: 'manual',
       url: '/_next/data/dJvEyLV8MW7CBLFf0Ecbk/test/redirect-with-headers.json',
@@ -323,7 +329,7 @@ describe('should run middleware on data requests', () => {
 
     const origin = new LocalServer()
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       redirect: 'manual',
       url: '/_next/data/dJvEyLV8MW7CBLFf0Ecbk/test/redirect-with-headers.json',
@@ -357,7 +363,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/api/edge-headers`,
     })
@@ -379,7 +385,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       headers: {
         'x-nextjs-data': '1',
       },
@@ -408,7 +414,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/_next/static/build-id/_devMiddlewareManifest.json?foo=1`,
     })
@@ -434,7 +440,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       headers: {
         'x-nextjs-data': '1',
       },
@@ -462,7 +468,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       headers: {
         'x-nextjs-data': '1',
       },
@@ -491,7 +497,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/fr/old-home`,
       redirect: 'manual',
@@ -515,7 +521,7 @@ describe('page router', () => {
     })
     ctx.cleanup?.push(() => origin.stop())
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/fr/redirect-to-same-page-but-default-locale`,
       redirect: 'manual',
@@ -540,7 +546,7 @@ describe('page router', () => {
     ctx.cleanup?.push(() => origin.stop())
 
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/json`,
     })
@@ -552,7 +558,7 @@ describe('page router', () => {
     expect(body.nextUrlLocale).toBe('en')
 
     const responseEn = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/en/json`,
     })
@@ -564,7 +570,7 @@ describe('page router', () => {
     expect(bodyEn.nextUrlLocale).toBe('en')
 
     const responseFr = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/fr/json`,
     })
@@ -591,7 +597,7 @@ describe('page router', () => {
     ctx.cleanup?.push(() => origin.stop())
 
     const response = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/json`,
     })
@@ -603,7 +609,7 @@ describe('page router', () => {
     expect(body.nextUrlLocale).toBe('en')
 
     const responseEn = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/en/json`,
     })
@@ -615,7 +621,7 @@ describe('page router', () => {
     expect(bodyEn.nextUrlLocale).toBe('en')
 
     const responseFr = await invokeEdgeFunction(ctx, {
-      functions: ['___netlify-edge-handler-middleware'],
+      functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
       origin,
       url: `/fr/json`,
     })
diff --git a/tests/integration/hello-world-turbopack.test.ts b/tests/integration/hello-world-turbopack.test.ts
index d7681179a3..68956e974c 100644
--- a/tests/integration/hello-world-turbopack.test.ts
+++ b/tests/integration/hello-world-turbopack.test.ts
@@ -5,7 +5,13 @@ import { setupServer } from 'msw/node'
 import { v4 } from 'uuid'
 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
 import { type FixtureTestContext } from '../utils/contexts.js'
-import { createFixture, invokeEdgeFunction, invokeFunction, runPlugin } from '../utils/fixture.js'
+import {
+  createFixture,
+  EDGE_MIDDLEWARE_FUNCTION_NAME,
+  invokeEdgeFunction,
+  invokeFunction,
+  runPlugin,
+} from '../utils/fixture.js'
 import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
 import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
 
@@ -93,7 +99,7 @@ describe.skipIf(!nextVersionSatisfies('>=15.3.0-canary.43'))(
       const pathname = '/middleware/test'
 
       const response = await invokeEdgeFunction(ctx, {
-        functions: ['___netlify-edge-handler-middleware'],
+        functions: [EDGE_MIDDLEWARE_FUNCTION_NAME],
         url: pathname,
       })
 
diff --git a/tests/integration/wasm.test.ts b/tests/integration/wasm.test.ts
index 2de9050400..a103d805bf 100644
--- a/tests/integration/wasm.test.ts
+++ b/tests/integration/wasm.test.ts
@@ -3,7 +3,14 @@ import { platform } from 'node:process'
 import { v4 } from 'uuid'
 import { beforeEach, describe, expect, test, vi } from 'vitest'
 import { type FixtureTestContext } from '../utils/contexts.js'
-import { createFixture, invokeEdgeFunction, invokeFunction, runPlugin } from '../utils/fixture.js'
+import {
+  createFixture,
+  EDGE_MIDDLEWARE_FUNCTION_NAME,
+  EDGE_MIDDLEWARE_SRC_FUNCTION_NAME,
+  invokeEdgeFunction,
+  invokeFunction,
+  runPlugin,
+} from '../utils/fixture.js'
 import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
 import { LocalServer } from '../utils/local-server.js'
 
@@ -23,8 +30,8 @@ beforeEach<FixtureTestContext>(async (ctx) => {
 })
 
 describe.each([
-  { fixture: 'wasm', edgeHandlerFunction: '___netlify-edge-handler-middleware' },
-  { fixture: 'wasm-src', edgeHandlerFunction: '___netlify-edge-handler-src-middleware' },
+  { fixture: 'wasm', edgeHandlerFunction: EDGE_MIDDLEWARE_FUNCTION_NAME },
+  { fixture: 'wasm-src', edgeHandlerFunction: EDGE_MIDDLEWARE_SRC_FUNCTION_NAME },
 ])('$fixture', ({ fixture, edgeHandlerFunction }) => {
   beforeEach<FixtureTestContext>(async (ctx) => {
     // set for each test a new deployID and siteID
diff --git a/tests/utils/fixture.ts b/tests/utils/fixture.ts
index 3ddd787dcb..9ad43619b1 100644
--- a/tests/utils/fixture.ts
+++ b/tests/utils/fixture.ts
@@ -560,3 +560,9 @@ export async function invokeSandboxedFunction(
   exit()
   return result
 }
+
+// export const EDGE_MIDDLEWARE_FUNCTION_NAME = '___netlify-edge-handler-middleware'
+// export const EDGE_MIDDLEWARE_SRC_FUNCTION_NAME = '___netlify-edge-handler-src-middleware'
+// for right now we will use node middleware in tests
+export const EDGE_MIDDLEWARE_FUNCTION_NAME = '___netlify-edge-handler-node-middleware'
+export const EDGE_MIDDLEWARE_SRC_FUNCTION_NAME = EDGE_MIDDLEWARE_FUNCTION_NAME

From de3a81a325954f791c170492294a9307e081911a Mon Sep 17 00:00:00 2001
From: Michal Piechowiak <misiek.piechowiak@gmail.com>
Date: Tue, 5 Aug 2025 10:52:42 +0200
Subject: [PATCH 9/9] use virtual CJS modules

---
 edge-runtime/lib/cjs.ts                       | 135 ++++++++++++++++++
 src/build/functions/edge.ts                   |  86 ++++++-----
 .../hello-world-turbopack/middleware.ts       |   1 +
 tests/fixtures/middleware/next.config.js      |   1 +
 4 files changed, 189 insertions(+), 34 deletions(-)
 create mode 100644 edge-runtime/lib/cjs.ts

diff --git a/edge-runtime/lib/cjs.ts b/edge-runtime/lib/cjs.ts
new file mode 100644
index 0000000000..b7c84f253f
--- /dev/null
+++ b/edge-runtime/lib/cjs.ts
@@ -0,0 +1,135 @@
+import { Module, createRequire } from 'node:module'
+import vm from 'node:vm'
+import { join, dirname } from 'node:path/posix'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+type RegisteredModule = {
+  source: string
+  loaded: boolean
+  filename: string
+}
+const registeredModules = new Map<string, RegisteredModule>()
+
+const require = createRequire(import.meta.url)
+
+let hookedIn = false
+
+function seedCJSModuleCacheAndReturnTarget(matchedModule: RegisteredModule, parent: Module) {
+  console.error('matched', matchedModule.filename)
+  if (matchedModule.loaded) {
+    return matchedModule.filename
+  }
+  const { source, filename } = matchedModule
+  console.error('evaluating module', { filename })
+
+  const mod = new Module(filename)
+  mod.parent = parent
+  mod.filename = filename
+  mod.path = dirname(filename)
+  // @ts-expect-error - private untyped API
+  mod.paths = Module._nodeModulePaths(mod.path)
+  require.cache[filename] = mod
+
+  const wrappedSource = `(function (exports, require, module, __filename, __dirname) { ${source}\n});`
+  const compiled = vm.runInThisContext(wrappedSource, {
+    filename,
+    lineOffset: 0,
+    displayErrors: true,
+  })
+  compiled(mod.exports, createRequire(pathToFileURL(filename)), mod, filename, dirname(filename))
+  mod.loaded = matchedModule.loaded = true
+
+  console.error('evaluated module', { filename })
+  return filename
+}
+
+const exts = ['.js', '.cjs', '.json']
+
+function tryWithExtensions(filename: string) {
+  // console.error('trying to match', filename)
+  let matchedModule = registeredModules.get(filename)
+  if (!matchedModule) {
+    for (const ext of exts) {
+      // require("./test") might resolve to ./test.js
+      const targetWithExt = filename + ext
+
+      matchedModule = registeredModules.get(targetWithExt)
+      if (matchedModule) {
+        break
+      }
+    }
+  }
+
+  return matchedModule
+}
+
+function tryMatchingWithIndex(target: string) {
+  console.error('trying to match', target)
+  let matchedModule = tryWithExtensions(target)
+  if (!matchedModule) {
+    // require("./test") might resolve to ./test/index.js
+    const indexTarget = join(target, 'index')
+    matchedModule = tryWithExtensions(indexTarget)
+  }
+
+  return matchedModule
+}
+
+export function registerCJSModules(baseUrl: URL, modules: Map<string, string>) {
+  const basePath = dirname(fileURLToPath(baseUrl))
+
+  for (const [filename, source] of modules.entries()) {
+    const target = join(basePath, filename)
+
+    registeredModules.set(target, { source, loaded: false, filename: target })
+  }
+
+  console.error([...registeredModules.values()].map((m) => m.filename))
+
+  if (!hookedIn) {
+    // magic
+    // @ts-expect-error - private untyped API
+    const original_resolveFilename = Module._resolveFilename.bind(Module)
+    // @ts-expect-error - private untyped API
+    Module._resolveFilename = (...args) => {
+      console.error(
+        'resolving file name for specifier',
+        args[0] ?? '--missing specifier--',
+        'from',
+        args[1]?.filename ?? 'unknown',
+      )
+      let target = args[0]
+      let isRelative = args?.[0].startsWith('.')
+
+      if (isRelative) {
+        // only handle relative require paths
+        const requireFrom = args?.[1]?.filename
+
+        target = join(dirname(requireFrom), args[0])
+      }
+
+      let matchedModule = tryMatchingWithIndex(target)
+
+      if (!isRelative && !target.startsWith('/')) {
+        console.log('not relative, checking node_modules', args[0])
+        for (const nodeModulePaths of args[1].paths) {
+          const potentialPath = join(nodeModulePaths, target)
+          console.log('checking potential path', potentialPath)
+          matchedModule = tryMatchingWithIndex(potentialPath)
+          if (matchedModule) {
+            break
+          }
+        }
+      }
+
+      if (matchedModule) {
+        console.log('matched module', matchedModule.filename)
+        return seedCJSModuleCacheAndReturnTarget(matchedModule, args[1])
+      }
+
+      return original_resolveFilename(...args)
+    }
+
+    hookedIn = true
+  }
+}
diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts
index 1c61c40067..d9f6d2167e 100644
--- a/src/build/functions/edge.ts
+++ b/src/build/functions/edge.ts
@@ -214,9 +214,9 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
     const srcDir = join(ctx.standaloneDir, ctx.nextDistDir)
     // const destDir = join(ctx.edgeFunctionsDir, getHandlerName({ name }))
 
-    const fakeNodeModuleName = 'fake-module-with-middleware'
+    // const fakeNodeModuleName = 'fake-module-with-middleware'
 
-    const fakeNodeModulePath = ctx.resolveFromPackagePath(join('node_modules', fakeNodeModuleName))
+    // const fakeNodeModulePath = ctx.resolveFromPackagePath(join('node_modules', fakeNodeModuleName))
 
     const nftFilesPath = join(process.cwd(), ctx.nextDistDir, nft)
     const nftManifest = JSON.parse(await readFile(nftFilesPath, 'utf8'))
@@ -246,47 +246,53 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
       prefixPath += `nested-${nestedIndex}/`
     }
 
+    let virtualModules = ''
     for (const file of files) {
       const srcPath = join(srcDir, file)
-      const destPath = join(fakeNodeModulePath, prefixPath, file)
-
-      await mkdir(dirname(destPath), { recursive: true })
-
-      if (file === entry) {
-        const content = await readFile(srcPath, 'utf8')
-        await writeFile(
-          destPath,
-          // Next.js needs to be set on global even if it's possible to just require it
-          // so somewhat similar to existing shim we have for edge runtime
-          `globalThis.AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage;\n${content}`,
-        )
-      } else {
-        await cp(srcPath, destPath, { force: true })
-      }
+
+      const content = await readFile(srcPath, 'utf8')
+
+      virtualModules += `virtualModules.set(${JSON.stringify(join(prefixPath, file))}, ${JSON.stringify(content)});\n`
+
+      // const destPath = join(fakeNodeModulePath, prefixPath, file)
+
+      // await mkdir(dirname(destPath), { recursive: true })
+
+      // if (file === entry) {
+      //   const content = await readFile(srcPath, 'utf8')
+      //   await writeFile(
+      //     destPath,
+      //     // Next.js needs to be set on global even if it's possible to just require it
+      //     // so somewhat similar to existing shim we have for edge runtime
+      //     `globalThis.AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage;\n${content}`,
+      //   )
+      // } else {
+      //   await cp(srcPath, destPath, { force: true })
+      // }
     }
 
-    await writeFile(join(fakeNodeModulePath, 'package.json'), JSON.stringify({ type: 'commonjs' }))
+    // await writeFile(join(fakeNodeModulePath, 'package.json'), JSON.stringify({ type: 'commonjs' }))
 
     // there is `/chunks/**/*` require coming from webpack-runtime that fails esbuild due to nothing matching,
     // so this ensure something does
-    const dummyChunkPath = join(fakeNodeModulePath, prefixPath, 'server', 'chunks', 'dummy.js')
-    await mkdir(dirname(dummyChunkPath), { recursive: true })
-    await writeFile(dummyChunkPath, '')
+    // const dummyChunkPath = join(fakeNodeModulePath, prefixPath, 'server', 'chunks', 'dummy.js')
+    // await mkdir(dirname(dummyChunkPath), { recursive: true })
+    // await writeFile(dummyChunkPath, '')
 
     // there is also `@opentelemetry/api` require that fails esbuild due to nothing matching,
     // next is try/catching it and fallback to bundled version of otel package in case of errors
-    const otelApiPath = join(
-      fakeNodeModulePath,
-      'node_modules',
-      '@opentelemetry',
-      'api',
-      'index.js',
-    )
-    await mkdir(dirname(otelApiPath), { recursive: true })
-    await writeFile(
-      otelApiPath,
-      `throw new Error('this is dummy to satisfy esbuild used for npm compat using fake module')`,
-    )
+    // const otelApiPath = join(
+    //   fakeNodeModulePath,
+    //   'node_modules',
+    //   '@opentelemetry',
+    //   'api',
+    //   'index.js',
+    // )
+    // await mkdir(dirname(otelApiPath), { recursive: true })
+    // await writeFile(
+    //   otelApiPath,
+    //   `throw new Error('this is dummy to satisfy esbuild used for npm compat using fake module')`,
+    // )
 
     // await writeHandlerFile(ctx, definition)
 
@@ -333,11 +339,23 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
     await writeFile(
       join(handlerDirectory, `${handlerName}.js`),
       `
+    import { createRequire } from "node:module";
     import { init as htmlRewriterInit } from './edge-runtime/vendor/deno.land/x/htmlrewriter@v1.0.0/src/index.ts'
     import { handleMiddleware } from './edge-runtime/middleware.ts';
+    import { registerCJSModules } from "./edge-runtime/lib/cjs.ts";
+    import { AsyncLocalStorage } from 'node:async_hooks';
+
+    globalThis.AsyncLocalStorage = AsyncLocalStorage;
+
+    // needed for path.relative and path.resolve to work
+    Deno.cwd = () => ''
 
-    import * as handlerMod from '${fakeNodeModuleName}/${prefixPath}${entry}';
+    const virtualModules = new Map();
+    ${virtualModules}
+    registerCJSModules(import.meta.url, virtualModules);
 
+    const require = createRequire(import.meta.url);
+    const handlerMod = require("./${prefixPath}/${entry}");
     const handler = handlerMod.default || handlerMod;
 
     await htmlRewriterInit({ module_or_path: Uint8Array.from(${JSON.stringify([
diff --git a/tests/fixtures/hello-world-turbopack/middleware.ts b/tests/fixtures/hello-world-turbopack/middleware.ts
index a2f7976a78..76529380bd 100644
--- a/tests/fixtures/hello-world-turbopack/middleware.ts
+++ b/tests/fixtures/hello-world-turbopack/middleware.ts
@@ -9,4 +9,5 @@ export function middleware(request: NextRequest) {
 
 export const config = {
   matcher: '/middleware/:path*',
+  runtime: 'nodejs',
 }
diff --git a/tests/fixtures/middleware/next.config.js b/tests/fixtures/middleware/next.config.js
index 4de8b236d0..90fbfe105b 100644
--- a/tests/fixtures/middleware/next.config.js
+++ b/tests/fixtures/middleware/next.config.js
@@ -7,6 +7,7 @@ const nextConfig = {
   webpack: (config) => {
     // this is a trigger to generate multiple `.next/server/middleware-[hash].js` files instead of
     // single `.next/server/middleware.js` file
+    // this doesn't seem to actually work with Node Middleware - it result in next build failures
     // config.optimization.splitChunks.maxSize = 100_000
 
     return config
