From 83e5c48f6001bb00e07b361442b4912204416920 Mon Sep 17 00:00:00 2001
From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Date: Tue, 18 Nov 2025 10:01:12 +0100
Subject: [PATCH 1/7] add test
---
.../hmr-iframe/app/page1/Component.tsx | 5 ++
.../app-dir/hmr-iframe/app/page1/page.tsx | 18 ++++++
.../hmr-iframe/app/page1/subscribeToHMR.ts | 55 +++++++++++++++++++
.../app-dir/hmr-iframe/app/page2/page.tsx | 13 +++++
.../app-dir/hmr-iframe/hmr-iframe.test.ts | 35 ++++++++++++
.../app-dir/hmr-iframe/next.config.js | 5 ++
6 files changed, 131 insertions(+)
create mode 100644 test/development/app-dir/hmr-iframe/app/page1/Component.tsx
create mode 100644 test/development/app-dir/hmr-iframe/app/page1/page.tsx
create mode 100644 test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts
create mode 100644 test/development/app-dir/hmr-iframe/app/page2/page.tsx
create mode 100644 test/development/app-dir/hmr-iframe/hmr-iframe.test.ts
create mode 100644 test/development/app-dir/hmr-iframe/next.config.js
diff --git a/test/development/app-dir/hmr-iframe/app/page1/Component.tsx b/test/development/app-dir/hmr-iframe/app/page1/Component.tsx
new file mode 100644
index 0000000000000..04755dd3be09f
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/app/page1/Component.tsx
@@ -0,0 +1,5 @@
+'use client'
+
+export function Component() {
+ return
Component
+}
diff --git a/test/development/app-dir/hmr-iframe/app/page1/page.tsx b/test/development/app-dir/hmr-iframe/app/page1/page.tsx
new file mode 100644
index 0000000000000..85595f6115044
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/app/page1/page.tsx
@@ -0,0 +1,18 @@
+import { subscribeToHMR } from './subscribeToHMR'
+import { Component } from './Component'
+
+const RootPage = async ({ Component }: any) => {
+ await subscribeToHMR()
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default function Page() {
+ return
+}
diff --git a/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts b/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts
new file mode 100644
index 0000000000000..8a72269fd4de0
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts
@@ -0,0 +1,55 @@
+if (!global._payload) {
+ global._payload = {
+ payload: null,
+ reload: false,
+ ws: null,
+ }
+}
+
+export const subscribeToHMR = async () => {
+ let cached = global._payload
+
+ if (cached.payload) {
+ if (cached.reload === true) {
+ let resolve: any
+
+ cached.reload = new Promise((res) => (resolve = res))
+
+ await new Promise((resolve) => setTimeout(resolve, 200))
+
+ resolve()
+ }
+ if (cached.reload instanceof Promise) {
+ await cached.reload
+ }
+
+ return cached.payload
+ }
+
+ cached.payload = {}
+
+ const port = process.env.PORT || '3000'
+ const hasHTTPS =
+ process.env.USE_HTTPS === 'true' ||
+ process.argv.includes('--experimental-https')
+ const protocol = hasHTTPS ? 'wss' : 'ws'
+ const path = '/_next/webpack-hmr'
+ // The __NEXT_ASSET_PREFIX env variable is set for both assetPrefix and basePath (tested in Next.js 15.1.6)
+ const prefix = process.env.__NEXT_ASSET_PREFIX ?? ''
+ cached.ws = new WebSocket(
+ process.env.PAYLOAD_HMR_URL_OVERRIDE ??
+ `${protocol}://localhost:${port}${prefix}${path}`
+ )
+ cached.ws.onmessage = (event: any) => {
+ if (typeof event.data === 'string') {
+ const data = JSON.parse(event.data)
+ if (
+ // On Next.js 15, we need to check for data.action. On Next.js 16, we need to check for data.type.
+ data.type === 'serverComponentChanges' ||
+ data.action === 'serverComponentChanges'
+ ) {
+ cached.reload = true
+ }
+ }
+ }
+}
diff --git a/test/development/app-dir/hmr-iframe/app/page2/page.tsx b/test/development/app-dir/hmr-iframe/app/page2/page.tsx
new file mode 100644
index 0000000000000..b17fac490ceaf
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/app/page2/page.tsx
@@ -0,0 +1,13 @@
+import { subscribeToHMR } from '../page1/subscribeToHMR'
+
+export default async function Page() {
+ await subscribeToHMR()
+
+ return (
+
+
+ content
+
+
+ )
+}
diff --git a/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts b/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts
new file mode 100644
index 0000000000000..eaf0334a7ece6
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts
@@ -0,0 +1,35 @@
+import { nextTestSetup } from 'e2e-utils'
+import { waitForNoRedbox } from 'next-test-utils'
+
+describe('hmr-iframe', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ })
+
+ it('should do HMR when rendering two server component changes at the same time', async () => {
+ let browser = await next.browser('/page1')
+
+ expect(
+ await (await (await browser.elementByCss('iframe')).contentFrame())
+ .locator('p')
+ .innerText()
+ ).toEqual('content')
+
+ await next.patchFile('app/page2/page.tsx', (content) =>
+ content.replace('content', 'content-new')
+ )
+
+ await waitForNoRedbox(browser)
+ expect(
+ await (await (await browser.elementByCss('iframe')).contentFrame())
+ .locator('p')
+ .innerText()
+ ).toEqual('content-new')
+
+ expect(next.cliOutput).not.toContain('Error')
+ expect(next.cliOutput).not.toContain('Could not find the module')
+
+ await next.stop()
+ await next.clean()
+ })
+})
diff --git a/test/development/app-dir/hmr-iframe/next.config.js b/test/development/app-dir/hmr-iframe/next.config.js
new file mode 100644
index 0000000000000..cfa3ac3d7aa94
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/next.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ experimental: {
+ appDir: true,
+ },
+}
From 1adcf68ffab78e221179a440399d787beb4bfa13 Mon Sep 17 00:00:00 2001
From: Hendrik Liebau
Date: Wed, 26 Nov 2025 16:18:48 +0100
Subject: [PATCH 2/7] Simplify the test case
---
.../app-dir/hmr-iframe/app/page1/page.tsx | 8 ++-
.../hmr-iframe/app/page1/subscribeToHMR.ts | 62 ++++++++-----------
.../app-dir/hmr-iframe/app/page2/page.tsx | 2 +-
.../app-dir/hmr-iframe/hmr-iframe.test.ts | 7 ++-
.../app-dir/hmr-iframe/next.config.js | 5 --
5 files changed, 39 insertions(+), 45 deletions(-)
delete mode 100644 test/development/app-dir/hmr-iframe/next.config.js
diff --git a/test/development/app-dir/hmr-iframe/app/page1/page.tsx b/test/development/app-dir/hmr-iframe/app/page1/page.tsx
index 85595f6115044..f5ade055497dc 100644
--- a/test/development/app-dir/hmr-iframe/app/page1/page.tsx
+++ b/test/development/app-dir/hmr-iframe/app/page1/page.tsx
@@ -1,7 +1,11 @@
import { subscribeToHMR } from './subscribeToHMR'
import { Component } from './Component'
-const RootPage = async ({ Component }: any) => {
+// The (unused) client component prop is crucial to reproduce the issue. It will
+// be serialized as a client reference in the props of this component, which
+// acts as the owner of the I/O inside subscribeToHMR, which is also serialized
+// as part of the async I/O sequence in page 2.
+const RootPage = async ({ Component }: { Component: React.ComponentType }) => {
await subscribeToHMR()
return (
@@ -13,6 +17,6 @@ const RootPage = async ({ Component }: any) => {
)
}
-export default function Page() {
+export default function Page1() {
return
}
diff --git a/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts b/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts
index 8a72269fd4de0..844ae5415db7e 100644
--- a/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts
+++ b/test/development/app-dir/hmr-iframe/app/page1/subscribeToHMR.ts
@@ -1,54 +1,46 @@
-if (!global._payload) {
- global._payload = {
- payload: null,
+declare global {
+ var _testState: {
+ initialized: boolean
+ reload: boolean | Promise
+ }
+}
+
+if (!globalThis._testState) {
+ globalThis._testState = {
+ initialized: false,
reload: false,
- ws: null,
}
}
export const subscribeToHMR = async () => {
- let cached = global._payload
-
- if (cached.payload) {
- if (cached.reload === true) {
- let resolve: any
-
- cached.reload = new Promise((res) => (resolve = res))
+ const state = globalThis._testState
+ if (state.initialized) {
+ if (state.reload === true) {
+ let resolve: () => void
+ state.reload = new Promise((res) => (resolve = res))
await new Promise((resolve) => setTimeout(resolve, 200))
-
resolve()
}
- if (cached.reload instanceof Promise) {
- await cached.reload
+
+ if (state.reload instanceof Promise) {
+ await state.reload
}
- return cached.payload
+ return
}
- cached.payload = {}
-
- const port = process.env.PORT || '3000'
- const hasHTTPS =
- process.env.USE_HTTPS === 'true' ||
- process.argv.includes('--experimental-https')
- const protocol = hasHTTPS ? 'wss' : 'ws'
- const path = '/_next/webpack-hmr'
- // The __NEXT_ASSET_PREFIX env variable is set for both assetPrefix and basePath (tested in Next.js 15.1.6)
- const prefix = process.env.__NEXT_ASSET_PREFIX ?? ''
- cached.ws = new WebSocket(
- process.env.PAYLOAD_HMR_URL_OVERRIDE ??
- `${protocol}://localhost:${port}${prefix}${path}`
+ state.initialized = true
+
+ const ws = new WebSocket(
+ `ws://localhost:${process.env.PORT}/_next/webpack-hmr`
)
- cached.ws.onmessage = (event: any) => {
+
+ ws.onmessage = (event: any) => {
if (typeof event.data === 'string') {
const data = JSON.parse(event.data)
- if (
- // On Next.js 15, we need to check for data.action. On Next.js 16, we need to check for data.type.
- data.type === 'serverComponentChanges' ||
- data.action === 'serverComponentChanges'
- ) {
- cached.reload = true
+ if (data.type === 'serverComponentChanges') {
+ state.reload = true
}
}
}
diff --git a/test/development/app-dir/hmr-iframe/app/page2/page.tsx b/test/development/app-dir/hmr-iframe/app/page2/page.tsx
index b17fac490ceaf..de16b7a997350 100644
--- a/test/development/app-dir/hmr-iframe/app/page2/page.tsx
+++ b/test/development/app-dir/hmr-iframe/app/page2/page.tsx
@@ -1,6 +1,6 @@
import { subscribeToHMR } from '../page1/subscribeToHMR'
-export default async function Page() {
+export default async function Page2() {
await subscribeToHMR()
return (
diff --git a/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts b/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts
index eaf0334a7ece6..6f320fe82b2f1 100644
--- a/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts
+++ b/test/development/app-dir/hmr-iframe/hmr-iframe.test.ts
@@ -15,6 +15,8 @@ describe('hmr-iframe', () => {
.innerText()
).toEqual('content')
+ let cliOutputLength = next.cliOutput.length
+
await next.patchFile('app/page2/page.tsx', (content) =>
content.replace('content', 'content-new')
)
@@ -26,8 +28,9 @@ describe('hmr-iframe', () => {
.innerText()
).toEqual('content-new')
- expect(next.cliOutput).not.toContain('Error')
- expect(next.cliOutput).not.toContain('Could not find the module')
+ const cliOutput = next.cliOutput.slice(cliOutputLength)
+ expect(cliOutput).not.toContain('Error')
+ expect(cliOutput).not.toContain('Could not find the module')
await next.stop()
await next.clean()
diff --git a/test/development/app-dir/hmr-iframe/next.config.js b/test/development/app-dir/hmr-iframe/next.config.js
deleted file mode 100644
index cfa3ac3d7aa94..0000000000000
--- a/test/development/app-dir/hmr-iframe/next.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- experimental: {
- appDir: true,
- },
-}
From 82e4bee53421b506c6084a7b5ea32bda93ab34a6 Mon Sep 17 00:00:00 2001
From: Hendrik Liebau
Date: Thu, 27 Nov 2025 18:53:19 +0100
Subject: [PATCH 3/7] Handle cross-page client reference contamination in
development
In development, React is tracking I/O for debugging purposes. Under some
circumstances it can happen that I/O that is cached in a global
(generally not recommended!) might be triggered by a component of one
page (the owner of that I/O) and also emitted as I/O debug info for a
component of another page that depends on it. If the owner references
client components in its props, those couldn't be serialized for the
other page because they don't exist in the client reference manifest of
that page, leading to the following error:
```
Error: Could not find the module "" in the React Client Manifest.
This is probably a bug in the React Server Components bundler.
```
To support that case, we're now looking for client references in
manifests of other pages, but only in development mode.
A better fix might be to pass a joint `debugBundlerConfig` option to
React, alongside the normal `bundlerConfig` option, or something like
that. But for now, this should suffice.
Another option we considered but disregarded is a change in React to
ignore missing client references when emitting them as debug info, and
emitting an omission placeholder instead. But for the general case this
would mask real bugs in the RSC bundler.
fixes #85883
---
packages/next/errors.json | 5 +-
packages/next/src/build/templates/app-page.ts | 10 +-
.../next/src/build/templates/app-route.ts | 8 +-
.../src/build/templates/edge-app-route.ts | 8 +-
.../next/src/build/templates/edge-ssr-app.ts | 12 +-
.../webpack/plugins/flight-manifest-plugin.ts | 2 +-
packages/next/src/export/index.ts | 5 -
.../src/server/app-render/action-handler.ts | 25 +-
.../src/server/app-render/action-utils.ts | 19 +-
.../next/src/server/app-render/app-render.tsx | 174 ++++---------
.../create-component-styles-and-scripts.tsx | 1 -
.../src/server/app-render/encryption-utils.ts | 157 +-----------
.../next/src/server/app-render/encryption.ts | 10 +-
.../app-render/get-css-inlined-link-tags.tsx | 24 +-
.../server/app-render/get-layer-assets.tsx | 1 -
.../server/app-render/manifests-singleton.ts | 231 ++++++++++++++++++
packages/next/src/server/app-render/types.ts | 2 -
.../server/app-render/use-flight-response.tsx | 19 +-
.../walk-tree-with-flight-router-state.tsx | 1 -
packages/next/src/server/load-components.ts | 12 +-
.../server/load-default-error-components.ts | 3 -
.../src/server/use-cache/use-cache-wrapper.ts | 16 +-
packages/next/types/$$compiled.internal.d.ts | 6 +-
23 files changed, 350 insertions(+), 401 deletions(-)
create mode 100644 packages/next/src/server/app-render/manifests-singleton.ts
diff --git a/packages/next/errors.json b/packages/next/errors.json
index 91dfce18d0632..d65159c5c5eeb 100644
--- a/packages/next/errors.json
+++ b/packages/next/errors.json
@@ -947,5 +947,8 @@
"946": "Failed to deserialize errors.",
"947": "Expected `sendErrorsToBrowser` to be defined in renderOpts.",
"948": "Failed to serialize errors.",
- "949": "Route %s errored during %s. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled.\n \nOriginal Error: %s"
+ "949": "Route %s errored during %s. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled.\n \nOriginal Error: %s",
+ "950": "The manifests singleton was not initialized.",
+ "951": "The client reference manifest for route \"%s\" does not exist.",
+ "952": "Cannot access \"%s\" without a work store."
}
diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts
index 9c18124ece7f7..b0123ecdc9bbd 100644
--- a/packages/next/src/build/templates/app-page.ts
+++ b/packages/next/src/build/templates/app-page.ts
@@ -21,12 +21,11 @@ import {
createOpaqueFallbackRouteParams,
type OpaqueFallbackRouteParams,
} from '../../server/request/fallback-params'
-import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
+import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import {
isHtmlBotRequest,
shouldServeStreamingMetadata,
} from '../../server/lib/streaming-metadata'
-import { createServerModuleMap } from '../../server/app-render/action-utils'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { getIsPossibleServerAction } from '../../server/lib/server-action-request-meta'
import {
@@ -400,13 +399,10 @@ export async function handler(
// set the reference manifests to our global store so Server Action's
// encryption util can access to them at the top level of the page module.
if (serverActionsManifest && clientReferenceManifest) {
- setReferenceManifestsSingleton({
+ setManifestsSingleton({
page: srcPage,
clientReferenceManifest,
serverActionsManifest,
- serverModuleMap: createServerModuleMap({
- serverActionsManifest,
- }),
})
}
@@ -540,8 +536,6 @@ export async function handler(
nextFontManifest,
reactLoadableManifest,
subresourceIntegrityManifest,
- serverActionsManifest,
- clientReferenceManifest,
setCacheStatus: routerServerContext?.setCacheStatus,
setIsrStatus: routerServerContext?.setIsrStatus,
setReactDebugChannel: routerServerContext?.setReactDebugChannel,
diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts
index eee8857c3607f..51885144ef010 100644
--- a/packages/next/src/build/templates/app-route.ts
+++ b/packages/next/src/build/templates/app-route.ts
@@ -8,8 +8,7 @@ import { patchFetch as _patchFetch } from '../../server/lib/patch-fetch'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { addRequestMeta, getRequestMeta } from '../../server/request-meta'
import { getTracer, type Span, SpanKind } from '../../server/lib/trace/tracer'
-import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
-import { createServerModuleMap } from '../../server/app-render/action-utils'
+import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node'
import {
@@ -185,13 +184,10 @@ export async function handler(
// set the reference manifests to our global store so Server Action's
// encryption util can access to them at the top level of the page module.
if (serverActionsManifest && clientReferenceManifest) {
- setReferenceManifestsSingleton({
+ setManifestsSingleton({
page: srcPage,
clientReferenceManifest,
serverActionsManifest,
- serverModuleMap: createServerModuleMap({
- serverActionsManifest,
- }),
})
}
diff --git a/packages/next/src/build/templates/edge-app-route.ts b/packages/next/src/build/templates/edge-app-route.ts
index 3401fdf7348fe..79fb78ff1f0cf 100644
--- a/packages/next/src/build/templates/edge-app-route.ts
+++ b/packages/next/src/build/templates/edge-app-route.ts
@@ -1,5 +1,4 @@
-import { createServerModuleMap } from '../../server/app-render/action-utils'
-import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
+import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import type { NextConfigComplete } from '../../server/config-shared'
import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapper'
@@ -16,13 +15,10 @@ const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
if (rscManifest && rscServerManifest) {
- setReferenceManifestsSingleton({
+ setManifestsSingleton({
page: 'VAR_PAGE',
clientReferenceManifest: rscManifest,
serverActionsManifest: rscServerManifest,
- serverModuleMap: createServerModuleMap({
- serverActionsManifest: rscServerManifest,
- }),
})
}
diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts
index 58fc2ad9cb602..800d63ba7ca87 100644
--- a/packages/next/src/build/templates/edge-ssr-app.ts
+++ b/packages/next/src/build/templates/edge-ssr-app.ts
@@ -6,8 +6,7 @@ import * as pageMod from 'VAR_USERLAND'
import type { RequestData } from '../../server/web/types'
import type { NextConfigComplete } from '../../server/config-shared'
-import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
-import { createServerModuleMap } from '../../server/app-render/action-utils'
+import { setManifestsSingleton } from '../../server/app-render/manifests-singleton'
import { initializeCacheHandlers } from '../../server/use-cache/handlers'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import { getTracer, SpanKind, type Span } from '../../server/lib/trace/tracer'
@@ -40,13 +39,10 @@ const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE']
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
if (rscManifest && rscServerManifest) {
- setReferenceManifestsSingleton({
+ setManifestsSingleton({
page: 'VAR_PAGE',
clientReferenceManifest: rscManifest,
serverActionsManifest: rscServerManifest,
- serverModuleMap: createServerModuleMap({
- serverActionsManifest: rscServerManifest,
- }),
})
}
@@ -81,12 +77,10 @@ async function requestHandler(
buildManifest,
prerenderManifest,
reactLoadableManifest,
- clientReferenceManifest,
subresourceIntegrityManifest,
dynamicCssManifest,
nextFontManifest,
resolvedPathname,
- serverActionsManifest,
interceptionRoutePatterns,
routerServerContext,
} = prepareResult
@@ -129,8 +123,6 @@ async function requestHandler(
reactLoadableManifest,
subresourceIntegrityManifest,
dynamicCssManifest,
- serverActionsManifest,
- clientReferenceManifest,
setIsrStatus: routerServerContext?.setIsrStatus,
dir: pageRouteModule.relativeProjectDir,
diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
index 25a317e7194ce..6726d4a601835 100644
--- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
+++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
@@ -41,7 +41,7 @@ interface Options {
type ModuleId = string | number /*| null*/
// double indexed chunkId, filename
-export type ManifestChunks = Array
+export type ManifestChunks = ReadonlyArray
const pluginState = getProxiedPluginState({
ssrModules: {} as { [ssrModuleId: string]: ModuleInfo },
diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts
index 95945ac2c1a09..c736915c4c162 100644
--- a/packages/next/src/export/index.ts
+++ b/packages/next/src/export/index.ts
@@ -385,11 +385,6 @@ async function exportAppImpl(
join(distDir, 'server', `${NEXT_FONT_MANIFEST}.json`)
),
images: nextConfig.images,
- ...(enabledDirectories.app
- ? {
- serverActionsManifest,
- }
- : {}),
deploymentId: nextConfig.deploymentId,
htmlLimitedBots: nextConfig.htmlLimitedBots.source,
experimental: {
diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts
index 9efbd6bfe5316..3283d261eccdd 100644
--- a/packages/next/src/server/app-render/action-handler.ts
+++ b/packages/next/src/server/app-render/action-handler.ts
@@ -49,7 +49,7 @@ import { warn } from '../../build/output/log'
import { RequestCookies, ResponseCookies } from '../web/spec-extension/cookies'
import { HeadersAdapter } from '../web/spec-extension/adapters/headers'
import { fromNodeOutgoingHttpHeaders } from '../web/utils'
-import { selectWorkerForForwarding } from './action-utils'
+import { selectWorkerForForwarding, type ServerModuleMap } from './action-utils'
import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers'
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
import { synchronizeMutableCookies } from '../async-storage/request-store'
@@ -59,7 +59,7 @@ import { InvariantError } from '../../shared/lib/invariant-error'
import { executeRevalidates } from '../revalidation-utils'
import { getRequestMeta } from '../request-meta'
import { setCacheBustingSearchParam } from '../../client/components/router-reducer/set-cache-busting-search-param'
-import { getServerModuleMap } from './encryption-utils'
+import { getServerModuleMap } from './manifests-singleton'
function formDataFromSearchQueryString(query: string) {
const searchParams = new URLSearchParams(query)
@@ -473,15 +473,6 @@ export function parseHostHeader(
: undefined
}
-type ServerModuleMap = Record<
- string,
- {
- id: string
- chunks: string[]
- name: string
- }
->
-
type ServerActionsConfig = {
bodySizeLimit?: SizeLimit
allowedOrigins?: string[]
@@ -522,7 +513,7 @@ export async function handleAction({
metadata: AppPageRenderResultMetadata
}): Promise {
const contentType = req.headers['content-type']
- const { serverActionsManifest, page } = ctx.renderOpts
+ const { page } = ctx.renderOpts
const serverModuleMap = getServerModuleMap()
const {
@@ -640,11 +631,7 @@ export async function handleAction({
const actionWasForwarded = Boolean(req.headers['x-action-forwarded'])
if (actionId) {
- const forwardedWorker = selectWorkerForForwarding(
- actionId,
- page,
- serverActionsManifest
- )
+ const forwardedWorker = selectWorkerForForwarding(actionId, page)
// If forwardedWorker is truthy, it means there isn't a worker for the action
// in the current handler, so we forward the request to a worker that has the action.
@@ -685,7 +672,7 @@ export async function handleAction({
{ isAction: true },
async (): Promise => {
// We only use these for fetch actions -- MPA actions handle them inside `decodeAction`.
- let actionModId: string | undefined
+ let actionModId: string | number | undefined
let boundActionArguments: unknown[] = []
if (
@@ -1214,7 +1201,7 @@ async function executeActionAndPrepareForRender<
function getActionModIdOrError(
actionId: string | null,
serverModuleMap: ServerModuleMap
-): string {
+): string | number {
// if we're missing the action ID header, we can't do any further processing
if (!actionId) {
throw new InvariantError("Missing 'next-action' header.")
diff --git a/packages/next/src/server/app-render/action-utils.ts b/packages/next/src/server/app-render/action-utils.ts
index 12ca7706da0d0..422286764c97f 100644
--- a/packages/next/src/server/app-render/action-utils.ts
+++ b/packages/next/src/server/app-render/action-utils.ts
@@ -2,8 +2,18 @@ import type { ActionManifest } from '../../build/webpack/plugins/flight-client-e
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
+import { getServerActionsManifest } from './manifests-singleton'
import { workAsyncStorage } from './work-async-storage.external'
+export interface ServerModuleMap {
+ readonly [name: string]: {
+ readonly id: string | number
+ readonly name: string
+ readonly chunks: Readonly> // currently not used
+ readonly async?: boolean
+ }
+}
+
// This function creates a Flight-acceptable server module map proxy from our
// Server Reference Manifest similar to our client module map.
// This is because our manifest contains a lot of internal Next.js data that
@@ -12,7 +22,7 @@ export function createServerModuleMap({
serverActionsManifest,
}: {
serverActionsManifest: ActionManifest
-}) {
+}): ServerModuleMap {
return new Proxy(
{},
{
@@ -61,11 +71,8 @@ export function createServerModuleMap({
* Checks if the requested action has a worker for the current page.
* If not, it returns the first worker that has a handler for the action.
*/
-export function selectWorkerForForwarding(
- actionId: string,
- pageName: string,
- serverActionsManifest: ActionManifest
-) {
+export function selectWorkerForForwarding(actionId: string, pageName: string) {
+ const serverActionsManifest = getServerActionsManifest()
const workers =
serverActionsManifest[
process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'
diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index 413209edc73e6..5d4802c2b6b8f 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -23,11 +23,6 @@ import type {
import type { NextParsedUrlQuery } from '../request-meta'
import type { LoaderTree } from '../lib/app-dir-module'
import type { AppPageModule } from '../route-modules/app-page/module'
-import type {
- ClientReferenceManifest,
- ManifestNode,
-} from '../../build/webpack/plugins/flight-manifest-plugin'
-import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import type { BaseNextRequest, BaseNextResponse } from '../base-http'
import type { IncomingHttpHeaders } from 'http'
import * as ReactClient from 'react'
@@ -100,7 +95,10 @@ import { makeGetServerInsertedHTML } from './make-get-server-inserted-html'
import { walkTreeWithFlightRouterState } from './walk-tree-with-flight-router-state'
import { createComponentTree, getRootParams } from './create-component-tree'
import { getAssetQueryString } from './get-asset-query-string'
-import { getServerModuleMap } from './encryption-utils'
+import {
+ getClientReferenceManifest,
+ getServerModuleMap,
+} from './manifests-singleton'
import {
DynamicState,
type PostponedState,
@@ -255,7 +253,6 @@ export type AppRenderContext = {
requestId: string
htmlRequestId: string
pagePath: string
- clientReferenceManifest: DeepReadonly
assetPrefix: string
isNotFoundPath: boolean
nonce: string | undefined
@@ -598,7 +595,6 @@ async function generateDynamicFlightRenderResult(
}
): Promise {
const {
- clientReferenceManifest,
componentMod: { renderToReadableStream },
htmlRequestId,
renderOpts,
@@ -635,6 +631,8 @@ async function generateDynamicFlightRenderResult(
setReactDebugChannel(debugChannel.clientSide, htmlRequestId, requestId)
}
+ const { clientModules } = getClientReferenceManifest()
+
// For app dir, use the bundled version of Flight server renderer (renderToReadableStream)
// which contains the subset React.
const rscPayload = await workUnitAsyncStorage.run(
@@ -648,7 +646,7 @@ async function generateDynamicFlightRenderResult(
requestStore,
renderToReadableStream,
rscPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
onError,
temporaryReferences: options?.temporaryReferences,
@@ -674,7 +672,6 @@ async function stagedRenderToReadableStreamWithoutCachesInDev(
ctx: AppRenderContext,
requestStore: RequestStore,
getPayload: (requestStore: RequestStore) => Promise,
- clientReferenceManifest: NonNullable,
options: Omit
) {
const {
@@ -722,6 +719,7 @@ async function stagedRenderToReadableStreamWithoutCachesInDev(
requestStore.headers
)
+ const { clientModules } = getClientReferenceManifest()
const rscPayload = await getPayload(requestStore)
return await workUnitAsyncStorage.run(
@@ -729,14 +727,10 @@ async function stagedRenderToReadableStreamWithoutCachesInDev(
scheduleInSequentialTasks,
() => {
stageController.advanceStage(RenderStage.Static)
- return renderToReadableStream(
- rscPayload,
- clientReferenceManifest.clientModules,
- {
- ...options,
- environmentName,
- }
- )
+ return renderToReadableStream(rscPayload, clientModules, {
+ ...options,
+ environmentName,
+ })
},
() => {
stageController.advanceStage(RenderStage.Dynamic)
@@ -769,10 +763,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
onInstrumentationRequestError,
setReactDebugChannel,
setCacheStatus,
- clientReferenceManifest,
nextExport = false,
} = renderOpts
- assertClientReferenceManifest(clientReferenceManifest)
function onFlightDataRenderError(err: DigestedError, silenceLog: boolean) {
return onInstrumentationRequestError?.(
@@ -871,7 +863,6 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
staticStageEndTime,
runtimeStageEndTime,
ctx,
- clientReferenceManifest,
finalRequestStore,
devFallbackParams,
validationDebugChannelClient
@@ -895,7 +886,6 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
ctx,
initialRequestStore,
getPayload,
- clientReferenceManifest,
{
onError: onError,
filterStackFrame,
@@ -1006,10 +996,7 @@ async function prospectiveRuntimeServerPrerender(
draftMode: PrerenderStoreModernRuntime['draftMode']
) {
const { implicitTags, renderOpts, workStore } = ctx
-
- const { clientReferenceManifest, ComponentMod } = renderOpts
-
- assertClientReferenceManifest(clientReferenceManifest)
+ const { ComponentMod } = renderOpts
// Prerender controller represents the lifetime of the prerender.
// It will be aborted when a Task is complete or a synchronously aborting
@@ -1056,6 +1043,8 @@ async function prospectiveRuntimeServerPrerender(
draftMode,
}
+ const { clientModules } = getClientReferenceManifest()
+
// We're not going to use the result of this render because the only time it could be used
// is if it completes in a microtask and that's likely very rare for any non-trivial app
const initialServerPayload = await workUnitAsyncStorage.run(
@@ -1067,7 +1056,7 @@ async function prospectiveRuntimeServerPrerender(
initialServerPrerenderStore,
ComponentMod.prerender,
initialServerPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: (err) => {
@@ -1260,16 +1249,7 @@ async function finalRuntimeServerPrerender(
runtimePrefetchSentinel: number
) {
const { implicitTags, renderOpts } = ctx
-
- const {
- clientReferenceManifest,
- ComponentMod,
- experimental,
- isDebugDynamicAccesses,
- } = renderOpts
-
- assertClientReferenceManifest(clientReferenceManifest)
-
+ const { ComponentMod, experimental, isDebugDynamicAccesses } = renderOpts
const selectStaleTime = createSelectStaleTime(experimental)
let serverIsDynamic = false
@@ -1309,6 +1289,8 @@ async function finalRuntimeServerPrerender(
draftMode,
}
+ const { clientModules } = getClientReferenceManifest()
+
const finalRSCPayload = await workUnitAsyncStorage.run(
finalServerPrerenderStore,
getPayload
@@ -1322,7 +1304,7 @@ async function finalRuntimeServerPrerender(
finalServerPrerenderStore,
ComponentMod.prerender,
finalRSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError,
@@ -1682,23 +1664,12 @@ async function getErrorRSCPayload(
} satisfies InitialRSCPayload
}
-function assertClientReferenceManifest(
- clientReferenceManifest: RenderOpts['clientReferenceManifest']
-): asserts clientReferenceManifest is NonNullable<
- RenderOpts['clientReferenceManifest']
-> {
- if (!clientReferenceManifest) {
- throw new InvariantError('Expected clientReferenceManifest to be defined.')
- }
-}
-
// This component must run in an SSR context. It will render the RSC root component
function App({
reactServerStream,
reactDebugStream,
debugEndTime,
preinitScripts,
- clientReferenceManifest,
ServerInsertedHTMLProvider,
nonce,
images,
@@ -1708,7 +1679,6 @@ function App({
reactDebugStream: Readable | ReadableStream | undefined
debugEndTime: number | undefined
preinitScripts: () => void
- clientReferenceManifest: NonNullable
ServerInsertedHTMLProvider: ComponentType<{
children: JSX.Element
}>
@@ -1721,7 +1691,6 @@ function App({
reactServerStream,
reactDebugStream,
debugEndTime,
- clientReferenceManifest,
nonce
)
)
@@ -1767,14 +1736,12 @@ function App({
function ErrorApp({
reactServerStream,
preinitScripts,
- clientReferenceManifest,
ServerInsertedHTMLProvider,
nonce,
images,
}: {
reactServerStream: BinaryStreamOf
preinitScripts: () => void
- clientReferenceManifest: NonNullable
ServerInsertedHTMLProvider: ComponentType<{
children: JSX.Element
}>
@@ -1788,7 +1755,6 @@ function ErrorApp({
reactServerStream,
undefined,
undefined,
- clientReferenceManifest,
nonce
)
)
@@ -1852,7 +1818,6 @@ async function renderToHTMLOrFlightImpl(
const requestTimestamp = Date.now()
const {
- clientReferenceManifest,
ComponentMod,
nextFontManifest,
serverActions,
@@ -1977,8 +1942,6 @@ async function renderToHTMLOrFlightImpl(
const appUsingSizeAdjustment = !!nextFontManifest?.appUsingSizeAdjust
- assertClientReferenceManifest(clientReferenceManifest)
-
ComponentMod.patchFetch()
// Pull out the hooks/references from the component.
@@ -2070,7 +2033,6 @@ async function renderToHTMLOrFlightImpl(
requestId,
htmlRequestId,
pagePath,
- clientReferenceManifest,
assetPrefix,
isNotFoundPath,
nonce,
@@ -2548,7 +2510,6 @@ async function renderToStream(
const {
basePath,
buildManifest,
- clientReferenceManifest,
ComponentMod: {
createElement,
renderToReadableStream: serverRenderToReadableStream,
@@ -2567,8 +2528,6 @@ async function renderToStream(
cacheComponents,
} = renderOpts
- assertClientReferenceManifest(clientReferenceManifest)
-
const { ServerInsertedHTMLProvider, renderServerInsertedHTML } =
createServerInsertedHTML()
const getServerInsertedMetadata = createServerInsertedMetadata(nonce)
@@ -2656,6 +2615,7 @@ async function renderToStream(
const setHeader = res.setHeader.bind(res)
const appendHeader = res.appendHeader.bind(res)
+ const { clientModules } = getClientReferenceManifest()
try {
if (
@@ -2739,7 +2699,6 @@ async function renderToStream(
staticStageEndTime,
runtimeStageEndTime,
ctx,
- clientReferenceManifest,
finalRequestStore,
devFallbackParams,
validationDebugChannelClient
@@ -2759,7 +2718,6 @@ async function renderToStream(
ctx,
requestStore,
getPayload,
- clientReferenceManifest,
{
onError: serverComponentsErrorHandler,
filterStackFrame,
@@ -2812,7 +2770,7 @@ async function renderToStream(
requestStore,
serverRenderToReadableStream,
RSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: serverComponentsErrorHandler,
@@ -2860,7 +2818,6 @@ async function renderToStream(
reactDebugStream={reactDebugStream}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -2907,7 +2864,6 @@ async function renderToStream(
reactDebugStream={reactDebugStream}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -3043,7 +2999,7 @@ async function renderToStream(
requestStore,
serverRenderToReadableStream,
errorRSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: serverComponentsErrorHandler,
@@ -3068,7 +3024,6 @@ async function renderToStream(
reactServerStream={errorServerStream}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
preinitScripts={errorPreinitScripts}
- clientReferenceManifest={clientReferenceManifest}
nonce={nonce}
images={ctx.renderOpts.images}
/>
@@ -3154,13 +3109,8 @@ async function renderWithRestartOnCacheMissInDev(
},
},
} = ctx
- const {
- clientReferenceManifest,
- ComponentMod,
- setCacheStatus,
- setReactDebugChannel,
- } = renderOpts
- assertClientReferenceManifest(clientReferenceManifest)
+
+ const { ComponentMod, setCacheStatus, setReactDebugChannel } = renderOpts
const hasRuntimePrefetch =
await anySegmentHasRuntimePrefetchEnabled(loaderTree)
@@ -3224,6 +3174,7 @@ async function renderWithRestartOnCacheMissInDev(
requestStore.cacheSignal = cacheSignal
let debugChannel = setReactDebugChannel && createDebugChannel()
+ const { clientModules } = getClientReferenceManifest()
// Note: The stage controller starts out in the `Before` stage,
// where sync IO does not cause aborts, so it's okay if it happens before render.
@@ -3239,7 +3190,7 @@ async function renderWithRestartOnCacheMissInDev(
const stream = ComponentMod.renderToReadableStream(
initialRscPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
onError,
environmentName,
@@ -3392,7 +3343,7 @@ async function renderWithRestartOnCacheMissInDev(
const stream = ComponentMod.renderToReadableStream(
finalRscPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
onError,
environmentName,
@@ -3602,13 +3553,7 @@ async function logMessagesAndSendErrorsToBrowser(
messages: unknown[],
ctx: AppRenderContext
): Promise {
- const {
- clientReferenceManifest,
- componentMod: ComponentMod,
- htmlRequestId,
- renderOpts,
- } = ctx
-
+ const { componentMod: ComponentMod, htmlRequestId, renderOpts } = ctx
const { sendErrorsToBrowser } = renderOpts
const errors: Error[] = []
@@ -3637,9 +3582,11 @@ async function logMessagesAndSendErrorsToBrowser(
)
}
+ const { clientModules } = getClientReferenceManifest()
+
const errorsRscStream = ComponentMod.renderToReadableStream(
errors,
- clientReferenceManifest.clientModules,
+ clientModules,
{ filterStackFrame }
)
@@ -3660,7 +3607,6 @@ async function spawnStaticShellValidationInDev(
staticStageEndTime: number,
runtimeStageEndTime: number,
ctx: AppRenderContext,
- clientReferenceManifest: NonNullable,
requestStore: RequestStore,
fallbackRouteParams: OpaqueFallbackRouteParams | null,
debugChannelClient: Readable | undefined
@@ -3708,8 +3654,7 @@ async function spawnStaticShellValidationInDev(
rootParams,
fallbackRouteParams,
allowEmptyStaticShell,
- ctx,
- clientReferenceManifest
+ ctx
)
let debugChunks: Uint8Array[] | null = null
@@ -3727,7 +3672,6 @@ async function spawnStaticShellValidationInDev(
fallbackRouteParams,
allowEmptyStaticShell,
ctx,
- clientReferenceManifest,
hmrRefreshHash,
trackDynamicHoleInRuntimeShell
)
@@ -3747,7 +3691,6 @@ async function spawnStaticShellValidationInDev(
fallbackRouteParams,
allowEmptyStaticShell,
ctx,
- clientReferenceManifest,
hmrRefreshHash,
trackDynamicHoleInStaticShell
)
@@ -3761,8 +3704,7 @@ async function warmupModuleCacheForRuntimeValidationInDev(
rootParams: Params,
fallbackRouteParams: OpaqueFallbackRouteParams | null,
allowEmptyStaticShell: boolean,
- ctx: AppRenderContext,
- clientReferenceManifest: NonNullable
+ ctx: AppRenderContext
) {
const { implicitTags, nonce, workStore } = ctx
@@ -3815,7 +3757,6 @@ async function warmupModuleCacheForRuntimeValidationInDev(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -3903,7 +3844,6 @@ async function validateStagedShell(
fallbackRouteParams: OpaqueFallbackRouteParams | null,
allowEmptyStaticShell: boolean,
ctx: AppRenderContext,
- clientReferenceManifest: NonNullable,
hmrRefreshHash: string | undefined,
trackDynamicHole:
| typeof trackDynamicHoleInStaticShell
@@ -3974,7 +3914,6 @@ async function validateStagedShell(
reactDebugStream={debugChannelClient}
debugEndTime={debugEndTime}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -4104,7 +4043,6 @@ async function prerenderToStream(
allowEmptyStaticShell = false,
basePath,
buildManifest,
- clientReferenceManifest,
ComponentMod,
crossOrigin,
dev = false,
@@ -4118,8 +4056,6 @@ async function prerenderToStream(
cacheComponents,
} = renderOpts
- assertClientReferenceManifest(clientReferenceManifest)
-
const rootParams = getRootParams(tree, getDynamicParamFromSegment)
const { ServerInsertedHTMLProvider, renderServerInsertedHTML } =
@@ -4224,6 +4160,7 @@ async function prerenderToStream(
}
const selectStaleTime = createSelectStaleTime(experimental)
+ const { clientModules } = getClientReferenceManifest()
let prerenderStore: PrerenderStore | null = null
@@ -4352,7 +4289,7 @@ async function prerenderToStream(
initialServerPrerenderStore,
ComponentMod.prerender,
initialServerPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: (err) => {
@@ -4479,7 +4416,6 @@ async function prerenderToStream(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -4631,7 +4567,7 @@ async function prerenderToStream(
ComponentMod.prerender,
// ... the arguments for the function to run
finalAttemptRSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: (err: unknown) => {
@@ -4721,7 +4657,6 @@ async function prerenderToStream(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -4881,7 +4816,6 @@ async function prerenderToStream(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={() => {}}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -4920,14 +4854,10 @@ async function prerenderToStream(
// segments, since those are the only ones whose data is not complete.
const emptyReactServerResult =
await createReactServerPrerenderResultFromRender(
- ComponentMod.renderToReadableStream(
- [],
- clientReferenceManifest.clientModules,
- {
- filterStackFrame,
- onError: serverComponentsErrorHandler,
- }
- )
+ ComponentMod.renderToReadableStream([], clientModules, {
+ filterStackFrame,
+ onError: serverComponentsErrorHandler,
+ })
)
finalStream = await continueStaticFallbackPrerender(htmlStream, {
inlinedDataStream: createInlinedDataReadableStream(
@@ -5005,7 +4935,7 @@ async function prerenderToStream(
ComponentMod.renderToReadableStream,
// ... the arguments for the function to run
RSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: serverComponentsErrorHandler,
@@ -5039,7 +4969,6 @@ async function prerenderToStream(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -5183,7 +5112,6 @@ async function prerenderToStream(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={() => {}}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -5250,7 +5178,7 @@ async function prerenderToStream(
prerenderLegacyStore,
ComponentMod.renderToReadableStream,
RSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: serverComponentsErrorHandler,
@@ -5270,7 +5198,6 @@ async function prerenderToStream(
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={preinitScripts}
- clientReferenceManifest={clientReferenceManifest}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
@@ -5423,7 +5350,7 @@ async function prerenderToStream(
prerenderLegacyStore,
ComponentMod.renderToReadableStream,
errorRSCPayload,
- clientReferenceManifest.clientModules,
+ clientModules,
{
filterStackFrame,
onError: serverComponentsErrorHandler,
@@ -5446,7 +5373,6 @@ async function prerenderToStream(
reactServerStream={errorServerStream}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
preinitScripts={errorPreinitScripts}
- clientReferenceManifest={clientReferenceManifest}
nonce={nonce}
images={ctx.renderOpts.images}
/>
@@ -5613,10 +5539,8 @@ async function collectSegmentData(
// generating the initial page HTML. The Flight stream for the whole page is
// decomposed into a separate stream per segment.
- const clientReferenceManifest = renderOpts.clientReferenceManifest
- if (!clientReferenceManifest) {
- return
- }
+ const { clientModules, edgeRscModuleMapping, rscModuleMapping } =
+ getClientReferenceManifest()
// Manifest passed to the Flight client for reading the full-page Flight
// stream. Based off similar code in use-cache-wrapper.ts.
@@ -5626,9 +5550,7 @@ async function collectSegmentData(
// to be added to the consumer. Instead, we'll wait for any ClientReference to be emitted
// which themselves will handle the preloading.
moduleLoading: null,
- moduleMap: isEdgeRuntime
- ? clientReferenceManifest.edgeRscModuleMapping
- : clientReferenceManifest.rscModuleMapping,
+ moduleMap: isEdgeRuntime ? edgeRscModuleMapping : rscModuleMapping,
serverModuleMap: getServerModuleMap(),
}
@@ -5638,7 +5560,7 @@ async function collectSegmentData(
renderOpts.cacheComponents,
fullPageDataBuffer,
staleTime,
- clientReferenceManifest.clientModules as ManifestNode,
+ clientModules,
serverConsumerManifest
)
}
diff --git a/packages/next/src/server/app-render/create-component-styles-and-scripts.tsx b/packages/next/src/server/app-render/create-component-styles-and-scripts.tsx
index 61991b2f32832..2b7e47eac3b98 100644
--- a/packages/next/src/server/app-render/create-component-styles-and-scripts.tsx
+++ b/packages/next/src/server/app-render/create-component-styles-and-scripts.tsx
@@ -22,7 +22,6 @@ export async function createComponentStylesAndScripts({
componentMod: { createElement },
} = ctx
const { styles: entryCssFiles, scripts: jsHrefs } = getLinkAndScriptTags(
- ctx.clientReferenceManifest,
filePath,
injectedCSS,
injectedJS
diff --git a/packages/next/src/server/app-render/encryption-utils.ts b/packages/next/src/server/app-render/encryption-utils.ts
index 9dd44c8c987d7..8b8329ee2f2b3 100644
--- a/packages/next/src/server/app-render/encryption-utils.ts
+++ b/packages/next/src/server/app-render/encryption-utils.ts
@@ -1,12 +1,5 @@
-import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin'
-import type {
- ClientReferenceManifest,
- ClientReferenceManifestForRsc,
-} from '../../build/webpack/plugins/flight-manifest-plugin'
-import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import { InvariantError } from '../../shared/lib/invariant-error'
-import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
-import { workAsyncStorage } from './work-async-storage.external'
+import { getServerActionsManifest } from './manifests-singleton'
let __next_loaded_action_key: CryptoKey
@@ -71,127 +64,16 @@ export function decrypt(
)
}
-// This is a global singleton that is used to encode/decode the action bound args from
-// the closure. This can't be using a AsyncLocalStorage as it might happen on the module
-// level. Since the client reference manifest won't be mutated, let's use a global singleton
-// to keep it.
-const SERVER_ACTION_MANIFESTS_SINGLETON = Symbol.for(
- 'next.server.action-manifests'
-)
-
-export function setReferenceManifestsSingleton({
- page,
- clientReferenceManifest,
- serverActionsManifest,
- serverModuleMap,
-}: {
- page: string
- clientReferenceManifest: DeepReadonly
- serverActionsManifest: DeepReadonly
- serverModuleMap: {
- [id: string]: {
- id: string
- chunks: string[]
- name: string
- }
- }
-}) {
- // @ts-expect-error
- const clientReferenceManifestsPerPage = globalThis[
- SERVER_ACTION_MANIFESTS_SINGLETON
- ]?.clientReferenceManifestsPerPage as
- | undefined
- | DeepReadonly>
-
- // @ts-expect-error
- globalThis[SERVER_ACTION_MANIFESTS_SINGLETON] = {
- clientReferenceManifestsPerPage: {
- ...clientReferenceManifestsPerPage,
- [normalizeAppPath(page)]: clientReferenceManifest,
- },
- serverActionsManifest,
- serverModuleMap,
- }
-}
-
-export function getServerModuleMap() {
- const serverActionsManifestSingleton = (globalThis as any)[
- SERVER_ACTION_MANIFESTS_SINGLETON
- ] as {
- serverModuleMap: {
- [id: string]: {
- id: string
- chunks: string[]
- name: string
- }
- }
- }
-
- if (!serverActionsManifestSingleton) {
- throw new InvariantError('Missing manifest for Server Actions.')
- }
-
- return serverActionsManifestSingleton.serverModuleMap
-}
-
-export function getClientReferenceManifestForRsc(): DeepReadonly {
- const serverActionsManifestSingleton = (globalThis as any)[
- SERVER_ACTION_MANIFESTS_SINGLETON
- ] as {
- clientReferenceManifestsPerPage: DeepReadonly<
- Record
- >
- }
-
- if (!serverActionsManifestSingleton) {
- throw new InvariantError('Missing manifest for Server Actions.')
- }
-
- const { clientReferenceManifestsPerPage } = serverActionsManifestSingleton
- const workStore = workAsyncStorage.getStore()
-
- if (!workStore) {
- // If there's no work store defined, we can assume that a client reference
- // manifest is needed during module evaluation, e.g. to create a server
- // action using a higher-order function. This might also use client
- // components which need to be serialized by Flight, and therefore client
- // references need to be resolvable. To make this work, we're returning a
- // merged manifest across all pages. This is fine as long as the module IDs
- // are not page specific, which they are not for Webpack. TODO: Fix this in
- // Turbopack.
- return mergeClientReferenceManifests(clientReferenceManifestsPerPage)
- }
-
- const clientReferenceManifest =
- clientReferenceManifestsPerPage[workStore.route]
-
- if (!clientReferenceManifest) {
- throw new InvariantError(
- `Missing Client Reference Manifest for ${workStore.route}.`
- )
- }
-
- return clientReferenceManifest
-}
-
export async function getActionEncryptionKey() {
if (__next_loaded_action_key) {
return __next_loaded_action_key
}
- const serverActionsManifestSingleton = (globalThis as any)[
- SERVER_ACTION_MANIFESTS_SINGLETON
- ] as {
- serverActionsManifest: DeepReadonly
- }
-
- if (!serverActionsManifestSingleton) {
- throw new InvariantError('Missing manifest for Server Actions.')
- }
+ const serverActionsManifest = getServerActionsManifest()
const rawKey =
process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY ||
- serverActionsManifestSingleton.serverActionsManifest.encryptionKey
+ serverActionsManifest.encryptionKey
if (rawKey === undefined) {
throw new InvariantError('Missing encryption key for Server Actions')
@@ -207,36 +89,3 @@ export async function getActionEncryptionKey() {
return __next_loaded_action_key
}
-
-function mergeClientReferenceManifests(
- clientReferenceManifestsPerPage: DeepReadonly<
- Record
- >
-): ClientReferenceManifestForRsc {
- const clientReferenceManifests = Object.values(
- clientReferenceManifestsPerPage as Record
- )
-
- const mergedClientReferenceManifest: ClientReferenceManifestForRsc = {
- clientModules: {},
- edgeRscModuleMapping: {},
- rscModuleMapping: {},
- }
-
- for (const clientReferenceManifest of clientReferenceManifests) {
- mergedClientReferenceManifest.clientModules = {
- ...mergedClientReferenceManifest.clientModules,
- ...clientReferenceManifest.clientModules,
- }
- mergedClientReferenceManifest.edgeRscModuleMapping = {
- ...mergedClientReferenceManifest.edgeRscModuleMapping,
- ...clientReferenceManifest.edgeRscModuleMapping,
- }
- mergedClientReferenceManifest.rscModuleMapping = {
- ...mergedClientReferenceManifest.rscModuleMapping,
- ...clientReferenceManifest.rscModuleMapping,
- }
- }
-
- return mergedClientReferenceManifest
-}
diff --git a/packages/next/src/server/app-render/encryption.ts b/packages/next/src/server/app-render/encryption.ts
index 142277ad8346e..86852170c8996 100644
--- a/packages/next/src/server/app-render/encryption.ts
+++ b/packages/next/src/server/app-render/encryption.ts
@@ -12,10 +12,12 @@ import {
decrypt,
encrypt,
getActionEncryptionKey,
- getClientReferenceManifestForRsc,
- getServerModuleMap,
stringToUint8Array,
} from './encryption-utils'
+import {
+ getClientReferenceManifest,
+ getServerModuleMap,
+} from './manifests-singleton'
import {
getCacheSignal,
getPrerenderResumeDataCache,
@@ -111,7 +113,7 @@ export const encryptActionBoundArgs = React.cache(
? getCacheSignal(workUnitStore)
: undefined
- const { clientModules } = getClientReferenceManifestForRsc()
+ const { clientModules } = getClientReferenceManifest()
// Create an error before any asynchronous calls, to capture the original
// call stack in case we need it when the serialization errors.
@@ -250,7 +252,7 @@ export async function decryptActionBoundArgs(
}
const { edgeRscModuleMapping, rscModuleMapping } =
- getClientReferenceManifestForRsc()
+ getClientReferenceManifest()
// Using Flight to deserialize the args from the string.
const deserialized = await createFromReadableStream(
diff --git a/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx b/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx
index cfb4ad9760eee..91aada324f9c1 100644
--- a/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx
+++ b/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx
@@ -1,14 +1,10 @@
-import type {
- ClientReferenceManifest,
- CssResource,
-} from '../../build/webpack/plugins/flight-manifest-plugin'
-import type { DeepReadonly } from '../../shared/lib/deep-readonly'
+import type { CssResource } from '../../build/webpack/plugins/flight-manifest-plugin'
+import { getClientReferenceManifest } from './manifests-singleton'
/**
* Get external stylesheet link hrefs based on server CSS manifest.
*/
export function getLinkAndScriptTags(
- clientReferenceManifest: DeepReadonly,
filePath: string,
injectedCSS: Set,
injectedScripts: Set,
@@ -17,14 +13,12 @@ export function getLinkAndScriptTags(
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, '')
const cssChunks = new Set()
const jsChunks = new Set()
+ const { entryCSSFiles, entryJSFiles } = getClientReferenceManifest()
+ const cssFiles = entryCSSFiles[filePathWithoutExt]
+ const jsFiles = entryJSFiles?.[filePathWithoutExt]
- const entryCSSFiles =
- clientReferenceManifest.entryCSSFiles[filePathWithoutExt]
- const entryJSFiles =
- clientReferenceManifest.entryJSFiles?.[filePathWithoutExt] ?? []
-
- if (entryCSSFiles) {
- for (const css of entryCSSFiles) {
+ if (cssFiles) {
+ for (const css of cssFiles) {
if (!injectedCSS.has(css.path)) {
if (collectNewImports) {
injectedCSS.add(css.path)
@@ -34,8 +28,8 @@ export function getLinkAndScriptTags(
}
}
- if (entryJSFiles) {
- for (const file of entryJSFiles) {
+ if (jsFiles) {
+ for (const file of jsFiles) {
if (!injectedScripts.has(file)) {
if (collectNewImports) {
injectedScripts.add(file)
diff --git a/packages/next/src/server/app-render/get-layer-assets.tsx b/packages/next/src/server/app-render/get-layer-assets.tsx
index 2e0eb9c400ae8..8f133810a7715 100644
--- a/packages/next/src/server/app-render/get-layer-assets.tsx
+++ b/packages/next/src/server/app-render/get-layer-assets.tsx
@@ -26,7 +26,6 @@ export function getLayerAssets({
} = ctx
const { styles: styleTags, scripts: scriptTags } = layoutOrPagePath
? getLinkAndScriptTags(
- ctx.clientReferenceManifest,
layoutOrPagePath,
injectedCSSWithCurrentLayout,
injectedJSWithCurrentLayout,
diff --git a/packages/next/src/server/app-render/manifests-singleton.ts b/packages/next/src/server/app-render/manifests-singleton.ts
new file mode 100644
index 0000000000000..880a76966270d
--- /dev/null
+++ b/packages/next/src/server/app-render/manifests-singleton.ts
@@ -0,0 +1,231 @@
+import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin'
+import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
+import type { DeepReadonly } from '../../shared/lib/deep-readonly'
+import { InvariantError } from '../../shared/lib/invariant-error'
+import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
+import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
+import { createServerModuleMap, type ServerModuleMap } from './action-utils'
+import { workAsyncStorage } from './work-async-storage.external'
+
+// This is a global singleton that is, among other things, also used to
+// encode/decode bound args of server function closures. This can't be using a
+// AsyncLocalStorage as it might happen at the module level.
+const MANIFESTS_SINGLETON = Symbol.for('next.server.manifests')
+
+interface ManifestsSingleton {
+ readonly clientReferenceManifestsPerRoute: Map<
+ string,
+ DeepReadonly
+ >
+ readonly proxiedClientReferenceManifest: DeepReadonly
+ serverActionsManifest: DeepReadonly
+ serverModuleMap: ServerModuleMap
+}
+
+type GlobalThisWithManifests = typeof globalThis & {
+ [MANIFESTS_SINGLETON]?: ManifestsSingleton
+}
+
+type ClientReferenceManifestMappingProp =
+ | 'clientModules'
+ | 'rscModuleMapping'
+ | 'edgeRscModuleMapping'
+ | 'ssrModuleMapping'
+ | 'edgeSSRModuleMapping'
+
+const globalThisWithManifests = globalThis as GlobalThisWithManifests
+
+function createProxiedClientReferenceManifest(
+ baseManifest: DeepReadonly,
+ clientReferenceManifestsPerRoute: Map<
+ string,
+ DeepReadonly
+ >
+): DeepReadonly {
+ const createMappingProxy = (prop: ClientReferenceManifestMappingProp) => {
+ return new Proxy(
+ {},
+ {
+ get(_, id: string) {
+ const workStore = workAsyncStorage.getStore()
+
+ if (workStore) {
+ const currentManifest = clientReferenceManifestsPerRoute.get(
+ workStore.route
+ )
+
+ if (currentManifest?.[prop][id]) {
+ return currentManifest[prop][id]
+ }
+
+ // In development, we also check all other manifests to see if the
+ // module exists there. This is to support a scenario where React's
+ // I/O tracking (dev-only) creates a connection from one page to
+ // another through an emitted async I/O node that references client
+ // components from the other page, e.g. in owner props.
+ // TODO: Maybe we need to add a `debugBundlerConfig` option to React
+ // to avoid this workaround. The current workaround has the
+ // disadvantage that one might accidentally or intentionally share
+ // client references across pages (e.g. by storing them in a global
+ // variable), which would then only be caught in production.
+ if (process.env.NODE_ENV !== 'production') {
+ for (const [
+ route,
+ manifest,
+ ] of clientReferenceManifestsPerRoute) {
+ if (route === workStore.route) {
+ continue
+ }
+
+ const entry = manifest[prop][id]
+
+ if (entry !== undefined) {
+ return entry
+ }
+ }
+ }
+ } else {
+ // If there's no work store defined, we can assume that a client
+ // reference manifest is needed during module evaluation, e.g. to
+ // create a server function using a higher-order function. This
+ // might also use client components which need to be serialized by
+ // Flight, and therefore client references need to be resolvable. In
+ // that case we search all page manifests to find the module.
+ for (const manifest of clientReferenceManifestsPerRoute.values()) {
+ const entry = manifest[prop][id]
+
+ if (entry !== undefined) {
+ return entry
+ }
+ }
+ }
+
+ return undefined
+ },
+ }
+ )
+ }
+
+ const mappingProxies = new Map<
+ ClientReferenceManifestMappingProp,
+ ReturnType
+ >()
+
+ return new Proxy(
+ {},
+ {
+ get(_, prop, receiver) {
+ const workStore = workAsyncStorage.getStore()
+
+ switch (prop) {
+ case 'moduleLoading':
+ case 'entryCSSFiles':
+ case 'entryJSFiles': {
+ if (!workStore) {
+ throw new InvariantError(
+ `Cannot access "${prop}" without a work store.`
+ )
+ }
+
+ const currentManifest = clientReferenceManifestsPerRoute.get(
+ workStore.route
+ )
+
+ if (!currentManifest) {
+ throw new InvariantError(
+ `The client reference manifest for route "${workStore.route}" does not exist.`
+ )
+ }
+
+ return currentManifest[prop]
+ }
+ case 'clientModules':
+ case 'rscModuleMapping':
+ case 'edgeRscModuleMapping':
+ case 'ssrModuleMapping':
+ case 'edgeSSRModuleMapping': {
+ let proxy = mappingProxies.get(prop)
+
+ if (!proxy) {
+ proxy = createMappingProxy(prop)
+ mappingProxies.set(prop, proxy)
+ }
+
+ return proxy
+ }
+ default: {
+ return ReflectAdapter.get(baseManifest, prop, receiver)
+ }
+ }
+ },
+ }
+ ) as DeepReadonly
+}
+
+export function setManifestsSingleton({
+ page,
+ clientReferenceManifest,
+ serverActionsManifest,
+}: {
+ page: string
+ clientReferenceManifest: DeepReadonly
+ serverActionsManifest: DeepReadonly
+}) {
+ const existingSingleton = globalThisWithManifests[MANIFESTS_SINGLETON]
+
+ if (existingSingleton) {
+ existingSingleton.clientReferenceManifestsPerRoute.set(
+ normalizeAppPath(page),
+ clientReferenceManifest
+ )
+
+ existingSingleton.serverActionsManifest = serverActionsManifest
+
+ existingSingleton.serverModuleMap = createServerModuleMap({
+ serverActionsManifest,
+ })
+ } else {
+ const clientReferenceManifestsPerRoute = new Map<
+ string,
+ DeepReadonly
+ >([[normalizeAppPath(page), clientReferenceManifest]])
+
+ const proxiedClientReferenceManifest = createProxiedClientReferenceManifest(
+ clientReferenceManifest,
+ clientReferenceManifestsPerRoute
+ )
+
+ const serverModuleMap = createServerModuleMap({
+ serverActionsManifest,
+ })
+
+ globalThisWithManifests[MANIFESTS_SINGLETON] = {
+ clientReferenceManifestsPerRoute,
+ proxiedClientReferenceManifest,
+ serverActionsManifest,
+ serverModuleMap,
+ }
+ }
+}
+
+function getManifestsSingleton(): ManifestsSingleton {
+ const manifestSingleton = globalThisWithManifests[MANIFESTS_SINGLETON]
+
+ if (!manifestSingleton) {
+ throw new InvariantError('The manifests singleton was not initialized.')
+ }
+
+ return manifestSingleton
+}
+
+export function getClientReferenceManifest(): DeepReadonly {
+ return getManifestsSingleton().proxiedClientReferenceManifest
+}
+
+export function getServerActionsManifest(): DeepReadonly {
+ return getManifestsSingleton().serverActionsManifest
+}
+
+export function getServerModuleMap() {
+ return getManifestsSingleton().serverModuleMap
+}
diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts
index 1359d460c0cf2..d98b94b864f75 100644
--- a/packages/next/src/server/app-render/types.ts
+++ b/packages/next/src/server/app-render/types.ts
@@ -4,7 +4,6 @@ import type {
ExperimentalConfig,
NextConfigComplete,
} from '../../server/config-shared'
-import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'
import type { ParsedUrlQuery } from 'querystring'
import type { AppPageModule } from '../route-modules/app-page/module'
@@ -94,7 +93,6 @@ export interface RenderOptsPartial {
cacheComponents: boolean
trailingSlash: boolean
images: ImageConfigComplete
- clientReferenceManifest?: DeepReadonly
supportsDynamicResponse: boolean
runtime?: ServerRuntime
serverComponents?: boolean
diff --git a/packages/next/src/server/app-render/use-flight-response.tsx b/packages/next/src/server/app-render/use-flight-response.tsx
index 1eafa866768e9..4de9babcde0fd 100644
--- a/packages/next/src/server/app-render/use-flight-response.tsx
+++ b/packages/next/src/server/app-render/use-flight-response.tsx
@@ -1,11 +1,10 @@
-import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type { BinaryStreamOf } from './app-render'
import type { Readable } from 'node:stream'
import { htmlEscapeJsonString } from '../htmlescape'
-import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import { workUnitAsyncStorage } from './work-unit-async-storage.external'
import { InvariantError } from '../../shared/lib/invariant-error'
+import { getClientReferenceManifest } from './manifests-singleton'
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
@@ -34,7 +33,6 @@ export function getFlightStream(
flightStream: Readable | BinaryStreamOf,
debugStream: Readable | ReadableStream | undefined,
debugEndTime: number | undefined,
- clientReferenceManifest: DeepReadonly,
nonce: string | undefined
): Promise {
const response = flightResponses.get(flightStream)
@@ -43,6 +41,9 @@ export function getFlightStream(
return response
}
+ const { moduleLoading, edgeSSRModuleMapping, ssrModuleMapping } =
+ getClientReferenceManifest()
+
let newResponse: Promise
if (flightStream instanceof ReadableStream) {
// The types of flightStream and debugStream should match.
@@ -58,10 +59,8 @@ export function getFlightStream(
newResponse = createFromReadableStream(flightStream, {
findSourceMapURL,
serverConsumerManifest: {
- moduleLoading: clientReferenceManifest.moduleLoading,
- moduleMap: isEdgeRuntime
- ? clientReferenceManifest.edgeSSRModuleMapping
- : clientReferenceManifest.ssrModuleMapping,
+ moduleLoading,
+ moduleMap: isEdgeRuntime ? edgeSSRModuleMapping : ssrModuleMapping,
serverModuleMap: null,
},
nonce,
@@ -90,10 +89,8 @@ export function getFlightStream(
newResponse = createFromNodeStream(
flightStream,
{
- moduleLoading: clientReferenceManifest.moduleLoading,
- moduleMap: isEdgeRuntime
- ? clientReferenceManifest.edgeSSRModuleMapping
- : clientReferenceManifest.ssrModuleMapping,
+ moduleLoading,
+ moduleMap: isEdgeRuntime ? edgeSSRModuleMapping : ssrModuleMapping,
serverModuleMap: null,
},
{
diff --git a/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx b/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx
index b4edfb6b2d6ae..0c9e6b791961a 100644
--- a/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx
+++ b/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx
@@ -252,7 +252,6 @@ export async function walkTreeWithFlightRouterState({
)
if (layoutPath) {
getLinkAndScriptTags(
- ctx.clientReferenceManifest,
layoutPath,
injectedCSSWithCurrentLayout,
injectedJSWithCurrentLayout,
diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts
index 462b65265994e..ebf6f0adbbacb 100644
--- a/packages/next/src/server/load-components.ts
+++ b/packages/next/src/server/load-components.ts
@@ -29,8 +29,7 @@ import { getTracer } from './lib/trace/tracer'
import { LoadComponentsSpan } from './lib/trace/constants'
import { evalManifest, loadManifest } from './load-manifest.external'
import { wait } from '../lib/wait'
-import { setReferenceManifestsSingleton } from './app-render/encryption-utils'
-import { createServerModuleMap } from './app-render/action-utils'
+import { setManifestsSingleton } from './app-render/manifests-singleton'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { isStaticMetadataRoute } from '../lib/metadata/is-metadata-route'
@@ -66,8 +65,6 @@ export type LoadComponentsReturnType = {
subresourceIntegrityManifest?: DeepReadonly>
reactLoadableManifest: DeepReadonly
dynamicCssManifest?: DeepReadonly
- clientReferenceManifest?: DeepReadonly
- serverActionsManifest?: any
Document: DocumentType
App: AppType
getStaticProps?: GetStaticProps
@@ -279,13 +276,10 @@ async function loadComponentsImpl({
// manifests to our global store so Server Action's encryption util can access
// to them at the top level of the page module.
if (serverActionsManifest && clientReferenceManifest) {
- setReferenceManifestsSingleton({
+ setManifestsSingleton({
page,
clientReferenceManifest,
serverActionsManifest,
- serverModuleMap: createServerModuleMap({
- serverActionsManifest,
- }),
})
}
@@ -311,8 +305,6 @@ async function loadComponentsImpl({
getServerSideProps,
getStaticProps,
getStaticPaths,
- clientReferenceManifest,
- serverActionsManifest,
isAppPath,
page,
routeModule,
diff --git a/packages/next/src/server/load-default-error-components.ts b/packages/next/src/server/load-default-error-components.ts
index d10d2c74a1e18..511d75500966f 100644
--- a/packages/next/src/server/load-default-error-components.ts
+++ b/packages/next/src/server/load-default-error-components.ts
@@ -3,7 +3,6 @@ import type {
DocumentType,
NextComponentType,
} from '../shared/lib/utils'
-import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin'
import type {
PageConfig,
GetStaticPaths,
@@ -32,8 +31,6 @@ export type LoadComponentsReturnType = {
buildManifest: BuildManifest
subresourceIntegrityManifest?: Record
reactLoadableManifest: ReactLoadableManifest
- clientReferenceManifest?: ClientReferenceManifest
- serverActionsManifest?: any
Document: DocumentType
App: AppType
getStaticProps?: GetStaticProps
diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts
index 0142b8d3097eb..37d0e7bf77c6a 100644
--- a/packages/next/src/server/use-cache/use-cache-wrapper.ts
+++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts
@@ -42,12 +42,12 @@ import {
makeHangingPromise,
} from '../dynamic-rendering-utils'
-import type { ClientReferenceManifestForRsc } from '../../build/webpack/plugins/flight-manifest-plugin'
+import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import {
- getClientReferenceManifestForRsc,
+ getClientReferenceManifest,
getServerModuleMap,
-} from '../app-render/encryption-utils'
+} from '../app-render/manifests-singleton'
import type { CacheEntry } from '../lib/cache-handlers/types'
import type { CacheSignal } from '../app-render/cache-signal'
import { decryptActionBoundArgs } from '../app-render/encryption'
@@ -134,7 +134,7 @@ const findSourceMapURL =
function generateCacheEntry(
workStore: WorkStore,
cacheContext: CacheContext,
- clientReferenceManifest: DeepReadonly,
+ clientReferenceManifest: DeepReadonly,
encodedArguments: FormData | string,
fn: (...args: unknown[]) => Promise,
timeoutError: UseCacheTimeoutError
@@ -158,7 +158,7 @@ function generateCacheEntry(
function generateCacheEntryWithRestoredWorkStore(
workStore: WorkStore,
cacheContext: CacheContext,
- clientReferenceManifest: DeepReadonly,
+ clientReferenceManifest: DeepReadonly,
encodedArguments: FormData | string,
fn: (...args: unknown[]) => Promise,
timeoutError: UseCacheTimeoutError
@@ -281,7 +281,7 @@ function assertDefaultCacheLife(
function generateCacheEntryWithCacheContext(
workStore: WorkStore,
cacheContext: CacheContext,
- clientReferenceManifest: DeepReadonly,
+ clientReferenceManifest: DeepReadonly,
encodedArguments: FormData | string,
fn: (...args: unknown[]) => Promise,
timeoutError: UseCacheTimeoutError
@@ -521,7 +521,7 @@ async function generateCacheEntryImpl(
workStore: WorkStore,
cacheContext: CacheContext,
innerCacheStore: UseCacheStore,
- clientReferenceManifest: DeepReadonly,
+ clientReferenceManifest: DeepReadonly,
encodedArguments: FormData | string,
fn: (...args: unknown[]) => Promise,
timeoutError: UseCacheTimeoutError
@@ -964,7 +964,7 @@ export async function cache(
// Get the clientReferenceManifest while we're still in the outer Context.
// In case getClientReferenceManifestSingleton is implemented using AsyncLocalStorage.
- const clientReferenceManifest = getClientReferenceManifestForRsc()
+ const clientReferenceManifest = getClientReferenceManifest()
// Because the Action ID is not yet unique per implementation of that Action we can't
// safely reuse the results across builds yet. In the meantime we add the buildId to the
diff --git a/packages/next/types/$$compiled.internal.d.ts b/packages/next/types/$$compiled.internal.d.ts
index 99abd8ff95699..a75c8560c1154 100644
--- a/packages/next/types/$$compiled.internal.d.ts
+++ b/packages/next/types/$$compiled.internal.d.ts
@@ -226,9 +226,9 @@ declare module 'react-server-dom-webpack/server.node' {
export type TemporaryReferenceSet = WeakMap
export type ImportManifestEntry = {
- id: string
+ id: string | number
// chunks is a double indexed array of chunkId / chunkFilename pairs
- chunks: Array
+ chunks: ReadonlyArray
name: string
async?: boolean
}
@@ -279,7 +279,7 @@ declare module 'react-server-dom-webpack/static' {
webpackMap: {
readonly [id: string]: {
readonly id: string | number
- readonly chunks: readonly string[]
+ readonly chunks: ReadonlyArray
readonly name: string
readonly async?: boolean
}
From f566e887b29206cfc4998fb37a460222cccc6fd1 Mon Sep 17 00:00:00 2001
From: Hendrik Liebau
Date: Thu, 27 Nov 2025 20:34:53 +0100
Subject: [PATCH 4/7] Make test compatible with Cache Components
---
test/development/app-dir/hmr-iframe/app/layout.tsx | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 test/development/app-dir/hmr-iframe/app/layout.tsx
diff --git a/test/development/app-dir/hmr-iframe/app/layout.tsx b/test/development/app-dir/hmr-iframe/app/layout.tsx
new file mode 100644
index 0000000000000..b78620f344b2f
--- /dev/null
+++ b/test/development/app-dir/hmr-iframe/app/layout.tsx
@@ -0,0 +1,5 @@
+import { Suspense } from 'react'
+
+export default function Root({ children }: { children: React.ReactNode }) {
+ return {children}
+}
From e17bdb0765f6b60a9d76241ea34b1b420bb5841e Mon Sep 17 00:00:00 2001
From: Hendrik Liebau
Date: Thu, 27 Nov 2025 23:04:10 +0100
Subject: [PATCH 5/7] Fix `/index/index` issue
---
.../src/build/webpack/loaders/next-app-loader/index.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts
index 53f703c92b6ef..e9fe2cc607125 100644
--- a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts
+++ b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts
@@ -682,7 +682,14 @@ const nextAppLoader: AppLoader = async function nextAppLoader() {
const buildInfo = getModuleBuildInfo((this as any)._module)
const collectedDeclarations: [string, string][] = []
- const page = name.replace(/^app/, '')
+
+ // Use the page from loaderOptions directly instead of deriving it from name.
+ // The name (bundlePath) may have been normalized with normalizePagePath()
+ // which is designed for Pages Router and incorrectly duplicates /index paths
+ // (e.g., /index/page -> /index/index/page). The page parameter contains the
+ // correct unnormalized value.
+ const page = loaderOptions.page
+
const middlewareConfig: ProxyConfig = JSON.parse(
Buffer.from(middlewareConfigBase64, 'base64').toString()
)
From a7b7ed7fa206b581fe1d331b1adfc0449b1747d6 Mon Sep 17 00:00:00 2001
From: Hendrik Liebau
Date: Fri, 28 Nov 2025 10:18:10 +0100
Subject: [PATCH 6/7] Handle race condition with Webpack compilation
Workaround for this issue:
https://github.com/webpack/webpack/issues/19837
---
packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts b/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts
index 2d895cd615d15..58aba64f8c42f 100644
--- a/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts
+++ b/packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts
@@ -105,6 +105,13 @@ function webpack5(this: ReactFreshWebpackPlugin, compiler: WebpackCompiler) {
`options.factory = ${runtimeTemplate.basicFunction(
'moduleObject, moduleExports, webpackRequire',
[
+ // If the original factory is missing, e.g. due to race condition
+ // when compiling multiple entries concurrently, recover by doing
+ // a full page reload.
+ 'if (!originalFactory) {',
+ Template.indent('document.location.reload();'),
+ Template.indent('return;'),
+ '}',
// Legacy CSS implementations will `eval` browser code in a Node.js
// context to extract CSS. For backwards compatibility, we need to check
// we're in a browser context before continuing.
From 56e2ce92c0753cfdcf94751e7dd7b6b460437222 Mon Sep 17 00:00:00 2001
From: Hendrik Liebau
Date: Mon, 1 Dec 2025 12:34:51 +0100
Subject: [PATCH 7/7] Don't misuse first client reference manifest as "base"
---
packages/next/errors.json | 3 ++-
.../next/src/server/app-render/manifests-singleton.ts | 9 ++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/next/errors.json b/packages/next/errors.json
index d65159c5c5eeb..30792573c713d 100644
--- a/packages/next/errors.json
+++ b/packages/next/errors.json
@@ -950,5 +950,6 @@
"949": "Route %s errored during %s. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled.\n \nOriginal Error: %s",
"950": "The manifests singleton was not initialized.",
"951": "The client reference manifest for route \"%s\" does not exist.",
- "952": "Cannot access \"%s\" without a work store."
+ "952": "Cannot access \"%s\" without a work store.",
+ "953": "This is a proxied client reference manifest. The property \"%s\" is not handled."
}
diff --git a/packages/next/src/server/app-render/manifests-singleton.ts b/packages/next/src/server/app-render/manifests-singleton.ts
index 880a76966270d..7cf3fc467cd70 100644
--- a/packages/next/src/server/app-render/manifests-singleton.ts
+++ b/packages/next/src/server/app-render/manifests-singleton.ts
@@ -3,7 +3,6 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight
import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import { InvariantError } from '../../shared/lib/invariant-error'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
-import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
import { createServerModuleMap, type ServerModuleMap } from './action-utils'
import { workAsyncStorage } from './work-async-storage.external'
@@ -36,7 +35,6 @@ type ClientReferenceManifestMappingProp =
const globalThisWithManifests = globalThis as GlobalThisWithManifests
function createProxiedClientReferenceManifest(
- baseManifest: DeepReadonly,
clientReferenceManifestsPerRoute: Map<
string,
DeepReadonly
@@ -114,7 +112,7 @@ function createProxiedClientReferenceManifest(
return new Proxy(
{},
{
- get(_, prop, receiver) {
+ get(_, prop) {
const workStore = workAsyncStorage.getStore()
switch (prop) {
@@ -154,7 +152,9 @@ function createProxiedClientReferenceManifest(
return proxy
}
default: {
- return ReflectAdapter.get(baseManifest, prop, receiver)
+ throw new InvariantError(
+ `This is a proxied client reference manifest. The property "${String(prop)}" is not handled.`
+ )
}
}
},
@@ -191,7 +191,6 @@ export function setManifestsSingleton({
>([[normalizeAppPath(page), clientReferenceManifest]])
const proxiedClientReferenceManifest = createProxiedClientReferenceManifest(
- clientReferenceManifest,
clientReferenceManifestsPerRoute
)