Skip to content

[wip] Feat/node middleware support #3018

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 53 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
a732739
fix: fail build/deploy when using unsupported Node.js Midleware
pieh Jul 28, 2025
fc55e71
[wip] feat: support node middleware
pieh Jul 30, 2025
2e1f698
shim otel, so things work when deploying from outside of this repo
pieh Jul 30, 2025
661496b
test: deploy no longer fail, so ignore this test
pieh Jul 30, 2025
3c2789b
test: try running existing tests against node middleware
pieh Jul 30, 2025
2203640
test: skip force chunking in middleware fixture
pieh Jul 30, 2025
182b683
Merge branch 'main' into feat/node-middleware-support
pieh Jul 31, 2025
b5a3269
fix: nft reading to work in integration tests
pieh Jul 31, 2025
2031077
test: make sure to use appropiate edge handler name
pieh Jul 31, 2025
de3a81a
use virtual CJS modules
pieh Aug 5, 2025
a6f1ac3
chore: remove dev/debug logs
pieh Aug 18, 2025
e0b5143
Merge remote-tracking branch 'origin/main' into feat/node-middleware-…
pieh Aug 18, 2025
487b0db
test: initial build variants setup
pieh Aug 20, 2025
91bb19d
Merge remote-tracking branch 'origin/main' into feat/node-middleware-…
pieh Aug 20, 2025
9ea923b
fix: shim global process
pieh Aug 20, 2025
dd6a383
fix: add additional context on CJS module compilation failures to poi…
pieh Aug 20, 2025
c5773b5
Merge remote-tracking branch 'origin/main' into feat/node-middleware-…
pieh Aug 20, 2025
e74af04
ci: move back to using latest now that node middleware landed as stable
pieh Aug 20, 2025
5a1dea1
chore: enable node middleware tests in stable
pieh Aug 20, 2025
4a17111
test: specify distDir in variants config
pieh Aug 20, 2025
31f83f5
test: comment out test checking expected deploy failure with node mid…
pieh Aug 20, 2025
1a0ab5c
test: move middleware-src to test variants
pieh Aug 20, 2025
abd729f
test: update hasNodeMiddlewareSupport check
pieh Aug 20, 2025
c1efaf1
tmp: run latest and canary for now
pieh Aug 20, 2025
3e7238c
test: remove .only
pieh Aug 21, 2025
99df488
test: move middleware-conditions to test variants
pieh Aug 21, 2025
5c0c948
test: move middleware-trailing-slash to test variants
pieh Aug 21, 2025
65209f6
test: adjust rest of middleware e2e
pieh Aug 21, 2025
8b5579f
test: move middleware-pages to test variants
pieh Aug 21, 2025
aec3a71
chore: don't throw immediately on failed build and instead allow for …
pieh Aug 21, 2025
45dacbb
Merge remote-tracking branch 'origin/main' into feat/node-middleware-…
pieh Aug 21, 2025
b04d1ca
test: move middleware-i18n to test variants
pieh Aug 21, 2025
55d1722
chore: remove debug log
pieh Aug 21, 2025
38a2f3b
test: move middleware-i18n-skip-normalize to test variants
pieh Aug 21, 2025
7492aac
test: add x-runtime res header to middleware-conditions
pieh Aug 21, 2025
6069a74
test: skip runtime check on some tests
pieh Aug 21, 2025
3d996dd
test: move middleware-i18n-exluded-path to test variants
pieh Aug 21, 2025
e974d7f
test: skip prebuilding some fixtures that are not used in integration…
pieh Aug 21, 2025
61d261e
test: subrequest vuln test is not applicable to node middleware
pieh Aug 21, 2025
8669b58
test: move middleware-i18n-static-asset-matcher to test variants
pieh Aug 21, 2025
db4f02d
refactor: adjust node middleware handling to share common handling wi…
pieh Aug 21, 2025
1276559
test: make sure to return middleware response in middleware-i18n
pieh Aug 21, 2025
2a5bdff
fix: duplicate type import
pieh Aug 21, 2025
9c302c0
chore: remove debug logs
pieh Aug 21, 2025
bfacdf8
Merge remote-tracking branch 'origin/main' into feat/node-middleware-…
pieh Aug 21, 2025
2782f43
test fixes
pieh Aug 21, 2025
bc6dc50
fix: don't pass unused manifest
pieh Aug 22, 2025
9a52755
fix: static html blobs if distDir is different than default .next
pieh Aug 22, 2025
cbf47e5
test: don't assert middleware runtime on tests that assert that middl…
pieh Aug 22, 2025
f5d32d4
test: fix expected redirect status
pieh Aug 22, 2025
37bb7da
test: convert middleware-conditions fixture to page router, as app ro…
pieh Aug 22, 2025
0eae894
test: adjust for /_global-error
pieh Aug 22, 2025
a9c05bb
test: add node.js runtime specific middleware tests checking availabi…
pieh Aug 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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=[\"latest\",\"canary\"]" >> $GITHUB_OUTPUT
fi

e2e:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
dist/
.next
.next-node-middleware
edge-runtime/vendor
# deno.json is ephemeral and generated for the purpose of vendoring remote modules in CI
tools/deno/deno.json
Expand Down
122 changes: 122 additions & 0 deletions edge-runtime/lib/cjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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) {
if (matchedModule.loaded) {
return matchedModule.filename
}
const { source, filename } = matchedModule

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});`
try {
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
} catch (error) {
throw new Error(`Failed to compile CJS module: ${filename}`, { cause: error })
}

return filename
}

const exts = ['.js', '.cjs', '.json']

function tryWithExtensions(filename: string) {
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) {
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 })
}

if (!hookedIn) {
// @ts-expect-error - private untyped API
const original_resolveFilename = Module._resolveFilename.bind(Module)
// @ts-expect-error - private untyped API
Module._resolveFilename = (...args) => {
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('/')) {
for (const nodeModulePaths of args[1].paths) {
const potentialPath = join(nodeModulePaths, target)
matchedModule = tryMatchingWithIndex(potentialPath)
if (matchedModule) {
break
}
}
}

if (matchedModule) {
return seedCJSModuleCacheAndReturnTarget(matchedModule, args[1])
}

return original_resolveFilename(...args)
}

hookedIn = true
}
}
File renamed without changes.
16 changes: 16 additions & 0 deletions edge-runtime/shim/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// NOTE: This is a fragment of a JavaScript program that will be inlined with
// a Webpack bundle. You should not import this file from anywhere in the
// application.
import { AsyncLocalStorage } from 'node:async_hooks'

import { createRequire } from 'node:module' // used in dynamically generated part
import process from 'node:process'

import { registerCJSModules } from '../edge-runtime/lib/cjs.ts' // used in dynamically generated part

globalThis.process = process

globalThis.AsyncLocalStorage = AsyncLocalStorage

// needed for path.relative and path.resolve to work
Deno.cwd = () => ''
46 changes: 37 additions & 9 deletions src/build/content/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -381,19 +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(
'Node.js middleware is not yet supported.\n\n' +
'Future @netlify/plugin-nextjs release will support node middleware with following limitations:\n' +
' - usage of C++ Addons (https://nodejs.org/api/addons.html) not supported (for example `bcrypt` npm module will not be supported, but `bcryptjs` will be supported),\n' +
' - usage of Filesystem (https://nodejs.org/api/fs.html) 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 })
}
}

Expand Down
Loading