From a732739629e9205d73e01b23b6c06c538b64a390 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Mon, 28 Jul 2025 11:08:29 +0200 Subject: [PATCH 01/10] 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 => { 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 ( + + {children} + + ) +} 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 ( +
+

Home

+
+ ) +} 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(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'))( + '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 Date: Wed, 30 Jul 2025 13:12:25 +0200 Subject: [PATCH 02/10] [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 => { } 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 { + 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 Date: Wed, 30 Jul 2025 14:02:34 +0200 Subject: [PATCH 03/10] 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 Date: Wed, 30 Jul 2025 14:03:57 +0200 Subject: [PATCH 04/10] 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'))( - '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'))( +// '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 Date: Wed, 30 Jul 2025 15:15:35 +0200 Subject: [PATCH 05/10] 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 Date: Wed, 30 Jul 2025 15:36:28 +0200 Subject: [PATCH 06/10] 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 Date: Thu, 31 Jul 2025 19:30:56 +0200 Subject: [PATCH 07/10] 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 Date: Thu, 31 Jul 2025 21:07:56 +0200 Subject: [PATCH 08/10] 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('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('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(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(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 Date: Tue, 5 Aug 2025 10:52:42 +0200 Subject: [PATCH 09/10] 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() + +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) { + 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 From a6f1ac36110eda96f7633c9a41deddffc18484ba Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Mon, 18 Aug 2025 10:17:14 +0200 Subject: [PATCH 10/10] chore: remove dev/debug logs --- edge-runtime/lib/cjs.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/edge-runtime/lib/cjs.ts b/edge-runtime/lib/cjs.ts index b7c84f253f..c8be0c561a 100644 --- a/edge-runtime/lib/cjs.ts +++ b/edge-runtime/lib/cjs.ts @@ -15,12 +15,10 @@ 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 @@ -39,14 +37,12 @@ function seedCJSModuleCacheAndReturnTarget(matchedModule: RegisteredModule, pare 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) { @@ -64,7 +60,6 @@ function tryWithExtensions(filename: string) { } function tryMatchingWithIndex(target: string) { - console.error('trying to match', target) let matchedModule = tryWithExtensions(target) if (!matchedModule) { // require("./test") might resolve to ./test/index.js @@ -84,20 +79,11 @@ export function registerCJSModules(baseUrl: URL, modules: Map) { 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('.') @@ -111,10 +97,8 @@ export function registerCJSModules(baseUrl: URL, modules: Map) { 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 @@ -123,7 +107,6 @@ export function registerCJSModules(baseUrl: URL, modules: Map) { } if (matchedModule) { - console.log('matched module', matchedModule.filename) return seedCJSModuleCacheAndReturnTarget(matchedModule, args[1]) }