main
js 7,239 lines 238 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 ReactNodeList,
12 ReactCustomFormAction,
13 Thenable,
14 } from 'shared/ReactTypes';
15 import type {
16 CrossOriginEnum,
17 PreloadImplOptions,
18 PreloadModuleImplOptions,
19 PreinitStyleOptions,
20 PreinitScriptOptions,
21 PreinitModuleScriptOptions,
22 ImportMap,
23 } from 'react-dom/src/shared/ReactDOMTypes';
24
25 import {
26 checkHtmlStringCoercion,
27 checkCSSPropertyStringCoercion,
28 checkAttributeStringCoercion,
29 checkOptionStringCoercion,
30 } from 'shared/CheckStringCoercion';
31
32 import {Children} from 'react';
33
34 import {
35 enableFizzExternalRuntime,
36 enableSrcObject,
37 enableFizzBlockingRender,
38 enableViewTransition,
39 enableViewTransitionParentEnterExit,
40 } from 'shared/ReactFeatureFlags';
41
42 import type {
43 Destination,
44 Chunk,
45 PrecomputedChunk,
46 } from 'react-server/src/ReactServerStreamConfig';
47
48 import type {FormStatus} from '../shared/ReactDOMFormActions';
49
50 import {
51 writeChunk,
52 writeChunkAndReturn,
53 stringToChunk,
54 stringToPrecomputedChunk,
55 readAsDataURL,
56 } from 'react-server/src/ReactServerStreamConfig';
57 import {
58 resolveRequest,
59 getResumableState,
60 getRenderState,
61 flushResources,
62 } from 'react-server/src/ReactFizzServer';
63
64 import isAttributeNameSafe from '../shared/isAttributeNameSafe';
65 import isUnitlessNumber from '../shared/isUnitlessNumber';
66 import getAttributeAlias from '../shared/getAttributeAlias';
67
68 import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
69 import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
70 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
71 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
72 import warnValidStyle from '../shared/warnValidStyle';
73 import {getCrossOriginString} from '../shared/crossOriginStrings';
74
75 import escapeTextForBrowser from './escapeTextForBrowser';
76 import hyphenateStyleName from '../shared/hyphenateStyleName';
77 import hasOwnProperty from 'shared/hasOwnProperty';
78 import sanitizeURL from '../shared/sanitizeURL';
79 import isArray from 'shared/isArray';
80
81 import {
82 clientRenderBoundary as clientRenderFunction,
83 completeBoundary as completeBoundaryFunction,
84 completeBoundaryUpgradeToViewTransitions as upgradeToViewTransitionsInstruction,
85 completeBoundaryWithStyles as styleInsertionFunction,
86 completeSegment as completeSegmentFunction,
87 formReplaying as formReplayingRuntime,
88 markShellTime,
89 } from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings';
90
91 import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMResourceValidation';
92
93 import {NotPending} from '../shared/ReactDOMFormActions';
94
95 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
96
97 const previousDispatcher =
98 ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */
99 ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */ = {
100 f /* flushSyncWork */: previousDispatcher.f /* flushSyncWork */,
101 r /* requestFormReset */: previousDispatcher.r /* requestFormReset */,
102 D /* prefetchDNS */: prefetchDNS,
103 C /* preconnect */: preconnect,
104 L /* preload */: preload,
105 m /* preloadModule */: preloadModule,
106 X /* preinitScript */: preinitScript,
107 S /* preinitStyle */: preinitStyle,
108 M /* preinitModuleScript */: preinitModuleScript,
109 };
110
111 // We make every property of the descriptor optional because it is not a contract that
112 // the headers provided by onHeaders has any particular header types.
113 export type HeadersDescriptor = {
114 Link?: string,
115 };
116
117 // Used to distinguish these contexts from ones used in other renderers.
118 // E.g. this can be used to distinguish legacy renderers from this modern one.
119 export const isPrimaryRenderer = true;
120
121 export const supportsClientAPIs = true;
122
123 export type StreamingFormat = 0 | 1;
124 const ScriptStreamingFormat: StreamingFormat = 0;
125 const DataStreamingFormat: StreamingFormat = 1;
126
127 export type InstructionState = number;
128 const NothingSent /* */ = 0b000000000;
129 const SentCompleteSegmentFunction /* */ = 0b000000001;
130 const SentCompleteBoundaryFunction /* */ = 0b000000010;
131 const SentClientRenderFunction /* */ = 0b000000100;
132 const SentStyleInsertionFunction /* */ = 0b000001000;
133 const SentFormReplayingRuntime /* */ = 0b000010000;
134 const SentCompletedShellId /* */ = 0b000100000;
135 const SentMarkShellTime /* */ = 0b001000000;
136 const NeedUpgradeToViewTransitions /* */ = 0b010000000;
137 const SentUpgradeToViewTransitions /* */ = 0b100000000;
138
139 type NonceOption =
140 | string
141 | {
142 script?: string,
143 style?: string,
144 };
145
146 // Per request, global state that is not contextual to the rendering subtree.
147 // This cannot be resumed and therefore should only contain things that are
148 // temporary working state or are never used in the prerender pass.
149 export type RenderState = {
150 // These can be recreated from resumable state.
151 placeholderPrefix: PrecomputedChunk,
152 segmentPrefix: PrecomputedChunk,
153 boundaryPrefix: PrecomputedChunk,
154
155 // inline script streaming format, unused if using external runtime / data
156 startInlineScript: PrecomputedChunk,
157
158 startInlineStyle: PrecomputedChunk,
159
160 // the preamble must always flush before resuming, so all these chunks must
161 // be null or empty when resuming.
162
163 // preamble chunks
164 preamble: PreambleState,
165
166 // external runtime script chunks
167 externalRuntimeScript: null | ExternalRuntimeScript,
168 bootstrapChunks: Array<Chunk | PrecomputedChunk>,
169 importMapChunks: Array<Chunk | PrecomputedChunk>,
170
171 // Hoistable chunks
172 charsetChunks: Array<Chunk | PrecomputedChunk>,
173 viewportChunks: Array<Chunk | PrecomputedChunk>,
174 hoistableChunks: Array<Chunk | PrecomputedChunk>,
175
176 // Headers queues for Resources that can flush early
177 onHeaders: void | ((headers: HeadersDescriptor) => void),
178 headers: null | {
179 preconnects: string,
180 fontPreloads: string,
181 highImagePreloads: string,
182 remainingCapacity: number,
183 },
184 resets: {
185 // corresponds to ResumableState.unknownResources["font"]
186 font: {
187 [href: string]: Preloaded,
188 },
189 // the rest correspond to ResumableState[<...>Resources]
190 dns: {[key: string]: Exists},
191 connect: {
192 default: {[key: string]: Exists},
193 anonymous: {[key: string]: Exists},
194 credentials: {[key: string]: Exists},
195 },
196 image: {
197 [key: string]: Preloaded,
198 },
199 style: {
200 [key: string]: Exists | Preloaded | PreloadedWithCredentials,
201 },
202 },
203
204 // Flushing queues for Resource dependencies
205 preconnects: Set<Resource>,
206 fontPreloads: Set<Resource>,
207 highImagePreloads: Set<Resource>,
208 // usedImagePreloads: Set<PreloadResource>,
209 styles: Map<string, StyleQueue>,
210 bootstrapScripts: Set<Resource>,
211 scripts: Set<Resource>,
212 bulkPreloads: Set<Resource>,
213
214 // Temporarily keeps track of key to preload resources before shell flushes.
215 preloads: {
216 images: Map<string, Resource>,
217 stylesheets: Map<string, Resource>,
218 scripts: Map<string, Resource>,
219 moduleScripts: Map<string, Resource>,
220 },
221
222 nonce: {
223 script: string | void,
224 style: string | void,
225 },
226
227 // Module-global-like reference for flushing/hoisting state of style resources
228 // We need to track whether the current request has flushed any style resources
229 // without sending an instruction to hoist them. we do that here
230 stylesToHoist: boolean,
231
232 // We allow the legacy renderer to extend this object.
233
234 ...
235 };
236
237 type Exists = null;
238 type Preloaded = [];
239 // Credentials here are things that affect whether a browser will make a request
240 // as well as things that affect which connection the browser will use for that request.
241 // We want these to be aligned across preloads and resources because otherwise the preload
242 // will be wasted.
243 // We investigated whether referrerPolicy should be included here but from experimentation
244 // it seems that browsers do not treat this as part of the http cache key and does not affect
245 // which connection is used.
246 type PreloadedWithCredentials = [
247 /* crossOrigin */ ?CrossOriginEnum,
248 /* integrity */ ?string,
249 ];
250
251 const EXISTS: Exists = null;
252 // This constant is to mark preloads that have no unique credentials
253 // to convey. It should never be checked by identity and we should not
254 // assume Preload values in ResumableState equal this value because they
255 // will have come from some parsed input.
256 const PRELOAD_NO_CREDS: Preloaded = [];
257 if (__DEV__) {
258 Object.freeze(PRELOAD_NO_CREDS);
259 }
260
261 // Per response, global state that is not contextual to the rendering subtree.
262 // This is resumable and therefore should be serializable.
263 export type ResumableState = {
264 idPrefix: string,
265 nextFormID: number,
266 streamingFormat: StreamingFormat,
267
268 // We carry the bootstrap intializers in resumable state in case we postpone in the shell
269 // of a prerender. On resume we will reinitialize the bootstrap scripts if necessary.
270 // If we end up flushing the bootstrap scripts we void these on the resumable state
271 bootstrapScriptContent?: string | void,
272 bootstrapScripts?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
273 bootstrapModules?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
274
275 // state for script streaming format, unused if using external runtime / data
276 instructions: InstructionState,
277
278 // postamble state
279 hasBody: boolean,
280 hasHtml: boolean,
281
282 // Resources - Request local cache
283 unknownResources: {
284 [asType: string]: {
285 [href: string]: Preloaded,
286 },
287 },
288 dnsResources: {[key: string]: Exists},
289 connectResources: {
290 default: {[key: string]: Exists},
291 anonymous: {[key: string]: Exists},
292 credentials: {[key: string]: Exists},
293 },
294 imageResources: {
295 [key: string]: Preloaded,
296 },
297 styleResources: {
298 [key: string]: Exists | Preloaded | PreloadedWithCredentials,
299 },
300 scriptResources: {
301 [key: string]: Exists | Preloaded | PreloadedWithCredentials,
302 },
303 moduleUnknownResources: {
304 [asType: string]: {
305 [href: string]: Preloaded,
306 },
307 },
308 moduleScriptResources: {
309 [key: string]: Exists | Preloaded | PreloadedWithCredentials,
310 },
311 };
312
313 let currentlyFlushingRenderState: RenderState | null = null;
314
315 const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');
316
317 const startInlineScript = stringToPrecomputedChunk('<script');
318 const endInlineScript = stringToPrecomputedChunk('</script>');
319
320 const startScriptSrc = stringToPrecomputedChunk('<script src="');
321 const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
322 const scriptNonce = stringToPrecomputedChunk(' nonce="');
323 const scriptIntegirty = stringToPrecomputedChunk(' integrity="');
324 const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="');
325 const endAsyncScript = stringToPrecomputedChunk(' async=""></script>');
326
327 const startInlineStyle = stringToPrecomputedChunk('<style');
328
329 /**
330 * This escaping function is designed to work with with inline scripts where the entire
331 * contents are escaped. Because we know we are escaping the entire script we can avoid for instance
332 * escaping html comment string sequences that are valid javascript as well because
333 * if there are no sebsequent <script sequences the html parser will never enter
334 * script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)
335 *
336 * While untrusted script content should be made safe before using this api it will
337 * ensure that the script cannot be early terminated or never terminated state
338 */
339 function escapeEntireInlineScriptContent(scriptText: string) {
340 if (__DEV__) {
341 checkHtmlStringCoercion(scriptText);
342 }
343 return ('' + scriptText).replace(scriptRegex, scriptReplacer);
344 }
345 const scriptRegex = /(<\/|<)(s)(cript)/gi;
346 const scriptReplacer = (
347 match: string,
348 prefix: string,
349 s: string,
350 suffix: string,
351 ) => `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;
352
353 export type BootstrapScriptDescriptor = {
354 src: string,
355 integrity?: string,
356 crossOrigin?: string,
357 };
358 export type ExternalRuntimeScript = {
359 src: string,
360 chunks: Array<Chunk | PrecomputedChunk>,
361 };
362
363 const importMapScriptStart = stringToPrecomputedChunk(
364 '<script type="importmap">',
365 );
366 const importMapScriptEnd = stringToPrecomputedChunk('</script>');
367
368 // Since we store headers as strings we deal with their length in utf16 code units
369 // rather than visual characters or the utf8 encoding that is used for most binary
370 // serialization. Some common HTTP servers only allow for headers to be 4kB in length.
371 // We choose a default length that is likely to be well under this already limited length however
372 // pathological cases may still cause the utf-8 encoding of the headers to approach this limit.
373 // It should also be noted that this maximum is a soft maximum. we have not reached the limit we will
374 // allow one more header to be captured which means in practice if the limit is approached it will be exceeded
375 const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;
376
377 let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
378 if (__DEV__) {
379 didWarnForNewBooleanPropsWithEmptyValue = {};
380 }
381
382 // Allows us to keep track of what we've already written so we can refer back to it.
383 // if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
384 // is set, the server will send instructions via data attributes (instead of inline scripts)
385 export function createRenderState(
386 resumableState: ResumableState,
387 nonce:
388 | string
389 | {
390 script?: string,
391 style?: string,
392 }
393 | void,
394 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
395 importMap: ImportMap | void,
396 onHeaders: void | ((headers: HeadersDescriptor) => void),
397 maxHeadersLength: void | number,
398 ): RenderState {
399 const nonceScript = typeof nonce === 'string' ? nonce : nonce && nonce.script;
400 const inlineScriptWithNonce =
401 nonceScript === undefined
402 ? startInlineScript
403 : stringToPrecomputedChunk(
404 '<script nonce="' + escapeTextForBrowser(nonceScript) + '"',
405 );
406 const nonceStyle =
407 typeof nonce === 'string' ? undefined : nonce && nonce.style;
408 const inlineStyleWithNonce =
409 nonceStyle === undefined
410 ? startInlineStyle
411 : stringToPrecomputedChunk(
412 '<style nonce="' + escapeTextForBrowser(nonceStyle) + '"',
413 );
414 const idPrefix = resumableState.idPrefix;
415
416 const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
417 let externalRuntimeScript: null | ExternalRuntimeScript = null;
418 const {bootstrapScriptContent, bootstrapScripts, bootstrapModules} =
419 resumableState;
420 if (bootstrapScriptContent !== undefined) {
421 bootstrapChunks.push(inlineScriptWithNonce);
422 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
423 bootstrapChunks.push(
424 endOfStartTag,
425 stringToChunk(escapeEntireInlineScriptContent(bootstrapScriptContent)),
426 endInlineScript,
427 );
428 }
429 if (enableFizzExternalRuntime) {
430 if (externalRuntimeConfig !== undefined) {
431 if (typeof externalRuntimeConfig === 'string') {
432 externalRuntimeScript = {
433 src: externalRuntimeConfig,
434 chunks: [],
435 };
436 pushScriptImpl(externalRuntimeScript.chunks, {
437 src: externalRuntimeConfig,
438 async: true,
439 integrity: undefined,
440 nonce: nonceScript,
441 });
442 } else {
443 externalRuntimeScript = {
444 src: externalRuntimeConfig.src,
445 chunks: [],
446 };
447 pushScriptImpl(externalRuntimeScript.chunks, {
448 src: externalRuntimeConfig.src,
449 async: true,
450 integrity: externalRuntimeConfig.integrity,
451 nonce: nonceScript,
452 });
453 }
454 }
455 }
456
457 const importMapChunks: Array<Chunk | PrecomputedChunk> = [];
458 if (importMap !== undefined) {
459 const map = importMap;
460 importMapChunks.push(importMapScriptStart);
461 importMapChunks.push(
462 stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map))),
463 );
464 importMapChunks.push(importMapScriptEnd);
465 }
466 if (__DEV__) {
467 if (onHeaders && typeof maxHeadersLength === 'number') {
468 if (maxHeadersLength <= 0) {
469 console.error(
470 'React expected a positive non-zero `maxHeadersLength` option but found %s instead. When using the `onHeaders` option you may supply an optional `maxHeadersLength` option as well however, when setting this value to zero or less no headers will be captured.',
471 maxHeadersLength === 0 ? 'zero' : maxHeadersLength,
472 );
473 }
474 }
475 }
476 const headers = onHeaders
477 ? {
478 preconnects: '',
479 fontPreloads: '',
480 highImagePreloads: '',
481 remainingCapacity:
482 // We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity
483 // we always assume we are inserting an interstitial ", " however the first header does not actually
484 // consume these two extra bytes.
485 2 +
486 (typeof maxHeadersLength === 'number'
487 ? maxHeadersLength
488 : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS),
489 }
490 : null;
491 const renderState: RenderState = {
492 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
493 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
494 boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),
495 startInlineScript: inlineScriptWithNonce,
496 startInlineStyle: inlineStyleWithNonce,
497 preamble: createPreambleState(),
498
499 externalRuntimeScript: externalRuntimeScript,
500 bootstrapChunks: bootstrapChunks,
501 importMapChunks,
502
503 onHeaders,
504 headers,
505 resets: {
506 font: {},
507 dns: {},
508 connect: {
509 default: {},
510 anonymous: {},
511 credentials: {},
512 },
513 image: {},
514 style: {},
515 },
516
517 charsetChunks: [],
518 viewportChunks: [],
519 hoistableChunks: [],
520
521 // cleared on flush
522 preconnects: new Set(),
523 fontPreloads: new Set(),
524 highImagePreloads: new Set(),
525 // usedImagePreloads: new Set(),
526 styles: new Map(),
527 bootstrapScripts: new Set(),
528 scripts: new Set(),
529 bulkPreloads: new Set(),
530
531 preloads: {
532 images: new Map(),
533 stylesheets: new Map(),
534 scripts: new Map(),
535 moduleScripts: new Map(),
536 },
537
538 nonce: {
539 script: nonceScript,
540 style: nonceStyle,
541 },
542 // like a module global for currently rendering boundary
543 hoistableState: null,
544 stylesToHoist: false,
545 };
546
547 if (bootstrapScripts !== undefined) {
548 for (let i = 0; i < bootstrapScripts.length; i++) {
549 const scriptConfig = bootstrapScripts[i];
550 let src, crossOrigin, integrity;
551 const props: PreloadAsProps = {
552 rel: 'preload',
553 as: 'script',
554 fetchPriority: 'low',
555 nonce,
556 } as any;
557 if (typeof scriptConfig === 'string') {
558 props.href = src = scriptConfig;
559 } else {
560 props.href = src = scriptConfig.src;
561 props.integrity = integrity =
562 typeof scriptConfig.integrity === 'string'
563 ? scriptConfig.integrity
564 : undefined;
565 props.crossOrigin = crossOrigin =
566 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
567 ? undefined
568 : scriptConfig.crossOrigin === 'use-credentials'
569 ? 'use-credentials'
570 : '';
571 }
572
573 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);
574
575 bootstrapChunks.push(
576 startScriptSrc,
577 stringToChunk(escapeTextForBrowser(src)),
578 attributeEnd,
579 );
580 if (nonceScript) {
581 bootstrapChunks.push(
582 scriptNonce,
583 stringToChunk(escapeTextForBrowser(nonceScript)),
584 attributeEnd,
585 );
586 }
587 if (typeof integrity === 'string') {
588 bootstrapChunks.push(
589 scriptIntegirty,
590 stringToChunk(escapeTextForBrowser(integrity)),
591 attributeEnd,
592 );
593 }
594 if (typeof crossOrigin === 'string') {
595 bootstrapChunks.push(
596 scriptCrossOrigin,
597 stringToChunk(escapeTextForBrowser(crossOrigin)),
598 attributeEnd,
599 );
600 }
601 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
602 bootstrapChunks.push(endAsyncScript);
603 }
604 }
605 if (bootstrapModules !== undefined) {
606 for (let i = 0; i < bootstrapModules.length; i++) {
607 const scriptConfig = bootstrapModules[i];
608 let src, crossOrigin, integrity;
609 const props: PreloadModuleProps = {
610 rel: 'modulepreload',
611 fetchPriority: 'low',
612 nonce: nonceScript,
613 } as any;
614 if (typeof scriptConfig === 'string') {
615 props.href = src = scriptConfig;
616 } else {
617 props.href = src = scriptConfig.src;
618 props.integrity = integrity =
619 typeof scriptConfig.integrity === 'string'
620 ? scriptConfig.integrity
621 : undefined;
622 props.crossOrigin = crossOrigin =
623 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
624 ? undefined
625 : scriptConfig.crossOrigin === 'use-credentials'
626 ? 'use-credentials'
627 : '';
628 }
629
630 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);
631
632 bootstrapChunks.push(
633 startModuleSrc,
634 stringToChunk(escapeTextForBrowser(src)),
635 attributeEnd,
636 );
637 if (nonceScript) {
638 bootstrapChunks.push(
639 scriptNonce,
640 stringToChunk(escapeTextForBrowser(nonceScript)),
641 attributeEnd,
642 );
643 }
644 if (typeof integrity === 'string') {
645 bootstrapChunks.push(
646 scriptIntegirty,
647 stringToChunk(escapeTextForBrowser(integrity)),
648 attributeEnd,
649 );
650 }
651 if (typeof crossOrigin === 'string') {
652 bootstrapChunks.push(
653 scriptCrossOrigin,
654 stringToChunk(escapeTextForBrowser(crossOrigin)),
655 attributeEnd,
656 );
657 }
658 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
659 bootstrapChunks.push(endAsyncScript);
660 }
661 }
662
663 return renderState;
664 }
665
666 export function resumeRenderState(
667 resumableState: ResumableState,
668 nonce: NonceOption | void,
669 ): RenderState {
670 return createRenderState(
671 resumableState,
672 nonce,
673 undefined,
674 undefined,
675 undefined,
676 undefined,
677 );
678 }
679
680 export function createResumableState(
681 identifierPrefix: string | void,
682 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
683 bootstrapScriptContent: string | void,
684 bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
685 bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
686 ): ResumableState {
687 const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
688
689 let streamingFormat = ScriptStreamingFormat;
690 if (enableFizzExternalRuntime) {
691 if (externalRuntimeConfig !== undefined) {
692 streamingFormat = DataStreamingFormat;
693 }
694 }
695 return {
696 idPrefix: idPrefix,
697 nextFormID: 0,
698 streamingFormat,
699 bootstrapScriptContent,
700 bootstrapScripts,
701 bootstrapModules,
702 instructions: NothingSent,
703 hasBody: false,
704 hasHtml: false,
705
706 // @TODO add bootstrap script to implicit preloads
707
708 // persistent
709 unknownResources: {},
710 dnsResources: {},
711 connectResources: {
712 default: {},
713 anonymous: {},
714 credentials: {},
715 },
716 imageResources: {},
717 styleResources: {},
718 scriptResources: {},
719 moduleUnknownResources: {},
720 moduleScriptResources: {},
721 };
722 }
723
724 export function resetResumableState(
725 resumableState: ResumableState,
726 renderState: RenderState,
727 ): void {
728 // Resets the resumable state based on what didn't manage to fully flush in the render state.
729 // This currently assumes nothing was flushed.
730 resumableState.nextFormID = 0;
731 resumableState.hasBody = false;
732 resumableState.hasHtml = false;
733 resumableState.unknownResources = {
734 font: renderState.resets.font,
735 };
736 resumableState.dnsResources = renderState.resets.dns;
737 resumableState.connectResources = renderState.resets.connect;
738 resumableState.imageResources = renderState.resets.image;
739 resumableState.styleResources = renderState.resets.style;
740 resumableState.scriptResources = {};
741 resumableState.moduleUnknownResources = {};
742 resumableState.moduleScriptResources = {};
743 resumableState.instructions = NothingSent; // Nothing was flushed so no instructions could've flushed.
744 }
745
746 export function completeResumableState(resumableState: ResumableState): void {
747 // This function is called when we have completed a prerender and there is a shell.
748 resumableState.bootstrapScriptContent = undefined;
749 resumableState.bootstrapScripts = undefined;
750 resumableState.bootstrapModules = undefined;
751 }
752
753 export type PreambleState = {
754 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
755 headChunks: null | Array<Chunk | PrecomputedChunk>,
756 bodyChunks: null | Array<Chunk | PrecomputedChunk>,
757 };
758 export function createPreambleState(): PreambleState {
759 return {
760 htmlChunks: null,
761 headChunks: null,
762 bodyChunks: null,
763 };
764 }
765
766 // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
767 // modes. We only include the variants as they matter for the sake of our purposes.
768 // We don't actually provide the namespace therefore we use constants instead of the string.
769 export const ROOT_HTML_MODE = 0; // Used for the root most element tag.
770 // We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it
771 // still makes sense
772 const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.
773 const HTML_MODE = 2;
774 const HTML_HEAD_MODE = 3;
775 const SVG_MODE = 4;
776 const MATHML_MODE = 5;
777 const HTML_TABLE_MODE = 6;
778 const HTML_TABLE_BODY_MODE = 7;
779 const HTML_TABLE_ROW_MODE = 8;
780 const HTML_COLGROUP_MODE = 9;
781 // We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
782 // still makes sense
783
784 type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
785
786 const NO_SCOPE = /* */ 0b0000000;
787 const NOSCRIPT_SCOPE = /* */ 0b0000001;
788 const PICTURE_SCOPE = /* */ 0b0000010;
789 const FALLBACK_SCOPE = /* */ 0b0000100;
790 const EXIT_SCOPE = /* */ 0b0001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
791 const ENTER_SCOPE = /* */ 0b0010000; // A direct Instance below Suspense content is the only thing that can "enter"
792 const UPDATE_SCOPE = /* */ 0b0100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.
793 const APPEARING_SCOPE = /* */ 0b1000000; // Below Suspense content subtree which might appear in an "enter" animation or "shared" animation.
794 const PARENT_EXIT_SCOPE = /* */ 0b10000000; // Below a ViewTransition that is exiting.
795 const PARENT_ENTER_SCOPE = /* */ 0b100000000; // Below a ViewTransition that is entering.
796
797 // Everything not listed here are tracked for the whole subtree as opposed to just
798 // until the next Instance.
799 const SUBTREE_SCOPE = ~(ENTER_SCOPE | EXIT_SCOPE);
800
801 type ViewTransitionContext = {
802 update: 'none' | 'auto' | string,
803 enter: 'none' | 'auto' | string,
804 exit: 'none' | 'auto' | string,
805 share: 'none' | 'auto' | string,
806 parentEnter: 'none' | 'auto' | string,
807 parentExit: 'none' | 'auto' | string,
808 name: 'auto' | string,
809 autoName: string, // a name that can be used if an explicit one is not defined.
810 nameIdx: number, // keeps track of how many duplicates of this name we've emitted.
811 };
812
813 // Lets us keep track of contextual state and pick it back up after suspending.
814 export type FormatContext = {
815 insertionMode: InsertionMode, // root/svg/html/mathml/table
816 selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>
817 tagScope: number,
818 viewTransition: null | ViewTransitionContext, // tracks if we're inside a ViewTransition outside the first DOM node
819 };
820
821 function createFormatContext(
822 insertionMode: InsertionMode,
823 selectedValue: null | string | Array<string>,
824 tagScope: number,
825 viewTransition: null | ViewTransitionContext,
826 ): FormatContext {
827 return {
828 insertionMode,
829 selectedValue,
830 tagScope,
831 viewTransition,
832 };
833 }
834
835 export function canHavePreamble(formatContext: FormatContext): boolean {
836 return formatContext.insertionMode < HTML_MODE;
837 }
838
839 export function createRootFormatContext(namespaceURI?: string): FormatContext {
840 const insertionMode =
841 namespaceURI === 'http://www.w3.org/2000/svg'
842 ? SVG_MODE
843 : namespaceURI === 'http://www.w3.org/1998/Math/MathML'
844 ? MATHML_MODE
845 : ROOT_HTML_MODE;
846 return createFormatContext(insertionMode, null, NO_SCOPE, null);
847 }
848
849 export function getChildFormatContext(
850 parentContext: FormatContext,
851 type: string,
852 props: Object,
853 ): FormatContext {
854 const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
855 switch (type) {
856 case 'noscript':
857 return createFormatContext(
858 HTML_MODE,
859 null,
860 subtreeScope | NOSCRIPT_SCOPE,
861 null,
862 );
863 case 'select':
864 return createFormatContext(
865 HTML_MODE,
866 props.value != null ? props.value : props.defaultValue,
867 subtreeScope,
868 null,
869 );
870 case 'svg':
871 return createFormatContext(SVG_MODE, null, subtreeScope, null);
872 case 'picture':
873 return createFormatContext(
874 HTML_MODE,
875 null,
876 subtreeScope | PICTURE_SCOPE,
877 null,
878 );
879 case 'math':
880 return createFormatContext(MATHML_MODE, null, subtreeScope, null);
881 case 'foreignObject':
882 return createFormatContext(HTML_MODE, null, subtreeScope, null);
883 // Table parents are special in that their children can only be created at all if they're
884 // wrapped in a table parent. So we need to encode that we're entering this mode.
885 case 'table':
886 return createFormatContext(HTML_TABLE_MODE, null, subtreeScope, null);
887 case 'thead':
888 case 'tbody':
889 case 'tfoot':
890 return createFormatContext(
891 HTML_TABLE_BODY_MODE,
892 null,
893 subtreeScope,
894 null,
895 );
896 case 'colgroup':
897 return createFormatContext(HTML_COLGROUP_MODE, null, subtreeScope, null);
898 case 'tr':
899 return createFormatContext(HTML_TABLE_ROW_MODE, null, subtreeScope, null);
900 case 'head':
901 if (parentContext.insertionMode < HTML_MODE) {
902 // We are either at the root or inside the <html> tag and can enter
903 // the <head> scope
904 return createFormatContext(HTML_HEAD_MODE, null, subtreeScope, null);
905 }
906 break;
907 case 'html':
908 if (parentContext.insertionMode === ROOT_HTML_MODE) {
909 return createFormatContext(HTML_HTML_MODE, null, subtreeScope, null);
910 }
911 break;
912 }
913 if (parentContext.insertionMode >= HTML_TABLE_MODE) {
914 // Whatever tag this was, it wasn't a table parent or other special parent, so we must have
915 // entered plain HTML again.
916 return createFormatContext(HTML_MODE, null, subtreeScope, null);
917 }
918 if (parentContext.insertionMode < HTML_MODE) {
919 return createFormatContext(HTML_MODE, null, subtreeScope, null);
920 }
921 if (enableViewTransition) {
922 if (parentContext.viewTransition !== null) {
923 // If we're inside a view transition, regardless what element we were in, it consumes
924 // the view transition context.
925 return createFormatContext(
926 parentContext.insertionMode,
927 parentContext.selectedValue,
928 subtreeScope,
929 null,
930 );
931 }
932 }
933 if (parentContext.tagScope !== subtreeScope) {
934 return createFormatContext(
935 parentContext.insertionMode,
936 parentContext.selectedValue,
937 subtreeScope,
938 null,
939 );
940 }
941 return parentContext;
942 }
943
944 function getSuspenseViewTransition(
945 parentViewTransition: null | ViewTransitionContext,
946 ): null | ViewTransitionContext {
947 if (parentViewTransition === null) {
948 return null;
949 }
950 // If a ViewTransition wraps a Suspense boundary it applies to the children Instances
951 // in both the fallback and the content.
952 // Since we only have a representation of ViewTransitions on the Instances themselves
953 // we cannot model the parent ViewTransition activating "enter", "exit" or "share"
954 // since those would be ambiguous with the Suspense boundary changing states and
955 // affecting the same Instances.
956 // We also can't model an "update" when that update is fallback nodes swapping for
957 // content nodes. However, we can model is as a "share" from the fallback nodes to
958 // the content nodes using the same name. We just have to assign the same name that
959 // we would've used (the parent ViewTransition name or auto-assign one).
960 const viewTransition: ViewTransitionContext = {
961 update: parentViewTransition.update, // For deep updates.
962 enter: 'none',
963 exit: 'none',
964 share: parentViewTransition.update, // For exit or enter of reveals.
965 parentEnter: 'none',
966 parentExit: 'none',
967 name: parentViewTransition.autoName,
968 autoName: parentViewTransition.autoName,
969 // TOOD: If we have more than just this Suspense boundary as a child of the ViewTransition
970 // then the parent needs to isolate the names so that they don't conflict.
971 nameIdx: 0,
972 };
973 return viewTransition;
974 }
975
976 export function getSuspenseFallbackFormatContext(
977 resumableState: ResumableState,
978 parentContext: FormatContext,
979 ): FormatContext {
980 if (parentContext.tagScope & UPDATE_SCOPE) {
981 // If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,
982 // which hasn't disabled updates, then revealing it might animate the parent so we need
983 // the ViewTransition instructions.
984 resumableState.instructions |= NeedUpgradeToViewTransitions;
985 }
986 return createFormatContext(
987 parentContext.insertionMode,
988 parentContext.selectedValue,
989 parentContext.tagScope | FALLBACK_SCOPE | EXIT_SCOPE,
990 getSuspenseViewTransition(parentContext.viewTransition),
991 );
992 }
993
994 export function getSuspenseContentFormatContext(
995 resumableState: ResumableState,
996 parentContext: FormatContext,
997 ): FormatContext {
998 const viewTransition = getSuspenseViewTransition(
999 parentContext.viewTransition,
1000 );
1001 let subtreeScope = parentContext.tagScope | ENTER_SCOPE;
1002 if (viewTransition !== null && viewTransition.share !== 'none') {
1003 // If we have a ViewTransition wrapping Suspense then the appearing animation
1004 // will be applied just like an "enter" below. Mark it as animating.
1005 subtreeScope |= APPEARING_SCOPE;
1006 }
1007 return createFormatContext(
1008 parentContext.insertionMode,
1009 parentContext.selectedValue,
1010 subtreeScope,
1011 viewTransition,
1012 );
1013 }
1014
1015 export function getViewTransitionFormatContext(
1016 resumableState: ResumableState,
1017 parentContext: FormatContext,
1018 update: ?string,
1019 enter: ?string,
1020 exit: ?string,
1021 share: ?string,
1022 parentEnter: ?string,
1023 parentExit: ?string,
1024 hasParentEnterHandler: boolean,
1025 hasParentExitHandler: boolean,
1026 name: ?string,
1027 autoName: string, // name or an autogenerated unique name
1028 ): FormatContext {
1029 // We're entering a <ViewTransition>. Normalize props.
1030 if (update == null) {
1031 update = 'auto';
1032 }
1033 if (enter == null) {
1034 enter = 'auto';
1035 }
1036 if (exit == null) {
1037 exit = 'auto';
1038 }
1039 if (name == null) {
1040 const parentViewTransition = parentContext.viewTransition;
1041 if (parentViewTransition !== null) {
1042 // If we have multiple nested ViewTransition and the parent has a "share"
1043 // but the child doesn't, then the parent ViewTransition can still activate
1044 // a share scenario so we reuse the name and share from the parent.
1045 name = parentViewTransition.name;
1046 share = parentViewTransition.share;
1047 } else {
1048 name = 'auto';
1049 share = 'none'; // share is only relevant if there's an explicit name
1050 }
1051 } else {
1052 if (share == null) {
1053 share = 'auto';
1054 }
1055 if (parentContext.tagScope & FALLBACK_SCOPE) {
1056 // If we have an explicit name and share is not disabled, and we're inside
1057 // a fallback, then that fallback might pair with content and so we might need
1058 // the ViewTransition instructions to animate between them.
1059 resumableState.instructions |= NeedUpgradeToViewTransitions;
1060 }
1061 }
1062 if (!(parentContext.tagScope & EXIT_SCOPE)) {
1063 exit = 'none'; // exit is only relevant for the first ViewTransition inside fallback
1064 } else {
1065 resumableState.instructions |= NeedUpgradeToViewTransitions;
1066 }
1067 if (!(parentContext.tagScope & ENTER_SCOPE)) {
1068 enter = 'none'; // enter is only relevant for the first ViewTransition inside content
1069 } else {
1070 resumableState.instructions |= NeedUpgradeToViewTransitions;
1071 }
1072 let resolvedParentEnter = 'none';
1073 let resolvedParentExit = 'none';
1074 if (enableViewTransitionParentEnterExit) {
1075 if (
1076 parentEnter != null &&
1077 (parentContext.tagScope & PARENT_ENTER_SCOPE) !== 0
1078 ) {
1079 resolvedParentEnter = parentEnter;
1080 }
1081 if (
1082 parentExit != null &&
1083 (parentContext.tagScope & PARENT_EXIT_SCOPE) !== 0
1084 ) {
1085 resolvedParentExit = parentExit;
1086 }
1087 }
1088 const viewTransition: ViewTransitionContext = {
1089 update,
1090 enter,
1091 exit,
1092 share,
1093 parentEnter: resolvedParentEnter,
1094 parentExit: resolvedParentExit,
1095 name,
1096 autoName,
1097 nameIdx: 0,
1098 };
1099 let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
1100 if (update !== 'none') {
1101 subtreeScope |= UPDATE_SCOPE;
1102 } else {
1103 subtreeScope &= ~UPDATE_SCOPE;
1104 }
1105 if (enter !== 'none') {
1106 subtreeScope |= APPEARING_SCOPE;
1107 }
1108 if (enableViewTransitionParentEnterExit) {
1109 // Parent enter relay: a ViewTransition that is itself entering starts a relay
1110 // for its subtree. A nested ViewTransition continues that relay as long as it
1111 // opts in with a parentEnter prop that doesn't resolve to "none", or with an
1112 // onParentEnter handler. A missing parentEnter (with no handler) or an
1113 // explicit "none" stops the relay so descendants below it don't participate.
1114 // This mirrors commitParentEnterViewTransitions.
1115 if (enter !== 'none') {
1116 subtreeScope |= PARENT_ENTER_SCOPE;
1117 } else if (
1118 (parentContext.tagScope & PARENT_ENTER_SCOPE) !== 0 &&
1119 (parentEnter === 'none' ||
1120 (parentEnter === undefined && !hasParentEnterHandler))
1121 ) {
1122 subtreeScope &= ~PARENT_ENTER_SCOPE;
1123 }
1124 // Parent exit relay: mirror of the enter relay above.
1125 if (exit !== 'none') {
1126 subtreeScope |= PARENT_EXIT_SCOPE;
1127 } else if (
1128 (parentContext.tagScope & PARENT_EXIT_SCOPE) !== 0 &&
1129 (parentExit === 'none' ||
1130 (parentExit === undefined && !hasParentExitHandler))
1131 ) {
1132 subtreeScope &= ~PARENT_EXIT_SCOPE;
1133 }
1134 }
1135 return createFormatContext(
1136 parentContext.insertionMode,
1137 parentContext.selectedValue,
1138 subtreeScope,
1139 viewTransition,
1140 );
1141 }
1142
1143 export function isPreambleContext(formatContext: FormatContext): boolean {
1144 return formatContext.insertionMode === HTML_HEAD_MODE;
1145 }
1146
1147 export function makeId(
1148 resumableState: ResumableState,
1149 treeId: string,
1150 localId: number,
1151 ): string {
1152 const idPrefix = resumableState.idPrefix;
1153
1154 let id = '_' + idPrefix + 'R_' + treeId;
1155
1156 // Unless this is the first id at this level, append a number at the end
1157 // that represents the position of this useId hook among all the useId
1158 // hooks for this fiber.
1159 if (localId > 0) {
1160 id += 'H' + localId.toString(32);
1161 }
1162
1163 return id + '_';
1164 }
1165
1166 function encodeHTMLTextNode(text: string): string {
1167 return escapeTextForBrowser(text);
1168 }
1169
1170 const textSeparator = stringToPrecomputedChunk('<!-- -->');
1171
1172 export function pushTextInstance(
1173 target: Array<Chunk | PrecomputedChunk>,
1174 text: string,
1175 renderState: RenderState,
1176 textEmbedded: boolean,
1177 ): boolean {
1178 if (text === '') {
1179 // Empty text doesn't have a DOM node representation and the hydration is aware of this.
1180 return textEmbedded;
1181 }
1182 if (textEmbedded) {
1183 target.push(textSeparator);
1184 }
1185 target.push(stringToChunk(encodeHTMLTextNode(text)));
1186 return true;
1187 }
1188
1189 // Called when Fizz is done with a Segment. Currently the only purpose is to conditionally
1190 // emit a text separator when we don't know for sure it is safe to omit
1191 export function pushSegmentFinale(
1192 target: Array<Chunk | PrecomputedChunk>,
1193 renderState: RenderState,
1194 lastPushedText: boolean,
1195 textEmbedded: boolean,
1196 ): void {
1197 if (lastPushedText && textEmbedded) {
1198 target.push(textSeparator);
1199 }
1200 }
1201
1202 function pushViewTransitionAttributes(
1203 target: Array<Chunk | PrecomputedChunk>,
1204 formatContext: FormatContext,
1205 ): void {
1206 if (!enableViewTransition) {
1207 return;
1208 }
1209 const viewTransition = formatContext.viewTransition;
1210 if (viewTransition === null) {
1211 return;
1212 }
1213 if (viewTransition.name !== 'auto') {
1214 pushStringAttribute(
1215 target,
1216 'vt-name',
1217 viewTransition.nameIdx === 0
1218 ? viewTransition.name
1219 : viewTransition.name + '_' + viewTransition.nameIdx,
1220 );
1221 // Increment the index in case we have multiple children to the same ViewTransition.
1222 // Because this is a side-effect in render, we should ideally call pushViewTransitionAttributes
1223 // after we've suspended (like forms do), so that we don't increment each attempt.
1224 // TODO: Make this deterministic.
1225 viewTransition.nameIdx++;
1226 }
1227 pushStringAttribute(target, 'vt-update', viewTransition.update);
1228 if (viewTransition.enter !== 'none') {
1229 pushStringAttribute(target, 'vt-enter', viewTransition.enter);
1230 }
1231 if (viewTransition.exit !== 'none') {
1232 pushStringAttribute(target, 'vt-exit', viewTransition.exit);
1233 }
1234 if (viewTransition.share !== 'none') {
1235 pushStringAttribute(target, 'vt-share', viewTransition.share);
1236 }
1237 if (
1238 enableViewTransitionParentEnterExit &&
1239 viewTransition.parentEnter !== 'none'
1240 ) {
1241 pushStringAttribute(target, 'vt-parent-enter', viewTransition.parentEnter);
1242 }
1243 if (
1244 enableViewTransitionParentEnterExit &&
1245 viewTransition.parentExit !== 'none'
1246 ) {
1247 pushStringAttribute(target, 'vt-parent-exit', viewTransition.parentExit);
1248 }
1249 }
1250
1251 const styleNameCache: Map<string, PrecomputedChunk> = new Map();
1252 function processStyleName(styleName: string): PrecomputedChunk {
1253 const chunk = styleNameCache.get(styleName);
1254 if (chunk !== undefined) {
1255 return chunk;
1256 }
1257 const result = stringToPrecomputedChunk(
1258 escapeTextForBrowser(hyphenateStyleName(styleName)),
1259 );
1260 styleNameCache.set(styleName, result);
1261 return result;
1262 }
1263
1264 const styleAttributeStart = stringToPrecomputedChunk(' style="');
1265 const styleAssign = stringToPrecomputedChunk(':');
1266 const styleSeparator = stringToPrecomputedChunk(';');
1267
1268 function pushStyleAttribute(
1269 target: Array<Chunk | PrecomputedChunk>,
1270 style: Object,
1271 ): void {
1272 if (typeof style !== 'object') {
1273 throw new Error(
1274 'The `style` prop expects a mapping from style properties to values, ' +
1275 "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
1276 'using JSX.',
1277 );
1278 }
1279
1280 let isFirst = true;
1281 for (const styleName in style) {
1282 if (!hasOwnProperty.call(style, styleName)) {
1283 continue;
1284 }
1285 // If you provide unsafe user data here they can inject arbitrary CSS
1286 // which may be problematic (I couldn't repro this):
1287 // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
1288 // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
1289 // This is not an XSS hole but instead a potential CSS injection issue
1290 // which has lead to a greater discussion about how we're going to
1291 // trust URLs moving forward. See #2115901
1292 const styleValue = style[styleName];
1293 if (
1294 styleValue == null ||
1295 typeof styleValue === 'boolean' ||
1296 styleValue === ''
1297 ) {
1298 // TODO: We used to set empty string as a style with an empty value. Does that ever make sense?
1299 continue;
1300 }
1301
1302 let nameChunk;
1303 let valueChunk;
1304 const isCustomProperty = styleName.indexOf('--') === 0;
1305 if (isCustomProperty) {
1306 nameChunk = stringToChunk(escapeTextForBrowser(styleName));
1307 if (__DEV__) {
1308 checkCSSPropertyStringCoercion(styleValue, styleName);
1309 }
1310 valueChunk = stringToChunk(
1311 escapeTextForBrowser(('' + styleValue).trim()),
1312 );
1313 } else {
1314 if (__DEV__) {
1315 warnValidStyle(styleName, styleValue);
1316 }
1317
1318 nameChunk = processStyleName(styleName);
1319 if (typeof styleValue === 'number') {
1320 if (styleValue !== 0 && !isUnitlessNumber(styleName)) {
1321 valueChunk = stringToChunk(styleValue + 'px'); // Presumes implicit 'px' suffix for unitless numbers
1322 } else {
1323 valueChunk = stringToChunk('' + styleValue);
1324 }
1325 } else {
1326 if (__DEV__) {
1327 checkCSSPropertyStringCoercion(styleValue, styleName);
1328 }
1329 valueChunk = stringToChunk(
1330 escapeTextForBrowser(('' + styleValue).trim()),
1331 );
1332 }
1333 }
1334 if (isFirst) {
1335 isFirst = false;
1336 // If it's first, we don't need any separators prefixed.
1337 target.push(styleAttributeStart, nameChunk, styleAssign, valueChunk);
1338 } else {
1339 target.push(styleSeparator, nameChunk, styleAssign, valueChunk);
1340 }
1341 }
1342 if (!isFirst) {
1343 target.push(attributeEnd);
1344 }
1345 }
1346
1347 const attributeSeparator = stringToPrecomputedChunk(' ');
1348 const attributeAssign = stringToPrecomputedChunk('="');
1349 const attributeEnd = stringToPrecomputedChunk('"');
1350 const attributeEmptyString = stringToPrecomputedChunk('=""');
1351
1352 function pushBooleanAttribute(
1353 target: Array<Chunk | PrecomputedChunk>,
1354 name: string,
1355 value: string | boolean | number | Function | Object, // not null or undefined
1356 ): void {
1357 if (value && typeof value !== 'function' && typeof value !== 'symbol') {
1358 target.push(attributeSeparator, stringToChunk(name), attributeEmptyString);
1359 }
1360 }
1361
1362 function pushStringAttribute(
1363 target: Array<Chunk | PrecomputedChunk>,
1364 name: string,
1365 value: string | boolean | number | Function | Object, // not null or undefined
1366 ): void {
1367 if (
1368 typeof value !== 'function' &&
1369 typeof value !== 'symbol' &&
1370 typeof value !== 'boolean'
1371 ) {
1372 target.push(
1373 attributeSeparator,
1374 stringToChunk(name),
1375 attributeAssign,
1376 stringToChunk(escapeTextForBrowser(value)),
1377 attributeEnd,
1378 );
1379 }
1380 }
1381
1382 function makeFormFieldPrefix(resumableState: ResumableState): string {
1383 // TODO: Make this deterministic.
1384 const id = resumableState.nextFormID++;
1385 return resumableState.idPrefix + id;
1386 }
1387
1388 // Since this will likely be repeated a lot in the HTML, we use a more concise message
1389 // than on the client and hopefully it's googleable.
1390 const actionJavaScriptURL = stringToPrecomputedChunk(
1391 escapeTextForBrowser(
1392 // eslint-disable-next-line no-script-url
1393 "javascript:throw new Error('React form unexpectedly submitted.')",
1394 ),
1395 );
1396
1397 const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
1398
1399 function pushAdditionalFormField(
1400 this: Array<Chunk | PrecomputedChunk>,
1401 value: string | File,
1402 key: string,
1403 ): void {
1404 const target: Array<Chunk | PrecomputedChunk> = this;
1405 target.push(startHiddenInputChunk);
1406 validateAdditionalFormField(value, key);
1407 pushStringAttribute(target, 'name', key);
1408 pushStringAttribute(target, 'value', value);
1409 target.push(endOfStartTagSelfClosing);
1410 }
1411
1412 function pushAdditionalFormFields(
1413 target: Array<Chunk | PrecomputedChunk>,
1414 formData: void | null | FormData,
1415 ) {
1416 if (formData != null) {
1417 // $FlowFixMe[prop-missing]: FormData has forEach.
1418 formData.forEach(pushAdditionalFormField, target);
1419 }
1420 }
1421
1422 function validateAdditionalFormField(value: string | File, key: string): void {
1423 if (typeof value !== 'string') {
1424 throw new Error(
1425 'File/Blob fields are not yet supported in progressive forms. ' +
1426 'Will fallback to client hydration.',
1427 );
1428 }
1429 }
1430
1431 function validateAdditionalFormFields(formData: void | null | FormData) {
1432 if (formData != null) {
1433 // $FlowFixMe[prop-missing]: FormData has forEach.
1434 formData.forEach(validateAdditionalFormField);
1435 }
1436 return formData;
1437 }
1438
1439 function getCustomFormFields(
1440 resumableState: ResumableState,
1441 formAction: any,
1442 ): null | ReactCustomFormAction {
1443 const customAction = formAction.$$FORM_ACTION;
1444 if (typeof customAction === 'function') {
1445 const prefix = makeFormFieldPrefix(resumableState);
1446 try {
1447 const customFields = formAction.$$FORM_ACTION(prefix);
1448 if (customFields) {
1449 validateAdditionalFormFields(customFields.data);
1450 }
1451 return customFields;
1452 } catch (x) {
1453 if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
1454 // Rethrow suspense.
1455 throw x;
1456 }
1457 // If we fail to encode the form action for progressive enhancement for some reason,
1458 // fallback to trying replaying on the client instead of failing the page. It might
1459 // work there.
1460 if (__DEV__) {
1461 // TODO: Should this be some kind of recoverable error?
1462 console.error(
1463 'Failed to serialize an action for progressive enhancement:\n%s',
1464 x,
1465 );
1466 }
1467 }
1468 }
1469 return null;
1470 }
1471
1472 function pushFormActionAttribute(
1473 target: Array<Chunk | PrecomputedChunk>,
1474 resumableState: ResumableState,
1475 renderState: RenderState,
1476 formAction: any,
1477 formEncType: any,
1478 formMethod: any,
1479 formTarget: any,
1480 name: any,
1481 ): void | null | FormData {
1482 let formData = null;
1483 if (typeof formAction === 'function') {
1484 // Function form actions cannot control the form properties
1485 if (__DEV__) {
1486 if (name !== null && !didWarnFormActionName) {
1487 didWarnFormActionName = true;
1488 console.error(
1489 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
1490 'React needs it to encode which action should be invoked. It will get overridden.',
1491 );
1492 }
1493 if (
1494 (formEncType !== null || formMethod !== null) &&
1495 !didWarnFormActionMethod
1496 ) {
1497 didWarnFormActionMethod = true;
1498 console.error(
1499 'Cannot specify a formEncType or formMethod for a button that specifies a ' +
1500 'function as a formAction. React provides those automatically. They will get overridden.',
1501 );
1502 }
1503 if (formTarget !== null && !didWarnFormActionTarget) {
1504 didWarnFormActionTarget = true;
1505 console.error(
1506 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
1507 'The function will always be executed in the same window.',
1508 );
1509 }
1510 }
1511 const customFields = getCustomFormFields(resumableState, formAction);
1512 if (customFields !== null) {
1513 // This action has a custom progressive enhancement form that can submit the form
1514 // back to the server if it's invoked before hydration. Such as a Server Action.
1515 name = customFields.name;
1516 formAction = customFields.action || '';
1517 formEncType = customFields.encType;
1518 formMethod = customFields.method;
1519 formTarget = customFields.target;
1520 formData = customFields.data;
1521 } else {
1522 // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
1523 // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
1524 // manually submitted or if someone calls stopPropagation before React gets the event.
1525 // If CSP is used to block javascript: URLs that's fine too. It just won't show this
1526 // error message but the URL will be logged.
1527 target.push(
1528 attributeSeparator,
1529 stringToChunk('formAction'),
1530 attributeAssign,
1531 actionJavaScriptURL,
1532 attributeEnd,
1533 );
1534 name = null;
1535 formAction = null;
1536 formEncType = null;
1537 formMethod = null;
1538 formTarget = null;
1539 injectFormReplayingRuntime(resumableState, renderState);
1540 }
1541 }
1542 if (name != null) {
1543 pushAttribute(target, 'name', name);
1544 }
1545 if (formAction != null) {
1546 pushAttribute(target, 'formAction', formAction);
1547 }
1548 if (formEncType != null) {
1549 pushAttribute(target, 'formEncType', formEncType);
1550 }
1551 if (formMethod != null) {
1552 pushAttribute(target, 'formMethod', formMethod);
1553 }
1554 if (formTarget != null) {
1555 pushAttribute(target, 'formTarget', formTarget);
1556 }
1557 return formData;
1558 }
1559
1560 let blobCache: null | WeakMap<Blob, Thenable<string>> = null;
1561
1562 function pushSrcObjectAttribute(
1563 target: Array<Chunk | PrecomputedChunk>,
1564 blob: Blob,
1565 ): void {
1566 // Throwing a Promise style suspense read of the Blob content.
1567 if (blobCache === null) {
1568 blobCache = new WeakMap();
1569 }
1570 const suspenseCache: WeakMap<Blob, Thenable<string>> = blobCache;
1571 let thenable = suspenseCache.get(blob);
1572 if (thenable === undefined) {
1573 thenable = readAsDataURL(blob) as any as Thenable<string>;
1574 thenable.then(
1575 result => {
1576 (thenable as any).status = 'fulfilled';
1577 (thenable as any).value = result;
1578 },
1579 error => {
1580 (thenable as any).status = 'rejected';
1581 (thenable as any).reason = error;
1582 },
1583 );
1584 suspenseCache.set(blob, thenable);
1585 }
1586 if (thenable.status === 'rejected') {
1587 throw thenable.reason;
1588 } else if (thenable.status !== 'fulfilled') {
1589 throw thenable;
1590 }
1591 const url = thenable.value;
1592 target.push(
1593 attributeSeparator,
1594 stringToChunk('src'),
1595 attributeAssign,
1596 stringToChunk(escapeTextForBrowser(url)),
1597 attributeEnd,
1598 );
1599 }
1600
1601 function pushAttribute(
1602 target: Array<Chunk | PrecomputedChunk>,
1603 name: string,
1604 value: string | boolean | number | Function | Object, // not null or undefined
1605 ): void {
1606 switch (name) {
1607 // These are very common props and therefore are in the beginning of the switch.
1608 // TODO: aria-label is a very common prop but allows booleans so is not like the others
1609 // but should ideally go in this list too.
1610 case 'className': {
1611 pushStringAttribute(target, 'class', value);
1612 break;
1613 }
1614 case 'tabIndex': {
1615 pushStringAttribute(target, 'tabindex', value);
1616 break;
1617 }
1618 case 'dir':
1619 case 'role':
1620 case 'viewBox':
1621 case 'width':
1622 case 'height': {
1623 pushStringAttribute(target, name, value);
1624 break;
1625 }
1626 case 'style': {
1627 pushStyleAttribute(target, value);
1628 return;
1629 }
1630 case 'src': {
1631 // $FlowFixMe[invalid-compare]
1632 if (enableSrcObject && typeof value === 'object' && value !== null) {
1633 if (typeof Blob === 'function' && value instanceof Blob) {
1634 pushSrcObjectAttribute(target, value);
1635 return;
1636 }
1637 }
1638 // Fallthrough to general urls
1639 }
1640 case 'href': {
1641 if (value === '') {
1642 if (__DEV__) {
1643 if (name === 'src') {
1644 console.error(
1645 'An empty string ("") was passed to the %s attribute. ' +
1646 'This may cause the browser to download the whole page again over the network. ' +
1647 'To fix this, either do not render the element at all ' +
1648 'or pass null to %s instead of an empty string.',
1649 name,
1650 name,
1651 );
1652 } else {
1653 console.error(
1654 'An empty string ("") was passed to the %s attribute. ' +
1655 'To fix this, either do not render the element at all ' +
1656 'or pass null to %s instead of an empty string.',
1657 name,
1658 name,
1659 );
1660 }
1661 }
1662 return;
1663 }
1664 }
1665 // Fall through to the last case which shouldn't remove empty strings.
1666 case 'action':
1667 case 'formAction': {
1668 // TODO: Consider only special casing these for each tag.
1669 if (
1670 value == null ||
1671 typeof value === 'function' ||
1672 typeof value === 'symbol' ||
1673 typeof value === 'boolean'
1674 ) {
1675 return;
1676 }
1677 if (__DEV__) {
1678 checkAttributeStringCoercion(value, name);
1679 }
1680 const sanitizedValue = sanitizeURL('' + value);
1681 target.push(
1682 attributeSeparator,
1683 stringToChunk(name),
1684 attributeAssign,
1685 stringToChunk(escapeTextForBrowser(sanitizedValue)),
1686 attributeEnd,
1687 );
1688 return;
1689 }
1690 case 'defaultValue':
1691 case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements.
1692 case 'innerHTML': // Must use dangerouslySetInnerHTML instead.
1693 case 'suppressContentEditableWarning':
1694 case 'suppressHydrationWarning':
1695 case 'ref':
1696 // Ignored. These are built-in to React on the client.
1697 return;
1698 case 'autoFocus':
1699 case 'multiple':
1700 case 'muted': {
1701 pushBooleanAttribute(target, name.toLowerCase(), value);
1702 return;
1703 }
1704 case 'xlinkHref': {
1705 if (
1706 typeof value === 'function' ||
1707 typeof value === 'symbol' ||
1708 typeof value === 'boolean'
1709 ) {
1710 return;
1711 }
1712 if (__DEV__) {
1713 checkAttributeStringCoercion(value, name);
1714 }
1715 const sanitizedValue = sanitizeURL('' + value);
1716 target.push(
1717 attributeSeparator,
1718 stringToChunk('xlink:href'),
1719 attributeAssign,
1720 stringToChunk(escapeTextForBrowser(sanitizedValue)),
1721 attributeEnd,
1722 );
1723 return;
1724 }
1725 case 'contentEditable':
1726 case 'spellCheck':
1727 case 'draggable':
1728 case 'value':
1729 case 'autoReverse':
1730 case 'externalResourcesRequired':
1731 case 'focusable':
1732 case 'preserveAlpha': {
1733 // Booleanish String
1734 // These are "enumerated" attributes that accept "true" and "false".
1735 // In React, we let users pass `true` and `false` even though technically
1736 // these aren't boolean attributes (they are coerced to strings).
1737 if (typeof value !== 'function' && typeof value !== 'symbol') {
1738 target.push(
1739 attributeSeparator,
1740 stringToChunk(name),
1741 attributeAssign,
1742 stringToChunk(escapeTextForBrowser(value)),
1743 attributeEnd,
1744 );
1745 }
1746 return;
1747 }
1748 case 'inert': {
1749 if (__DEV__) {
1750 if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[name]) {
1751 didWarnForNewBooleanPropsWithEmptyValue[name] = true;
1752 console.error(
1753 'Received an empty string for a boolean attribute `%s`. ' +
1754 'This will treat the attribute as if it were false. ' +
1755 'Either pass `false` to silence this warning, or ' +
1756 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
1757 name,
1758 );
1759 }
1760 }
1761 }
1762 // Fallthrough for boolean props that don't have a warning for empty strings.
1763 case 'allowFullScreen':
1764 case 'async':
1765 case 'autoPlay':
1766 case 'controls':
1767 case 'credentialless':
1768 case 'default':
1769 case 'defer':
1770 case 'disabled':
1771 case 'disablePictureInPicture':
1772 case 'disableRemotePlayback':
1773 case 'formNoValidate':
1774 case 'hidden':
1775 case 'loop':
1776 case 'noModule':
1777 case 'noValidate':
1778 case 'open':
1779 case 'playsInline':
1780 case 'readOnly':
1781 case 'required':
1782 case 'reversed':
1783 case 'scoped':
1784 case 'seamless':
1785 case 'itemScope': {
1786 // Boolean
1787 if (value && typeof value !== 'function' && typeof value !== 'symbol') {
1788 target.push(
1789 attributeSeparator,
1790 stringToChunk(name),
1791 attributeEmptyString,
1792 );
1793 }
1794 return;
1795 }
1796 case 'capture':
1797 case 'download': {
1798 // Overloaded Boolean
1799 if (value === true) {
1800 target.push(
1801 attributeSeparator,
1802 stringToChunk(name),
1803 attributeEmptyString,
1804 );
1805 } else if (value === false) {
1806 // Ignored
1807 } else if (typeof value !== 'function' && typeof value !== 'symbol') {
1808 target.push(
1809 attributeSeparator,
1810 stringToChunk(name),
1811 attributeAssign,
1812 stringToChunk(escapeTextForBrowser(value)),
1813 attributeEnd,
1814 );
1815 }
1816 return;
1817 }
1818 case 'cols':
1819 case 'rows':
1820 case 'size':
1821 case 'span': {
1822 // These are HTML attributes that must be positive numbers.
1823 if (
1824 typeof value !== 'function' &&
1825 typeof value !== 'symbol' &&
1826 !isNaN(value) &&
1827 (value as any) >= 1
1828 ) {
1829 target.push(
1830 attributeSeparator,
1831 stringToChunk(name),
1832 attributeAssign,
1833 stringToChunk(escapeTextForBrowser(value)),
1834 attributeEnd,
1835 );
1836 }
1837 return;
1838 }
1839 case 'rowSpan':
1840 case 'start': {
1841 // These are HTML attributes that must be numbers.
1842 if (
1843 typeof value !== 'function' &&
1844 typeof value !== 'symbol' &&
1845 !isNaN(value)
1846 ) {
1847 target.push(
1848 attributeSeparator,
1849 stringToChunk(name),
1850 attributeAssign,
1851 stringToChunk(escapeTextForBrowser(value)),
1852 attributeEnd,
1853 );
1854 }
1855 return;
1856 }
1857 case 'xlinkActuate':
1858 pushStringAttribute(target, 'xlink:actuate', value);
1859 return;
1860 case 'xlinkArcrole':
1861 pushStringAttribute(target, 'xlink:arcrole', value);
1862 return;
1863 case 'xlinkRole':
1864 pushStringAttribute(target, 'xlink:role', value);
1865 return;
1866 case 'xlinkShow':
1867 pushStringAttribute(target, 'xlink:show', value);
1868 return;
1869 case 'xlinkTitle':
1870 pushStringAttribute(target, 'xlink:title', value);
1871 return;
1872 case 'xlinkType':
1873 pushStringAttribute(target, 'xlink:type', value);
1874 return;
1875 case 'xmlBase':
1876 pushStringAttribute(target, 'xml:base', value);
1877 return;
1878 case 'xmlLang':
1879 pushStringAttribute(target, 'xml:lang', value);
1880 return;
1881 case 'xmlSpace':
1882 pushStringAttribute(target, 'xml:space', value);
1883 return;
1884 default:
1885 if (
1886 // shouldIgnoreAttribute
1887 // We have already filtered out null/undefined and reserved words.
1888 name.length > 2 &&
1889 (name[0] === 'o' || name[0] === 'O') &&
1890 (name[1] === 'n' || name[1] === 'N')
1891 ) {
1892 return;
1893 }
1894
1895 const attributeName = getAttributeAlias(name);
1896 if (isAttributeNameSafe(attributeName)) {
1897 // shouldRemoveAttribute
1898 switch (typeof value) {
1899 case 'function':
1900 case 'symbol':
1901 return;
1902 case 'boolean': {
1903 const prefix = attributeName.toLowerCase().slice(0, 5);
1904 if (prefix !== 'data-' && prefix !== 'aria-') {
1905 return;
1906 }
1907 }
1908 }
1909 target.push(
1910 attributeSeparator,
1911 stringToChunk(attributeName),
1912 attributeAssign,
1913 stringToChunk(escapeTextForBrowser(value)),
1914 attributeEnd,
1915 );
1916 }
1917 }
1918 }
1919
1920 const endOfStartTag = stringToPrecomputedChunk('>');
1921 const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');
1922
1923 function pushInnerHTML(
1924 target: Array<Chunk | PrecomputedChunk>,
1925 innerHTML: any,
1926 children: any,
1927 ) {
1928 if (innerHTML != null) {
1929 if (children != null) {
1930 throw new Error(
1931 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
1932 );
1933 }
1934
1935 if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {
1936 throw new Error(
1937 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
1938 'Please visit https://react.dev/link/dangerously-set-inner-html ' +
1939 'for more information.',
1940 );
1941 }
1942
1943 const html = innerHTML.__html;
1944 if (html !== null && html !== undefined) {
1945 if (__DEV__) {
1946 checkHtmlStringCoercion(html);
1947 }
1948 target.push(stringToChunk('' + html));
1949 }
1950 }
1951 }
1952
1953 // TODO: Move these to RenderState so that we warn for every request.
1954 // It would help debugging in stateful servers (e.g. service worker).
1955 let didWarnDefaultInputValue = false;
1956 let didWarnDefaultChecked = false;
1957 let didWarnDefaultSelectValue = false;
1958 let didWarnDefaultTextareaValue = false;
1959 let didWarnInvalidOptionChildren = false;
1960 let didWarnInvalidOptionInnerHTML = false;
1961 let didWarnSelectedSetOnOption = false;
1962 let didWarnFormActionType = false;
1963 let didWarnFormActionName = false;
1964 let didWarnFormActionTarget = false;
1965 let didWarnFormActionMethod = false;
1966
1967 function checkSelectProp(props: any, propName: string) {
1968 if (__DEV__) {
1969 const value = props[propName];
1970 if (value != null) {
1971 const array = isArray(value);
1972 if (props.multiple && !array) {
1973 console.error(
1974 'The `%s` prop supplied to <select> must be an array if ' +
1975 '`multiple` is true.',
1976 propName,
1977 );
1978 } else if (!props.multiple && array) {
1979 console.error(
1980 'The `%s` prop supplied to <select> must be a scalar ' +
1981 'value if `multiple` is false.',
1982 propName,
1983 );
1984 }
1985 }
1986 }
1987 }
1988
1989 function pushStartAnchor(
1990 target: Array<Chunk | PrecomputedChunk>,
1991 props: Object,
1992 formatContext: FormatContext,
1993 ): ReactNodeList {
1994 target.push(startChunkForTag('a'));
1995
1996 let children = null;
1997 let innerHTML = null;
1998 for (const propKey in props) {
1999 if (hasOwnProperty.call(props, propKey)) {
2000 const propValue = props[propKey];
2001 if (propValue == null) {
2002 continue;
2003 }
2004 switch (propKey) {
2005 case 'children':
2006 children = propValue;
2007 break;
2008 case 'dangerouslySetInnerHTML':
2009 innerHTML = propValue;
2010 break;
2011 case 'href':
2012 if (propValue === '') {
2013 // Empty `href` is special on anchors so we're short-circuiting here.
2014 // On other tags it should trigger a warning
2015 pushStringAttribute(target, 'href', '');
2016 } else {
2017 pushAttribute(target, propKey, propValue);
2018 }
2019 break;
2020 default:
2021 pushAttribute(target, propKey, propValue);
2022 break;
2023 }
2024 }
2025 }
2026
2027 pushViewTransitionAttributes(target, formatContext);
2028
2029 target.push(endOfStartTag);
2030 pushInnerHTML(target, innerHTML, children);
2031 if (typeof children === 'string') {
2032 // Special case children as a string to avoid the unnecessary comment.
2033 // TODO: Remove this special case after the general optimization is in place.
2034 target.push(stringToChunk(encodeHTMLTextNode(children)));
2035 return null;
2036 }
2037 return children;
2038 }
2039
2040 function pushStartObject(
2041 target: Array<Chunk | PrecomputedChunk>,
2042 props: Object,
2043 formatContext: FormatContext,
2044 ): ReactNodeList {
2045 target.push(startChunkForTag('object'));
2046
2047 let children = null;
2048 let innerHTML = null;
2049 for (const propKey in props) {
2050 if (hasOwnProperty.call(props, propKey)) {
2051 const propValue = props[propKey];
2052 if (propValue == null) {
2053 continue;
2054 }
2055 switch (propKey) {
2056 case 'children':
2057 children = propValue;
2058 break;
2059 case 'dangerouslySetInnerHTML':
2060 innerHTML = propValue;
2061 break;
2062 case 'data': {
2063 if (__DEV__) {
2064 checkAttributeStringCoercion(propValue, 'data');
2065 }
2066 const sanitizedValue = sanitizeURL('' + propValue);
2067 if (sanitizedValue === '') {
2068 if (__DEV__) {
2069 console.error(
2070 'An empty string ("") was passed to the %s attribute. ' +
2071 'To fix this, either do not render the element at all ' +
2072 'or pass null to %s instead of an empty string.',
2073 propKey,
2074 propKey,
2075 );
2076 }
2077 break;
2078 }
2079 target.push(
2080 attributeSeparator,
2081 stringToChunk('data'),
2082 attributeAssign,
2083 stringToChunk(escapeTextForBrowser(sanitizedValue)),
2084 attributeEnd,
2085 );
2086 break;
2087 }
2088 default:
2089 pushAttribute(target, propKey, propValue);
2090 break;
2091 }
2092 }
2093 }
2094
2095 pushViewTransitionAttributes(target, formatContext);
2096
2097 target.push(endOfStartTag);
2098 pushInnerHTML(target, innerHTML, children);
2099 if (typeof children === 'string') {
2100 // Special case children as a string to avoid the unnecessary comment.
2101 // TODO: Remove this special case after the general optimization is in place.
2102 target.push(stringToChunk(encodeHTMLTextNode(children)));
2103 return null;
2104 }
2105 return children;
2106 }
2107
2108 function pushStartSelect(
2109 target: Array<Chunk | PrecomputedChunk>,
2110 props: Object,
2111 formatContext: FormatContext,
2112 ): ReactNodeList {
2113 if (__DEV__) {
2114 checkControlledValueProps('select', props);
2115
2116 checkSelectProp(props, 'value');
2117 checkSelectProp(props, 'defaultValue');
2118
2119 if (
2120 props.value !== undefined &&
2121 props.defaultValue !== undefined &&
2122 !didWarnDefaultSelectValue
2123 ) {
2124 console.error(
2125 'Select elements must be either controlled or uncontrolled ' +
2126 '(specify either the value prop, or the defaultValue prop, but not ' +
2127 'both). Decide between using a controlled or uncontrolled select ' +
2128 'element and remove one of these props. More info: ' +
2129 'https://react.dev/link/controlled-components',
2130 );
2131 didWarnDefaultSelectValue = true;
2132 }
2133 }
2134
2135 target.push(startChunkForTag('select'));
2136
2137 let children = null;
2138 let innerHTML = null;
2139 for (const propKey in props) {
2140 if (hasOwnProperty.call(props, propKey)) {
2141 const propValue = props[propKey];
2142 if (propValue == null) {
2143 continue;
2144 }
2145 switch (propKey) {
2146 case 'children':
2147 children = propValue;
2148 break;
2149 case 'dangerouslySetInnerHTML':
2150 // TODO: This doesn't really make sense for select since it can't use the controlled
2151 // value in the innerHTML.
2152 innerHTML = propValue;
2153 break;
2154 case 'defaultValue':
2155 case 'value':
2156 // These are set on the Context instead and applied to the nested options.
2157 break;
2158 default:
2159 pushAttribute(target, propKey, propValue);
2160 break;
2161 }
2162 }
2163 }
2164
2165 pushViewTransitionAttributes(target, formatContext);
2166
2167 target.push(endOfStartTag);
2168 pushInnerHTML(target, innerHTML, children);
2169 return children;
2170 }
2171
2172 function flattenOptionChildren(children: mixed): string {
2173 let content = '';
2174 // Flatten children and warn if they aren't strings or numbers;
2175 // invalid types are ignored.
2176 Children.forEach(children as any, function (child) {
2177 if (child == null) {
2178 return;
2179 }
2180 content += child as any;
2181 if (__DEV__) {
2182 if (
2183 !didWarnInvalidOptionChildren &&
2184 typeof child !== 'string' &&
2185 typeof child !== 'number' &&
2186 typeof child !== 'bigint'
2187 ) {
2188 didWarnInvalidOptionChildren = true;
2189 console.error(
2190 'Cannot infer the option value of complex children. ' +
2191 'Pass a `value` prop or use a plain string as children to <option>.',
2192 );
2193 }
2194 }
2195 });
2196 return content;
2197 }
2198
2199 const selectedMarkerAttribute = stringToPrecomputedChunk(' selected=""');
2200
2201 function pushStartOption(
2202 target: Array<Chunk | PrecomputedChunk>,
2203 props: Object,
2204 formatContext: FormatContext,
2205 ): ReactNodeList {
2206 const selectedValue = formatContext.selectedValue;
2207
2208 target.push(startChunkForTag('option'));
2209
2210 let children = null;
2211 let value = null;
2212 let selected = null;
2213 let innerHTML = null;
2214 for (const propKey in props) {
2215 if (hasOwnProperty.call(props, propKey)) {
2216 const propValue = props[propKey];
2217 if (propValue == null) {
2218 continue;
2219 }
2220 switch (propKey) {
2221 case 'children':
2222 children = propValue;
2223 break;
2224 case 'selected':
2225 // ignore
2226 selected = propValue;
2227 if (__DEV__) {
2228 // TODO: Remove support for `selected` in <option>.
2229 if (!didWarnSelectedSetOnOption) {
2230 console.error(
2231 'Use the `defaultValue` or `value` props on <select> instead of ' +
2232 'setting `selected` on <option>.',
2233 );
2234 didWarnSelectedSetOnOption = true;
2235 }
2236 }
2237 break;
2238 case 'dangerouslySetInnerHTML':
2239 innerHTML = propValue;
2240 break;
2241 case 'value':
2242 value = propValue;
2243 // We intentionally fallthrough to also set the attribute on the node.
2244 default:
2245 pushAttribute(target, propKey, propValue);
2246 break;
2247 }
2248 }
2249 }
2250
2251 if (selectedValue != null) {
2252 let stringValue;
2253 if (value !== null) {
2254 if (__DEV__) {
2255 checkAttributeStringCoercion(value, 'value');
2256 }
2257 stringValue = '' + value;
2258 } else {
2259 if (__DEV__) {
2260 if (innerHTML !== null) {
2261 if (!didWarnInvalidOptionInnerHTML) {
2262 didWarnInvalidOptionInnerHTML = true;
2263 console.error(
2264 'Pass a `value` prop if you set dangerouslyInnerHTML so React knows ' +
2265 'which value should be selected.',
2266 );
2267 }
2268 }
2269 }
2270 stringValue = flattenOptionChildren(children);
2271 }
2272 if (isArray(selectedValue)) {
2273 // multiple
2274 for (let i = 0; i < selectedValue.length; i++) {
2275 if (__DEV__) {
2276 checkAttributeStringCoercion(selectedValue[i], 'value');
2277 }
2278 const v = '' + selectedValue[i];
2279 if (v === stringValue) {
2280 target.push(selectedMarkerAttribute);
2281 break;
2282 }
2283 }
2284 } else {
2285 if (__DEV__) {
2286 checkAttributeStringCoercion(selectedValue, 'select.value');
2287 }
2288 if ('' + selectedValue === stringValue) {
2289 target.push(selectedMarkerAttribute);
2290 }
2291 }
2292 } else if (selected) {
2293 target.push(selectedMarkerAttribute);
2294 }
2295
2296 // Options never participate as ViewTransitions.
2297 target.push(endOfStartTag);
2298 pushInnerHTML(target, innerHTML, children);
2299 return children;
2300 }
2301
2302 const formReplayingRuntimeScript =
2303 stringToPrecomputedChunk(formReplayingRuntime);
2304
2305 function injectFormReplayingRuntime(
2306 resumableState: ResumableState,
2307 renderState: RenderState,
2308 ): void {
2309 // If we haven't sent it yet, inject the runtime that tracks submitted JS actions
2310 // for later replaying by Fiber. If we use an external runtime, we don't need
2311 // to emit anything. It's always used.
2312 if (
2313 (resumableState.instructions & SentFormReplayingRuntime) === NothingSent &&
2314 (!enableFizzExternalRuntime || !renderState.externalRuntimeScript)
2315 ) {
2316 resumableState.instructions |= SentFormReplayingRuntime;
2317 const preamble = renderState.preamble;
2318 const bootstrapChunks = renderState.bootstrapChunks;
2319 if (
2320 (preamble.htmlChunks || preamble.headChunks) &&
2321 bootstrapChunks.length === 0
2322 ) {
2323 // If we rendered the whole document, then we emitted a rel="expect" that needs a
2324 // matching target. If we haven't emitted that yet, we need to include it in this
2325 // script tag.
2326 bootstrapChunks.push(renderState.startInlineScript);
2327 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
2328 bootstrapChunks.push(
2329 endOfStartTag,
2330 formReplayingRuntimeScript,
2331 endInlineScript,
2332 );
2333 } else {
2334 // Otherwise we added to the beginning of the scripts. This will mean that it
2335 // appears before the shell ID unfortunately.
2336 bootstrapChunks.unshift(
2337 renderState.startInlineScript,
2338 endOfStartTag,
2339 formReplayingRuntimeScript,
2340 endInlineScript,
2341 );
2342 }
2343 }
2344 }
2345
2346 const formStateMarkerIsMatching = stringToPrecomputedChunk('<!--F!-->');
2347 const formStateMarkerIsNotMatching = stringToPrecomputedChunk('<!--F-->');
2348
2349 export function pushFormStateMarkerIsMatching(
2350 target: Array<Chunk | PrecomputedChunk>,
2351 ) {
2352 target.push(formStateMarkerIsMatching);
2353 }
2354
2355 export function pushFormStateMarkerIsNotMatching(
2356 target: Array<Chunk | PrecomputedChunk>,
2357 ) {
2358 target.push(formStateMarkerIsNotMatching);
2359 }
2360
2361 function pushStartForm(
2362 target: Array<Chunk | PrecomputedChunk>,
2363 props: Object,
2364 resumableState: ResumableState,
2365 renderState: RenderState,
2366 formatContext: FormatContext,
2367 ): ReactNodeList {
2368 target.push(startChunkForTag('form'));
2369
2370 let children = null;
2371 let innerHTML = null;
2372 let formAction = null;
2373 let formEncType = null;
2374 let formMethod = null;
2375 let formTarget = null;
2376
2377 for (const propKey in props) {
2378 if (hasOwnProperty.call(props, propKey)) {
2379 const propValue = props[propKey];
2380 if (propValue == null) {
2381 continue;
2382 }
2383 switch (propKey) {
2384 case 'children':
2385 children = propValue;
2386 break;
2387 case 'dangerouslySetInnerHTML':
2388 innerHTML = propValue;
2389 break;
2390 case 'action':
2391 formAction = propValue;
2392 break;
2393 case 'encType':
2394 formEncType = propValue;
2395 break;
2396 case 'method':
2397 formMethod = propValue;
2398 break;
2399 case 'target':
2400 formTarget = propValue;
2401 break;
2402 default:
2403 pushAttribute(target, propKey, propValue);
2404 break;
2405 }
2406 }
2407 }
2408
2409 let formData = null;
2410 let formActionName = null;
2411 if (typeof formAction === 'function') {
2412 // Function form actions cannot control the form properties
2413 if (__DEV__) {
2414 if (
2415 (formEncType !== null || formMethod !== null) &&
2416 !didWarnFormActionMethod
2417 ) {
2418 didWarnFormActionMethod = true;
2419 console.error(
2420 'Cannot specify a encType or method for a form that specifies a ' +
2421 'function as the action. React provides those automatically. ' +
2422 'They will get overridden.',
2423 );
2424 }
2425 if (formTarget !== null && !didWarnFormActionTarget) {
2426 didWarnFormActionTarget = true;
2427 console.error(
2428 'Cannot specify a target for a form that specifies a function as the action. ' +
2429 'The function will always be executed in the same window.',
2430 );
2431 }
2432 }
2433 const customFields = getCustomFormFields(resumableState, formAction);
2434 if (customFields !== null) {
2435 // This action has a custom progressive enhancement form that can submit the form
2436 // back to the server if it's invoked before hydration. Such as a Server Action.
2437 formAction = customFields.action || '';
2438 formEncType = customFields.encType;
2439 formMethod = customFields.method;
2440 formTarget = customFields.target;
2441 formData = customFields.data;
2442 formActionName = customFields.name;
2443 } else {
2444 // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
2445 // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
2446 // manually submitted or if someone calls stopPropagation before React gets the event.
2447 // If CSP is used to block javascript: URLs that's fine too. It just won't show this
2448 // error message but the URL will be logged.
2449 target.push(
2450 attributeSeparator,
2451 stringToChunk('action'),
2452 attributeAssign,
2453 actionJavaScriptURL,
2454 attributeEnd,
2455 );
2456 formAction = null;
2457 formEncType = null;
2458 formMethod = null;
2459 formTarget = null;
2460 injectFormReplayingRuntime(resumableState, renderState);
2461 }
2462 }
2463 if (formAction != null) {
2464 pushAttribute(target, 'action', formAction);
2465 }
2466 if (formEncType != null) {
2467 pushAttribute(target, 'encType', formEncType);
2468 }
2469 if (formMethod != null) {
2470 pushAttribute(target, 'method', formMethod);
2471 }
2472 if (formTarget != null) {
2473 pushAttribute(target, 'target', formTarget);
2474 }
2475
2476 pushViewTransitionAttributes(target, formatContext);
2477
2478 target.push(endOfStartTag);
2479
2480 if (formActionName !== null) {
2481 target.push(startHiddenInputChunk);
2482 pushStringAttribute(target, 'name', formActionName);
2483 target.push(endOfStartTagSelfClosing);
2484 pushAdditionalFormFields(target, formData);
2485 }
2486
2487 pushInnerHTML(target, innerHTML, children);
2488 if (typeof children === 'string') {
2489 // Special case children as a string to avoid the unnecessary comment.
2490 // TODO: Remove this special case after the general optimization is in place.
2491 target.push(stringToChunk(encodeHTMLTextNode(children)));
2492 return null;
2493 }
2494 return children;
2495 }
2496
2497 function pushInput(
2498 target: Array<Chunk | PrecomputedChunk>,
2499 props: Object,
2500 resumableState: ResumableState,
2501 renderState: RenderState,
2502 formatContext: FormatContext,
2503 ): ReactNodeList {
2504 if (__DEV__) {
2505 checkControlledValueProps('input', props);
2506 }
2507
2508 target.push(startChunkForTag('input'));
2509
2510 let name = null;
2511 let formAction = null;
2512 let formEncType = null;
2513 let formMethod = null;
2514 let formTarget = null;
2515 let value = null;
2516 let defaultValue = null;
2517 let checked = null;
2518 let defaultChecked = null;
2519
2520 for (const propKey in props) {
2521 if (hasOwnProperty.call(props, propKey)) {
2522 const propValue = props[propKey];
2523 if (propValue == null) {
2524 continue;
2525 }
2526 switch (propKey) {
2527 case 'children':
2528 case 'dangerouslySetInnerHTML':
2529 throw new Error(
2530 `${'input'} is a self-closing tag and must neither have \`children\` nor ` +
2531 'use `dangerouslySetInnerHTML`.',
2532 );
2533 case 'name':
2534 name = propValue;
2535 break;
2536 case 'formAction':
2537 formAction = propValue;
2538 break;
2539 case 'formEncType':
2540 formEncType = propValue;
2541 break;
2542 case 'formMethod':
2543 formMethod = propValue;
2544 break;
2545 case 'formTarget':
2546 formTarget = propValue;
2547 break;
2548 case 'defaultChecked':
2549 defaultChecked = propValue;
2550 break;
2551 case 'defaultValue':
2552 defaultValue = propValue;
2553 break;
2554 case 'checked':
2555 checked = propValue;
2556 break;
2557 case 'value':
2558 value = propValue;
2559 break;
2560 default:
2561 pushAttribute(target, propKey, propValue);
2562 break;
2563 }
2564 }
2565 }
2566
2567 if (__DEV__) {
2568 if (
2569 formAction !== null &&
2570 props.type !== 'image' &&
2571 props.type !== 'submit' &&
2572 !didWarnFormActionType
2573 ) {
2574 didWarnFormActionType = true;
2575 console.error(
2576 'An input can only specify a formAction along with type="submit" or type="image".',
2577 );
2578 }
2579 }
2580
2581 const formData = pushFormActionAttribute(
2582 target,
2583 resumableState,
2584 renderState,
2585 formAction,
2586 formEncType,
2587 formMethod,
2588 formTarget,
2589 name,
2590 );
2591
2592 if (__DEV__) {
2593 if (checked !== null && defaultChecked !== null && !didWarnDefaultChecked) {
2594 console.error(
2595 '%s contains an input of type %s with both checked and defaultChecked props. ' +
2596 'Input elements must be either controlled or uncontrolled ' +
2597 '(specify either the checked prop, or the defaultChecked prop, but not ' +
2598 'both). Decide between using a controlled or uncontrolled input ' +
2599 'element and remove one of these props. More info: ' +
2600 'https://react.dev/link/controlled-components',
2601 'A component',
2602 props.type,
2603 );
2604 didWarnDefaultChecked = true;
2605 }
2606 if (value !== null && defaultValue !== null && !didWarnDefaultInputValue) {
2607 console.error(
2608 '%s contains an input of type %s with both value and defaultValue props. ' +
2609 'Input elements must be either controlled or uncontrolled ' +
2610 '(specify either the value prop, or the defaultValue prop, but not ' +
2611 'both). Decide between using a controlled or uncontrolled input ' +
2612 'element and remove one of these props. More info: ' +
2613 'https://react.dev/link/controlled-components',
2614 'A component',
2615 props.type,
2616 );
2617 didWarnDefaultInputValue = true;
2618 }
2619 }
2620
2621 if (checked !== null) {
2622 pushBooleanAttribute(target, 'checked', checked);
2623 } else if (defaultChecked !== null) {
2624 pushBooleanAttribute(target, 'checked', defaultChecked);
2625 }
2626 if (value !== null) {
2627 pushAttribute(target, 'value', value);
2628 } else if (defaultValue !== null) {
2629 pushAttribute(target, 'value', defaultValue);
2630 }
2631
2632 pushViewTransitionAttributes(target, formatContext);
2633
2634 target.push(endOfStartTagSelfClosing);
2635
2636 // We place any additional hidden form fields after the input.
2637 pushAdditionalFormFields(target, formData);
2638
2639 return null;
2640 }
2641
2642 function pushStartButton(
2643 target: Array<Chunk | PrecomputedChunk>,
2644 props: Object,
2645 resumableState: ResumableState,
2646 renderState: RenderState,
2647 formatContext: FormatContext,
2648 ): ReactNodeList {
2649 target.push(startChunkForTag('button'));
2650
2651 let children = null;
2652 let innerHTML = null;
2653 let name = null;
2654 let formAction = null;
2655 let formEncType = null;
2656 let formMethod = null;
2657 let formTarget = null;
2658
2659 for (const propKey in props) {
2660 if (hasOwnProperty.call(props, propKey)) {
2661 const propValue = props[propKey];
2662 if (propValue == null) {
2663 continue;
2664 }
2665 switch (propKey) {
2666 case 'children':
2667 children = propValue;
2668 break;
2669 case 'dangerouslySetInnerHTML':
2670 innerHTML = propValue;
2671 break;
2672 case 'name':
2673 name = propValue;
2674 break;
2675 case 'formAction':
2676 formAction = propValue;
2677 break;
2678 case 'formEncType':
2679 formEncType = propValue;
2680 break;
2681 case 'formMethod':
2682 formMethod = propValue;
2683 break;
2684 case 'formTarget':
2685 formTarget = propValue;
2686 break;
2687 default:
2688 pushAttribute(target, propKey, propValue);
2689 break;
2690 }
2691 }
2692 }
2693
2694 if (__DEV__) {
2695 if (
2696 formAction !== null &&
2697 props.type != null &&
2698 props.type !== 'submit' &&
2699 !didWarnFormActionType
2700 ) {
2701 didWarnFormActionType = true;
2702 console.error(
2703 'A button can only specify a formAction along with type="submit" or no type.',
2704 );
2705 }
2706 }
2707
2708 const formData = pushFormActionAttribute(
2709 target,
2710 resumableState,
2711 renderState,
2712 formAction,
2713 formEncType,
2714 formMethod,
2715 formTarget,
2716 name,
2717 );
2718
2719 pushViewTransitionAttributes(target, formatContext);
2720
2721 target.push(endOfStartTag);
2722
2723 // We place any additional hidden form fields we need to include inside the button itself.
2724 pushAdditionalFormFields(target, formData);
2725
2726 pushInnerHTML(target, innerHTML, children);
2727 if (typeof children === 'string') {
2728 // Special case children as a string to avoid the unnecessary comment.
2729 // TODO: Remove this special case after the general optimization is in place.
2730 target.push(stringToChunk(encodeHTMLTextNode(children)));
2731 return null;
2732 }
2733
2734 return children;
2735 }
2736
2737 function pushStartTextArea(
2738 target: Array<Chunk | PrecomputedChunk>,
2739 props: Object,
2740 formatContext: FormatContext,
2741 ): ReactNodeList {
2742 if (__DEV__) {
2743 checkControlledValueProps('textarea', props);
2744 if (
2745 props.value !== undefined &&
2746 props.defaultValue !== undefined &&
2747 !didWarnDefaultTextareaValue
2748 ) {
2749 console.error(
2750 'Textarea elements must be either controlled or uncontrolled ' +
2751 '(specify either the value prop, or the defaultValue prop, but not ' +
2752 'both). Decide between using a controlled or uncontrolled textarea ' +
2753 'and remove one of these props. More info: ' +
2754 'https://react.dev/link/controlled-components',
2755 );
2756 didWarnDefaultTextareaValue = true;
2757 }
2758 }
2759
2760 target.push(startChunkForTag('textarea'));
2761
2762 let value = null;
2763 let defaultValue = null;
2764 let children = null;
2765 for (const propKey in props) {
2766 if (hasOwnProperty.call(props, propKey)) {
2767 const propValue = props[propKey];
2768 if (propValue == null) {
2769 continue;
2770 }
2771 switch (propKey) {
2772 case 'children':
2773 children = propValue;
2774 break;
2775 case 'value':
2776 value = propValue;
2777 break;
2778 case 'defaultValue':
2779 defaultValue = propValue;
2780 break;
2781 case 'dangerouslySetInnerHTML':
2782 throw new Error(
2783 '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
2784 );
2785 default:
2786 pushAttribute(target, propKey, propValue);
2787 break;
2788 }
2789 }
2790 }
2791 if (value === null && defaultValue !== null) {
2792 value = defaultValue;
2793 }
2794
2795 pushViewTransitionAttributes(target, formatContext);
2796
2797 target.push(endOfStartTag);
2798
2799 // TODO (yungsters): Remove support for children content in <textarea>.
2800 if (children != null) {
2801 if (__DEV__) {
2802 console.error(
2803 'Use the `defaultValue` or `value` props instead of setting ' +
2804 'children on <textarea>.',
2805 );
2806 }
2807
2808 if (value != null) {
2809 throw new Error(
2810 'If you supply `defaultValue` on a <textarea>, do not pass children.',
2811 );
2812 }
2813
2814 if (isArray(children)) {
2815 if (children.length > 1) {
2816 throw new Error('<textarea> can only have at most one child.');
2817 }
2818
2819 // TODO: remove the coercion and the DEV check below because it will
2820 // always be overwritten by the coercion several lines below it. #22309
2821 if (__DEV__) {
2822 checkHtmlStringCoercion(children[0]);
2823 }
2824 value = '' + children[0];
2825 }
2826 if (__DEV__) {
2827 checkHtmlStringCoercion(children);
2828 }
2829 value = '' + children;
2830 }
2831
2832 if (typeof value === 'string' && value[0] === '\n') {
2833 // text/html ignores the first character in these tags if it's a newline
2834 // Prefer to break application/xml over text/html (for now) by adding
2835 // a newline specifically to get eaten by the parser. (Alternately for
2836 // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
2837 // \r is normalized out by HTMLTextAreaElement#value.)
2838 // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
2839 // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
2840 // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
2841 // See: Parsing of "textarea" "listing" and "pre" elements
2842 // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
2843 target.push(leadingNewline);
2844 }
2845
2846 // ToString and push directly instead of recurse over children.
2847 // We don't really support complex children in the value anyway.
2848 // This also currently avoids a trailing comment node which breaks textarea.
2849 if (value !== null) {
2850 if (__DEV__) {
2851 checkAttributeStringCoercion(value, 'value');
2852 }
2853 target.push(stringToChunk(encodeHTMLTextNode('' + value)));
2854 }
2855
2856 return null;
2857 }
2858
2859 function pushMeta(
2860 target: Array<Chunk | PrecomputedChunk>,
2861 props: Object,
2862 renderState: RenderState,
2863 textEmbedded: boolean,
2864 formatContext: FormatContext,
2865 ): null {
2866 const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
2867 const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
2868 if (
2869 formatContext.insertionMode === SVG_MODE ||
2870 noscriptTagInScope ||
2871 props.itemProp != null
2872 ) {
2873 return pushSelfClosing(target, props, 'meta', formatContext);
2874 } else {
2875 if (textEmbedded) {
2876 // This link follows text but we aren't writing a tag. while not as efficient as possible we need
2877 // to be safe and assume text will follow by inserting a textSeparator
2878 target.push(textSeparator);
2879 }
2880
2881 if (isFallback) {
2882 // Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2883 // because they are likely superceded by primary content and we want to avoid needing to clean
2884 // them up when the primary content is ready. They are never hydrated on the client anyway because
2885 // boundaries in fallback are awaited or client render, in either case there is never hydration
2886 return null;
2887 } else if (typeof props.charSet === 'string') {
2888 // "charset" Should really be config and not picked up from tags however since this is
2889 // the only way to embed the tag today we flush it on a special queue on the Request so it
2890 // can go before everything else. Like viewport this means that the tag will escape it's
2891 // parent container.
2892 return pushSelfClosing(
2893 renderState.charsetChunks,
2894 props,
2895 'meta',
2896 formatContext,
2897 );
2898 } else if (props.name === 'viewport') {
2899 // "viewport" is flushed on the Request so it can go earlier that Float resources that
2900 // might be affected by it. This means it can escape the boundary it is rendered within.
2901 // This is a pragmatic solution to viewport being incredibly sensitive to document order
2902 // without requiring all hoistables to be flushed too early.
2903 return pushSelfClosing(
2904 renderState.viewportChunks,
2905 props,
2906 'meta',
2907 formatContext,
2908 );
2909 } else {
2910 return pushSelfClosing(
2911 renderState.hoistableChunks,
2912 props,
2913 'meta',
2914 formatContext,
2915 );
2916 }
2917 }
2918 }
2919
2920 function pushLink(
2921 target: Array<Chunk | PrecomputedChunk>,
2922 props: Object,
2923 resumableState: ResumableState,
2924 renderState: RenderState,
2925 hoistableState: null | HoistableState,
2926 textEmbedded: boolean,
2927 formatContext: FormatContext,
2928 ): null {
2929 const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
2930 const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
2931 const rel = props.rel;
2932 const href = props.href;
2933 const precedence = props.precedence;
2934 if (
2935 formatContext.insertionMode === SVG_MODE ||
2936 noscriptTagInScope ||
2937 props.itemProp != null ||
2938 typeof rel !== 'string' ||
2939 typeof href !== 'string' ||
2940 href === ''
2941 ) {
2942 if (__DEV__) {
2943 if (rel === 'stylesheet' && typeof props.precedence === 'string') {
2944 if (typeof href !== 'string' || !href) {
2945 console.error(
2946 'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and expected the `href` prop to be a non-empty string but ecountered %s instead. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop ensure there is a non-empty string `href` prop as well, otherwise remove the `precedence` prop.',
2947 getValueDescriptorExpectingObjectForWarning(href),
2948 );
2949 }
2950 }
2951 }
2952 pushLinkImpl(target, props);
2953 return null;
2954 }
2955
2956 if (props.rel === 'stylesheet') {
2957 // This <link> may hoistable as a Stylesheet Resource, otherwise it will emit in place
2958 const key = getResourceKey(href);
2959 if (
2960 typeof precedence !== 'string' ||
2961 props.disabled != null ||
2962 props.onLoad ||
2963 props.onError
2964 ) {
2965 // This stylesheet is either not opted into Resource semantics or has conflicting properties which
2966 // disqualify it for such. We can still create a preload resource to help it load faster on the
2967 // client
2968 if (__DEV__) {
2969 if (typeof precedence === 'string') {
2970 if (props.disabled != null) {
2971 console.error(
2972 'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and a `disabled` prop. The presence of the `disabled` prop indicates an intent to manage the stylesheet active state from your from your Component code and React will not hoist or deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop remove the `disabled` prop, otherwise remove the `precedence` prop.',
2973 );
2974 } else if (props.onLoad || props.onError) {
2975 const propDescription =
2976 props.onLoad && props.onError
2977 ? '`onLoad` and `onError` props'
2978 : props.onLoad
2979 ? '`onLoad` prop'
2980 : '`onError` prop';
2981 console.error(
2982 'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and %s. The presence of loading and error handlers indicates an intent to manage the stylesheet loading state from your from your Component code and React will not hoist or deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop remove the %s, otherwise remove the `precedence` prop.',
2983 propDescription,
2984 propDescription,
2985 );
2986 }
2987 }
2988 }
2989 return pushLinkImpl(target, props);
2990 } else {
2991 // This stylesheet refers to a Resource and we create a new one if necessary
2992 let styleQueue = renderState.styles.get(precedence);
2993 const hasKey = resumableState.styleResources.hasOwnProperty(key);
2994 const resourceState = hasKey
2995 ? resumableState.styleResources[key]
2996 : undefined;
2997 if (resourceState !== EXISTS) {
2998 // We are going to create this resource now so it is marked as Exists
2999 resumableState.styleResources[key] = EXISTS;
3000
3001 // If this is the first time we've encountered this precedence we need
3002 // to create a StyleQueue
3003 if (!styleQueue) {
3004 styleQueue = {
3005 precedence: stringToChunk(escapeTextForBrowser(precedence)),
3006 rules: [] as Array<Chunk | PrecomputedChunk>,
3007 hrefs: [] as Array<Chunk | PrecomputedChunk>,
3008 sheets: new Map() as Map<string, StylesheetResource>,
3009 };
3010 renderState.styles.set(precedence, styleQueue);
3011 }
3012
3013 const resource: StylesheetResource = {
3014 state: PENDING,
3015 props: stylesheetPropsFromRawProps(props),
3016 };
3017
3018 if (resourceState) {
3019 // When resourceState is truty it is a Preload state. We cast it for clarity
3020 const preloadState: Preloaded | PreloadedWithCredentials =
3021 resourceState;
3022 if (preloadState.length === 2) {
3023 adoptPreloadCredentials(resource.props, preloadState);
3024 }
3025
3026 const preloadResource = renderState.preloads.stylesheets.get(key);
3027 if (preloadResource && preloadResource.length > 0) {
3028 // The Preload for this resource was created in this render pass and has not flushed yet so
3029 // we need to clear it to avoid it flushing.
3030 preloadResource.length = 0;
3031 } else {
3032 // Either the preload resource from this render already flushed in this render pass
3033 // or the preload flushed in a prior pass (prerender). In either case we need to mark
3034 // this resource as already having been preloaded.
3035 resource.state = PRELOADED;
3036 }
3037 } else {
3038 // We don't need to check whether a preloadResource exists in the renderState
3039 // because if it did exist then the resourceState would also exist and we would
3040 // have hit the primary if condition above.
3041 }
3042
3043 // We add the newly created resource to our StyleQueue and if necessary
3044 // track the resource with the currently rendering boundary
3045 styleQueue.sheets.set(key, resource);
3046 if (hoistableState) {
3047 hoistableState.stylesheets.add(resource);
3048 }
3049 } else {
3050 // We need to track whether this boundary should wait on this resource or not.
3051 // Typically this resource should always exist since we either had it or just created
3052 // it. However, it's possible when you resume that the style has already been emitted
3053 // and then it wouldn't be recreated in the RenderState and there's no need to track
3054 // it again since we should've hoisted it to the shell already.
3055 if (styleQueue) {
3056 const resource = styleQueue.sheets.get(key);
3057 if (resource) {
3058 if (hoistableState) {
3059 hoistableState.stylesheets.add(resource);
3060 }
3061 }
3062 }
3063 }
3064 if (textEmbedded) {
3065 // This link follows text but we aren't writing a tag. while not as efficient as possible we need
3066 // to be safe and assume text will follow by inserting a textSeparator
3067 target.push(textSeparator);
3068 }
3069 return null;
3070 }
3071 } else if (props.onLoad || props.onError) {
3072 // When using load handlers we cannot hoist and need to emit links in place
3073 return pushLinkImpl(target, props);
3074 } else {
3075 // We can hoist this link so we may need to emit a text separator.
3076 // @TODO refactor text separators so we don't have to defensively add
3077 // them when we don't end up emitting a tag as a result of pushStartInstance
3078 if (textEmbedded) {
3079 // This link follows text but we aren't writing a tag. while not as efficient as possible we need
3080 // to be safe and assume text will follow by inserting a textSeparator
3081 target.push(textSeparator);
3082 }
3083
3084 if (isFallback) {
3085 // Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
3086 // because they are likely superceded by primary content and we want to avoid needing to clean
3087 // them up when the primary content is ready. They are never hydrated on the client anyway because
3088 // boundaries in fallback are awaited or client render, in either case there is never hydration
3089 return null;
3090 } else {
3091 return pushLinkImpl(renderState.hoistableChunks, props);
3092 }
3093 }
3094 }
3095
3096 function pushLinkImpl(
3097 target: Array<Chunk | PrecomputedChunk>,
3098 props: Object,
3099 ): null {
3100 target.push(startChunkForTag('link'));
3101
3102 for (const propKey in props) {
3103 if (hasOwnProperty.call(props, propKey)) {
3104 const propValue = props[propKey];
3105 if (propValue == null) {
3106 continue;
3107 }
3108 switch (propKey) {
3109 case 'children':
3110 case 'dangerouslySetInnerHTML':
3111 throw new Error(
3112 `${'link'} is a self-closing tag and must neither have \`children\` nor ` +
3113 'use `dangerouslySetInnerHTML`.',
3114 );
3115 default:
3116 pushAttribute(target, propKey, propValue);
3117 break;
3118 }
3119 }
3120 }
3121
3122 // Link never participate as a ViewTransition
3123
3124 target.push(endOfStartTagSelfClosing);
3125 return null;
3126 }
3127
3128 function pushStyle(
3129 target: Array<Chunk | PrecomputedChunk>,
3130 props: Object,
3131 resumableState: ResumableState,
3132 renderState: RenderState,
3133 hoistableState: null | HoistableState,
3134 textEmbedded: boolean,
3135 formatContext: FormatContext,
3136 ): ReactNodeList {
3137 const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
3138 if (__DEV__) {
3139 if (hasOwnProperty.call(props, 'children')) {
3140 const children = props.children;
3141
3142 const child = Array.isArray(children)
3143 ? children.length < 2
3144 ? children[0]
3145 : null
3146 : children;
3147
3148 if (
3149 typeof child === 'function' ||
3150 typeof child === 'symbol' ||
3151 Array.isArray(child)
3152 ) {
3153 const childType =
3154 typeof child === 'function'
3155 ? 'a Function'
3156 : typeof child === 'symbol'
3157 ? 'a Sybmol'
3158 : 'an Array';
3159 console.error(
3160 'React expect children of <style> tags to be a string, number, or object with a `toString` method but found %s instead. ' +
3161 'In browsers style Elements can only have `Text` Nodes as children.',
3162 childType,
3163 );
3164 }
3165 }
3166 }
3167 const precedence = props.precedence;
3168 const href = props.href;
3169 const nonce = props.nonce;
3170
3171 if (
3172 formatContext.insertionMode === SVG_MODE ||
3173 noscriptTagInScope ||
3174 props.itemProp != null ||
3175 typeof precedence !== 'string' ||
3176 typeof href !== 'string' ||
3177 href === ''
3178 ) {
3179 // This style tag is not able to be turned into a Style Resource
3180 return pushStyleImpl(target, props);
3181 }
3182
3183 if (__DEV__) {
3184 if (href.includes(' ')) {
3185 console.error(
3186 'React expected the `href` prop for a <style> tag opting into hoisting semantics using the `precedence` prop to not have any spaces but ecountered spaces instead. using spaces in this prop will cause hydration of this style to fail on the client. The href for the <style> where this ocurred is "%s".',
3187 href,
3188 );
3189 }
3190 }
3191
3192 const key = getResourceKey(href);
3193 let styleQueue = renderState.styles.get(precedence);
3194 const hasKey = resumableState.styleResources.hasOwnProperty(key);
3195 const resourceState = hasKey ? resumableState.styleResources[key] : undefined;
3196 if (resourceState !== EXISTS) {
3197 // We are going to create this resource now so it is marked as Exists
3198 resumableState.styleResources[key] = EXISTS;
3199
3200 if (__DEV__) {
3201 if (resourceState) {
3202 console.error(
3203 'React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.',
3204 href,
3205 );
3206 }
3207 }
3208
3209 if (!styleQueue) {
3210 // This is the first time we've encountered this precedence we need
3211 // to create a StyleQueue.
3212 styleQueue = {
3213 precedence: stringToChunk(escapeTextForBrowser(precedence)),
3214 rules: [] as Array<Chunk | PrecomputedChunk>,
3215 hrefs: [] as Array<Chunk | PrecomputedChunk>,
3216 sheets: new Map() as Map<string, StylesheetResource>,
3217 };
3218 renderState.styles.set(precedence, styleQueue);
3219 }
3220
3221 const nonceStyle = renderState.nonce.style;
3222 if (!nonceStyle || nonceStyle === nonce) {
3223 if (__DEV__) {
3224 if (!nonceStyle && nonce) {
3225 console.error(
3226 'React encountered a style tag with `precedence` "%s" and `nonce` "%s". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.',
3227 precedence,
3228 nonce,
3229 );
3230 }
3231 }
3232 styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href)));
3233 pushStyleContents(styleQueue.rules, props);
3234 } else if (__DEV__) {
3235 console.error(
3236 'React encountered a style tag with `precedence` "%s" and `nonce` "%s". When React manages style rules using `precedence` it will only include rules if the nonce matches the style nonce "%s" that was included with this render.',
3237 precedence,
3238 nonce,
3239 nonceStyle,
3240 );
3241 }
3242 }
3243 if (styleQueue) {
3244 // We need to track whether this boundary should wait on this resource or not.
3245 // Typically this resource should always exist since we either had it or just created
3246 // it. However, it's possible when you resume that the style has already been emitted
3247 // and then it wouldn't be recreated in the RenderState and there's no need to track
3248 // it again since we should've hoisted it to the shell already.
3249 if (hoistableState) {
3250 hoistableState.styles.add(styleQueue);
3251 }
3252 }
3253
3254 if (textEmbedded) {
3255 // This link follows text but we aren't writing a tag. while not as efficient as possible we need
3256 // to be safe and assume text will follow by inserting a textSeparator
3257 target.push(textSeparator);
3258 }
3259 }
3260
3261 /**
3262 * This escaping function is designed to work with style tag textContent only.
3263 *
3264 * While untrusted style content should be made safe before using this api it will
3265 * ensure that the style cannot be early terminated or never terminated state
3266 */
3267 function escapeStyleTextContent(styleText: string) {
3268 if (__DEV__) {
3269 checkHtmlStringCoercion(styleText);
3270 }
3271 return ('' + styleText).replace(styleRegex, styleReplacer);
3272 }
3273 const styleRegex = /(<\/|<)(s)(tyle)/gi;
3274 const styleReplacer = (
3275 match: string,
3276 prefix: string,
3277 s: string,
3278 suffix: string,
3279 ) => `${prefix}${s === 's' ? '\\73 ' : '\\53 '}${suffix}`;
3280
3281 function pushStyleImpl(
3282 target: Array<Chunk | PrecomputedChunk>,
3283 props: Object,
3284 ): ReactNodeList {
3285 target.push(startChunkForTag('style'));
3286
3287 let children = null;
3288 let innerHTML = null;
3289 for (const propKey in props) {
3290 if (hasOwnProperty.call(props, propKey)) {
3291 const propValue = props[propKey];
3292 if (propValue == null) {
3293 continue;
3294 }
3295 switch (propKey) {
3296 case 'children':
3297 children = propValue;
3298 break;
3299 case 'dangerouslySetInnerHTML':
3300 innerHTML = propValue;
3301 break;
3302 default:
3303 pushAttribute(target, propKey, propValue);
3304 break;
3305 }
3306 }
3307 }
3308
3309 // Style never participate as a ViewTransition.
3310 target.push(endOfStartTag);
3311
3312 const child = Array.isArray(children)
3313 ? children.length < 2
3314 ? children[0]
3315 : null
3316 : children;
3317 if (
3318 typeof child !== 'function' &&
3319 typeof child !== 'symbol' &&
3320 child !== null &&
3321 child !== undefined
3322 ) {
3323 target.push(stringToChunk(escapeStyleTextContent(child)));
3324 }
3325 pushInnerHTML(target, innerHTML, children);
3326 target.push(endChunkForTag('style'));
3327 return null;
3328 }
3329
3330 function pushStyleContents(
3331 target: Array<Chunk | PrecomputedChunk>,
3332 props: Object,
3333 ): void {
3334 let children = null;
3335 let innerHTML = null;
3336 for (const propKey in props) {
3337 if (hasOwnProperty.call(props, propKey)) {
3338 const propValue = props[propKey];
3339 if (propValue == null) {
3340 continue;
3341 }
3342 switch (propKey) {
3343 case 'children':
3344 children = propValue;
3345 break;
3346 case 'dangerouslySetInnerHTML':
3347 innerHTML = propValue;
3348 break;
3349 }
3350 }
3351 }
3352
3353 const child = Array.isArray(children)
3354 ? children.length < 2
3355 ? children[0]
3356 : null
3357 : children;
3358 if (
3359 typeof child !== 'function' &&
3360 typeof child !== 'symbol' &&
3361 child !== null &&
3362 child !== undefined
3363 ) {
3364 target.push(stringToChunk(escapeStyleTextContent(child)));
3365 }
3366 pushInnerHTML(target, innerHTML, children);
3367 return;
3368 }
3369
3370 function pushImg(
3371 target: Array<Chunk | PrecomputedChunk>,
3372 props: Object,
3373 resumableState: ResumableState,
3374 renderState: RenderState,
3375 hoistableState: null | HoistableState,
3376 formatContext: FormatContext,
3377 ): null {
3378 const pictureOrNoScriptTagInScope =
3379 formatContext.tagScope & (PICTURE_SCOPE | NOSCRIPT_SCOPE);
3380 const {src, srcSet} = props;
3381 if (
3382 props.loading !== 'lazy' &&
3383 (src || srcSet) &&
3384 (typeof src === 'string' || src == null) &&
3385 (typeof srcSet === 'string' || srcSet == null) &&
3386 props.fetchPriority !== 'low' &&
3387 !pictureOrNoScriptTagInScope &&
3388 // We exclude data URIs in src and srcSet since these should not be preloaded
3389 !(
3390 typeof src === 'string' &&
3391 src[4] === ':' &&
3392 (src[0] === 'd' || src[0] === 'D') &&
3393 (src[1] === 'a' || src[1] === 'A') &&
3394 (src[2] === 't' || src[2] === 'T') &&
3395 (src[3] === 'a' || src[3] === 'A')
3396 ) &&
3397 !(
3398 typeof srcSet === 'string' &&
3399 srcSet[4] === ':' &&
3400 (srcSet[0] === 'd' || srcSet[0] === 'D') &&
3401 (srcSet[1] === 'a' || srcSet[1] === 'A') &&
3402 (srcSet[2] === 't' || srcSet[2] === 'T') &&
3403 (srcSet[3] === 'a' || srcSet[3] === 'A')
3404 )
3405 ) {
3406 // We have a suspensey image and ought to preload it to optimize the loading of display blocking
3407 // resumableState.
3408
3409 if (hoistableState !== null) {
3410 // Mark this boundary's state as having suspensey images.
3411 // Only do that if we have a ViewTransition that might trigger a parent Suspense boundary
3412 // to animate its appearing. Since that's the only case we'd actually apply suspensey images
3413 // for SSR reveals.
3414 const isInSuspenseWithEnterViewTransition =
3415 formatContext.tagScope & APPEARING_SCOPE;
3416 if (isInSuspenseWithEnterViewTransition) {
3417 hoistableState.suspenseyImages = true;
3418 }
3419 }
3420
3421 const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
3422 const key = getImageResourceKey(src, srcSet, sizes);
3423
3424 const promotablePreloads = renderState.preloads.images;
3425
3426 let resource = promotablePreloads.get(key);
3427 if (resource) {
3428 // We consider whether this preload can be promoted to higher priority flushing queue.
3429 // The only time a resource will exist here is if it was created during this render
3430 // and was not already in the high priority queue.
3431 if (
3432 props.fetchPriority === 'high' ||
3433 renderState.highImagePreloads.size < 10
3434 ) {
3435 // Delete the resource from the map since we are promoting it and don't want to
3436 // reenter this branch in a second pass for duplicate img hrefs.
3437 promotablePreloads.delete(key);
3438
3439 // $FlowFixMe[incompatible-type] - Flow should understand that this is a Resource if the condition was true
3440 renderState.highImagePreloads.add(resource);
3441 }
3442 } else if (!resumableState.imageResources.hasOwnProperty(key)) {
3443 // We must construct a new preload resource
3444 resumableState.imageResources[key] = PRELOAD_NO_CREDS;
3445 const crossOrigin = getCrossOriginString(props.crossOrigin);
3446
3447 const headers = renderState.headers;
3448 let header;
3449 if (
3450 headers &&
3451 headers.remainingCapacity > 0 &&
3452 // browsers today don't support preloading responsive images from link headers so we bail out
3453 // if the img has srcset defined
3454 typeof props.srcSet !== 'string' &&
3455 // this is a hueristic similar to capping element preloads to 10 unless explicitly
3456 // fetchPriority="high". We use length here which means it will fit fewer images when
3457 // the urls are long and more when short. arguably byte size is a better hueristic because
3458 // it directly translates to how much we send down before content is actually seen.
3459 // We could unify the counts and also make it so the total is tracked regardless of
3460 // flushing output but since the headers are likely to be go earlier than content
3461 // they don't really conflict so for now I've kept them separate
3462 (props.fetchPriority === 'high' ||
3463 headers.highImagePreloads.length < 500) &&
3464 // We manually construct the options for the preload only from strings. We don't want to pollute
3465 // the params list with arbitrary props and if we copied everything over as it we might get
3466 // coercion errors. We have checks for this in Dev but it seems safer to just only accept values
3467 // that are strings
3468 ((header = getPreloadAsHeader(src, 'image', {
3469 imageSrcSet: props.srcSet,
3470 imageSizes: props.sizes,
3471 crossOrigin,
3472 integrity: props.integrity,
3473 nonce: props.nonce,
3474 type: props.type,
3475 fetchPriority: props.fetchPriority,
3476 referrerPolicy: props.referrerPolicy,
3477 })),
3478 // We always consume the header length since once we find one header that doesn't fit
3479 // we assume all the rest won't as well. This is to avoid getting into a situation
3480 // where we have a very small remaining capacity but no headers will ever fit and we end
3481 // up constantly trying to see if the next resource might make it. In the future we can
3482 // make this behavior different between render and prerender since in the latter case
3483 // we are less sensitive to the current requests runtime per and more sensitive to maximizing
3484 // headers.
3485 (headers.remainingCapacity -= header.length + 2) >= 0)
3486 ) {
3487 // If we postpone in the shell we will still emit this preload so we track
3488 // it to make sure we don't reset it.
3489 renderState.resets.image[key] = PRELOAD_NO_CREDS;
3490 if (headers.highImagePreloads) {
3491 headers.highImagePreloads += ', ';
3492 }
3493 // $FlowFixMe[unsafe-addition]: we assign header during the if condition
3494 headers.highImagePreloads += header;
3495 } else {
3496 resource = [];
3497 pushLinkImpl(resource, {
3498 rel: 'preload',
3499 as: 'image',
3500 // There is a bug in Safari where imageSrcSet is not respected on preload links
3501 // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
3502 // This harms older browers that do not support imageSrcSet by making their preloads not work
3503 // but this population is shrinking fast and is already small so we accept this tradeoff.
3504 href: srcSet ? undefined : src,
3505 imageSrcSet: srcSet,
3506 imageSizes: sizes,
3507 crossOrigin: crossOrigin,
3508 integrity: props.integrity,
3509 type: props.type,
3510 fetchPriority: props.fetchPriority,
3511 referrerPolicy: props.referrerPolicy,
3512 } as PreloadProps);
3513 if (
3514 props.fetchPriority === 'high' ||
3515 renderState.highImagePreloads.size < 10
3516 ) {
3517 renderState.highImagePreloads.add(resource);
3518 } else {
3519 renderState.bulkPreloads.add(resource);
3520 // We can bump the priority up if the same img is rendered later
3521 // with fetchPriority="high"
3522 promotablePreloads.set(key, resource);
3523 }
3524 }
3525 }
3526 }
3527 return pushSelfClosing(target, props, 'img', formatContext);
3528 }
3529
3530 function pushSelfClosing(
3531 target: Array<Chunk | PrecomputedChunk>,
3532 props: Object,
3533 tag: string,
3534 formatContext: FormatContext,
3535 ): null {
3536 target.push(startChunkForTag(tag));
3537
3538 for (const propKey in props) {
3539 if (hasOwnProperty.call(props, propKey)) {
3540 const propValue = props[propKey];
3541 if (propValue == null) {
3542 continue;
3543 }
3544 switch (propKey) {
3545 case 'children':
3546 case 'dangerouslySetInnerHTML':
3547 throw new Error(
3548 `${tag} is a self-closing tag and must neither have \`children\` nor ` +
3549 'use `dangerouslySetInnerHTML`.',
3550 );
3551 default:
3552 pushAttribute(target, propKey, propValue);
3553 break;
3554 }
3555 }
3556 }
3557
3558 pushViewTransitionAttributes(target, formatContext);
3559
3560 target.push(endOfStartTagSelfClosing);
3561 return null;
3562 }
3563
3564 function pushStartMenuItem(
3565 target: Array<Chunk | PrecomputedChunk>,
3566 props: Object,
3567 formatContext: FormatContext,
3568 ): ReactNodeList {
3569 target.push(startChunkForTag('menuitem'));
3570
3571 for (const propKey in props) {
3572 if (hasOwnProperty.call(props, propKey)) {
3573 const propValue = props[propKey];
3574 if (propValue == null) {
3575 continue;
3576 }
3577 switch (propKey) {
3578 case 'children':
3579 case 'dangerouslySetInnerHTML':
3580 throw new Error(
3581 'menuitems cannot have `children` nor `dangerouslySetInnerHTML`.',
3582 );
3583 default:
3584 pushAttribute(target, propKey, propValue);
3585 break;
3586 }
3587 }
3588 }
3589
3590 pushViewTransitionAttributes(target, formatContext);
3591
3592 target.push(endOfStartTag);
3593 return null;
3594 }
3595
3596 function pushTitle(
3597 target: Array<Chunk | PrecomputedChunk>,
3598 props: Object,
3599 renderState: RenderState,
3600 formatContext: FormatContext,
3601 ): ReactNodeList {
3602 const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
3603 const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
3604 if (__DEV__) {
3605 if (hasOwnProperty.call(props, 'children')) {
3606 const children = props.children;
3607
3608 const child = Array.isArray(children)
3609 ? children.length < 2
3610 ? children[0]
3611 : null
3612 : children;
3613
3614 if (Array.isArray(children) && children.length > 1) {
3615 console.error(
3616 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length %s instead.' +
3617 ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' +
3618 ' which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes.' +
3619 ' For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' +
3620 ' is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.',
3621 children.length,
3622 );
3623 } else if (typeof child === 'function' || typeof child === 'symbol') {
3624 const childType =
3625 typeof child === 'function' ? 'a Function' : 'a Sybmol';
3626 console.error(
3627 'React expect children of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found %s instead.' +
3628 ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' +
3629 ' tags to a single string value.',
3630 childType,
3631 );
3632 // $FlowFixMe[invalid-compare]
3633 // $FlowFixMe[constant-condition]
3634 } else if (child && child.toString === {}.toString) {
3635 if (child.$$typeof != null) {
3636 console.error(
3637 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that appears to be' +
3638 ' a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to' +
3639 ' be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is' +
3640 ' a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.',
3641 );
3642 } else {
3643 console.error(
3644 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that does not implement' +
3645 ' a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags' +
3646 ' to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title>' +
3647 ' is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.',
3648 );
3649 }
3650 }
3651 }
3652 }
3653
3654 if (
3655 formatContext.insertionMode !== SVG_MODE &&
3656 !noscriptTagInScope &&
3657 props.itemProp == null
3658 ) {
3659 if (isFallback) {
3660 // Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
3661 // because they are likely superceded by primary content and we want to avoid needing to clean
3662 // them up when the primary content is ready. They are never hydrated on the client anyway because
3663 // boundaries in fallback are awaited or client render, in either case there is never hydration
3664 return null;
3665 } else {
3666 pushTitleImpl(renderState.hoistableChunks, props);
3667 }
3668 } else {
3669 return pushTitleImpl(target, props);
3670 }
3671 }
3672
3673 function pushTitleImpl(
3674 target: Array<Chunk | PrecomputedChunk>,
3675 props: Object,
3676 ): null {
3677 target.push(startChunkForTag('title'));
3678
3679 let children = null;
3680 let innerHTML = null;
3681 for (const propKey in props) {
3682 if (hasOwnProperty.call(props, propKey)) {
3683 const propValue = props[propKey];
3684 if (propValue == null) {
3685 continue;
3686 }
3687 switch (propKey) {
3688 case 'children':
3689 children = propValue;
3690 break;
3691 case 'dangerouslySetInnerHTML':
3692 innerHTML = propValue;
3693 break;
3694 default:
3695 pushAttribute(target, propKey, propValue);
3696 break;
3697 }
3698 }
3699 }
3700 // Title never participate as a ViewTransition
3701 target.push(endOfStartTag);
3702
3703 const child = Array.isArray(children)
3704 ? children.length < 2
3705 ? children[0]
3706 : null
3707 : children;
3708 if (
3709 typeof child !== 'function' &&
3710 typeof child !== 'symbol' &&
3711 child !== null &&
3712 child !== undefined
3713 ) {
3714 // eslint-disable-next-line react-internal/safe-string-coercion
3715 target.push(stringToChunk(escapeTextForBrowser('' + child)));
3716 }
3717 pushInnerHTML(target, innerHTML, children);
3718 target.push(endChunkForTag('title'));
3719 return null;
3720 }
3721
3722 // These are used by the client if we clear a boundary and we find these, then we
3723 // also clear the singleton as well.
3724 const headPreambleContributionChunk = stringToPrecomputedChunk('<!--head-->');
3725 const bodyPreambleContributionChunk = stringToPrecomputedChunk('<!--body-->');
3726 const htmlPreambleContributionChunk = stringToPrecomputedChunk('<!--html-->');
3727
3728 function pushStartHead(
3729 target: Array<Chunk | PrecomputedChunk>,
3730 props: Object,
3731 renderState: RenderState,
3732 preambleState: null | PreambleState,
3733 formatContext: FormatContext,
3734 ): ReactNodeList {
3735 if (formatContext.insertionMode < HTML_MODE) {
3736 // This <head> is the Document.head and should be part of the preamble
3737 const preamble = preambleState || renderState.preamble;
3738
3739 if (preamble.headChunks) {
3740 throw new Error(`The ${'`<head>`'} tag may only be rendered once.`);
3741 }
3742
3743 // Insert a marker in the body where the contribution to the head was in case we need to clear it.
3744 if (preambleState !== null) {
3745 target.push(headPreambleContributionChunk);
3746 }
3747
3748 preamble.headChunks = [];
3749 return pushStartSingletonElement(
3750 preamble.headChunks,
3751 props,
3752 'head',
3753 formatContext,
3754 );
3755 } else {
3756 // This <head> is deep and is likely just an error. we emit it inline though.
3757 // Validation should warn that this tag is the the wrong spot.
3758 return pushStartGenericElement(target, props, 'head', formatContext);
3759 }
3760 }
3761
3762 function pushStartBody(
3763 target: Array<Chunk | PrecomputedChunk>,
3764 props: Object,
3765 renderState: RenderState,
3766 preambleState: null | PreambleState,
3767 formatContext: FormatContext,
3768 ): ReactNodeList {
3769 if (formatContext.insertionMode < HTML_MODE) {
3770 // This <body> is the Document.body
3771 const preamble = preambleState || renderState.preamble;
3772
3773 if (preamble.bodyChunks) {
3774 throw new Error(`The ${'`<body>`'} tag may only be rendered once.`);
3775 }
3776
3777 // Insert a marker in the body where the contribution to the body tag was in case we need to clear it.
3778 if (preambleState !== null) {
3779 target.push(bodyPreambleContributionChunk);
3780 }
3781
3782 preamble.bodyChunks = [];
3783 return pushStartSingletonElement(
3784 preamble.bodyChunks,
3785 props,
3786 'body',
3787 formatContext,
3788 );
3789 } else {
3790 // This <head> is deep and is likely just an error. we emit it inline though.
3791 // Validation should warn that this tag is the the wrong spot.
3792 return pushStartGenericElement(target, props, 'body', formatContext);
3793 }
3794 }
3795
3796 function pushStartHtml(
3797 target: Array<Chunk | PrecomputedChunk>,
3798 props: Object,
3799 renderState: RenderState,
3800 preambleState: null | PreambleState,
3801 formatContext: FormatContext,
3802 ): ReactNodeList {
3803 if (formatContext.insertionMode === ROOT_HTML_MODE) {
3804 // This <html> is the Document.documentElement
3805 const preamble = preambleState || renderState.preamble;
3806
3807 if (preamble.htmlChunks) {
3808 throw new Error(`The ${'`<html>`'} tag may only be rendered once.`);
3809 }
3810
3811 // Insert a marker in the body where the contribution to the head was in case we need to clear it.
3812 if (preambleState !== null) {
3813 target.push(htmlPreambleContributionChunk);
3814 }
3815
3816 preamble.htmlChunks = [DOCTYPE];
3817 return pushStartSingletonElement(
3818 preamble.htmlChunks,
3819 props,
3820 'html',
3821 formatContext,
3822 );
3823 } else {
3824 // This <html> is deep and is likely just an error. we emit it inline though.
3825 // Validation should warn that this tag is the the wrong spot.
3826 return pushStartGenericElement(target, props, 'html', formatContext);
3827 }
3828 }
3829
3830 function pushScript(
3831 target: Array<Chunk | PrecomputedChunk>,
3832 props: Object,
3833 resumableState: ResumableState,
3834 renderState: RenderState,
3835 textEmbedded: boolean,
3836 formatContext: FormatContext,
3837 ): null {
3838 const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
3839 const asyncProp = props.async;
3840 if (
3841 typeof props.src !== 'string' ||
3842 !props.src ||
3843 !(
3844 asyncProp &&
3845 typeof asyncProp !== 'function' &&
3846 typeof asyncProp !== 'symbol'
3847 ) ||
3848 props.onLoad ||
3849 props.onError ||
3850 formatContext.insertionMode === SVG_MODE ||
3851 noscriptTagInScope ||
3852 props.itemProp != null
3853 ) {
3854 // This script will not be a resource, we bailout early and emit it in place.
3855 return pushScriptImpl(target, props);
3856 }
3857
3858 const src = props.src;
3859 const key = getResourceKey(src);
3860 // We can make this <script> into a ScriptResource
3861
3862 let resources, preloads;
3863 if (props.type === 'module') {
3864 resources = resumableState.moduleScriptResources;
3865 preloads = renderState.preloads.moduleScripts;
3866 } else {
3867 resources = resumableState.scriptResources;
3868 preloads = renderState.preloads.scripts;
3869 }
3870
3871 const hasKey = resources.hasOwnProperty(key);
3872 const resourceState = hasKey ? resources[key] : undefined;
3873 if (resourceState !== EXISTS) {
3874 // We are going to create this resource now so it is marked as Exists
3875 resources[key] = EXISTS;
3876
3877 let scriptProps = props;
3878 if (resourceState) {
3879 // When resourceState is truty it is a Preload state. We cast it for clarity
3880 const preloadState: Preloaded | PreloadedWithCredentials = resourceState;
3881 if (preloadState.length === 2) {
3882 scriptProps = {...props};
3883 adoptPreloadCredentials(scriptProps, preloadState);
3884 }
3885
3886 const preloadResource = preloads.get(key);
3887 if (preloadResource) {
3888 // the preload resource exists was created in this render. Now that we have
3889 // a script resource which will emit earlier than a preload would if it
3890 // hasn't already flushed we prevent it from flushing by zeroing the length
3891 preloadResource.length = 0;
3892 }
3893 }
3894
3895 const resource: Resource = [];
3896 // Add to the script flushing queue
3897 renderState.scripts.add(resource);
3898 // encode the tag as Chunks
3899 pushScriptImpl(resource, scriptProps);
3900 }
3901
3902 if (textEmbedded) {
3903 // This script follows text but we aren't writing a tag. while not as efficient as possible we need
3904 // to be safe and assume text will follow by inserting a textSeparator
3905 target.push(textSeparator);
3906 }
3907 return null;
3908 }
3909
3910 function pushScriptImpl(
3911 target: Array<Chunk | PrecomputedChunk>,
3912 props: Object,
3913 ): null {
3914 target.push(startChunkForTag('script'));
3915
3916 let children = null;
3917 let innerHTML = null;
3918 for (const propKey in props) {
3919 if (hasOwnProperty.call(props, propKey)) {
3920 const propValue = props[propKey];
3921 if (propValue == null) {
3922 continue;
3923 }
3924 switch (propKey) {
3925 case 'children':
3926 children = propValue;
3927 break;
3928 case 'dangerouslySetInnerHTML':
3929 innerHTML = propValue;
3930 break;
3931 default:
3932 pushAttribute(target, propKey, propValue);
3933 break;
3934 }
3935 }
3936 }
3937 // Scripts never participate as a ViewTransition
3938 target.push(endOfStartTag);
3939
3940 if (__DEV__) {
3941 if (children != null && typeof children !== 'string') {
3942 const descriptiveStatement =
3943 typeof children === 'number'
3944 ? 'a number for children'
3945 : Array.isArray(children)
3946 ? 'an array for children'
3947 : 'something unexpected for children';
3948 console.error(
3949 'A script element was rendered with %s. If script element has children it must be a single string.' +
3950 ' Consider using dangerouslySetInnerHTML or passing a plain string as children.',
3951 descriptiveStatement,
3952 );
3953 }
3954 }
3955
3956 pushInnerHTML(target, innerHTML, children);
3957 if (typeof children === 'string') {
3958 target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
3959 }
3960 target.push(endChunkForTag('script'));
3961 return null;
3962 }
3963
3964 // This is a fork of pushStartGenericElement because we don't ever want to do
3965 // the children as strign optimization on that path when rendering singletons.
3966 // When we eliminate that special path we can delete this fork and unify it again
3967 function pushStartSingletonElement(
3968 target: Array<Chunk | PrecomputedChunk>,
3969 props: Object,
3970 tag: string,
3971 formatContext: FormatContext,
3972 ): ReactNodeList {
3973 target.push(startChunkForTag(tag));
3974
3975 let children = null;
3976 let innerHTML = null;
3977 for (const propKey in props) {
3978 if (hasOwnProperty.call(props, propKey)) {
3979 const propValue = props[propKey];
3980 if (propValue == null) {
3981 continue;
3982 }
3983 switch (propKey) {
3984 case 'children':
3985 children = propValue;
3986 break;
3987 case 'dangerouslySetInnerHTML':
3988 innerHTML = propValue;
3989 break;
3990 default:
3991 pushAttribute(target, propKey, propValue);
3992 break;
3993 }
3994 }
3995 }
3996
3997 pushViewTransitionAttributes(target, formatContext);
3998
3999 target.push(endOfStartTag);
4000 pushInnerHTML(target, innerHTML, children);
4001 return children;
4002 }
4003
4004 function pushStartGenericElement(
4005 target: Array<Chunk | PrecomputedChunk>,
4006 props: Object,
4007 tag: string,
4008 formatContext: FormatContext,
4009 ): ReactNodeList {
4010 target.push(startChunkForTag(tag));
4011
4012 let children = null;
4013 let innerHTML = null;
4014 for (const propKey in props) {
4015 if (hasOwnProperty.call(props, propKey)) {
4016 const propValue = props[propKey];
4017 if (propValue == null) {
4018 continue;
4019 }
4020 switch (propKey) {
4021 case 'children':
4022 children = propValue;
4023 break;
4024 case 'dangerouslySetInnerHTML':
4025 innerHTML = propValue;
4026 break;
4027 default:
4028 pushAttribute(target, propKey, propValue);
4029 break;
4030 }
4031 }
4032 }
4033
4034 pushViewTransitionAttributes(target, formatContext);
4035
4036 target.push(endOfStartTag);
4037 pushInnerHTML(target, innerHTML, children);
4038 if (typeof children === 'string') {
4039 // Special case children as a string to avoid the unnecessary comment.
4040 // TODO: Remove this special case after the general optimization is in place.
4041 target.push(stringToChunk(encodeHTMLTextNode(children)));
4042 return null;
4043 }
4044 return children;
4045 }
4046
4047 function pushStartCustomElement(
4048 target: Array<Chunk | PrecomputedChunk>,
4049 props: Object,
4050 tag: string,
4051 formatContext: FormatContext,
4052 ): ReactNodeList {
4053 target.push(startChunkForTag(tag));
4054
4055 let children = null;
4056 let innerHTML = null;
4057 for (const propKey in props) {
4058 if (hasOwnProperty.call(props, propKey)) {
4059 let propValue = props[propKey];
4060 if (propValue == null) {
4061 continue;
4062 }
4063 let attributeName = propKey;
4064 switch (propKey) {
4065 case 'children':
4066 children = propValue;
4067 break;
4068 case 'dangerouslySetInnerHTML':
4069 innerHTML = propValue;
4070 break;
4071 case 'style':
4072 pushStyleAttribute(target, propValue);
4073 break;
4074 case 'suppressContentEditableWarning':
4075 case 'suppressHydrationWarning':
4076 case 'ref':
4077 // Ignored. These are built-in to React on the client.
4078 break;
4079 case 'className':
4080 // className gets rendered as class on the client, so it should be
4081 // rendered as class on the server.
4082 attributeName = 'class';
4083 // intentional fallthrough
4084 default:
4085 if (
4086 isAttributeNameSafe(propKey) &&
4087 typeof propValue !== 'function' &&
4088 typeof propValue !== 'symbol'
4089 ) {
4090 // $FlowFixMe[invalid-compare]
4091 if (propValue === false) {
4092 continue;
4093 // $FlowFixMe[invalid-compare]
4094 } else if (propValue === true) {
4095 propValue = '';
4096 } else if (typeof propValue === 'object') {
4097 continue;
4098 }
4099 target.push(
4100 attributeSeparator,
4101 stringToChunk(attributeName),
4102 attributeAssign,
4103 stringToChunk(escapeTextForBrowser(propValue)),
4104 attributeEnd,
4105 );
4106 }
4107 break;
4108 }
4109 }
4110 }
4111
4112 // TODO: ViewTransition attributes gets observed by the Custom Element which is a bit sketchy.
4113 pushViewTransitionAttributes(target, formatContext);
4114
4115 target.push(endOfStartTag);
4116 pushInnerHTML(target, innerHTML, children);
4117 return children;
4118 }
4119
4120 const leadingNewline = stringToPrecomputedChunk('\n');
4121
4122 function pushStartPreformattedElement(
4123 target: Array<Chunk | PrecomputedChunk>,
4124 props: Object,
4125 tag: string,
4126 formatContext: FormatContext,
4127 ): ReactNodeList {
4128 target.push(startChunkForTag(tag));
4129
4130 let children = null;
4131 let innerHTML = null;
4132 for (const propKey in props) {
4133 if (hasOwnProperty.call(props, propKey)) {
4134 const propValue = props[propKey];
4135 if (propValue == null) {
4136 continue;
4137 }
4138 switch (propKey) {
4139 case 'children':
4140 children = propValue;
4141 break;
4142 case 'dangerouslySetInnerHTML':
4143 innerHTML = propValue;
4144 break;
4145 default:
4146 pushAttribute(target, propKey, propValue);
4147 break;
4148 }
4149 }
4150 }
4151
4152 pushViewTransitionAttributes(target, formatContext);
4153
4154 target.push(endOfStartTag);
4155
4156 // text/html ignores the first character in these tags if it's a newline
4157 // Prefer to break application/xml over text/html (for now) by adding
4158 // a newline specifically to get eaten by the parser. (Alternately for
4159 // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
4160 // \r is normalized out by HTMLTextAreaElement#value.)
4161 // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
4162 // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
4163 // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
4164 // See: Parsing of "textarea" "listing" and "pre" elements
4165 // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
4166 // TODO: This doesn't deal with the case where the child is an array
4167 // or component that returns a string.
4168 if (innerHTML != null) {
4169 if (children != null) {
4170 throw new Error(
4171 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
4172 );
4173 }
4174
4175 if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {
4176 throw new Error(
4177 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
4178 'Please visit https://react.dev/link/dangerously-set-inner-html ' +
4179 'for more information.',
4180 );
4181 }
4182
4183 const html = innerHTML.__html;
4184 if (html !== null && html !== undefined) {
4185 if (typeof html === 'string' && html.length > 0 && html[0] === '\n') {
4186 target.push(leadingNewline, stringToChunk(html));
4187 } else {
4188 if (__DEV__) {
4189 checkHtmlStringCoercion(html);
4190 }
4191 target.push(stringToChunk('' + html));
4192 }
4193 }
4194 }
4195 if (typeof children === 'string' && children[0] === '\n') {
4196 target.push(leadingNewline);
4197 }
4198 return children;
4199 }
4200
4201 // We accept any tag to be rendered but since this gets injected into arbitrary
4202 // HTML, we want to make sure that it's a safe tag.
4203 // http://www.w3.org/TR/REC-xml/#NT-Name
4204 const VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
4205 const validatedTagCache = new Map<string, PrecomputedChunk>();
4206 function startChunkForTag(tag: string): PrecomputedChunk {
4207 let tagStartChunk = validatedTagCache.get(tag);
4208 if (tagStartChunk === undefined) {
4209 if (!VALID_TAG_REGEX.test(tag)) {
4210 throw new Error(`Invalid tag: ${tag}`);
4211 }
4212
4213 tagStartChunk = stringToPrecomputedChunk('<' + tag);
4214 validatedTagCache.set(tag, tagStartChunk);
4215 }
4216 return tagStartChunk;
4217 }
4218
4219 export const doctypeChunk: PrecomputedChunk =
4220 stringToPrecomputedChunk('<!DOCTYPE html>');
4221
4222 import {doctypeChunk as DOCTYPE} from 'react-server/src/ReactFizzConfig';
4223
4224 export function pushStartInstance(
4225 target: Array<Chunk | PrecomputedChunk>,
4226 type: string,
4227 props: Object,
4228 resumableState: ResumableState,
4229 renderState: RenderState,
4230 preambleState: null | PreambleState,
4231 hoistableState: null | HoistableState,
4232 formatContext: FormatContext,
4233 textEmbedded: boolean,
4234 ): ReactNodeList {
4235 if (__DEV__) {
4236 validateARIAProperties(type, props);
4237 validateInputProperties(type, props);
4238 validateUnknownProperties(type, props, null);
4239
4240 if (
4241 !props.suppressContentEditableWarning &&
4242 props.contentEditable &&
4243 props.children != null
4244 ) {
4245 console.error(
4246 'A component is `contentEditable` and contains `children` managed by ' +
4247 'React. It is now your responsibility to guarantee that none of ' +
4248 'those nodes are unexpectedly modified or duplicated. This is ' +
4249 'probably not intentional.',
4250 );
4251 }
4252
4253 if (
4254 formatContext.insertionMode !== SVG_MODE &&
4255 formatContext.insertionMode !== MATHML_MODE
4256 ) {
4257 if (type.indexOf('-') === -1 && type.toLowerCase() !== type) {
4258 console.error(
4259 '<%s /> is using incorrect casing. ' +
4260 'Use PascalCase for React components, ' +
4261 'or lowercase for HTML elements.',
4262 type,
4263 );
4264 }
4265 }
4266 }
4267
4268 switch (type) {
4269 case 'div':
4270 case 'span':
4271 case 'svg':
4272 case 'path':
4273 // Fast track very common tags
4274 break;
4275 case 'a':
4276 return pushStartAnchor(target, props, formatContext);
4277 case 'g':
4278 case 'p':
4279 case 'li':
4280 // Fast track very common tags
4281 break;
4282 // Special tags
4283 case 'select':
4284 return pushStartSelect(target, props, formatContext);
4285 case 'option':
4286 return pushStartOption(target, props, formatContext);
4287 case 'textarea':
4288 return pushStartTextArea(target, props, formatContext);
4289 case 'input':
4290 return pushInput(
4291 target,
4292 props,
4293 resumableState,
4294 renderState,
4295 formatContext,
4296 );
4297 case 'button':
4298 return pushStartButton(
4299 target,
4300 props,
4301 resumableState,
4302 renderState,
4303 formatContext,
4304 );
4305 case 'form':
4306 return pushStartForm(
4307 target,
4308 props,
4309 resumableState,
4310 renderState,
4311 formatContext,
4312 );
4313 case 'menuitem':
4314 return pushStartMenuItem(target, props, formatContext);
4315 case 'object':
4316 return pushStartObject(target, props, formatContext);
4317 case 'title':
4318 return pushTitle(target, props, renderState, formatContext);
4319 case 'link':
4320 return pushLink(
4321 target,
4322 props,
4323 resumableState,
4324 renderState,
4325 hoistableState,
4326 textEmbedded,
4327 formatContext,
4328 );
4329 case 'script':
4330 return pushScript(
4331 target,
4332 props,
4333 resumableState,
4334 renderState,
4335 textEmbedded,
4336 formatContext,
4337 );
4338 case 'style':
4339 return pushStyle(
4340 target,
4341 props,
4342 resumableState,
4343 renderState,
4344 hoistableState,
4345 textEmbedded,
4346 formatContext,
4347 );
4348 case 'meta':
4349 return pushMeta(target, props, renderState, textEmbedded, formatContext);
4350 // Newline eating tags
4351 case 'listing':
4352 case 'pre': {
4353 return pushStartPreformattedElement(target, props, type, formatContext);
4354 }
4355 case 'img': {
4356 return pushImg(
4357 target,
4358 props,
4359 resumableState,
4360 renderState,
4361 hoistableState,
4362 formatContext,
4363 );
4364 }
4365 // Omitted close tags
4366 case 'base':
4367 case 'area':
4368 case 'br':
4369 case 'col':
4370 case 'embed':
4371 case 'hr':
4372 case 'keygen':
4373 case 'param':
4374 case 'source':
4375 case 'track':
4376 case 'wbr': {
4377 return pushSelfClosing(target, props, type, formatContext);
4378 }
4379 // These are reserved SVG and MathML elements, that are never custom elements.
4380 // https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-core-concepts
4381 case 'annotation-xml':
4382 case 'color-profile':
4383 case 'font-face':
4384 case 'font-face-src':
4385 case 'font-face-uri':
4386 case 'font-face-format':
4387 case 'font-face-name':
4388 case 'missing-glyph': {
4389 break;
4390 }
4391 // Preamble start tags
4392 case 'head':
4393 return pushStartHead(
4394 target,
4395 props,
4396 renderState,
4397 preambleState,
4398 formatContext,
4399 );
4400 case 'body':
4401 return pushStartBody(
4402 target,
4403 props,
4404 renderState,
4405 preambleState,
4406 formatContext,
4407 );
4408 case 'html': {
4409 return pushStartHtml(
4410 target,
4411 props,
4412 renderState,
4413 preambleState,
4414 formatContext,
4415 );
4416 }
4417 default: {
4418 if (type.indexOf('-') !== -1) {
4419 // Custom element
4420 return pushStartCustomElement(target, props, type, formatContext);
4421 }
4422 }
4423 }
4424 // Generic element
4425 return pushStartGenericElement(target, props, type, formatContext);
4426 }
4427
4428 const endTagCache = new Map<string, PrecomputedChunk>();
4429 function endChunkForTag(tag: string): PrecomputedChunk {
4430 let chunk = endTagCache.get(tag);
4431 if (chunk === undefined) {
4432 chunk = stringToPrecomputedChunk('</' + tag + '>');
4433 endTagCache.set(tag, chunk);
4434 }
4435 return chunk;
4436 }
4437
4438 export function pushEndInstance(
4439 target: Array<Chunk | PrecomputedChunk>,
4440 type: string,
4441 props: Object,
4442 resumableState: ResumableState,
4443 formatContext: FormatContext,
4444 ): void {
4445 switch (type) {
4446 // We expect title and script tags to always be pushed in a unit and never
4447 // return children. when we end up pushing the end tag we want to ensure
4448 // there is no extra closing tag pushed
4449 case 'title':
4450 case 'style':
4451 case 'script':
4452 // Omitted close tags
4453 // TODO: Instead of repeating this switch we could try to pass a flag from above.
4454 // That would require returning a tuple. Which might be ok if it gets inlined.
4455 // fallthrough
4456 case 'area':
4457 case 'base':
4458 case 'br':
4459 case 'col':
4460 case 'embed':
4461 case 'hr':
4462 case 'img':
4463 case 'input':
4464 case 'keygen':
4465 case 'link':
4466 case 'meta':
4467 case 'param':
4468 case 'source':
4469 case 'track':
4470 case 'wbr': {
4471 // No close tag needed.
4472 return;
4473 }
4474 // Postamble end tags
4475 // When float is enabled we omit the end tags for body and html when
4476 // they represent the Document.body and Document.documentElement Nodes.
4477 // This is so we can withhold them until the postamble when we know
4478 // we won't emit any more tags
4479 case 'body': {
4480 if (formatContext.insertionMode <= HTML_HTML_MODE) {
4481 resumableState.hasBody = true;
4482 return;
4483 }
4484 break;
4485 }
4486 case 'html':
4487 if (formatContext.insertionMode === ROOT_HTML_MODE) {
4488 resumableState.hasHtml = true;
4489 return;
4490 }
4491 break;
4492 case 'head':
4493 if (formatContext.insertionMode <= HTML_HTML_MODE) {
4494 return;
4495 }
4496 break;
4497 }
4498 target.push(endChunkForTag(type));
4499 }
4500
4501 export function hoistPreambleState(
4502 renderState: RenderState,
4503 preambleState: PreambleState,
4504 ) {
4505 const rootPreamble = renderState.preamble;
4506 if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) {
4507 rootPreamble.htmlChunks = preambleState.htmlChunks;
4508 }
4509 if (rootPreamble.headChunks === null && preambleState.headChunks) {
4510 rootPreamble.headChunks = preambleState.headChunks;
4511 }
4512 if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) {
4513 rootPreamble.bodyChunks = preambleState.bodyChunks;
4514 }
4515 }
4516
4517 export function isPreambleReady(
4518 renderState: RenderState,
4519 // This means there are unfinished Suspense boundaries which could contain
4520 // a preamble. In the case of DOM we constrain valid programs to only having
4521 // one instance of each singleton so we can determine the preamble is ready
4522 // as long as we have chunks for each of these tags.
4523 hasPendingPreambles: boolean,
4524 ): boolean {
4525 const preamble = renderState.preamble;
4526 return (
4527 // There are no remaining boundaries which might contain a preamble so
4528 // the preamble is as complete as it is going to get
4529 hasPendingPreambles === false ||
4530 // we have a head and body tag. we don't need to wait for any more
4531 // because it would be invalid to render additional copies of these tags
4532 !!(preamble.headChunks && preamble.bodyChunks)
4533 );
4534 }
4535
4536 function writeBootstrap(
4537 destination: Destination,
4538 renderState: RenderState,
4539 ): boolean {
4540 const bootstrapChunks = renderState.bootstrapChunks;
4541 let i = 0;
4542 for (; i < bootstrapChunks.length - 1; i++) {
4543 writeChunk(destination, bootstrapChunks[i]);
4544 }
4545 if (i < bootstrapChunks.length) {
4546 const lastChunk = bootstrapChunks[i];
4547 bootstrapChunks.length = 0;
4548 return writeChunkAndReturn(destination, lastChunk);
4549 }
4550 return true;
4551 }
4552
4553 const shellTimeRuntimeScript = stringToPrecomputedChunk(markShellTime);
4554
4555 function writeShellTimeInstruction(
4556 destination: Destination,
4557 resumableState: ResumableState,
4558 renderState: RenderState,
4559 ): boolean {
4560 if (
4561 enableFizzExternalRuntime &&
4562 resumableState.streamingFormat !== ScriptStreamingFormat
4563 ) {
4564 // External runtime always tracks the shell time in the runtime.
4565 return true;
4566 }
4567 if ((resumableState.instructions & SentMarkShellTime) !== NothingSent) {
4568 // We already sent this instruction.
4569 return true;
4570 }
4571 resumableState.instructions |= SentMarkShellTime;
4572 writeChunk(destination, renderState.startInlineScript);
4573 writeCompletedShellIdAttribute(destination, resumableState);
4574 writeChunk(destination, endOfStartTag);
4575 writeChunk(destination, shellTimeRuntimeScript);
4576 return writeChunkAndReturn(destination, endInlineScript);
4577 }
4578
4579 export function writeCompletedRoot(
4580 destination: Destination,
4581 resumableState: ResumableState,
4582 renderState: RenderState,
4583 isComplete: boolean,
4584 ): boolean {
4585 if (!isComplete) {
4586 // If we're not already fully complete, we might complete another boundary. If so,
4587 // we need to track the paint time of the shell so we know how much to throttle the reveal.
4588 writeShellTimeInstruction(destination, resumableState, renderState);
4589 }
4590 if (enableFizzBlockingRender) {
4591 const preamble = renderState.preamble;
4592 if (preamble.htmlChunks || preamble.headChunks) {
4593 // If we rendered the whole document, then we emitted a rel="expect" that needs a
4594 // matching target. Normally we use one of the bootstrap scripts for this but if
4595 // there are none, then we need to emit a tag to complete the shell.
4596 if (
4597 (resumableState.instructions & SentCompletedShellId) ===
4598 NothingSent
4599 ) {
4600 writeChunk(destination, startChunkForTag('template'));
4601 writeCompletedShellIdAttribute(destination, resumableState);
4602 writeChunk(destination, endOfStartTag);
4603 writeChunk(destination, endChunkForTag('template'));
4604 }
4605 }
4606 }
4607 return writeBootstrap(destination, renderState);
4608 }
4609
4610 // Structural Nodes
4611
4612 // A placeholder is a node inside a hidden partial tree that can be filled in later, but before
4613 // display. It's never visible to users. We use the template tag because it can be used in every
4614 // type of parent. <script> tags also work in every other tag except <colgroup>.
4615 const placeholder1 = stringToPrecomputedChunk('<template id="');
4616 const placeholder2 = stringToPrecomputedChunk('"></template>');
4617 export function writePlaceholder(
4618 destination: Destination,
4619 renderState: RenderState,
4620 id: number,
4621 ): boolean {
4622 writeChunk(destination, placeholder1);
4623 writeChunk(destination, renderState.placeholderPrefix);
4624 const formattedID = stringToChunk(id.toString(16));
4625 writeChunk(destination, formattedID);
4626 return writeChunkAndReturn(destination, placeholder2);
4627 }
4628
4629 // Activity boundaries are encoded as comments.
4630 const startActivityBoundary = stringToPrecomputedChunk('<!--&-->');
4631 const endActivityBoundary = stringToPrecomputedChunk('<!--/&-->');
4632
4633 export function pushStartActivityBoundary(
4634 target: Array<Chunk | PrecomputedChunk>,
4635 renderState: RenderState,
4636 ): void {
4637 target.push(startActivityBoundary);
4638 }
4639
4640 export function pushEndActivityBoundary(
4641 target: Array<Chunk | PrecomputedChunk>,
4642 renderState: RenderState,
4643 ): void {
4644 target.push(endActivityBoundary);
4645 }
4646
4647 // Suspense boundaries are encoded as comments.
4648 const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
4649 const startPendingSuspenseBoundary1 = stringToPrecomputedChunk(
4650 '<!--$?--><template id="',
4651 );
4652 const startPendingSuspenseBoundary2 = stringToPrecomputedChunk('"></template>');
4653 const startClientRenderedSuspenseBoundary =
4654 stringToPrecomputedChunk('<!--$!-->');
4655 const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');
4656
4657 const clientRenderedSuspenseBoundaryError1 =
4658 stringToPrecomputedChunk('<template');
4659 const clientRenderedSuspenseBoundaryErrorAttrInterstitial =
4660 stringToPrecomputedChunk('"');
4661 const clientRenderedSuspenseBoundaryError1A =
4662 stringToPrecomputedChunk(' data-dgst="');
4663 const clientRenderedSuspenseBoundaryError1B =
4664 stringToPrecomputedChunk(' data-msg="');
4665 const clientRenderedSuspenseBoundaryError1C =
4666 stringToPrecomputedChunk(' data-stck="');
4667 const clientRenderedSuspenseBoundaryError1D =
4668 stringToPrecomputedChunk(' data-cstck="');
4669 const clientRenderedSuspenseBoundaryError2 =
4670 stringToPrecomputedChunk('></template>');
4671
4672 export function writeStartCompletedSuspenseBoundary(
4673 destination: Destination,
4674 renderState: RenderState,
4675 ): boolean {
4676 return writeChunkAndReturn(destination, startCompletedSuspenseBoundary);
4677 }
4678 export function writeStartPendingSuspenseBoundary(
4679 destination: Destination,
4680 renderState: RenderState,
4681 id: number,
4682 ): boolean {
4683 writeChunk(destination, startPendingSuspenseBoundary1);
4684
4685 // $FlowFixMe[invalid-compare]
4686 if (id === null) {
4687 throw new Error(
4688 'An ID must have been assigned before we can complete the boundary.',
4689 );
4690 }
4691
4692 writeChunk(destination, renderState.boundaryPrefix);
4693 writeChunk(destination, stringToChunk(id.toString(16)));
4694 return writeChunkAndReturn(destination, startPendingSuspenseBoundary2);
4695 }
4696 export function writeStartClientRenderedSuspenseBoundary(
4697 destination: Destination,
4698 renderState: RenderState,
4699 errorDigest: ?string,
4700 errorMessage: ?string,
4701 errorStack: ?string,
4702 errorComponentStack: ?string,
4703 ): boolean {
4704 let result;
4705 result = writeChunkAndReturn(
4706 destination,
4707 startClientRenderedSuspenseBoundary,
4708 );
4709 writeChunk(destination, clientRenderedSuspenseBoundaryError1);
4710 if (errorDigest != null) {
4711 writeChunk(destination, clientRenderedSuspenseBoundaryError1A);
4712 writeChunk(destination, stringToChunk(escapeTextForBrowser(errorDigest)));
4713 writeChunk(
4714 destination,
4715 clientRenderedSuspenseBoundaryErrorAttrInterstitial,
4716 );
4717 }
4718 if (__DEV__) {
4719 if (errorMessage) {
4720 writeChunk(destination, clientRenderedSuspenseBoundaryError1B);
4721 writeChunk(
4722 destination,
4723 stringToChunk(escapeTextForBrowser(errorMessage)),
4724 );
4725 writeChunk(
4726 destination,
4727 clientRenderedSuspenseBoundaryErrorAttrInterstitial,
4728 );
4729 }
4730 if (errorStack) {
4731 writeChunk(destination, clientRenderedSuspenseBoundaryError1C);
4732 writeChunk(destination, stringToChunk(escapeTextForBrowser(errorStack)));
4733 writeChunk(
4734 destination,
4735 clientRenderedSuspenseBoundaryErrorAttrInterstitial,
4736 );
4737 }
4738 if (errorComponentStack) {
4739 writeChunk(destination, clientRenderedSuspenseBoundaryError1D);
4740 writeChunk(
4741 destination,
4742 stringToChunk(escapeTextForBrowser(errorComponentStack)),
4743 );
4744 writeChunk(
4745 destination,
4746 clientRenderedSuspenseBoundaryErrorAttrInterstitial,
4747 );
4748 }
4749 }
4750 result = writeChunkAndReturn(
4751 destination,
4752 clientRenderedSuspenseBoundaryError2,
4753 );
4754 return result;
4755 }
4756 export function writeEndCompletedSuspenseBoundary(
4757 destination: Destination,
4758 renderState: RenderState,
4759 ): boolean {
4760 return writeChunkAndReturn(destination, endSuspenseBoundary);
4761 }
4762 export function writeEndPendingSuspenseBoundary(
4763 destination: Destination,
4764 renderState: RenderState,
4765 ): boolean {
4766 return writeChunkAndReturn(destination, endSuspenseBoundary);
4767 }
4768 export function writeEndClientRenderedSuspenseBoundary(
4769 destination: Destination,
4770 renderState: RenderState,
4771 ): boolean {
4772 return writeChunkAndReturn(destination, endSuspenseBoundary);
4773 }
4774
4775 const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
4776 const startSegmentHTML2 = stringToPrecomputedChunk('">');
4777 const endSegmentHTML = stringToPrecomputedChunk('</div>');
4778
4779 const startSegmentSVG = stringToPrecomputedChunk(
4780 '<svg aria-hidden="true" style="display:none" id="',
4781 );
4782 const startSegmentSVG2 = stringToPrecomputedChunk('">');
4783 const endSegmentSVG = stringToPrecomputedChunk('</svg>');
4784
4785 const startSegmentMathML = stringToPrecomputedChunk(
4786 '<math aria-hidden="true" style="display:none" id="',
4787 );
4788 const startSegmentMathML2 = stringToPrecomputedChunk('">');
4789 const endSegmentMathML = stringToPrecomputedChunk('</math>');
4790
4791 const startSegmentTable = stringToPrecomputedChunk('<table hidden id="');
4792 const startSegmentTable2 = stringToPrecomputedChunk('">');
4793 const endSegmentTable = stringToPrecomputedChunk('</table>');
4794
4795 const startSegmentTableBody = stringToPrecomputedChunk(
4796 '<table hidden><tbody id="',
4797 );
4798 const startSegmentTableBody2 = stringToPrecomputedChunk('">');
4799 const endSegmentTableBody = stringToPrecomputedChunk('</tbody></table>');
4800
4801 const startSegmentTableRow = stringToPrecomputedChunk('<table hidden><tr id="');
4802 const startSegmentTableRow2 = stringToPrecomputedChunk('">');
4803 const endSegmentTableRow = stringToPrecomputedChunk('</tr></table>');
4804
4805 const startSegmentColGroup = stringToPrecomputedChunk(
4806 '<table hidden><colgroup id="',
4807 );
4808 const startSegmentColGroup2 = stringToPrecomputedChunk('">');
4809 const endSegmentColGroup = stringToPrecomputedChunk('</colgroup></table>');
4810
4811 export function writeStartSegment(
4812 destination: Destination,
4813 renderState: RenderState,
4814 formatContext: FormatContext,
4815 id: number,
4816 ): boolean {
4817 switch (formatContext.insertionMode) {
4818 case ROOT_HTML_MODE:
4819 case HTML_HTML_MODE:
4820 case HTML_HEAD_MODE:
4821 case HTML_MODE: {
4822 writeChunk(destination, startSegmentHTML);
4823 writeChunk(destination, renderState.segmentPrefix);
4824 writeChunk(destination, stringToChunk(id.toString(16)));
4825 return writeChunkAndReturn(destination, startSegmentHTML2);
4826 }
4827 case SVG_MODE: {
4828 writeChunk(destination, startSegmentSVG);
4829 writeChunk(destination, renderState.segmentPrefix);
4830 writeChunk(destination, stringToChunk(id.toString(16)));
4831 return writeChunkAndReturn(destination, startSegmentSVG2);
4832 }
4833 case MATHML_MODE: {
4834 writeChunk(destination, startSegmentMathML);
4835 writeChunk(destination, renderState.segmentPrefix);
4836 writeChunk(destination, stringToChunk(id.toString(16)));
4837 return writeChunkAndReturn(destination, startSegmentMathML2);
4838 }
4839 case HTML_TABLE_MODE: {
4840 writeChunk(destination, startSegmentTable);
4841 writeChunk(destination, renderState.segmentPrefix);
4842 writeChunk(destination, stringToChunk(id.toString(16)));
4843 return writeChunkAndReturn(destination, startSegmentTable2);
4844 }
4845 // TODO: For the rest of these, there will be extra wrapper nodes that never
4846 // get deleted from the document. We need to delete the table too as part
4847 // of the injected scripts. They are invisible though so it's not too terrible
4848 // and it's kind of an edge case to suspend in a table. Totally supported though.
4849 case HTML_TABLE_BODY_MODE: {
4850 writeChunk(destination, startSegmentTableBody);
4851 writeChunk(destination, renderState.segmentPrefix);
4852 writeChunk(destination, stringToChunk(id.toString(16)));
4853 return writeChunkAndReturn(destination, startSegmentTableBody2);
4854 }
4855 case HTML_TABLE_ROW_MODE: {
4856 writeChunk(destination, startSegmentTableRow);
4857 writeChunk(destination, renderState.segmentPrefix);
4858 writeChunk(destination, stringToChunk(id.toString(16)));
4859 return writeChunkAndReturn(destination, startSegmentTableRow2);
4860 }
4861 case HTML_COLGROUP_MODE: {
4862 writeChunk(destination, startSegmentColGroup);
4863 writeChunk(destination, renderState.segmentPrefix);
4864 writeChunk(destination, stringToChunk(id.toString(16)));
4865 return writeChunkAndReturn(destination, startSegmentColGroup2);
4866 }
4867 default: {
4868 throw new Error('Unknown insertion mode. This is a bug in React.');
4869 }
4870 }
4871 }
4872 export function writeEndSegment(
4873 destination: Destination,
4874 formatContext: FormatContext,
4875 ): boolean {
4876 switch (formatContext.insertionMode) {
4877 case ROOT_HTML_MODE:
4878 case HTML_HTML_MODE:
4879 case HTML_HEAD_MODE:
4880 case HTML_MODE: {
4881 return writeChunkAndReturn(destination, endSegmentHTML);
4882 }
4883 case SVG_MODE: {
4884 return writeChunkAndReturn(destination, endSegmentSVG);
4885 }
4886 case MATHML_MODE: {
4887 return writeChunkAndReturn(destination, endSegmentMathML);
4888 }
4889 case HTML_TABLE_MODE: {
4890 return writeChunkAndReturn(destination, endSegmentTable);
4891 }
4892 case HTML_TABLE_BODY_MODE: {
4893 return writeChunkAndReturn(destination, endSegmentTableBody);
4894 }
4895 case HTML_TABLE_ROW_MODE: {
4896 return writeChunkAndReturn(destination, endSegmentTableRow);
4897 }
4898 case HTML_COLGROUP_MODE: {
4899 return writeChunkAndReturn(destination, endSegmentColGroup);
4900 }
4901 default: {
4902 throw new Error('Unknown insertion mode. This is a bug in React.');
4903 }
4904 }
4905 }
4906
4907 const completeSegmentScript1Full = stringToPrecomputedChunk(
4908 completeSegmentFunction + '$RS("',
4909 );
4910 const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
4911 const completeSegmentScript2 = stringToPrecomputedChunk('","');
4912 const completeSegmentScriptEnd = stringToPrecomputedChunk('")</script>');
4913
4914 const completeSegmentData1 = stringToPrecomputedChunk(
4915 '<template data-rsi="" data-sid="',
4916 );
4917 const completeSegmentData2 = stringToPrecomputedChunk('" data-pid="');
4918 const completeSegmentDataEnd = dataElementQuotedEnd;
4919
4920 export function writeCompletedSegmentInstruction(
4921 destination: Destination,
4922 resumableState: ResumableState,
4923 renderState: RenderState,
4924 contentSegmentID: number,
4925 ): boolean {
4926 const scriptFormat =
4927 !enableFizzExternalRuntime ||
4928 resumableState.streamingFormat === ScriptStreamingFormat;
4929 if (scriptFormat) {
4930 writeChunk(destination, renderState.startInlineScript);
4931 writeChunk(destination, endOfStartTag);
4932 if (
4933 (resumableState.instructions & SentCompleteSegmentFunction) ===
4934 NothingSent
4935 ) {
4936 // The first time we write this, we'll need to include the full implementation.
4937 resumableState.instructions |= SentCompleteSegmentFunction;
4938 writeChunk(destination, completeSegmentScript1Full);
4939 } else {
4940 // Future calls can just reuse the same function.
4941 writeChunk(destination, completeSegmentScript1Partial);
4942 }
4943 } else {
4944 writeChunk(destination, completeSegmentData1);
4945 }
4946
4947 // Write function arguments, which are string literals
4948 writeChunk(destination, renderState.segmentPrefix);
4949 const formattedID = stringToChunk(contentSegmentID.toString(16));
4950 writeChunk(destination, formattedID);
4951 if (scriptFormat) {
4952 writeChunk(destination, completeSegmentScript2);
4953 } else {
4954 writeChunk(destination, completeSegmentData2);
4955 }
4956 writeChunk(destination, renderState.placeholderPrefix);
4957 writeChunk(destination, formattedID);
4958
4959 if (scriptFormat) {
4960 return writeChunkAndReturn(destination, completeSegmentScriptEnd);
4961 } else {
4962 return writeChunkAndReturn(destination, completeSegmentDataEnd);
4963 }
4964 }
4965
4966 const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
4967 completeBoundaryFunction,
4968 );
4969 const completeBoundaryUpgradeToViewTransitionsInstruction = stringToChunk(
4970 upgradeToViewTransitionsInstruction,
4971 );
4972 const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
4973
4974 const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
4975 styleInsertionFunction + '$RR("',
4976 );
4977
4978 const completeBoundaryWithStylesScript1Partial =
4979 stringToPrecomputedChunk('$RR("');
4980 const completeBoundaryScript2 = stringToPrecomputedChunk('","');
4981 const completeBoundaryScript3a = stringToPrecomputedChunk('",');
4982 const completeBoundaryScript3b = stringToPrecomputedChunk('"');
4983 const completeBoundaryScriptEnd = stringToPrecomputedChunk(')</script>');
4984
4985 const completeBoundaryData1 = stringToPrecomputedChunk(
4986 '<template data-rci="" data-bid="',
4987 );
4988 const completeBoundaryWithStylesData1 = stringToPrecomputedChunk(
4989 '<template data-rri="" data-bid="',
4990 );
4991 const completeBoundaryData2 = stringToPrecomputedChunk('" data-sid="');
4992 const completeBoundaryData3a = stringToPrecomputedChunk('" data-sty="');
4993 const completeBoundaryDataEnd = dataElementQuotedEnd;
4994
4995 export function writeCompletedBoundaryInstruction(
4996 destination: Destination,
4997 resumableState: ResumableState,
4998 renderState: RenderState,
4999 id: number,
5000 hoistableState: HoistableState,
Showing first 5,000 of 7,239 lines. View raw