@@ -135,8 +135,7 @@ export type RenderState = {
135135 // be null or empty when resuming.
136136
137137 // preamble chunks
138- htmlChunks : null | Array < Chunk | PrecomputedChunk > ,
139- headChunks : null | Array < Chunk | PrecomputedChunk > ,
138+ preamble : PreambleState ,
140139
141140 // external runtime script chunks
142141 externalRuntimeScript : null | ExternalRuntimeScript ,
@@ -442,8 +441,7 @@ export function createRenderState(
442441 segmentPrefix : stringToPrecomputedChunk ( idPrefix + 'S:' ) ,
443442 boundaryPrefix : stringToPrecomputedChunk ( idPrefix + 'B:' ) ,
444443 startInlineScript : inlineScriptWithNonce ,
445- htmlChunks : null ,
446- headChunks : null ,
444+ preamble : createPreambleState ( ) ,
447445
448446 externalRuntimeScript : externalRuntimeScript ,
449447 bootstrapChunks : bootstrapChunks ,
@@ -686,6 +684,19 @@ export function completeResumableState(resumableState: ResumableState): void {
686684 resumableState . bootstrapModules = undefined ;
687685}
688686
687+ export type PreambleState = {
688+ htmlChunks : null | Array < Chunk | PrecomputedChunk > ,
689+ headChunks : null | Array < Chunk | PrecomputedChunk > ,
690+ bodyChunks : null | Array < Chunk | PrecomputedChunk > ,
691+ } ;
692+ export function createPreambleState ( ) : PreambleState {
693+ return {
694+ htmlChunks : null ,
695+ headChunks : null ,
696+ bodyChunks : null ,
697+ } ;
698+ }
699+
689700// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
690701// modes. We only include the variants as they matter for the sake of our purposes.
691702// We don't actually provide the namespace therefore we use constants instead of the string.
@@ -694,16 +705,17 @@ export const ROOT_HTML_MODE = 0; // Used for the root most element tag.
694705// still makes sense
695706const HTML_HTML_MODE = 1 ; // Used for the <html> if it is at the top level.
696707const 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 ;
708+ const HTML_HEAD_MODE = 3 ;
709+ const SVG_MODE = 4 ;
710+ const MATHML_MODE = 5 ;
711+ const HTML_TABLE_MODE = 6 ;
712+ const HTML_TABLE_BODY_MODE = 7 ;
713+ const HTML_TABLE_ROW_MODE = 8 ;
714+ const HTML_COLGROUP_MODE = 9 ;
703715// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
704716// still makes sense
705717
706- type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ;
718+ type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ;
707719
708720const NO_SCOPE = /* */ 0b00 ;
709721const NOSCRIPT_SCOPE = /* */ 0b01 ;
@@ -728,6 +740,10 @@ function createFormatContext(
728740 } ;
729741}
730742
743+ export function canHavePreamble ( formatContext : FormatContext ) : boolean {
744+ return formatContext . insertionMode < HTML_MODE ;
745+ }
746+
731747export function createRootFormatContext ( namespaceURI ?: string ) : FormatContext {
732748 const insertionMode =
733749 namespaceURI === 'http://www.w3.org/2000/svg'
@@ -792,27 +808,42 @@ export function getChildFormatContext(
792808 null ,
793809 parentContext . tagScope ,
794810 ) ;
811+ case 'head' :
812+ if ( parentContext . insertionMode < HTML_MODE ) {
813+ // We are either at the root or inside the <html> tag and can enter
814+ // the <head> scope
815+ return createFormatContext (
816+ HTML_HEAD_MODE ,
817+ null ,
818+ parentContext . tagScope ,
819+ ) ;
820+ }
821+ break ;
822+ case 'html' :
823+ if ( parentContext . insertionMode === ROOT_HTML_MODE ) {
824+ return createFormatContext (
825+ HTML_HTML_MODE ,
826+ null ,
827+ parentContext . tagScope ,
828+ ) ;
829+ }
830+ break ;
795831 }
796832 if ( parentContext . insertionMode >= HTML_TABLE_MODE ) {
797833 // Whatever tag this was, it wasn't a table parent or other special parent, so we must have
798834 // entered plain HTML again.
799835 return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
800836 }
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 ) ;
808- }
809- } else if ( parentContext . insertionMode === HTML_HTML_MODE ) {
810- // We've emitted the document element and is now in plain HTML mode.
837+ if ( parentContext . insertionMode < HTML_MODE ) {
811838 return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
812839 }
813840 return parentContext ;
814841}
815842
843+ export function isPreambleContext ( formatContext : FormatContext ) : boolean {
844+ return formatContext . insertionMode === HTML_HEAD_MODE ;
845+ }
846+
816847export function makeId (
817848 resumableState : ResumableState ,
818849 treeId : string ,
@@ -3185,29 +3216,66 @@ function pushStartHead(
31853216 target : Array < Chunk | PrecomputedChunk > ,
31863217 props : Object ,
31873218 renderState : RenderState ,
3219+ preambleState : null | PreambleState ,
31883220 insertionMode : InsertionMode ,
31893221) : ReactNodeList {
3190- if ( insertionMode < HTML_MODE && renderState . headChunks === null ) {
3222+ if ( insertionMode < HTML_MODE ) {
31913223 // This <head> is the Document.head and should be part of the preamble
3192- renderState . headChunks = [ ] ;
3193- return pushStartGenericElement ( renderState . headChunks , props , 'head' ) ;
3224+ const preamble = preambleState || renderState . preamble ;
3225+
3226+ if ( preamble . headChunks ) {
3227+ throw new Error ( `The ${ '`<head>`' } tag may only be rendered once.` ) ;
3228+ }
3229+ preamble . headChunks = [ ] ;
3230+ return pushStartGenericElement ( preamble . headChunks , props , 'head' ) ;
31943231 } else {
31953232 // This <head> is deep and is likely just an error. we emit it inline though.
31963233 // Validation should warn that this tag is the the wrong spot.
31973234 return pushStartGenericElement ( target , props , 'head' ) ;
31983235 }
31993236}
32003237
3238+ function pushStartBody (
3239+ target : Array < Chunk | PrecomputedChunk > ,
3240+ props : Object ,
3241+ renderState : RenderState ,
3242+ preambleState : null | PreambleState ,
3243+ insertionMode : InsertionMode ,
3244+ ) : ReactNodeList {
3245+ if ( insertionMode < HTML_MODE ) {
3246+ // This <body> is the Document.body
3247+ const preamble = preambleState || renderState . preamble ;
3248+
3249+ if ( preamble . bodyChunks ) {
3250+ throw new Error ( `The ${ '`<body>`' } tag may only be rendered once.` ) ;
3251+ }
3252+
3253+ preamble . bodyChunks = [ ] ;
3254+ return pushStartGenericElement ( preamble . bodyChunks , props , 'body' ) ;
3255+ } else {
3256+ // This <head> is deep and is likely just an error. we emit it inline though.
3257+ // Validation should warn that this tag is the the wrong spot.
3258+ return pushStartGenericElement ( target , props , 'body' ) ;
3259+ }
3260+ }
3261+
32013262function pushStartHtml (
32023263 target : Array < Chunk | PrecomputedChunk > ,
32033264 props : Object ,
32043265 renderState : RenderState ,
3266+ preambleState : null | PreambleState ,
32053267 insertionMode : InsertionMode ,
32063268) : 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' ) ;
3269+ if ( insertionMode === ROOT_HTML_MODE ) {
3270+ // This <html> is the Document.documentElement
3271+ const preamble = preambleState || renderState . preamble ;
3272+
3273+ if ( preamble . htmlChunks ) {
3274+ throw new Error ( `The ${ '`<html>`' } tag may only be rendered once.` ) ;
3275+ }
3276+
3277+ preamble . htmlChunks = [ DOCTYPE ] ;
3278+ return pushStartGenericElement ( preamble . htmlChunks , props , 'html' ) ;
32113279 } else {
32123280 // This <html> is deep and is likely just an error. we emit it inline though.
32133281 // Validation should warn that this tag is the the wrong spot.
@@ -3562,6 +3630,7 @@ export function pushStartInstance(
35623630 props : Object ,
35633631 resumableState : ResumableState ,
35643632 renderState : RenderState ,
3633+ preambleState : null | PreambleState ,
35653634 hoistableState : null | HoistableState ,
35663635 formatContext : FormatContext ,
35673636 textEmbedded : boolean ,
@@ -3729,13 +3798,23 @@ export function pushStartInstance(
37293798 target ,
37303799 props ,
37313800 renderState ,
3801+ preambleState ,
3802+ formatContext . insertionMode ,
3803+ ) ;
3804+ case 'body' :
3805+ return pushStartBody (
3806+ target ,
3807+ props ,
3808+ renderState ,
3809+ preambleState ,
37323810 formatContext . insertionMode ,
37333811 ) ;
37343812 case 'html' : {
37353813 return pushStartHtml (
37363814 target ,
37373815 props ,
37383816 renderState ,
3817+ preambleState ,
37393818 formatContext . insertionMode ,
37403819 ) ;
37413820 }
@@ -3814,10 +3893,50 @@ export function pushEndInstance(
38143893 return ;
38153894 }
38163895 break ;
3896+ case 'head' :
3897+ if ( formatContext . insertionMode <= HTML_HTML_MODE ) {
3898+ return ;
3899+ }
3900+ break ;
38173901 }
38183902 target . push ( endChunkForTag ( type ) ) ;
38193903}
38203904
3905+ export function hoistPreambleState (
3906+ renderState : RenderState ,
3907+ preambleState : PreambleState ,
3908+ ) {
3909+ const rootPreamble = renderState . preamble ;
3910+ if ( rootPreamble . htmlChunks === null ) {
3911+ rootPreamble . htmlChunks = preambleState . htmlChunks ;
3912+ }
3913+ if ( rootPreamble . headChunks === null ) {
3914+ rootPreamble . headChunks = preambleState . headChunks ;
3915+ }
3916+ if ( rootPreamble . bodyChunks === null ) {
3917+ rootPreamble . bodyChunks = preambleState . bodyChunks ;
3918+ }
3919+ }
3920+
3921+ export function isPreambleReady (
3922+ renderState : RenderState ,
3923+ // This means there are unfinished Suspense boundaries which could contain
3924+ // a preamble. In the case of DOM we constrain valid programs to only having
3925+ // one instance of each singleton so we can determine the preamble is ready
3926+ // as long as we have chunks for each of these tags.
3927+ hasPendingPreambles : boolean ,
3928+ ) : boolean {
3929+ const preamble = renderState . preamble ;
3930+ return (
3931+ // There are no remaining boundaries which might contain a preamble so
3932+ // the preamble is as complete as it is going to get
3933+ hasPendingPreambles === false ||
3934+ // we have a head and body tag. we don't need to wait for any more
3935+ // because it would be invalid to render additional copies of these tags
3936+ ! ! ( preamble . headChunks && preamble . bodyChunks )
3937+ ) ;
3938+ }
3939+
38213940function writeBootstrap (
38223941 destination : Destination ,
38233942 renderState : RenderState ,
@@ -4033,6 +4152,7 @@ export function writeStartSegment(
40334152 switch ( formatContext . insertionMode ) {
40344153 case ROOT_HTML_MODE :
40354154 case HTML_HTML_MODE :
4155+ case HTML_HEAD_MODE :
40364156 case HTML_MODE : {
40374157 writeChunk ( destination , startSegmentHTML ) ;
40384158 writeChunk ( destination , renderState . segmentPrefix ) ;
@@ -4091,6 +4211,7 @@ export function writeEndSegment(
40914211 switch ( formatContext . insertionMode ) {
40924212 case ROOT_HTML_MODE :
40934213 case HTML_HTML_MODE :
4214+ case HTML_HEAD_MODE :
40944215 case HTML_MODE : {
40954216 return writeChunkAndReturn ( destination , endSegmentHTML ) ;
40964217 }
@@ -4679,7 +4800,7 @@ function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
46794800// flush the entire preamble in a single pass. This probably should be modified
46804801// in the future to be backpressure sensitive but that requires a larger refactor
46814802// of the flushing code in Fizz.
4682- export function writePreamble (
4803+ export function writePreambleStart (
46834804 destination : Destination ,
46844805 resumableState : ResumableState ,
46854806 renderState : RenderState ,
@@ -4700,8 +4821,10 @@ export function writePreamble(
47004821 internalPreinitScript ( resumableState , renderState , src , chunks ) ;
47014822 }
47024823
4703- const htmlChunks = renderState . htmlChunks ;
4704- const headChunks = renderState . headChunks ;
4824+ const preamble = renderState . preamble ;
4825+
4826+ const htmlChunks = preamble . htmlChunks ;
4827+ const headChunks = preamble . headChunks ;
47054828
47064829 let i = 0 ;
47074830
@@ -4773,12 +4896,31 @@ export function writePreamble(
47734896 writeChunk ( destination , hoistableChunks [ i ] ) ;
47744897 }
47754898 hoistableChunks . length = 0 ;
4899+ }
47764900
4777- if ( htmlChunks && headChunks === null ) {
4901+ // We don't bother reporting backpressure at the moment because we expect to
4902+ // flush the entire preamble in a single pass. This probably should be modified
4903+ // in the future to be backpressure sensitive but that requires a larger refactor
4904+ // of the flushing code in Fizz.
4905+ export function writePreambleEnd (
4906+ destination : Destination ,
4907+ renderState : RenderState ,
4908+ ) : void {
4909+ const preamble = renderState . preamble ;
4910+ const htmlChunks = preamble . htmlChunks ;
4911+ const headChunks = preamble . headChunks ;
4912+ if ( htmlChunks || headChunks ) {
47784913 // we have an <html> but we inserted an implicit <head> tag. We need
47794914 // to close it since the main content won't have it
47804915 writeChunk ( destination , endChunkForTag ( 'head' ) ) ;
47814916 }
4917+
4918+ const bodyChunks = preamble . bodyChunks ;
4919+ if ( bodyChunks ) {
4920+ for ( let i = 0 ; i < bodyChunks . length ; i ++ ) {
4921+ writeChunk ( destination , bodyChunks [ i ] ) ;
4922+ }
4923+ }
47824924}
47834925
47844926// We don't bother reporting backpressure at the moment because we expect to
0 commit comments