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/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/content/server.ts b/src/build/content/server.ts index 40e15663f3..fbd289533c 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,16 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise => { return } + if (path === 'server/functions-config-manifest.json') { + 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 }) }), ) @@ -376,6 +387,44 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) = await writeFile(destPath, newData) } +// 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']?.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 }) + } +} + export const verifyHandlerDirStructure = async (ctx: PluginContext) => { const { nextConfig } = JSON.parse( await readFile(join(ctx.serverHandlerDir, RUN_CONFIG_FILE), 'utf-8'), diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts index af6405b57c..d9f6d2167e 100644 --- a/src/build/functions/edge.ts +++ b/src/build/functions/edge.ts @@ -194,11 +194,203 @@ 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(process.cwd(), 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}/` + } + + let virtualModules = '' + for (const file of files) { + const srcPath = join(srcDir, file) + + 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' })) + + // 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, '') + + // 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 + 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 { 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 = () => '' + + 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([ + ...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/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-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/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..33328de8d5 --- /dev/null +++ b/tests/fixtures/middleware-node/middleware.ts @@ -0,0 +1,12 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { join } from 'path' + +export default async function middleware(req: NextRequest) { + const response = NextResponse.next() + response.headers.set('x-added-middleware-headers-join', join('a', 'b')) + return response +} + +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..94c39a2d81 --- /dev/null +++ b/tests/fixtures/middleware-node/next.config.js @@ -0,0 +1,17 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + eslint: { + ignoreDuringBuilds: true, + }, + experimental: { + nodeMiddleware: true, + }, + webpack: (config) => { + // disable minification for easier inspection of produced build output + config.optimization.minimize = false + return config + }, +} + +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/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', } diff --git a/tests/fixtures/middleware/next.config.js b/tests/fixtures/middleware/next.config.js index 28875fd694..90fbfe105b 100644 --- a/tests/fixtures/middleware/next.config.js +++ b/tests/fixtures/middleware/next.config.js @@ -7,7 +7,8 @@ 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 + // this doesn't seem to actually work with Node Middleware - it result in next build failures + // config.optimization.splitChunks.maxSize = 100_000 return config }, diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index 825ed6fac1..23e009079c 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -1,9 +1,16 @@ 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' beforeEach(async (ctx) => { // set for each test a new deployID and siteID @@ -29,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', }) @@ -57,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', }) @@ -77,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', @@ -100,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', @@ -139,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`, }) @@ -166,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', @@ -195,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', }) @@ -222,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', }) @@ -237,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', }) @@ -263,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, }) @@ -277,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, }) @@ -298,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', @@ -322,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', @@ -356,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`, }) @@ -378,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', }, @@ -407,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`, }) @@ -433,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', }, @@ -461,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', }, @@ -490,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', @@ -514,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', @@ -539,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`, }) @@ -551,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`, }) @@ -563,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`, }) @@ -590,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`, }) @@ -602,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`, }) @@ -614,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`, }) @@ -626,3 +633,14 @@ describe('page router', () => { expect(bodyFr.nextUrlLocale).toBe('fr') }) }) + +// 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.', +// ) +// }, +// ) 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