88 */
99
1010import type { ReactNodeList , ReactCustomFormAction } from 'shared/ReactTypes' ;
11+ import type { FizzPreamble } from 'react-server/src/ReactFizzPreamble' ;
1112import type {
1213 CrossOriginEnum ,
1314 PreloadImplOptions ,
@@ -49,6 +50,7 @@ import {
4950 getRenderState ,
5051 flushResources ,
5152} from 'react-server/src/ReactFizzServer' ;
53+ import { createFizzPreamble } from 'react-server/src/ReactFizzPreamble' ;
5254
5355import isAttributeNameSafe from '../shared/isAttributeNameSafe' ;
5456import isUnitlessNumber from '../shared/isUnitlessNumber' ;
@@ -135,8 +137,7 @@ export type RenderState = {
135137 // be null or empty when resuming.
136138
137139 // preamble chunks
138- htmlChunks : null | Array < Chunk | PrecomputedChunk > ,
139- headChunks : null | Array < Chunk | PrecomputedChunk > ,
140+ preamble : PreambleState ,
140141
141142 // external runtime script chunks
142143 externalRuntimeScript : null | ExternalRuntimeScript ,
@@ -442,8 +443,7 @@ export function createRenderState(
442443 segmentPrefix : stringToPrecomputedChunk ( idPrefix + 'S:' ) ,
443444 boundaryPrefix : stringToPrecomputedChunk ( idPrefix + 'B:' ) ,
444445 startInlineScript : inlineScriptWithNonce ,
445- htmlChunks : null ,
446- headChunks : null ,
446+ preamble : createPreambleState ( ) ,
447447
448448 externalRuntimeScript : externalRuntimeScript ,
449449 bootstrapChunks : bootstrapChunks ,
@@ -686,6 +686,19 @@ export function completeResumableState(resumableState: ResumableState): void {
686686 resumableState . bootstrapModules = undefined ;
687687}
688688
689+ export type PreambleState = {
690+ htmlChunks : null | Array < Chunk | PrecomputedChunk > ,
691+ headChunks : null | Array < Chunk | PrecomputedChunk > ,
692+ bodyChunks : null | Array < Chunk | PrecomputedChunk > ,
693+ } ;
694+ export function createPreambleState ( ) : PreambleState {
695+ return {
696+ htmlChunks : null ,
697+ headChunks : null ,
698+ bodyChunks : null ,
699+ } ;
700+ }
701+
689702// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
690703// modes. We only include the variants as they matter for the sake of our purposes.
691704// We don't actually provide the namespace therefore we use constants instead of the string.
@@ -694,16 +707,17 @@ export const ROOT_HTML_MODE = 0; // Used for the root most element tag.
694707// still makes sense
695708const HTML_HTML_MODE = 1 ; // Used for the <html> if it is at the top level.
696709const HTML_MODE = 2 ;
697- const SVG_MODE = 3 ;
698- const MATHML_MODE = 4 ;
699- const HTML_TABLE_MODE = 5 ;
700- const HTML_TABLE_BODY_MODE = 6 ;
701- const HTML_TABLE_ROW_MODE = 7 ;
702- const HTML_COLGROUP_MODE = 8 ;
710+ const HTML_HEAD_MODE = 3 ;
711+ const SVG_MODE = 4 ;
712+ const MATHML_MODE = 5 ;
713+ const HTML_TABLE_MODE = 6 ;
714+ const HTML_TABLE_BODY_MODE = 7 ;
715+ const HTML_TABLE_ROW_MODE = 8 ;
716+ const HTML_COLGROUP_MODE = 9 ;
703717// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
704718// still makes sense
705719
706- type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ;
720+ type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ;
707721
708722const NO_SCOPE = /* */ 0b00 ;
709723const NOSCRIPT_SCOPE = /* */ 0b01 ;
@@ -728,6 +742,10 @@ function createFormatContext(
728742 } ;
729743}
730744
745+ export function canHavePreamble ( formatContext : FormatContext ) : boolean {
746+ return formatContext . insertionMode < HTML_MODE ;
747+ }
748+
731749export function createRootFormatContext ( namespaceURI ?: string ) : FormatContext {
732750 const insertionMode =
733751 namespaceURI === 'http://www.w3.org/2000/svg'
@@ -798,17 +816,35 @@ export function getChildFormatContext(
798816 // entered plain HTML again.
799817 return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
800818 }
801- if ( parentContext . insertionMode === ROOT_HTML_MODE ) {
802- if ( type === 'html' ) {
803- // We've emitted the root and is now in <html> mode.
804- return createFormatContext ( HTML_HTML_MODE , null , parentContext . tagScope ) ;
805- } else {
806- // We've emitted the root and is now in plain HTML mode.
807- return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
819+ if ( parentContext . insertionMode < HTML_MODE ) {
820+ // We use the ordinal comparison to convey whether we're in the root
821+ // or in the documentElement scope
822+ switch ( type ) {
823+ case 'head' :
824+ // We are either at the root or inside the <html> tag and can enter
825+ // the <head> scope
826+ return createFormatContext (
827+ HTML_HEAD_MODE ,
828+ null ,
829+ parentContext . tagScope ,
830+ ) ;
831+ case 'html' :
832+ if ( parentContext . insertionMode === ROOT_HTML_MODE ) {
833+ // We only enter HTML_HTML_MODE if we're in the root
834+ // otherwise we fall through to the default case and
835+ // enter HTML_MODE
836+ return createFormatContext (
837+ HTML_HTML_MODE ,
838+ null ,
839+ parentContext . tagScope ,
840+ ) ;
841+ }
842+ // Intentional fallthrough
843+ default :
844+ // We emitted the root or the <html> and we're not in normal HTML scope
845+ // this includes the body
846+ return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
808847 }
809- } else if ( parentContext . insertionMode === HTML_HTML_MODE ) {
810- // We've emitted the document element and is now in plain HTML mode.
811- return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
812848 }
813849 return parentContext ;
814850}
@@ -3185,29 +3221,71 @@ function pushStartHead(
31853221 target : Array < Chunk | PrecomputedChunk > ,
31863222 props : Object ,
31873223 renderState : RenderState ,
3224+ preambleState : null | PreambleState ,
31883225 insertionMode : InsertionMode ,
3189- ) : ReactNodeList {
3190- if ( insertionMode < HTML_MODE && renderState . headChunks === null ) {
3226+ ) : ReactNodeList | FizzPreamble {
3227+ if ( insertionMode < HTML_MODE ) {
31913228 // This <head> is the Document.head and should be part of the preamble
3192- renderState . headChunks = [ ] ;
3193- return pushStartGenericElement ( renderState . headChunks , props , 'head' ) ;
3229+ const preamble = preambleState || renderState . preamble ;
3230+
3231+ if ( preamble . headChunks ) {
3232+ throw new Error ( `${ '<head>' } may only be rendered once per application` ) ;
3233+ }
3234+ preamble . headChunks = [ ] ;
3235+ const children = pushStartGenericElement (
3236+ preamble . headChunks ,
3237+ props ,
3238+ 'head' ,
3239+ ) ;
3240+ return createFizzPreamble ( children ) ;
31943241 } else {
31953242 // This <head> is deep and is likely just an error. we emit it inline though.
31963243 // Validation should warn that this tag is the the wrong spot.
31973244 return pushStartGenericElement ( target , props , 'head' ) ;
31983245 }
31993246}
32003247
3201- function pushStartHtml (
3248+ function pushStartBody (
32023249 target : Array < Chunk | PrecomputedChunk > ,
32033250 props : Object ,
32043251 renderState : RenderState ,
3252+ preambleState : null | PreambleState ,
32053253 insertionMode : InsertionMode ,
32063254) : ReactNodeList {
3207- if ( insertionMode === ROOT_HTML_MODE && renderState . htmlChunks === null ) {
3208- // This <html> is the Document.documentElement and should be part of the preamble
3209- renderState . htmlChunks = [ DOCTYPE ] ;
3210- return pushStartGenericElement ( renderState . htmlChunks , props , 'html' ) ;
3255+ if ( insertionMode < HTML_MODE ) {
3256+ // This <body> is the Document.body
3257+ const preamble = preambleState || renderState . preamble ;
3258+
3259+ if ( preamble . bodyChunks ) {
3260+ throw new Error ( `${ '<body>' } may only be rendered once per application` ) ;
3261+ }
3262+
3263+ preamble . bodyChunks = [ ] ;
3264+ return pushStartGenericElement ( preamble . bodyChunks , props , 'body' ) ;
3265+ } else {
3266+ // This <head> is deep and is likely just an error. we emit it inline though.
3267+ // Validation should warn that this tag is the the wrong spot.
3268+ return pushStartGenericElement ( target , props , 'body' ) ;
3269+ }
3270+ }
3271+
3272+ function pushStartHtml (
3273+ target : Array < Chunk | PrecomputedChunk > ,
3274+ props : Object ,
3275+ renderState : RenderState ,
3276+ preambleState : null | PreambleState ,
3277+ insertionMode : InsertionMode ,
3278+ ) : ReactNodeList | FizzPreamble {
3279+ if ( insertionMode === ROOT_HTML_MODE ) {
3280+ // This <html> is the Document.documentElement
3281+ const preamble = preambleState || renderState . preamble ;
3282+
3283+ if ( preamble . htmlChunks ) {
3284+ throw new Error ( `${ '<html>' } may only be rendered once per application.` ) ;
3285+ }
3286+
3287+ preamble . htmlChunks = [ DOCTYPE ] ;
3288+ return pushStartGenericElement ( preamble . htmlChunks , props , 'html' ) ;
32113289 } else {
32123290 // This <html> is deep and is likely just an error. we emit it inline though.
32133291 // Validation should warn that this tag is the the wrong spot.
@@ -3562,11 +3640,12 @@ export function pushStartInstance(
35623640 props : Object ,
35633641 resumableState : ResumableState ,
35643642 renderState : RenderState ,
3643+ preambleState : null | PreambleState ,
35653644 hoistableState : null | HoistableState ,
35663645 formatContext : FormatContext ,
35673646 textEmbedded : boolean ,
35683647 isFallback : boolean ,
3569- ) : ReactNodeList {
3648+ ) : ReactNodeList | FizzPreamble {
35703649 if ( __DEV__ ) {
35713650 validateARIAProperties ( type , props ) ;
35723651 validateInputProperties ( type , props ) ;
@@ -3729,13 +3808,23 @@ export function pushStartInstance(
37293808 target ,
37303809 props ,
37313810 renderState ,
3811+ preambleState ,
3812+ formatContext . insertionMode ,
3813+ ) ;
3814+ case 'body' :
3815+ return pushStartBody (
3816+ target ,
3817+ props ,
3818+ renderState ,
3819+ preambleState ,
37323820 formatContext . insertionMode ,
37333821 ) ;
37343822 case 'html' : {
37353823 return pushStartHtml (
37363824 target ,
37373825 props ,
37383826 renderState ,
3827+ preambleState ,
37393828 formatContext . insertionMode ,
37403829 ) ;
37413830 }
@@ -3814,10 +3903,31 @@ export function pushEndInstance(
38143903 return ;
38153904 }
38163905 break ;
3906+ case 'head' :
3907+ if ( formatContext . insertionMode <= HTML_HTML_MODE ) {
3908+ return ;
3909+ }
3910+ break ;
38173911 }
38183912 target . push ( endChunkForTag ( type ) ) ;
38193913}
38203914
3915+ export function preparePreamble (
3916+ renderState : RenderState ,
3917+ preambleState : PreambleState ,
3918+ ) {
3919+ const rootPreamble = renderState . preamble ;
3920+ if ( rootPreamble . htmlChunks === null ) {
3921+ rootPreamble . htmlChunks = preambleState . htmlChunks ;
3922+ }
3923+ if ( rootPreamble . headChunks === null ) {
3924+ rootPreamble . headChunks = preambleState . headChunks ;
3925+ }
3926+ if ( rootPreamble . bodyChunks === null ) {
3927+ rootPreamble . bodyChunks = preambleState . bodyChunks ;
3928+ }
3929+ }
3930+
38213931function writeBootstrap (
38223932 destination : Destination ,
38233933 renderState : RenderState ,
@@ -4033,6 +4143,7 @@ export function writeStartSegment(
40334143 switch ( formatContext . insertionMode ) {
40344144 case ROOT_HTML_MODE :
40354145 case HTML_HTML_MODE :
4146+ case HTML_HEAD_MODE :
40364147 case HTML_MODE : {
40374148 writeChunk ( destination , startSegmentHTML ) ;
40384149 writeChunk ( destination , renderState . segmentPrefix ) ;
@@ -4091,6 +4202,7 @@ export function writeEndSegment(
40914202 switch ( formatContext . insertionMode ) {
40924203 case ROOT_HTML_MODE :
40934204 case HTML_HTML_MODE :
4205+ case HTML_HEAD_MODE :
40944206 case HTML_MODE : {
40954207 return writeChunkAndReturn ( destination , endSegmentHTML ) ;
40964208 }
@@ -4679,7 +4791,7 @@ function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
46794791// flush the entire preamble in a single pass. This probably should be modified
46804792// in the future to be backpressure sensitive but that requires a larger refactor
46814793// of the flushing code in Fizz.
4682- export function writePreamble (
4794+ export function writePreambleStart (
46834795 destination : Destination ,
46844796 resumableState : ResumableState ,
46854797 renderState : RenderState ,
@@ -4700,8 +4812,10 @@ export function writePreamble(
47004812 internalPreinitScript ( resumableState , renderState , src , chunks ) ;
47014813 }
47024814
4703- const htmlChunks = renderState . htmlChunks ;
4704- const headChunks = renderState . headChunks ;
4815+ const preamble = renderState . preamble ;
4816+
4817+ const htmlChunks = preamble . htmlChunks ;
4818+ const headChunks = preamble . headChunks ;
47054819
47064820 let i = 0 ;
47074821
@@ -4773,12 +4887,31 @@ export function writePreamble(
47734887 writeChunk ( destination , hoistableChunks [ i ] ) ;
47744888 }
47754889 hoistableChunks . length = 0 ;
4890+ }
47764891
4777- if ( htmlChunks && headChunks === null ) {
4892+ // We don't bother reporting backpressure at the moment because we expect to
4893+ // flush the entire preamble in a single pass. This probably should be modified
4894+ // in the future to be backpressure sensitive but that requires a larger refactor
4895+ // of the flushing code in Fizz.
4896+ export function writePreambleEnd (
4897+ destination : Destination ,
4898+ renderState : RenderState ,
4899+ ) : void {
4900+ const preamble = renderState . preamble ;
4901+ const htmlChunks = preamble . htmlChunks ;
4902+ const headChunks = preamble . headChunks ;
4903+ if ( htmlChunks || headChunks ) {
47784904 // we have an <html> but we inserted an implicit <head> tag. We need
47794905 // to close it since the main content won't have it
47804906 writeChunk ( destination , endChunkForTag ( 'head' ) ) ;
47814907 }
4908+
4909+ const bodyChunks = preamble . bodyChunks ;
4910+ if ( bodyChunks ) {
4911+ for ( let i = 0 ; i < bodyChunks . length ; i ++ ) {
4912+ writeChunk ( destination , bodyChunks [ i ] ) ;
4913+ }
4914+ }
47824915}
47834916
47844917// We don't bother reporting backpressure at the moment because we expect to
0 commit comments