Skip to content

fix: handle edge runtime pages and middleware for turbopack builds #3009

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

Merged
merged 4 commits into from
Jul 14, 2025
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
5 changes: 5 additions & 0 deletions edge-runtime/shim/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ globalThis.require = function nextRuntimeMinimalRequireShim(name) {
}

var _ENTRIES = {}
try {
// turbopack builds use self._ENTRIES not globalThis._ENTRIES
// so making sure both points to same object
self._ENTRIES = _ENTRIES
} catch {}
1 change: 1 addition & 0 deletions src/build/content/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
`server/*`,
`server/chunks/**/*`,
`server/edge-chunks/**/*`,
`server/edge/chunks/**/*`,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

needed for edge runtime pages when building with turbopack

server/edge-chunks seems to contain things like assets and wasm files (?) for middleware for non-turbopack builds

`server/+(app|pages)/**/*.js`,
],
{
Expand Down
9 changes: 7 additions & 2 deletions src/build/functions/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,15 @@ const copyHandlerDependencies = async (
const entrypoint = await readFile(join(srcDir, file), 'utf8')
parts.push(`;// Concatenated file: ${file} \n`, entrypoint)
}
const exports = `const middlewareEntryKey = Object.keys(_ENTRIES).find(entryKey => entryKey.startsWith("middleware_${name}")); export default _ENTRIES[middlewareEntryKey].default;`
parts.push(
`const middlewareEntryKey = Object.keys(_ENTRIES).find(entryKey => entryKey.startsWith("middleware_${name}"));`,
// turbopack entries are promises so we await here to get actual entry
// non-turbopack entries are already resolved, so await does not change anything
`export default await _ENTRIES[middlewareEntryKey].default;`,
)
Comment on lines +153 to +158
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the only real change here is adding await to exported value, but I did very small change to add some context about that in comment

await mkdir(dirname(outputFile), { recursive: true })

await writeFile(outputFile, [...parts, exports].join('\n'))
await writeFile(outputFile, parts.join('\n'))
}

const createEdgeHandler = async (ctx: PluginContext, definition: NextDefinition): Promise<void> => {
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/hello-world-turbopack/app/edge-page/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { connection } from 'next/server'

export const runtime = 'edge'

export default async function Page() {
await connection()
return <h1>Hello, Next.js!</h1>
}
5 changes: 4 additions & 1 deletion tests/fixtures/hello-world-turbopack/app/page.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export default function Page() {
import { connection } from 'next/server'

export default async function Page() {
await connection()
return <h1>Hello, Next.js!</h1>
}
12 changes: 12 additions & 0 deletions tests/fixtures/hello-world-turbopack/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
return NextResponse.json({
message: `Hello from middleware at ${request.nextUrl.pathname}`,
})
}

export const config = {
matcher: '/middleware/:path*',
}
51 changes: 40 additions & 11 deletions tests/integration/hello-world-turbopack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { getLogger } from 'lambda-local'
import { HttpResponse, http, passthrough } from 'msw'
import { setupServer } from 'msw/node'
import { v4 } from 'uuid'
import { afterAll, afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
import { type FixtureTestContext } from '../utils/contexts.js'
import { createFixture, invokeFunction, runPlugin } from '../utils/fixture.js'
import { createFixture, invokeEdgeFunction, invokeFunction, runPlugin } from '../utils/fixture.js'
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'

Expand Down Expand Up @@ -63,15 +63,44 @@ afterEach(() => {

// https://github.com/vercel/next.js/pull/77808 makes turbopack builds no longer gated only to canaries
// allowing to run this test on both stable and canary versions of Next.js
test.skipIf(!nextVersionSatisfies('>=15.3.0-canary.43'))<FixtureTestContext>(
describe.skipIf(!nextVersionSatisfies('>=15.3.0-canary.43'))(
'Test that the hello-world-turbopack next app is working',
async (ctx) => {
await createFixture('hello-world-turbopack', ctx)
await runPlugin(ctx)

// test the function call
const home = await invokeFunction(ctx)
expect(home.statusCode).toBe(200)
expect(load(home.body)('h1').text()).toBe('Hello, Next.js!')
() => {
test<FixtureTestContext>('regular page is working', async (ctx) => {
await createFixture('hello-world-turbopack', ctx)
await runPlugin(ctx)

// test the function call
const home = await invokeFunction(ctx)
expect(home.statusCode).toBe(200)
expect(load(home.body)('h1').text()).toBe('Hello, Next.js!')
})

test<FixtureTestContext>('edge page is working', async (ctx) => {
await createFixture('hello-world-turbopack', ctx)
await runPlugin(ctx)

// test the function call
const home = await invokeFunction(ctx, { url: '/edge-page' })
expect(home.statusCode).toBe(200)
expect(load(home.body)('h1').text()).toBe('Hello, Next.js!')
})

test<FixtureTestContext>('middleware is working', async (ctx) => {
await createFixture('hello-world-turbopack', ctx)
await runPlugin(ctx)

const pathname = '/middleware/test'

const response = await invokeEdgeFunction(ctx, {
functions: ['___netlify-edge-handler-middleware'],
url: pathname,
})

expect(response.status).toBe(200)
expect(await response.json()).toEqual({
message: `Hello from middleware at ${pathname}`,
})
})
},
)
Loading