@@ -135,8 +135,7 @@ export type RenderState = {
135
135
// be null or empty when resuming.
136
136
137
137
// preamble chunks
138
- htmlChunks : null | Array < Chunk | PrecomputedChunk > ,
139
- headChunks : null | Array < Chunk | PrecomputedChunk > ,
138
+ preamble : PreambleState ,
140
139
141
140
// external runtime script chunks
142
141
externalRuntimeScript : null | ExternalRuntimeScript ,
@@ -442,8 +441,7 @@ export function createRenderState(
442
441
segmentPrefix : stringToPrecomputedChunk ( idPrefix + 'S:' ) ,
443
442
boundaryPrefix : stringToPrecomputedChunk ( idPrefix + 'B:' ) ,
444
443
startInlineScript : inlineScriptWithNonce ,
445
- htmlChunks : null ,
446
- headChunks : null ,
444
+ preamble : createPreambleState ( ) ,
447
445
448
446
externalRuntimeScript : externalRuntimeScript ,
449
447
bootstrapChunks : bootstrapChunks ,
@@ -686,6 +684,19 @@ export function completeResumableState(resumableState: ResumableState): void {
686
684
resumableState . bootstrapModules = undefined ;
687
685
}
688
686
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
+
689
700
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
690
701
// modes. We only include the variants as they matter for the sake of our purposes.
691
702
// 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.
694
705
// still makes sense
695
706
const HTML_HTML_MODE = 1 ; // Used for the <html> if it is at the top level.
696
707
const 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 ;
703
715
// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
704
716
// still makes sense
705
717
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 ;
707
719
708
720
const NO_SCOPE = /* */ 0b00 ;
709
721
const NOSCRIPT_SCOPE = /* */ 0b01 ;
@@ -728,6 +740,10 @@ function createFormatContext(
728
740
} ;
729
741
}
730
742
743
+ export function canHavePreamble ( formatContext : FormatContext ) : boolean {
744
+ return formatContext . insertionMode < HTML_MODE ;
745
+ }
746
+
731
747
export function createRootFormatContext ( namespaceURI ?: string ) : FormatContext {
732
748
const insertionMode =
733
749
namespaceURI === 'http://www.w3.org/2000/svg'
@@ -792,27 +808,42 @@ export function getChildFormatContext(
792
808
null ,
793
809
parentContext . tagScope ,
794
810
) ;
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 ;
795
831
}
796
832
if ( parentContext . insertionMode >= HTML_TABLE_MODE ) {
797
833
// Whatever tag this was, it wasn't a table parent or other special parent, so we must have
798
834
// entered plain HTML again.
799
835
return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
800
836
}
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 ) {
811
838
return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
812
839
}
813
840
return parentContext ;
814
841
}
815
842
843
+ export function isPreambleContext ( formatContext : FormatContext ) : boolean {
844
+ return formatContext . insertionMode === HTML_HEAD_MODE ;
845
+ }
846
+
816
847
export function makeId (
817
848
resumableState : ResumableState ,
818
849
treeId : string ,
@@ -3185,29 +3216,66 @@ function pushStartHead(
3185
3216
target : Array < Chunk | PrecomputedChunk > ,
3186
3217
props : Object ,
3187
3218
renderState : RenderState ,
3219
+ preambleState : null | PreambleState ,
3188
3220
insertionMode : InsertionMode ,
3189
3221
) : ReactNodeList {
3190
- if ( insertionMode < HTML_MODE && renderState . headChunks === null ) {
3222
+ if ( insertionMode < HTML_MODE ) {
3191
3223
// 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' ) ;
3194
3231
} else {
3195
3232
// This <head> is deep and is likely just an error. we emit it inline though.
3196
3233
// Validation should warn that this tag is the the wrong spot.
3197
3234
return pushStartGenericElement ( target , props , 'head' ) ;
3198
3235
}
3199
3236
}
3200
3237
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
+
3201
3262
function pushStartHtml (
3202
3263
target : Array < Chunk | PrecomputedChunk > ,
3203
3264
props : Object ,
3204
3265
renderState : RenderState ,
3266
+ preambleState : null | PreambleState ,
3205
3267
insertionMode : InsertionMode ,
3206
3268
) : 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' ) ;
3211
3279
} else {
3212
3280
// This <html> is deep and is likely just an error. we emit it inline though.
3213
3281
// Validation should warn that this tag is the the wrong spot.
@@ -3562,6 +3630,7 @@ export function pushStartInstance(
3562
3630
props : Object ,
3563
3631
resumableState : ResumableState ,
3564
3632
renderState : RenderState ,
3633
+ preambleState : null | PreambleState ,
3565
3634
hoistableState : null | HoistableState ,
3566
3635
formatContext : FormatContext ,
3567
3636
textEmbedded : boolean ,
@@ -3729,13 +3798,23 @@ export function pushStartInstance(
3729
3798
target ,
3730
3799
props ,
3731
3800
renderState ,
3801
+ preambleState ,
3802
+ formatContext . insertionMode ,
3803
+ ) ;
3804
+ case 'body' :
3805
+ return pushStartBody (
3806
+ target ,
3807
+ props ,
3808
+ renderState ,
3809
+ preambleState ,
3732
3810
formatContext . insertionMode ,
3733
3811
) ;
3734
3812
case 'html' : {
3735
3813
return pushStartHtml (
3736
3814
target ,
3737
3815
props ,
3738
3816
renderState ,
3817
+ preambleState ,
3739
3818
formatContext . insertionMode ,
3740
3819
) ;
3741
3820
}
@@ -3814,10 +3893,50 @@ export function pushEndInstance(
3814
3893
return ;
3815
3894
}
3816
3895
break ;
3896
+ case 'head' :
3897
+ if ( formatContext . insertionMode <= HTML_HTML_MODE ) {
3898
+ return ;
3899
+ }
3900
+ break ;
3817
3901
}
3818
3902
target . push ( endChunkForTag ( type ) ) ;
3819
3903
}
3820
3904
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
+
3821
3940
function writeBootstrap (
3822
3941
destination : Destination ,
3823
3942
renderState : RenderState ,
@@ -4033,6 +4152,7 @@ export function writeStartSegment(
4033
4152
switch ( formatContext . insertionMode ) {
4034
4153
case ROOT_HTML_MODE :
4035
4154
case HTML_HTML_MODE :
4155
+ case HTML_HEAD_MODE :
4036
4156
case HTML_MODE : {
4037
4157
writeChunk ( destination , startSegmentHTML ) ;
4038
4158
writeChunk ( destination , renderState . segmentPrefix ) ;
@@ -4091,6 +4211,7 @@ export function writeEndSegment(
4091
4211
switch ( formatContext . insertionMode ) {
4092
4212
case ROOT_HTML_MODE :
4093
4213
case HTML_HTML_MODE :
4214
+ case HTML_HEAD_MODE :
4094
4215
case HTML_MODE : {
4095
4216
return writeChunkAndReturn ( destination , endSegmentHTML ) ;
4096
4217
}
@@ -4679,7 +4800,7 @@ function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
4679
4800
// flush the entire preamble in a single pass. This probably should be modified
4680
4801
// in the future to be backpressure sensitive but that requires a larger refactor
4681
4802
// of the flushing code in Fizz.
4682
- export function writePreamble (
4803
+ export function writePreambleStart (
4683
4804
destination : Destination ,
4684
4805
resumableState : ResumableState ,
4685
4806
renderState : RenderState ,
@@ -4700,8 +4821,10 @@ export function writePreamble(
4700
4821
internalPreinitScript ( resumableState , renderState , src , chunks ) ;
4701
4822
}
4702
4823
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 ;
4705
4828
4706
4829
let i = 0 ;
4707
4830
@@ -4773,12 +4896,31 @@ export function writePreamble(
4773
4896
writeChunk ( destination , hoistableChunks [ i ] ) ;
4774
4897
}
4775
4898
hoistableChunks . length = 0 ;
4899
+ }
4776
4900
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 ) {
4778
4913
// we have an <html> but we inserted an implicit <head> tag. We need
4779
4914
// to close it since the main content won't have it
4780
4915
writeChunk ( destination , endChunkForTag ( 'head' ) ) ;
4781
4916
}
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
+ }
4782
4924
}
4783
4925
4784
4926
// We don't bother reporting backpressure at the moment because we expect to
0 commit comments