diff --git a/packages/next/errors.json b/packages/next/errors.json index 91dfce18d0632..30792573c713d 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -947,5 +947,9 @@ "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.", + "953": "This is a proxied client reference manifest. The property \"%s\" is not handled." } 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/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() ) 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..7cf3fc467cd70 --- /dev/null +++ b/packages/next/src/server/app-render/manifests-singleton.ts @@ -0,0 +1,230 @@ +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 { 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( + 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) { + 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: { + throw new InvariantError( + `This is a proxied client reference manifest. The property "${String(prop)}" is not handled.` + ) + } + } + }, + } + ) 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( + 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 } 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. 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} +} 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..f5ade055497dc --- /dev/null +++ b/test/development/app-dir/hmr-iframe/app/page1/page.tsx @@ -0,0 +1,22 @@ +import { subscribeToHMR } from './subscribeToHMR' +import { Component } from './Component' + +// 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 ( + + +