Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 19 additions & 7 deletions src/build/functions/edge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { dirname, join, relative } from 'node:path/posix'

import type { Manifest, ManifestFunction } from '@netlify/edge-functions'
Expand Down Expand Up @@ -259,14 +259,26 @@ const copyHandlerDependenciesForNodeMiddleware = async (ctx: PluginContext) => {

parts.push(`const virtualModules = new Map();`)

for (const file of files) {
const srcPath = join(srcDir, file)
const handleFileOrDirectory = async (fileOrDir: string) => {
const srcPath = join(srcDir, fileOrDir)

const content = await readFile(srcPath, 'utf8')
const stats = await stat(srcPath)
if (stats.isDirectory()) {
Copy link
Contributor Author

@pieh pieh Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes here:

While this make it work, it's actually not ideal, but the problem itself is either with Next.js or NFT itself where they don't track to individual modules and instead just output package root.

For example - here's list of node_modules/next files listed if npm is used:

    "../../node_modules/next/dist/client/components/app-router-headers.js",
    "../../node_modules/next/dist/compiled/@opentelemetry/api/index.js",
    "../../node_modules/next/dist/compiled/@opentelemetry/api/package.json",
    "../../node_modules/next/dist/compiled/jsonwebtoken/index.js",
    "../../node_modules/next/dist/compiled/jsonwebtoken/package.json",
    "../../node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js",
    "../../node_modules/next/dist/lib/client-and-server-references.js",
    "../../node_modules/next/dist/lib/constants.js",
    "../../node_modules/next/dist/lib/interop-default.js",
    "../../node_modules/next/dist/lib/is-error.js",
    "../../node_modules/next/dist/lib/semver-noop.js",
    "../../node_modules/next/dist/server/app-render/action-async-storage-instance.js",
    "../../node_modules/next/dist/server/app-render/action-async-storage.external.js",
    "../../node_modules/next/dist/server/app-render/after-task-async-storage-instance.js",
    "../../node_modules/next/dist/server/app-render/after-task-async-storage.external.js",
    "../../node_modules/next/dist/server/app-render/async-local-storage.js",
    "../../node_modules/next/dist/server/app-render/cache-signal.js",
    "../../node_modules/next/dist/server/app-render/module-loading/track-module-loading.external.js",
    "../../node_modules/next/dist/server/app-render/module-loading/track-module-loading.instance.js",
    "../../node_modules/next/dist/server/app-render/work-async-storage-instance.js",
    "../../node_modules/next/dist/server/app-render/work-async-storage.external.js",
    "../../node_modules/next/dist/server/app-render/work-unit-async-storage-instance.js",
    "../../node_modules/next/dist/server/app-render/work-unit-async-storage.external.js",
    "../../node_modules/next/dist/server/lib/cache-handlers/default.external.js",
    "../../node_modules/next/dist/server/lib/incremental-cache/memory-cache.external.js",
    "../../node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js",
    "../../node_modules/next/dist/server/lib/incremental-cache/tags-manifest.external.js",
    "../../node_modules/next/dist/server/lib/lru-cache.js",
    "../../node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js",
    "../../node_modules/next/dist/server/lib/router-utils/instrumentation-node-extensions.js",
    "../../node_modules/next/dist/server/lib/trace/constants.js",
    "../../node_modules/next/dist/server/lib/trace/tracer.js",
    "../../node_modules/next/dist/server/load-manifest.external.js",
    "../../node_modules/next/dist/server/response-cache/types.js",
    "../../node_modules/next/dist/shared/lib/deep-freeze.js",
    "../../node_modules/next/dist/shared/lib/invariant-error.js",
    "../../node_modules/next/dist/shared/lib/is-plain-object.js",
    "../../node_modules/next/dist/shared/lib/is-thenable.js",
    "../../node_modules/next/dist/shared/lib/server-reference-info.js",
    "../../node_modules/next/package.json",

While this is what happens when pnpm is used:

    "../../node_modules/next"

With npm, this is subset of all the files in next package, but with pnpm case we will "over-bundle", but only alternative here we can do is to intentionally not support pnpm case with more meaningful error message than current non-descriptive error message (current non-descriptive error is reported in #3114 ).

Finally - ideally the issue is handled upstream, so traced files are actually files and that pnpm usage is not not resulting in dirs being reported. This is mostly work-arounding this issue

const filesInDir = await readdir(srcPath)
for (const fileInDir of filesInDir) {
await handleFileOrDirectory(join(fileOrDir, fileInDir))
}
} else {
const content = await readFile(srcPath, 'utf8')

parts.push(
`virtualModules.set(${JSON.stringify(join(commonPrefix, file))}, ${JSON.stringify(content)});`,
)
parts.push(
`virtualModules.set(${JSON.stringify(join(commonPrefix, fileOrDir))}, ${JSON.stringify(content)});`,
)
}
}

for (const file of files) {
await handleFileOrDirectory(file)
}
parts.push(`registerCJSModules(import.meta.url, virtualModules);

Expand Down
83 changes: 59 additions & 24 deletions tests/e2e/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,33 +569,68 @@ for (const { expectedRuntime, isNodeMiddleware, label, testWithSwitchableMiddlew
if (isNodeMiddleware) {
// Node.js Middleware specific tests to test features not available in Edge Runtime
test.describe('Node.js Middleware specific', () => {
test('node:crypto module', async ({ middlewareNodeRuntimeSpecific }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecific.url}/test/crypto`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.random,
'random should have 16 random bytes generated with `randomBytes` function from node:crypto in hex format',
).toMatch(/[0-9a-f]{32}/)
})
test.describe('npm package manager', () => {
test('node:crypto module', async ({ middlewareNodeRuntimeSpecific }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecific.url}/test/crypto`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.random,
'random should have 16 random bytes generated with `randomBytes` function from node:crypto in hex format',
).toMatch(/[0-9a-f]{32}/)
})

test('node:http(s) module', async ({ middlewareNodeRuntimeSpecific }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecific.url}/test/http`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.proxiedWithHttpRequest,
'proxiedWithHttpRequest should be the result of `http.request` from node:http fetching static asset',
).toStrictEqual({ hello: 'world' })
test('node:http(s) module', async ({ middlewareNodeRuntimeSpecific }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecific.url}/test/http`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.proxiedWithHttpRequest,
'proxiedWithHttpRequest should be the result of `http.request` from node:http fetching static asset',
).toStrictEqual({ hello: 'world' })
})

test('node:path module', async ({ middlewareNodeRuntimeSpecific }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecific.url}/test/path`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.joined,
'joined should be the result of `join` function from node:path',
).toBe('a/b')
})
})

