@@ -168,7 +168,10 @@ import {
168168 prerenderAndAbortInSequentialTasks ,
169169} from './app-render-prerender-utils'
170170import { printDebugThrownValueForProspectiveRender } from './prospective-render-utils'
171- import { pipelineInSequentialTasks } from './app-render-render-utils'
171+ import {
172+ pipelineInSequentialTasks ,
173+ scheduleInSequentialTasks ,
174+ } from './app-render-render-utils'
172175import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler'
173176import {
174177 workUnitAsyncStorage ,
@@ -604,52 +607,149 @@ async function generateDynamicFlightRenderResult(
604607 onFlightDataRenderError
605608 )
606609
607- const RSCPayload : RSCPayload & {
608- /** Only available during cacheComponents development builds. Used for logging errors. */
609- _validation ?: Promise < ReactNode >
610- _bypassCachesInDev ?: React . ReactNode
611- } = await workUnitAsyncStorage . run (
612- requestStore ,
613- generateDynamicRSCPayload ,
614- ctx ,
615- options
616- )
617-
618- if (
619- process . env . NODE_ENV === 'development' &&
620- isBypassingCachesInDev ( renderOpts , requestStore )
621- ) {
622- RSCPayload . _bypassCachesInDev = createElement ( WarnForBypassCachesInDev , {
623- route : workStore . route ,
624- } )
625- }
626-
627610 const debugChannel = setReactDebugChannel && createDebugChannel ( )
628611
629612 if ( debugChannel ) {
630613 setReactDebugChannel ( debugChannel . clientSide , htmlRequestId , requestId )
631614 }
632615
633- // For app dir, use the bundled version of Flight server renderer (renderToReadableStream)
634- // which contains the subset React.
635- const flightReadableStream = workUnitAsyncStorage . run (
636- requestStore ,
637- renderToReadableStream ,
638- RSCPayload ,
639- clientReferenceManifest . clientModules ,
640- {
641- onError,
642- temporaryReferences : options ?. temporaryReferences ,
643- filterStackFrame,
644- debugChannel : debugChannel ?. serverSide ,
616+ let stream : ReadableStream < Uint8Array < ArrayBufferLike > >
617+ if ( process . env . NODE_ENV === 'development' && renderOpts . cacheComponents ) {
618+ // If we got here, it means we didn't go into `generateDynamicFlightRenderResultWithCachesInDev`,
619+ // so caches must be disabled. Render dynamically, but include basic environment labels.
620+
621+ const getPayload = async (
622+ // eslint-disable-next-line @typescript-eslint/no-shadow
623+ requestStore : RequestStore
624+ ) => {
625+ const rscPayload : RSCPayload & RSCPayloadDevProperties =
626+ await workUnitAsyncStorage . run (
627+ requestStore ,
628+ generateDynamicRSCPayload ,
629+ ctx ,
630+ options
631+ )
632+ if ( isBypassingCachesInDev ( renderOpts , requestStore ) ) {
633+ rscPayload . _bypassCachesInDev = createElement (
634+ WarnForBypassCachesInDev ,
635+ {
636+ route : workStore . route ,
637+ }
638+ )
639+ }
640+ return rscPayload
645641 }
646- )
647642
648- return new FlightRenderResult ( flightReadableStream , {
643+ stream = await stagedRenderToReadableStreamWithoutCachesInDev (
644+ ctx ,
645+ requestStore ,
646+ getPayload ,
647+ clientReferenceManifest ,
648+ {
649+ onError,
650+ temporaryReferences : options ?. temporaryReferences ,
651+ filterStackFrame,
652+ debugChannel : debugChannel ?. serverSide ,
653+ }
654+ )
655+ } else {
656+ // For app dir, use the bundled version of Flight server renderer (renderToReadableStream)
657+ // which contains the subset React.
658+ const rscPayload = await workUnitAsyncStorage . run (
659+ requestStore ,
660+ generateDynamicRSCPayload ,
661+ ctx ,
662+ options
663+ )
664+
665+ stream = workUnitAsyncStorage . run (
666+ requestStore ,
667+ renderToReadableStream ,
668+ rscPayload ,
669+ clientReferenceManifest . clientModules ,
670+ {
671+ onError,
672+ temporaryReferences : options ?. temporaryReferences ,
673+ filterStackFrame,
674+ debugChannel : debugChannel ?. serverSide ,
675+ }
676+ )
677+ }
678+
679+ return new FlightRenderResult ( stream , {
649680 fetchMetrics : workStore . fetchMetrics ,
650681 } )
651682}
652683
684+ type RenderToReadableStreamServerOptions = NonNullable <
685+ Parameters <
686+ ( typeof import ( 'react-server-dom-webpack/server.node' ) ) [ 'renderToReadableStream' ]
687+ > [ 2 ]
688+ >
689+
690+ async function stagedRenderToReadableStreamWithoutCachesInDev (
691+ ctx : AppRenderContext ,
692+ requestStore : RequestStore ,
693+ getPayload : ( requestStore : RequestStore ) => Promise < RSCPayload > ,
694+ clientReferenceManifest : NonNullable < RenderOpts [ 'clientReferenceManifest' ] > ,
695+ options : Omit < RenderToReadableStreamServerOptions , 'environmentName' >
696+ ) {
697+ const {
698+ componentMod : { renderToReadableStream } ,
699+ } = ctx
700+ // We're rendering while bypassing caches,
701+ // so we have no hope of showing a useful runtime stage.
702+ // But we still want things like `params` to show up in devtools correctly,
703+ // which relies on mechanisms we've set up for staged rendering,
704+ // so we do a 2-task version (Static -> Dynamic) instead.
705+
706+ const stageController = new StagedRenderingController ( )
707+ console . log ( 'rendering RSC payload without caches' )
708+ const environmentName = ( ) => {
709+ const currentStage = stageController . currentStage
710+ switch ( currentStage ) {
711+ case RenderStage . Static :
712+ return 'Prerender'
713+ case RenderStage . Runtime :
714+ case RenderStage . Dynamic :
715+ return 'Server'
716+ default :
717+ currentStage satisfies never
718+ throw new InvariantError ( `Invalid render stage: ${ currentStage } ` )
719+ }
720+ }
721+
722+ requestStore . stagedRendering = stageController
723+ requestStore . asyncApiPromises = createAsyncApiPromisesInDev (
724+ stageController ,
725+ requestStore . cookies ,
726+ requestStore . mutableCookies ,
727+ requestStore . headers
728+ )
729+
730+ const rscPayload = await getPayload ( requestStore )
731+
732+ return await workUnitAsyncStorage . run (
733+ requestStore ,
734+ scheduleInSequentialTasks ,
735+ ( ) => {
736+ const stream = renderToReadableStream (
737+ rscPayload ,
738+ clientReferenceManifest . clientModules ,
739+ {
740+ ...options ,
741+ environmentName,
742+ }
743+ )
744+ stageController . advanceStage ( RenderStage . Dynamic )
745+ return stream
746+ } ,
747+ ( ) => {
748+ stageController . advanceStage ( RenderStage . Dynamic )
749+ }
750+ )
751+ }
752+
653753/**
654754 * Fork of `generateDynamicFlightRenderResult` that renders using `renderWithRestartOnCacheMissInDev`
655755 * to ensure correct separation of environments Prerender/Server (for use in Cache Components)
@@ -2192,6 +2292,12 @@ function applyMetadataFromPrerenderResult(
21922292 }
21932293}
21942294
2295+ type RSCPayloadDevProperties = {
2296+ /** Only available during cacheComponents development builds. Used for logging errors. */
2297+ _validation ?: Promise < ReactNode >
2298+ _bypassCachesInDev ?: ReactNode
2299+ }
2300+
21952301async function renderToStream (
21962302 requestStore : RequestStore ,
21972303 req : BaseNextRequest ,
@@ -2334,26 +2440,15 @@ async function renderToStream(
23342440 // Edge routes never prerender so we don't have a Prerender environment for anything in edge runtime
23352441 process . env . NEXT_RUNTIME !== 'edge' &&
23362442 // We only have a Prerender environment for projects opted into cacheComponents
2337- cacheComponents &&
2338- // We only do this flow if we can safely recreate the store from scratch
2339- // (which is not the case for renders after an action)
2340- createRequestStore &&
2341- // We only do this flow if we're not bypassing caches in dev using
2342- // "disable cache" in devtools or a hard refresh (cache-control: "no-store")
2343- ! isBypassingCachesInDev ( renderOpts , requestStore )
2443+ cacheComponents
23442444 ) {
2345- type RSCPayloadWithValidation = InitialRSCPayload & {
2346- /** Only available during cacheComponents development builds. Used for logging errors. */
2347- _validation ?: Promise < ReactNode >
2348- }
2349-
23502445 const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
2351-
2446+ let debugChannel : DebugChannelPair | undefined
23522447 const getPayload = async (
23532448 // eslint-disable-next-line @typescript-eslint/no-shadow
23542449 requestStore : RequestStore
2355- ) : Promise < RSCPayloadWithValidation > => {
2356- const payload : RSCPayloadWithValidation =
2450+ ) => {
2451+ const payload : InitialRSCPayload & RSCPayloadDevProperties =
23572452 await workUnitAsyncStorage . run (
23582453 requestStore ,
23592454 getRSCPayload ,
@@ -2366,23 +2461,61 @@ async function renderToStream(
23662461 // because we're not going to wait for the stream to complete,
23672462 // so leaving the validation unresolved is fine.
23682463 payload . _validation = validationOutlet
2464+
2465+ if ( isBypassingCachesInDev ( renderOpts , requestStore ) ) {
2466+ // Mark the RSC payload to indicate that caches were bypassed in dev.
2467+ // This lets the client know not to cache anything based on this render.
2468+ payload . _bypassCachesInDev = createElement ( WarnForBypassCachesInDev , {
2469+ route : workStore . route ,
2470+ } )
2471+ }
2472+
23692473 return payload
23702474 }
23712475
2372- const {
2373- stream : serverStream ,
2374- debugChannel,
2375- requestStore : finalRequestStore ,
2376- } = await renderWithRestartOnCacheMissInDev (
2377- ctx ,
2378- requestStore ,
2379- createRequestStore ,
2380- getPayload ,
2381- serverComponentsErrorHandler
2382- )
2476+ if (
2477+ // We only do this flow if we can safely recreate the store from scratch
2478+ // (which is not the case for renders after an action)
2479+ createRequestStore &&
2480+ // We only do this flow if we're not bypassing caches in dev using
2481+ // "disable cache" in devtools or a hard refresh (cache-control: "no-store")
2482+ ! isBypassingCachesInDev ( renderOpts , requestStore )
2483+ ) {
2484+ const {
2485+ stream : serverStream ,
2486+ debugChannel : returnedDebugChannel ,
2487+ requestStore : finalRequestStore ,
2488+ } = await renderWithRestartOnCacheMissInDev (
2489+ ctx ,
2490+ requestStore ,
2491+ createRequestStore ,
2492+ getPayload ,
2493+ serverComponentsErrorHandler
2494+ )
2495+
2496+ reactServerResult = new ReactServerResult ( serverStream )
2497+ requestStore = finalRequestStore
2498+ debugChannel = returnedDebugChannel
2499+ } else {
2500+ // We're either bypassing caches or we can't restart the render.
2501+ // Do a dynamic render, but with (basic) environment labels.
23832502
2384- reactServerResult = new ReactServerResult ( serverStream )
2385- requestStore = finalRequestStore
2503+ debugChannel = setReactDebugChannel && createDebugChannel ( )
2504+
2505+ const serverStream =
2506+ await stagedRenderToReadableStreamWithoutCachesInDev (
2507+ ctx ,
2508+ requestStore ,
2509+ getPayload ,
2510+ clientReferenceManifest ,
2511+ {
2512+ onError : serverComponentsErrorHandler ,
2513+ filterStackFrame,
2514+ debugChannel : debugChannel ?. serverSide ,
2515+ }
2516+ )
2517+ reactServerResult = new ReactServerResult ( serverStream )
2518+ }
23862519
23872520 if ( debugChannel && setReactDebugChannel ) {
23882521 const [ readableSsr , readableBrowser ] =
@@ -2413,15 +2546,14 @@ async function renderToStream(
24132546 )
24142547 } else {
24152548 // This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2416- const RSCPayload : RSCPayload & {
2417- _bypassCachesInDev ?: React . ReactNode
2418- } = await workUnitAsyncStorage . run (
2419- requestStore ,
2420- getRSCPayload ,
2421- tree ,
2422- ctx ,
2423- res . statusCode === 404
2424- )
2549+ const RSCPayload : RSCPayload & RSCPayloadDevProperties =
2550+ await workUnitAsyncStorage . run (
2551+ requestStore ,
2552+ getRSCPayload ,
2553+ tree ,
2554+ ctx ,
2555+ res . statusCode === 404
2556+ )
24252557
24262558 if ( isBypassingCachesInDev ( renderOpts , requestStore ) ) {
24272559 // Mark the RSC payload to indicate that caches were bypassed in dev.
0 commit comments