mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
d0798a0416
## Summary ### Original issue Pages API functions produced through a build adapter can fail during function initialization, before the customer handler executes: ``` Cannot find module 'next/dist/compiled/next-server/pages-turbo.runtime.prod.js' ``` The failure is triggered when an externalized dependency used by the API route imports a Next.js module such as `next/head`. At runtime the load path is: ``` external dependency -> next/head -> head-manager-context.shared-runtime -> Pages vendored head-manager context -> pages/module.compiled.js -> pages[-turbo].runtime.prod.js ``` The two important edges at the end of this chain are selected dynamically. The Next.js require hook redirects the shared-runtime import to the Pages vendored context, and `pages/module.compiled.js` selects the bundler-specific production renderer. As a result, a Pages API entry trace does not discover the regular Pages renderer. It naturally contains `pages-api[-turbo].runtime.prod.js`, which is the runtime for the API route module, but not `pages[-turbo].runtime.prod.js`, which is reached through the external `next/head` import. This is why the failure is specific to Pages API routes. A regular Pages SSR entry already references and traces the Pages renderer. App Router entries use different route modules and traces. ### Fix Trace the hidden Pages renderer dependency using the mechanism appropriate to each bundler: - **Turbopack:** add `pages-turbo.runtime.prod.js` as an explicit entry in `Project::pages_traced_modules`. The existing native Turbopack module graph then traces its full runtime closure. This does not run Node File Trace and remains scoped to Pages endpoints. - **Webpack:** run the existing Node File Trace path on `pages.runtime.prod.js` and merge that closure into the Pages shared assets. This code remains inside the non-Turbopack branch. The existing require-hook modules remain separate, and App Router or neutral endpoint trace sets are unchanged. No adapter change is required. ### Regression test The fixture externalizes a package that imports `next/head`, matching the reported dependency boundary. The test builds with an adapter, materializes only the generated Pages API function entry and its provided assets into a clean directory, initializes the production environment, and requires the handler in a child process. This verifies the observable initialization behavior under both Turbopack and Webpack rather than asserting a particular output file list. ## Verification - `cargo check -p next-api` - `pnpm --filter=next build` - `pnpm test-start-turbo test/production/adapter-pages-api-runtime/adapter-pages-api-runtime.test.ts` - `pnpm test-start-webpack test/production/adapter-pages-api-runtime/adapter-pages-api-runtime.test.ts` <!-- NEXT_JS_LLM --> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import fs from 'fs/promises'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
import execa from 'execa'
|
|
import { nextTestSetup } from 'e2e-utils'
|
|
import type { NextAdapter } from 'next'
|
|
|
|
describe('adapter-pages-api-runtime', () => {
|
|
const { next } = nextTestSetup({
|
|
files: __dirname,
|
|
skipStart: true,
|
|
})
|
|
|
|
it('loads an external next/head dependency from a Pages API output', async () => {
|
|
await next.build()
|
|
|
|
const { outputs, repoRoot }: Parameters<NextAdapter['onBuildComplete']>[0] =
|
|
await next.readJSON('build-complete.json')
|
|
const apiOutput = outputs.pagesApi.find(
|
|
(output) => output.pathname === '/api/hello'
|
|
)
|
|
|
|
expect(apiOutput).toBeDefined()
|
|
|
|
const functionDir = await fs.mkdtemp(
|
|
path.join(os.tmpdir(), 'next-adapter-pages-api-')
|
|
)
|
|
|
|
try {
|
|
for (const [target, source] of Object.entries(apiOutput!.assets)) {
|
|
const destination = path.join(functionDir, target)
|
|
await fs.mkdir(path.dirname(destination), { recursive: true })
|
|
await fs.cp(source, destination, {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
})
|
|
}
|
|
|
|
const entryPath = path.relative(repoRoot, apiOutput!.filePath)
|
|
const entryDestination = path.join(functionDir, entryPath)
|
|
await fs.mkdir(path.dirname(entryDestination), { recursive: true })
|
|
await fs.copyFile(apiOutput!.filePath, entryDestination)
|
|
|
|
const env: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
NODE_ENV: 'production',
|
|
}
|
|
if (process.env.IS_TURBOPACK_TEST) {
|
|
env.TURBOPACK = '1'
|
|
} else {
|
|
delete env.TURBOPACK
|
|
}
|
|
|
|
await execa(
|
|
process.execPath,
|
|
[
|
|
// Newer Node.js versions select @swc/helpers' `module-sync` export,
|
|
// which is unrelated to the Pages runtime behavior under test and is
|
|
// not represented in Turbopack's current output trace.
|
|
...(process.allowedNodeEnvironmentFlags.has(
|
|
'--no-experimental-require-module'
|
|
)
|
|
? ['--no-experimental-require-module']
|
|
: []),
|
|
'-e',
|
|
`require('next/setup-node-env'); const mod = require(${JSON.stringify(entryDestination)}); if (typeof mod.handler !== 'function') process.exit(1)`,
|
|
],
|
|
{ cwd: functionDir, env }
|
|
)
|
|
} finally {
|
|
await fs.rm(functionDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|