mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
#95815 refactored the NFT flow and ended up only tracing the RSC template and it's subgraph. Not the server actions loader module whose module graph is completely separate. --- Clauded. Attempts to fix https://github.com/vercel/next.js/discussions/95130#discussioncomment-17652155. --- <details> ## Summary Fixes the NFT tracing regression behind #95815 and the deploy failures reported in https://github.com/vercel/next.js/issues/87737#issuecomment-4897366831. Since the 16.3 NFT rewrite (#94224 / #92901), a `serverExternalPackages` entry that is imported **only from a `'use server'` action referenced by a client component** ends up in the route's trace as the content-hashed alias symlink (`.next/node_modules/<pkg>-<hash>`) **without the store files the symlink points to**. Any output assembled from the traces (Vercel lambdas, `output: 'standalone'`) then contains a dangling symlink, and invoking the action throws `Failed to load external module <pkg>-<hash>` (HTTP 500). 16.2.9 traces both. Both CJS and ESM externals are affected, with any package manager — the ESM/bun/monorepo framing in the issue reports was a coincidence of which packages were only reachable through actions. The cause: `AppEndpoint::trace_result` started the trace DFS only from `rsc_entry`, but the server actions loader is a separate module-graph entry (`additional_entries`). It is chunked into the endpoint output (which emits the alias symlink via the chunk's output assets), yet its subgraph — including the externals' traced target files — was never visited by the tracer. `trace_endpoint` now accepts multiple entry modules and the app endpoint passes the actions loader alongside `rsc_entry`. The test observes the user-visible failure: it builds with `output: 'standalone'`, deletes everything except the standalone output (so only traced files are available, like a deployed lambda), runs `server.js`, and invokes the server action in a browser. Without the fix the action fails with `Failed to load external module lodash-<hash>`; on 16.2.9 the same fixture responds with the action result. Note the fixture must not import the externals from any other route: the standalone output is the union of all route traces, so a route that traces them correctly would mask the missing entries. ## Verification - `pnpm test-start-turbo test/production/standalone-mode/server-action-externals/standalone-mode-server-action-externals.test.ts` — fails without the fix (`Failed to load external module lodash-fb78ad0536ffcdab` from the standalone server), passes with it. The equivalent standalone flow on `[email protected]` returns the action result with HTTP 200. - `pnpm test-start-turbo test/e2e/externals-transitive/externals-transitive.test.ts` - `pnpm test-start-turbo test/production/standalone-mode/server-actions/standalone-mode-server-actions.test.ts` <!-- NEXT_JS_LLM --> </details>
65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
import { ChildProcess } from 'child_process'
|
|
import { nextTestSetup } from 'e2e-utils'
|
|
import fs from 'fs-extra'
|
|
import { findPort, initNextServerScript, killApp, retry } from 'next-test-utils'
|
|
import { join } from 'path'
|
|
|
|
// The external packages are only imported by a server action, so the
|
|
// standalone output only contains them if the route's trace includes them.
|
|
describe('standalone mode: server action externals', () => {
|
|
let server: ChildProcess
|
|
let appPort: number
|
|
|
|
const { next } = nextTestSetup({
|
|
files: __dirname,
|
|
dependencies: require('./package.json').dependencies,
|
|
skipStart: true,
|
|
})
|
|
|
|
beforeAll(async () => {
|
|
await next.build()
|
|
|
|
await fs.move(
|
|
join(next.testDir, '.next/standalone'),
|
|
join(next.testDir, 'standalone')
|
|
)
|
|
|
|
await fs.copy(
|
|
join(next.testDir, '.next/static'),
|
|
join(next.testDir, 'standalone/.next/static')
|
|
)
|
|
|
|
// Remove everything else (including node_modules) so that the server can
|
|
// only load what the file traces brought into the standalone output.
|
|
for (const file of await fs.readdir(next.testDir)) {
|
|
if (file !== 'standalone') {
|
|
await fs.remove(join(next.testDir, file))
|
|
}
|
|
}
|
|
|
|
const testServer = join(next.testDir, 'standalone/server.js')
|
|
appPort = await findPort()
|
|
server = await initNextServerScript(testServer, /- Local:/, {
|
|
...process.env,
|
|
...next.env,
|
|
HOSTNAME: '::',
|
|
PORT: '' + appPort,
|
|
})
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (server) {
|
|
await killApp(server)
|
|
}
|
|
})
|
|
|
|
it('should execute a server action that uses external packages', async () => {
|
|
const browser = await next.browser('/', { baseUrl: appPort })
|
|
await browser.elementByCss('button').click()
|
|
|
|
await retry(async () => {
|
|
expect(await browser.elementByCss('#result').text()).toBe('helloWorld')
|
|
})
|
|
})
|
|
})
|