test('node:path module', async ({ middlewareNodeRuntimeSpecific }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecific.url}/test/path`)
expect(response.status).toBe(200)
const body = await response.json()
expect(body.joined, 'joined should be the result of `join` function from node:path').toBe(
'a/b',
)
test.describe('pnpm package manager', () => {
test('node:crypto module', async ({ middlewareNodeRuntimeSpecificPnpm }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecificPnpm.url}/test/crypto`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.random,
'random should have 16 random bytes generated with `randomBytes` function from node:crypto in hex format',
).toMatch(/[0-9a-f]{32}/)
})

test('node:http(s) module', async ({ middlewareNodeRuntimeSpecificPnpm }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecificPnpm.url}/test/http`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.proxiedWithHttpRequest,
'proxiedWithHttpRequest should be the result of `http.request` from node:http fetching static asset',
).toStrictEqual({ hello: 'world' })
})

test('node:path module', async ({ middlewareNodeRuntimeSpecificPnpm }) => {
const response = await fetch(`${middlewareNodeRuntimeSpecificPnpm.url}/test/path`)
expect(response.status).toBe(200)
const body = await response.json()
expect(
body.joined,
'joined should be the result of `join` function from node:path',
).toBe('a/b')
})
})
})
}
Expand Down
4 changes: 4 additions & 0 deletions tests/utils/create-e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ export const fixtureFactories = {
publishDirectory: '.next-node-middleware',
}),
middlewareNodeRuntimeSpecific: () => createE2EFixture('middleware-node-runtime-specific'),
middlewareNodeRuntimeSpecificPnpm: () =>
createE2EFixture('middleware-node-runtime-specific', {
packageManger: 'pnpm',
}),
middlewareI18n: () => createE2EFixture('middleware-i18n'),
middlewareI18nNode: () =>
createE2EFixture('middleware-i18n', {
Expand Down
Loading