Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 139d813

Browse files
committed
[wip] feat: support node middleware
1 parent a732739 commit 139d813

File tree

5 files changed

+236
-10
lines changed

5 files changed

+236
-10
lines changed

‎src/build/content/server.ts‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,13 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
133133
}
134134

135135
if (path === 'server/functions-config-manifest.json') {
136-
await verifyFunctionsConfigManifest(join(srcDir, path))
136+
try {
137+
await replaceFunctionsConfigManifest(srcPath, destPath)
138+
} catch (error) {
139+
throw new Error('Could not patch functions config manifest file', { cause: error })
140+
}
141+
142+
return
137143
}
138144

139145
await cp(srcPath, destPath, { recursive: true, force: true })
@@ -381,16 +387,41 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) =
381387
await writeFile(destPath, newData)
382388
}
383389

384-
const verifyFunctionsConfigManifest = async (sourcePath: string) => {
390+
// similar to the middleware manifest, we need to patch the functions config manifest to disable
391+
// the middleware that is defined in the functions config manifest. This is needed to avoid running
392+
// the middleware in the server handler, while still allowing next server to enable some middleware
393+
// 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 )
394+
const replaceFunctionsConfigManifest = async (sourcePath: string, destPath: string) => {
385395
const data = await readFile(sourcePath, 'utf8')
386396
const manifest = JSON.parse(data) as FunctionsConfigManifest
387397

388398
// https://github.com/vercel/next.js/blob/8367faedd61501025299e92d43a28393c7bb50e2/packages/next/src/build/index.ts#L2465
389399
// Node.js Middleware has hardcoded /_middleware path
390-
if (manifest.functions['/_middleware']) {
391-
throw new Error(
392-
'Only Edge Runtime Middleware is supported. Node.js Middleware is not supported.',
393-
)
400+
if (manifest?.functions?.['/_middleware']?.matchers) {
401+
const newManifest = {
402+
...manifest,
403+
functions: {
404+
...manifest.functions,
405+
'/_middleware': {
406+
...manifest.functions['/_middleware'],
407+
matchers: manifest.functions['/_middleware'].matchers.map((matcher) => {
408+
return {
409+
...matcher,
410+
// matcher that won't match on anything
411+
// this is meant to disable actually running middleware in the server handler,
412+
// while still allowing next server to enable some middleware specific handling
413+
// such as _next/data normalization ( https://github.com/vercel/next.js/blob/7bb72e508572237fe0d4aac5418546d4b4b3a363/packages/next/src/server/lib/router-utils/resolve-routes.ts#L395 )
414+
regexp: '(?!.*)',
415+
}
416+
}),
417+
},
418+
},
419+
}
420+
const newData = JSON.stringify(newManifest)
421+
422+
await writeFile(destPath, newData)
423+
} else {
424+
await cp(sourcePath, destPath, { recursive: true, force: true })
394425
}
395426
}
396427

‎src/build/functions/edge.ts‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,182 @@ export const clearStaleEdgeHandlers = async (ctx: PluginContext) => {
194194
}
195195

196196
export const createEdgeHandlers = async (ctx: PluginContext) => {
197+
// Edge middleware
197198
const nextManifest = await ctx.getMiddlewareManifest()
199+
// Node middleware
200+
const functionsConfigManifest = await ctx.getFunctionsConfigManifest()
201+
198202
const nextDefinitions = [...Object.values(nextManifest.middleware)]
199203
await Promise.all(nextDefinitions.map((def) => createEdgeHandler(ctx, def)))
200204

201205
const netlifyDefinitions = nextDefinitions.flatMap((def) => buildHandlerDefinition(ctx, def))
206+
207+
if (functionsConfigManifest?.functions?.['/_middleware']) {
208+
const middlewareDefinition = functionsConfigManifest?.functions?.['/_middleware']
209+
const entry = 'server/middleware.js'
210+
const nft = `${entry}.nft.json`
211+
const name = 'node-middleware'
212+
213+
// await copyHandlerDependencies(ctx, definition)
214+
const srcDir = join(ctx.standaloneDir, ctx.nextDistDir)
215+
// const destDir = join(ctx.edgeFunctionsDir, getHandlerName({ name }))
216+
217+
const fakeNodeModuleName = 'fake-module-with-middleware'
218+
219+
const fakeNodeModulePath = ctx.resolveFromPackagePath(join('node_modules', fakeNodeModuleName))
220+
221+
const nftFilesPath = join(ctx.nextDistDir, nft)
222+
const nftManifest = JSON.parse(await readFile(nftFilesPath, 'utf8'))
223+
224+
const files: string[] = nftManifest.files.map((file: string) => join('server', file))
225+
files.push(entry)
226+
227+
// files are relative to location of middleware entrypoint
228+
// we need to capture all of them
229+
// they might be going to parent directories, so first we check how many directories we need to go up
230+
const maxDirsUp = files.reduce((max, file) => {
231+
let dirsUp = 0
232+
for (const part of file.split('/')) {
233+
if (part === '..') {
234+
dirsUp += 1
235+
} else {
236+
break
237+
}
238+
}
239+
return Math.max(max, dirsUp)
240+
}, 0)
241+
242+
let prefixPath = ''
243+
for (let nestedIndex = 1; nestedIndex <= maxDirsUp; nestedIndex++) {
244+
// TODO: ideally we preserve the original directory structure
245+
// this is just hack to use arbitrary computed names to speed up hooking things up
246+
prefixPath += `nested-${nestedIndex}/`
247+
}
248+
249+
for (const file of files) {
250+
const srcPath = join(srcDir, file)
251+
const destPath = join(fakeNodeModulePath, prefixPath, file)
252+
253+
await mkdir(dirname(destPath), { recursive: true })
254+
255+
if (file === entry) {
256+
const content = await readFile(srcPath, 'utf8')
257+
await writeFile(
258+
destPath,
259+
// Next.js needs to be set on global even if it's possible to just require it
260+
// so somewhat similar to existing shim we have for edge runtime
261+
`globalThis.AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage;\n${content}`,
262+
)
263+
} else {
264+
await cp(srcPath, destPath, { force: true })
265+
}
266+
}
267+
268+
await writeFile(join(fakeNodeModulePath, 'package.json'), JSON.stringify({ type: 'commonjs' }))
269+
270+
// there is `/chunks/**/*` require coming from webpack-runtime that fails esbuild due to nothing matching,
271+
// so this ensure something does
272+
const dummyChunkPath = join(fakeNodeModulePath, prefixPath, 'server', 'chunks', 'dummy.js')
273+
await mkdir(dirname(dummyChunkPath), { recursive: true })
274+
await writeFile(dummyChunkPath, '')
275+
276+
// await writeHandlerFile(ctx, definition)
277+
278+
const nextConfig = ctx.buildConfig
279+
const handlerName = getHandlerName({ name })
280+
const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName)
281+
const handlerRuntimeDirectory = join(handlerDirectory, 'edge-runtime')
282+
283+
// Copying the runtime files. These are the compatibility layer between
284+
// Netlify Edge Functions and the Next.js edge runtime.
285+
await copyRuntime(ctx, handlerDirectory)
286+
287+
// Writing a file with the matchers that should trigger this function. We'll
288+
// read this file from the function at runtime.
289+
await writeFile(
290+
join(handlerRuntimeDirectory, 'matchers.json'),
291+
JSON.stringify(middlewareDefinition.matchers ?? []),
292+
)
293+
294+
// The config is needed by the edge function to match and normalize URLs. To
295+
// avoid shipping and parsing a large file at runtime, let's strip it down to
296+
// just the properties that the edge function actually needs.
297+
const minimalNextConfig = {
298+
basePath: nextConfig.basePath,
299+
i18n: nextConfig.i18n,
300+
trailingSlash: nextConfig.trailingSlash,
301+
skipMiddlewareUrlNormalize: nextConfig.skipMiddlewareUrlNormalize,
302+
}
303+
304+
await writeFile(
305+
join(handlerRuntimeDirectory, 'next.config.json'),
306+
JSON.stringify(minimalNextConfig),
307+
)
308+
309+
const htmlRewriterWasm = await readFile(
310+
join(
311+
ctx.pluginDir,
312+
'edge-runtime/vendor/deno.land/x/htmlrewriter@v1.0.0/pkg/htmlrewriter_bg.wasm',
313+
),
314+
)
315+
316+
// Writing the function entry file. It wraps the middleware code with the
317+
// compatibility layer mentioned above.
318+
await writeFile(
319+
join(handlerDirectory, `${handlerName}.js`),
320+
`
321+
import { init as htmlRewriterInit } from './edge-runtime/vendor/deno.land/x/htmlrewriter@v1.0.0/src/index.ts'
322+
import { handleMiddleware } from './edge-runtime/middleware.ts';
323+
324+
import * as handlerMod from '${fakeNodeModuleName}/${prefixPath}${entry}';
325+
326+
const handler = handlerMod.default || handlerMod;
327+
328+
await htmlRewriterInit({ module_or_path: Uint8Array.from(${JSON.stringify([
329+
...htmlRewriterWasm,
330+
])}) });
331+
332+
export default (req, context) => {
333+
return handleMiddleware(req, context, handler);
334+
};
335+
`,
336+
)
337+
338+
// buildHandlerDefinition(ctx, def)
339+
const netlifyDefinitions: Manifest['functions'] = augmentMatchers(
340+
middlewareDefinition.matchers ?? [],
341+
ctx,
342+
).map((matcher) => {
343+
return {
344+
function: getHandlerName({ name }),
345+
name: `Next.js Node Middleware Handler`,
346+
pattern: matcher.regexp,
347+
cache: undefined,
348+
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
349+
}
350+
})
351+
352+
const netlifyManifest: Manifest = {
353+
version: 1,
354+
functions: netlifyDefinitions,
355+
}
356+
await writeEdgeManifest(ctx, netlifyManifest)
357+
358+
return
359+
}
360+
361+
// if (functionsConfigManifest?.functions?.['/_middleware']) {
362+
// const nextDefinition: Pick<
363+
// (typeof nextManifest.middleware)[''],
364+
// 'name' | 'env' | 'files' | 'wasm' | 'matchers'
365+
// > = {
366+
// name: 'middleware',
367+
// env: {},
368+
// files: [],
369+
// wasm: [],
370+
// }
371+
// }
372+
202373
const netlifyManifest: Manifest = {
203374
version: 1,
204375
functions: netlifyDefinitions,

‎src/build/plugin-context.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { PrerenderManifest, RoutesManifest } from 'next/dist/build/index.js
1414
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
1515
import type { PagesManifest } from 'next/dist/build/webpack/plugins/pages-manifest-plugin.js'
1616
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
17+
import type { FunctionsConfigManifest } from 'next-with-cache-handler-v2/dist/build/index.js'
1718
import { satisfies } from 'semver'
1819

1920
const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
@@ -259,6 +260,23 @@ export class PluginContext {
259260
)
260261
}
261262

263+
/**
264+
* Get Next.js Functions Config Manifest config if it exists from the build output
265+
*/
266+
async getFunctionsConfigManifest(): Promise<FunctionsConfigManifest | null> {
267+
const functionsConfigManifestPath = join(
268+
this.publishDir,
269+
'server/functions-config-manifest.json',
270+
)
271+
272+
if (existsSync(functionsConfigManifestPath)) {
273+
return JSON.parse(await readFile(functionsConfigManifestPath, 'utf-8'))
274+
}
275+
276+
// this file might not have been produced
277+
return null
278+
}
279+
262280
// don't make private as it is handy inside testing to override the config
263281
_requiredServerFiles: RequiredServerFilesManifest | null = null
264282

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { NextRequest } from 'next/server'
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { join } from 'path'
23

3-
export async function middleware(request: NextRequest) {
4-
console.log('Node.js Middleware request:',request.method,request.nextUrl.pathname)
4+
export defaultasync function middleware(req: NextRequest) {
5+
returnNextResponse.json({message: 'Hello, world!',joined: join('a','b')})
56
}
67

78
export const config = {
8-
runtime: 'nodejs',
9+
// runtime: 'nodejs',
910
}

‎tests/fixtures/middleware-node/next.config.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ const nextConfig = {
77
experimental: {
88
nodeMiddleware: true,
99
},
10+
webpack: (config) => {
11+
// disable minification for easier inspection of produced build output
12+
config.optimization.minimize = false
13+
return config
14+
},
1015
}
1116

1217
module.exports = nextConfig

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /