8
8
*/
9
9
10
10
import type { ReactNodeList , ReactCustomFormAction } from 'shared/ReactTypes' ;
11
+ import type { FizzPreamble } from 'react-server/src/ReactFizzPreamble' ;
11
12
import type {
12
13
CrossOriginEnum ,
13
14
PreloadImplOptions ,
@@ -49,6 +50,7 @@ import {
49
50
getRenderState ,
50
51
flushResources ,
51
52
} from 'react-server/src/ReactFizzServer' ;
53
+ import { createFizzPreamble } from 'react-server/src/ReactFizzPreamble' ;
52
54
53
55
import isAttributeNameSafe from '../shared/isAttributeNameSafe' ;
54
56
import isUnitlessNumber from '../shared/isUnitlessNumber' ;
@@ -135,8 +137,7 @@ export type RenderState = {
135
137
// be null or empty when resuming.
136
138
137
139
// preamble chunks
138
- htmlChunks : null | Array < Chunk | PrecomputedChunk > ,
139
- headChunks : null | Array < Chunk | PrecomputedChunk > ,
140
+ preamble : PreambleState ,
140
141
141
142
// external runtime script chunks
142
143
externalRuntimeScript : null | ExternalRuntimeScript ,
@@ -442,8 +443,7 @@ export function createRenderState(
442
443
segmentPrefix : stringToPrecomputedChunk ( idPrefix + 'S:' ) ,
443
444
boundaryPrefix : stringToPrecomputedChunk ( idPrefix + 'B:' ) ,
444
445
startInlineScript : inlineScriptWithNonce ,
445
- htmlChunks : null ,
446
- headChunks : null ,
446
+ preamble : createPreambleState ( ) ,
447
447
448
448
externalRuntimeScript : externalRuntimeScript ,
449
449
bootstrapChunks : bootstrapChunks ,
@@ -686,6 +686,19 @@ export function completeResumableState(resumableState: ResumableState): void {
686
686
resumableState . bootstrapModules = undefined ;
687
687
}
688
688
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
+
689
702
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
690
703
// modes. We only include the variants as they matter for the sake of our purposes.
691
704
// 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.
694
707
// still makes sense
695
708
const HTML_HTML_MODE = 1 ; // Used for the <html> if it is at the top level.
696
709
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 ;
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 ;
703
717
// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
704
718
// still makes sense
705
719
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 ;
707
721
708
722
const NO_SCOPE = /* */ 0b00 ;
709
723
const NOSCRIPT_SCOPE = /* */ 0b01 ;
@@ -728,6 +742,10 @@ function createFormatContext(
728
742
} ;
729
743
}
730
744
745
+ export function canHavePreamble ( formatContext : FormatContext ) : boolean {
746
+ return formatContext . insertionMode < HTML_MODE ;
747
+ }
748
+
731
749
export function createRootFormatContext ( namespaceURI ?: string ) : FormatContext {
732
750
const insertionMode =
733
751
namespaceURI === 'http://www.w3.org/2000/svg'
@@ -807,6 +825,10 @@ export function getChildFormatContext(
807
825
return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
808
826
}
809
827
} else if ( parentContext . insertionMode === HTML_HTML_MODE ) {
828
+ if ( type === 'head' ) {
829
+ // We've emitted the document element and is now in <head> mode.
830
+ return createFormatContext ( HTML_HEAD_MODE , null , parentContext . tagScope ) ;
831
+ }
810
832
// We've emitted the document element and is now in plain HTML mode.
811
833
return createFormatContext ( HTML_MODE , null , parentContext . tagScope ) ;
812
834
}
@@ -3185,29 +3207,71 @@ function pushStartHead(
3185
3207
target : Array < Chunk | PrecomputedChunk > ,
3186
3208
props : Object ,
3187
3209
renderState : RenderState ,
3210
+ preambleState : null | PreambleState ,
3188
3211
insertionMode : InsertionMode ,
3189
- ) : ReactNodeList {
3190
- if ( insertionMode < HTML_MODE && renderState . headChunks === null ) {
3212
+ ) : ReactNodeList | FizzPreamble {
3213
+ if ( insertionMode < HTML_MODE ) {
3191
3214
// This <head> is the Document.head and should be part of the preamble
3192
- renderState . headChunks = [ ] ;
3193
- return pushStartGenericElement ( renderState . headChunks , props , 'head' ) ;
3215
+ const preamble = preambleState || renderState . preamble ;
3216
+
3217
+ if ( preamble . headChunks ) {
3218
+ throw new Error ( `${ '<head>' } may only be rendered once per application` ) ;
3219
+ }
3220
+ preamble . headChunks = [ ] ;
3221
+ const children = pushStartGenericElement (
3222
+ preamble . headChunks ,
3223
+ props ,
3224
+ 'head' ,
3225
+ ) ;
3226
+ return createFizzPreamble ( children ) ;
3194
3227
} else {
3195
3228
// This <head> is deep and is likely just an error. we emit it inline though.
3196
3229
// Validation should warn that this tag is the the wrong spot.
3197
3230
return pushStartGenericElement ( target , props , 'head' ) ;
3198
3231
}
3199
3232
}
3200
3233
3201
- function pushStartHtml (
3234
+ function pushStartBody (
3202
3235
target : Array < Chunk | PrecomputedChunk > ,
3203
3236
props : Object ,
3204
3237
renderState : RenderState ,
3238
+ preambleState : null | PreambleState ,
3205
3239
insertionMode : InsertionMode ,
3206
3240
) : 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' ) ;
3241
+ if ( insertionMode < HTML_MODE ) {
3242
+ // This <body> is the Document.body
3243
+ const preamble = preambleState || renderState . preamble ;
3244
+
3245
+ if ( preamble . bodyChunks ) {
3246
+ throw new Error ( `${ '<body>' } may only be rendered once per application` ) ;
3247
+ }
3248
+
3249
+ preamble . bodyChunks = [ ] ;
3250
+ return pushStartGenericElement ( preamble . bodyChunks , props , 'body' ) ;
3251
+ } else {
3252
+ // This <head> is deep and is likely just an error. we emit it inline though.
3253
+ // Validation should warn that this tag is the the wrong spot.
3254
+ return pushStartGenericElement ( target , props , 'body' ) ;
3255
+ }
3256
+ }
3257
+
3258
+ function pushStartHtml (
3259
+ target : Array < Chunk | PrecomputedChunk > ,
3260
+ props : Object ,
3261
+ renderState : RenderState ,
3262
+ preambleState : null | PreambleState ,
3263
+ insertionMode : InsertionMode ,
3264
+ ) : ReactNodeList | FizzPreamble {
3265
+ if ( insertionMode === ROOT_HTML_MODE ) {
3266
+ // This <html> is the Document.documentElement
3267
+ const preamble = preambleState || renderState . preamble ;
3268
+
3269
+ if ( preamble . htmlChunks ) {
3270
+ throw new Error ( `${ '<html>' } may only be rendered once per application` ) ;
3271
+ }
3272
+
3273
+ preamble . htmlChunks = [ DOCTYPE ] ;
3274
+ return pushStartGenericElement ( preamble . htmlChunks , props , 'html' ) ;
3211
3275
} else {
3212
3276
// This <html> is deep and is likely just an error. we emit it inline though.
3213
3277
// Validation should warn that this tag is the the wrong spot.
@@ -3562,11 +3626,12 @@ export function pushStartInstance(
3562
3626
props : Object ,
3563
3627
resumableState : ResumableState ,
3564
3628
renderState : RenderState ,
3629
+ preambleState : null | PreambleState ,
3565
3630
hoistableState : null | HoistableState ,
3566
3631
formatContext : FormatContext ,
3567
3632
textEmbedded : boolean ,
3568
3633
isFallback : boolean ,
3569
- ) : ReactNodeList {
3634
+ ) : ReactNodeList | FizzPreamble {
3570
3635
if ( __DEV__ ) {
3571
3636
validateARIAProperties ( type , props ) ;
3572
3637
validateInputProperties ( type , props ) ;
@@ -3729,13 +3794,23 @@ export function pushStartInstance(
3729
3794
target ,
3730
3795
props ,
3731
3796
renderState ,
3797
+ preambleState ,
3798
+ formatContext . insertionMode ,
3799
+ ) ;
3800
+ case 'body' :
3801
+ return pushStartBody (
3802
+ target ,
3803
+ props ,
3804
+ renderState ,
3805
+ preambleState ,
3732
3806
formatContext . insertionMode ,
3733
3807
) ;
3734
3808
case 'html' : {
3735
3809
return pushStartHtml (
3736
3810
target ,
3737
3811
props ,
3738
3812
renderState ,
3813
+ preambleState ,
3739
3814
formatContext . insertionMode ,
3740
3815
) ;
3741
3816
}
@@ -3814,10 +3889,31 @@ export function pushEndInstance(
3814
3889
return ;
3815
3890
}
3816
3891
break ;
3892
+ case 'head' :
3893
+ if ( formatContext . insertionMode <= HTML_HTML_MODE ) {
3894
+ return ;
3895
+ }
3896
+ break ;
3817
3897
}
3818
3898
target . push ( endChunkForTag ( type ) ) ;
3819
3899
}
3820
3900
3901
+ export function preparePreamble (
3902
+ renderState : RenderState ,
3903
+ preambleState : PreambleState ,
3904
+ ) {
3905
+ const rootPreamble = renderState . preamble ;
3906
+ if ( rootPreamble . htmlChunks === null ) {
3907
+ rootPreamble . htmlChunks = preambleState . htmlChunks ;
3908
+ }
3909
+ if ( rootPreamble . headChunks === null ) {
3910
+ rootPreamble . headChunks = preambleState . headChunks ;
3911
+ }
3912
+ if ( rootPreamble . bodyChunks === null ) {
3913
+ rootPreamble . bodyChunks = preambleState . bodyChunks ;
3914
+ }
3915
+ }
3916
+
3821
3917
function writeBootstrap (
3822
3918
destination : Destination ,
3823
3919
renderState : RenderState ,
@@ -4033,6 +4129,7 @@ export function writeStartSegment(
4033
4129
switch ( formatContext . insertionMode ) {
4034
4130
case ROOT_HTML_MODE :
4035
4131
case HTML_HTML_MODE :
4132
+ case HTML_HEAD_MODE :
4036
4133
case HTML_MODE : {
4037
4134
writeChunk ( destination , startSegmentHTML ) ;
4038
4135
writeChunk ( destination , renderState . segmentPrefix ) ;
@@ -4091,6 +4188,7 @@ export function writeEndSegment(
4091
4188
switch ( formatContext . insertionMode ) {
4092
4189
case ROOT_HTML_MODE :
4093
4190
case HTML_HTML_MODE :
4191
+ case HTML_HEAD_MODE :
4094
4192
case HTML_MODE : {
4095
4193
return writeChunkAndReturn ( destination , endSegmentHTML ) ;
4096
4194
}
@@ -4679,7 +4777,7 @@ function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
4679
4777
// flush the entire preamble in a single pass. This probably should be modified
4680
4778
// in the future to be backpressure sensitive but that requires a larger refactor
4681
4779
// of the flushing code in Fizz.
4682
- export function writePreamble (
4780
+ export function writePreambleStart (
4683
4781
destination : Destination ,
4684
4782
resumableState : ResumableState ,
4685
4783
renderState : RenderState ,
@@ -4700,8 +4798,10 @@ export function writePreamble(
4700
4798
internalPreinitScript ( resumableState , renderState , src , chunks ) ;
4701
4799
}
4702
4800
4703
- const htmlChunks = renderState . htmlChunks ;
4704
- const headChunks = renderState . headChunks ;
4801
+ const preamble = renderState . preamble ;
4802
+
4803
+ const htmlChunks = preamble . htmlChunks ;
4804
+ const headChunks = preamble . headChunks ;
4705
4805
4706
4806
let i = 0 ;
4707
4807
@@ -4773,12 +4873,31 @@ export function writePreamble(
4773
4873
writeChunk ( destination , hoistableChunks [ i ] ) ;
4774
4874
}
4775
4875
hoistableChunks . length = 0 ;
4876
+ }
4776
4877
4777
- if ( htmlChunks && headChunks === null ) {
4878
+ // We don't bother reporting backpressure at the moment because we expect to
4879
+ // flush the entire preamble in a single pass. This probably should be modified
4880
+ // in the future to be backpressure sensitive but that requires a larger refactor
4881
+ // of the flushing code in Fizz.
4882
+ export function writePreambleEnd (
4883
+ destination : Destination ,
4884
+ renderState : RenderState ,
4885
+ ) : void {
4886
+ const preamble = renderState . preamble ;
4887
+ const htmlChunks = preamble . htmlChunks ;
4888
+ const headChunks = preamble . headChunks ;
4889
+ if ( htmlChunks || headChunks ) {
4778
4890
// we have an <html> but we inserted an implicit <head> tag. We need
4779
4891
// to close it since the main content won't have it
4780
4892
writeChunk ( destination , endChunkForTag ( 'head' ) ) ;
4781
4893
}
4894
+
4895
+ const bodyChunks = preamble . bodyChunks ;
4896
+ if ( bodyChunks ) {
4897
+ for ( let i = 0 ; i < bodyChunks . length ; i ++ ) {
4898
+ writeChunk ( destination , bodyChunks [ i ] ) ;
4899
+ }
4900
+ }
4782
4901
}
4783
4902
4784
4903
// We don't bother reporting backpressure at the moment because we expect to
0 commit comments