1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {11 ReactNodeList,12 ReactCustomFormAction,13 Thenable,14} from 'shared/ReactTypes';15import type {16 CrossOriginEnum,17 PreloadImplOptions,18 PreloadModuleImplOptions,19 PreinitStyleOptions,20 PreinitScriptOptions,21 PreinitModuleScriptOptions,22 ImportMap,23} from 'react-dom/src/shared/ReactDOMTypes';2425import {26 checkHtmlStringCoercion,27 checkCSSPropertyStringCoercion,28 checkAttributeStringCoercion,29 checkOptionStringCoercion,30} from 'shared/CheckStringCoercion';3132import {Children} from 'react';3334import {35 enableFizzExternalRuntime,36 enableSrcObject,37 enableFizzBlockingRender,38 enableViewTransition,39} from 'shared/ReactFeatureFlags';4041import type {42 Destination,43 Chunk,44 PrecomputedChunk,45} from 'react-server/src/ReactServerStreamConfig';4647import type {FormStatus} from '../shared/ReactDOMFormActions';4849import {50 writeChunk,51 writeChunkAndReturn,52 stringToChunk,53 stringToPrecomputedChunk,54 readAsDataURL,55} from 'react-server/src/ReactServerStreamConfig';56import {57 resolveRequest,58 getResumableState,59 getRenderState,60 flushResources,61} from 'react-server/src/ReactFizzServer';6263import isAttributeNameSafe from '../shared/isAttributeNameSafe';64import isUnitlessNumber from '../shared/isUnitlessNumber';65import getAttributeAlias from '../shared/getAttributeAlias';6667import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';68import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';69import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';70import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';71import warnValidStyle from '../shared/warnValidStyle';72import {getCrossOriginString} from '../shared/crossOriginStrings';7374import escapeTextForBrowser from './escapeTextForBrowser';75import hyphenateStyleName from '../shared/hyphenateStyleName';76import hasOwnProperty from 'shared/hasOwnProperty';77import sanitizeURL from '../shared/sanitizeURL';78import isArray from 'shared/isArray';7980import {81 clientRenderBoundary as clientRenderFunction,82 completeBoundary as completeBoundaryFunction,83 completeBoundaryUpgradeToViewTransitions as upgradeToViewTransitionsInstruction,84 completeBoundaryWithStyles as styleInsertionFunction,85 completeSegment as completeSegmentFunction,86 formReplaying as formReplayingRuntime,87 markShellTime,88} from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings';8990import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMResourceValidation';9192import {NotPending} from '../shared/ReactDOMFormActions';9394import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';9596const previousDispatcher =97 ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */98ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */ = {99 f /* flushSyncWork */: previousDispatcher.f /* flushSyncWork */,100 r /* requestFormReset */: previousDispatcher.r /* requestFormReset */,101 D /* prefetchDNS */: prefetchDNS,102 C /* preconnect */: preconnect,103 L /* preload */: preload,104 m /* preloadModule */: preloadModule,105 X /* preinitScript */: preinitScript,106 S /* preinitStyle */: preinitStyle,107 M /* preinitModuleScript */: preinitModuleScript,108};109110// We make every property of the descriptor optional because it is not a contract that111// the headers provided by onHeaders has any particular header types.112export type HeadersDescriptor = {113 Link?: string,114};115116// Used to distinguish these contexts from ones used in other renderers.117// E.g. this can be used to distinguish legacy renderers from this modern one.118export const isPrimaryRenderer = true;119120export const supportsClientAPIs = true;121122export type StreamingFormat = 0 | 1;123const ScriptStreamingFormat: StreamingFormat = 0;124const DataStreamingFormat: StreamingFormat = 1;125126export type InstructionState = number;127const NothingSent /* */ = 0b000000000;128const SentCompleteSegmentFunction /* */ = 0b000000001;129const SentCompleteBoundaryFunction /* */ = 0b000000010;130const SentClientRenderFunction /* */ = 0b000000100;131const SentStyleInsertionFunction /* */ = 0b000001000;132const SentFormReplayingRuntime /* */ = 0b000010000;133const SentCompletedShellId /* */ = 0b000100000;134const SentMarkShellTime /* */ = 0b001000000;135const NeedUpgradeToViewTransitions /* */ = 0b010000000;136const SentUpgradeToViewTransitions /* */ = 0b100000000;137138type NonceOption =139 | string140 | {141 script?: string,142 style?: string,143 };144145// Per request, global state that is not contextual to the rendering subtree.146// This cannot be resumed and therefore should only contain things that are147// temporary working state or are never used in the prerender pass.148export type RenderState = {149 // These can be recreated from resumable state.150 placeholderPrefix: PrecomputedChunk,151 segmentPrefix: PrecomputedChunk,152 boundaryPrefix: PrecomputedChunk,153154 // inline script streaming format, unused if using external runtime / data155 startInlineScript: PrecomputedChunk,156157 startInlineStyle: PrecomputedChunk,158159 // the preamble must always flush before resuming, so all these chunks must160 // be null or empty when resuming.161162 // preamble chunks163 preamble: PreambleState,164165 // external runtime script chunks166 externalRuntimeScript: null | ExternalRuntimeScript,167 bootstrapChunks: Array<Chunk | PrecomputedChunk>,168 importMapChunks: Array<Chunk | PrecomputedChunk>,169170 // Hoistable chunks171 charsetChunks: Array<Chunk | PrecomputedChunk>,172 viewportChunks: Array<Chunk | PrecomputedChunk>,173 hoistableChunks: Array<Chunk | PrecomputedChunk>,174175 // Headers queues for Resources that can flush early176 onHeaders: void | ((headers: HeadersDescriptor) => void),177 headers: null | {178 preconnects: string,179 fontPreloads: string,180 highImagePreloads: string,181 remainingCapacity: number,182 },183 resets: {184 // corresponds to ResumableState.unknownResources["font"]185 font: {186 [href: string]: Preloaded,187 },188 // the rest correspond to ResumableState[<...>Resources]189 dns: {[key: string]: Exists},190 connect: {191 default: {[key: string]: Exists},192 anonymous: {[key: string]: Exists},193 credentials: {[key: string]: Exists},194 },195 image: {196 [key: string]: Preloaded,197 },198 style: {199 [key: string]: Exists | Preloaded | PreloadedWithCredentials,200 },201 },202203 // Flushing queues for Resource dependencies204 preconnects: Set<Resource>,205 fontPreloads: Set<Resource>,206 highImagePreloads: Set<Resource>,207 // usedImagePreloads: Set<PreloadResource>,208 styles: Map<string, StyleQueue>,209 bootstrapScripts: Set<Resource>,210 scripts: Set<Resource>,211 bulkPreloads: Set<Resource>,212213 // Temporarily keeps track of key to preload resources before shell flushes.214 preloads: {215 images: Map<string, Resource>,216 stylesheets: Map<string, Resource>,217 scripts: Map<string, Resource>,218 moduleScripts: Map<string, Resource>,219 },220221 nonce: {222 script: string | void,223 style: string | void,224 },225226 // Module-global-like reference for flushing/hoisting state of style resources227 // We need to track whether the current request has flushed any style resources228 // without sending an instruction to hoist them. we do that here229 stylesToHoist: boolean,230231 // We allow the legacy renderer to extend this object.232233 ...234};235236type Exists = null;237type Preloaded = [];238// Credentials here are things that affect whether a browser will make a request239// as well as things that affect which connection the browser will use for that request.240// We want these to be aligned across preloads and resources because otherwise the preload241// will be wasted.242// We investigated whether referrerPolicy should be included here but from experimentation243// it seems that browsers do not treat this as part of the http cache key and does not affect244// which connection is used.245type PreloadedWithCredentials = [246 /* crossOrigin */ ?CrossOriginEnum,247 /* integrity */ ?string,248];249250const EXISTS: Exists = null;251// This constant is to mark preloads that have no unique credentials252// to convey. It should never be checked by identity and we should not253// assume Preload values in ResumableState equal this value because they254// will have come from some parsed input.255const PRELOAD_NO_CREDS: Preloaded = [];256if (__DEV__) {257 Object.freeze(PRELOAD_NO_CREDS);258}259260// Per response, global state that is not contextual to the rendering subtree.261// This is resumable and therefore should be serializable.262export type ResumableState = {263 idPrefix: string,264 nextFormID: number,265 streamingFormat: StreamingFormat,266267 // We carry the bootstrap intializers in resumable state in case we postpone in the shell268 // of a prerender. On resume we will reinitialize the bootstrap scripts if necessary.269 // If we end up flushing the bootstrap scripts we void these on the resumable state270 bootstrapScriptContent?: string | void,271 bootstrapScripts?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,272 bootstrapModules?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,273274 // state for script streaming format, unused if using external runtime / data275 instructions: InstructionState,276277 // postamble state278 hasBody: boolean,279 hasHtml: boolean,280281 // Resources - Request local cache282 unknownResources: {283 [asType: string]: {284 [href: string]: Preloaded,285 },286 },287 dnsResources: {[key: string]: Exists},288 connectResources: {289 default: {[key: string]: Exists},290 anonymous: {[key: string]: Exists},291 credentials: {[key: string]: Exists},292 },293 imageResources: {294 [key: string]: Preloaded,295 },296 styleResources: {297 [key: string]: Exists | Preloaded | PreloadedWithCredentials,298 },299 scriptResources: {300 [key: string]: Exists | Preloaded | PreloadedWithCredentials,301 },302 moduleUnknownResources: {303 [asType: string]: {304 [href: string]: Preloaded,305 },306 },307 moduleScriptResources: {308 [key: string]: Exists | Preloaded | PreloadedWithCredentials,309 },310};311312let currentlyFlushingRenderState: RenderState | null = null;313314const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');315316const startInlineScript = stringToPrecomputedChunk('<script');317const endInlineScript = stringToPrecomputedChunk('</script>');318319const startScriptSrc = stringToPrecomputedChunk('<script src="');320const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');321const scriptNonce = stringToPrecomputedChunk(' nonce="');322const scriptIntegirty = stringToPrecomputedChunk(' integrity="');323const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="');324const endAsyncScript = stringToPrecomputedChunk(' async=""></script>');325326const startInlineStyle = stringToPrecomputedChunk('<style');327328/**329 * This escaping function is designed to work with with inline scripts where the entire330 * contents are escaped. Because we know we are escaping the entire script we can avoid for instance331 * escaping html comment string sequences that are valid javascript as well because332 * if there are no sebsequent <script sequences the html parser will never enter333 * script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)334 *335 * While untrusted script content should be made safe before using this api it will336 * ensure that the script cannot be early terminated or never terminated state337 */338function escapeEntireInlineScriptContent(scriptText: string) {339 if (__DEV__) {340 checkHtmlStringCoercion(scriptText);341 }342 return ('' + scriptText).replace(scriptRegex, scriptReplacer);343}344const scriptRegex = /(<\/|<)(s)(cript)/gi;345const scriptReplacer = (346 match: string,347 prefix: string,348 s: string,349 suffix: string,350) => `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;351352export type BootstrapScriptDescriptor = {353 src: string,354 integrity?: string,355 crossOrigin?: string,356};357export type ExternalRuntimeScript = {358 src: string,359 chunks: Array<Chunk | PrecomputedChunk>,360};361362const importMapScriptStart = stringToPrecomputedChunk(363 '<script type="importmap">',364);365const importMapScriptEnd = stringToPrecomputedChunk('</script>');366367// Since we store headers as strings we deal with their length in utf16 code units368// rather than visual characters or the utf8 encoding that is used for most binary369// serialization. Some common HTTP servers only allow for headers to be 4kB in length.370// We choose a default length that is likely to be well under this already limited length however371// pathological cases may still cause the utf-8 encoding of the headers to approach this limit.372// It should also be noted that this maximum is a soft maximum. we have not reached the limit we will373// allow one more header to be captured which means in practice if the limit is approached it will be exceeded374const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;375376let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};377if (__DEV__) {378 didWarnForNewBooleanPropsWithEmptyValue = {};379}380381// Allows us to keep track of what we've already written so we can refer back to it.382// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag383// is set, the server will send instructions via data attributes (instead of inline scripts)384export function createRenderState(385 resumableState: ResumableState,386 nonce:387 | string388 | {389 script?: string,390 style?: string,391 }392 | void,393 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,394 importMap: ImportMap | void,395 onHeaders: void | ((headers: HeadersDescriptor) => void),396 maxHeadersLength: void | number,397): RenderState {398 const nonceScript = typeof nonce === 'string' ? nonce : nonce && nonce.script;399 const inlineScriptWithNonce =400 nonceScript === undefined401 ? startInlineScript402 : stringToPrecomputedChunk(403 '<script nonce="' + escapeTextForBrowser(nonceScript) + '"',404 );405 const nonceStyle =406 typeof nonce === 'string' ? undefined : nonce && nonce.style;407 const inlineStyleWithNonce =408 nonceStyle === undefined409 ? startInlineStyle410 : stringToPrecomputedChunk(411 '<style nonce="' + escapeTextForBrowser(nonceStyle) + '"',412 );413 const idPrefix = resumableState.idPrefix;414415 const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];416 let externalRuntimeScript: null | ExternalRuntimeScript = null;417 const {bootstrapScriptContent, bootstrapScripts, bootstrapModules} =418 resumableState;419 if (bootstrapScriptContent !== undefined) {420 bootstrapChunks.push(inlineScriptWithNonce);421 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);422 bootstrapChunks.push(423 endOfStartTag,424 stringToChunk(escapeEntireInlineScriptContent(bootstrapScriptContent)),425 endInlineScript,426 );427 }428 if (enableFizzExternalRuntime) {429 if (externalRuntimeConfig !== undefined) {430 if (typeof externalRuntimeConfig === 'string') {431 externalRuntimeScript = {432 src: externalRuntimeConfig,433 chunks: [],434 };435 pushScriptImpl(externalRuntimeScript.chunks, {436 src: externalRuntimeConfig,437 async: true,438 integrity: undefined,439 nonce: nonceScript,440 });441 } else {442 externalRuntimeScript = {443 src: externalRuntimeConfig.src,444 chunks: [],445 };446 pushScriptImpl(externalRuntimeScript.chunks, {447 src: externalRuntimeConfig.src,448 async: true,449 integrity: externalRuntimeConfig.integrity,450 nonce: nonceScript,451 });452 }453 }454 }455456 const importMapChunks: Array<Chunk | PrecomputedChunk> = [];457 if (importMap !== undefined) {458 const map = importMap;459 importMapChunks.push(importMapScriptStart);460 importMapChunks.push(461 stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map))),462 );463 importMapChunks.push(importMapScriptEnd);464 }465 if (__DEV__) {466 if (onHeaders && typeof maxHeadersLength === 'number') {467 if (maxHeadersLength <= 0) {468 console.error(469 '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.',470 maxHeadersLength === 0 ? 'zero' : maxHeadersLength,471 );472 }473 }474 }475 const headers = onHeaders476 ? {477 preconnects: '',478 fontPreloads: '',479 highImagePreloads: '',480 remainingCapacity:481 // We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity482 // we always assume we are inserting an interstitial ", " however the first header does not actually483 // consume these two extra bytes.484 2 +485 (typeof maxHeadersLength === 'number'486 ? maxHeadersLength487 : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS),488 }489 : null;490 const renderState: RenderState = {491 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),492 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),493 boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),494 startInlineScript: inlineScriptWithNonce,495 startInlineStyle: inlineStyleWithNonce,496 preamble: createPreambleState(),497498 externalRuntimeScript: externalRuntimeScript,499 bootstrapChunks: bootstrapChunks,500 importMapChunks,501502 onHeaders,503 headers,504 resets: {505 font: {},506 dns: {},507 connect: {508 default: {},509 anonymous: {},510 credentials: {},511 },512 image: {},513 style: {},514 },515516 charsetChunks: [],517 viewportChunks: [],518 hoistableChunks: [],519520 // cleared on flush521 preconnects: new Set(),522 fontPreloads: new Set(),523 highImagePreloads: new Set(),524 // usedImagePreloads: new Set(),525 styles: new Map(),526 bootstrapScripts: new Set(),527 scripts: new Set(),528 bulkPreloads: new Set(),529530 preloads: {531 images: new Map(),532 stylesheets: new Map(),533 scripts: new Map(),534 moduleScripts: new Map(),535 },536537 nonce: {538 script: nonceScript,539 style: nonceStyle,540 },541 // like a module global for currently rendering boundary542 hoistableState: null,543 stylesToHoist: false,544 };545546 if (bootstrapScripts !== undefined) {547 for (let i = 0; i < bootstrapScripts.length; i++) {548 const scriptConfig = bootstrapScripts[i];549 let src, crossOrigin, integrity;550 const props: PreloadAsProps = {551 rel: 'preload',552 as: 'script',553 fetchPriority: 'low',554 nonce,555 } as any;556 if (typeof scriptConfig === 'string') {557 props.href = src = scriptConfig;558 } else {559 props.href = src = scriptConfig.src;560 props.integrity = integrity =561 typeof scriptConfig.integrity === 'string'562 ? scriptConfig.integrity563 : undefined;564 props.crossOrigin = crossOrigin =565 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null566 ? undefined567 : scriptConfig.crossOrigin === 'use-credentials'568 ? 'use-credentials'569 : '';570 }571572 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);573574 bootstrapChunks.push(575 startScriptSrc,576 stringToChunk(escapeTextForBrowser(src)),577 attributeEnd,578 );579 if (nonceScript) {580 bootstrapChunks.push(581 scriptNonce,582 stringToChunk(escapeTextForBrowser(nonceScript)),583 attributeEnd,584 );585 }586 if (typeof integrity === 'string') {587 bootstrapChunks.push(588 scriptIntegirty,589 stringToChunk(escapeTextForBrowser(integrity)),590 attributeEnd,591 );592 }593 if (typeof crossOrigin === 'string') {594 bootstrapChunks.push(595 scriptCrossOrigin,596 stringToChunk(escapeTextForBrowser(crossOrigin)),597 attributeEnd,598 );599 }600 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);601 bootstrapChunks.push(endAsyncScript);602 }603 }604 if (bootstrapModules !== undefined) {605 for (let i = 0; i < bootstrapModules.length; i++) {606 const scriptConfig = bootstrapModules[i];607 let src, crossOrigin, integrity;608 const props: PreloadModuleProps = {609 rel: 'modulepreload',610 fetchPriority: 'low',611 nonce: nonceScript,612 } as any;613 if (typeof scriptConfig === 'string') {614 props.href = src = scriptConfig;615 } else {616 props.href = src = scriptConfig.src;617 props.integrity = integrity =618 typeof scriptConfig.integrity === 'string'619 ? scriptConfig.integrity620 : undefined;621 props.crossOrigin = crossOrigin =622 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null623 ? undefined624 : scriptConfig.crossOrigin === 'use-credentials'625 ? 'use-credentials'626 : '';627 }628629 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);630631 bootstrapChunks.push(632 startModuleSrc,633 stringToChunk(escapeTextForBrowser(src)),634 attributeEnd,635 );636 if (nonceScript) {637 bootstrapChunks.push(638 scriptNonce,639 stringToChunk(escapeTextForBrowser(nonceScript)),640 attributeEnd,641 );642 }643 if (typeof integrity === 'string') {644 bootstrapChunks.push(645 scriptIntegirty,646 stringToChunk(escapeTextForBrowser(integrity)),647 attributeEnd,648 );649 }650 if (typeof crossOrigin === 'string') {651 bootstrapChunks.push(652 scriptCrossOrigin,653 stringToChunk(escapeTextForBrowser(crossOrigin)),654 attributeEnd,655 );656 }657 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);658 bootstrapChunks.push(endAsyncScript);659 }660 }661662 return renderState;663}664665export function resumeRenderState(666 resumableState: ResumableState,667 nonce: NonceOption | void,668): RenderState {669 return createRenderState(670 resumableState,671 nonce,672 undefined,673 undefined,674 undefined,675 undefined,676 );677}678679export function createResumableState(680 identifierPrefix: string | void,681 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,682 bootstrapScriptContent: string | void,683 bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,684 bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,685): ResumableState {686 const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;687688 let streamingFormat = ScriptStreamingFormat;689 if (enableFizzExternalRuntime) {690 if (externalRuntimeConfig !== undefined) {691 streamingFormat = DataStreamingFormat;692 }693 }694 return {695 idPrefix: idPrefix,696 nextFormID: 0,697 streamingFormat,698 bootstrapScriptContent,699 bootstrapScripts,700 bootstrapModules,701 instructions: NothingSent,702 hasBody: false,703 hasHtml: false,704705 // @TODO add bootstrap script to implicit preloads706707 // persistent708 unknownResources: {},709 dnsResources: {},710 connectResources: {711 default: {},712 anonymous: {},713 credentials: {},714 },715 imageResources: {},716 styleResources: {},717 scriptResources: {},718 moduleUnknownResources: {},719 moduleScriptResources: {},720 };721}722723export function resetResumableState(724 resumableState: ResumableState,725 renderState: RenderState,726): void {727 // Resets the resumable state based on what didn't manage to fully flush in the render state.728 // This currently assumes nothing was flushed.729 resumableState.nextFormID = 0;730 resumableState.hasBody = false;731 resumableState.hasHtml = false;732 resumableState.unknownResources = {733 font: renderState.resets.font,734 };735 resumableState.dnsResources = renderState.resets.dns;736 resumableState.connectResources = renderState.resets.connect;737 resumableState.imageResources = renderState.resets.image;738 resumableState.styleResources = renderState.resets.style;739 resumableState.scriptResources = {};740 resumableState.moduleUnknownResources = {};741 resumableState.moduleScriptResources = {};742 resumableState.instructions = NothingSent; // Nothing was flushed so no instructions could've flushed.743}744745export function completeResumableState(resumableState: ResumableState): void {746 // This function is called when we have completed a prerender and there is a shell.747 resumableState.bootstrapScriptContent = undefined;748 resumableState.bootstrapScripts = undefined;749 resumableState.bootstrapModules = undefined;750}751752export type PreambleState = {753 htmlChunks: null | Array<Chunk | PrecomputedChunk>,754 headChunks: null | Array<Chunk | PrecomputedChunk>,755 bodyChunks: null | Array<Chunk | PrecomputedChunk>,756};757export function createPreambleState(): PreambleState {758 return {759 htmlChunks: null,760 headChunks: null,761 bodyChunks: null,762 };763}764765// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion766// modes. We only include the variants as they matter for the sake of our purposes.767// We don't actually provide the namespace therefore we use constants instead of the string.768export const ROOT_HTML_MODE = 0; // Used for the root most element tag.769// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it770// still makes sense771const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.772const HTML_MODE = 2;773const HTML_HEAD_MODE = 3;774const SVG_MODE = 4;775const MATHML_MODE = 5;776const HTML_TABLE_MODE = 6;777const HTML_TABLE_BODY_MODE = 7;778const HTML_TABLE_ROW_MODE = 8;779const HTML_COLGROUP_MODE = 9;780// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it781// still makes sense782783type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;784785const NO_SCOPE = /* */ 0b0000000;786const NOSCRIPT_SCOPE = /* */ 0b0000001;787const PICTURE_SCOPE = /* */ 0b0000010;788const FALLBACK_SCOPE = /* */ 0b0000100;789const EXIT_SCOPE = /* */ 0b0001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"790const ENTER_SCOPE = /* */ 0b0010000; // A direct Instance below Suspense content is the only thing that can "enter"791const UPDATE_SCOPE = /* */ 0b0100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.792const APPEARING_SCOPE = /* */ 0b1000000; // Below Suspense content subtree which might appear in an "enter" animation or "shared" animation.793794// Everything not listed here are tracked for the whole subtree as opposed to just795// until the next Instance.796const SUBTREE_SCOPE = ~(ENTER_SCOPE | EXIT_SCOPE);797798type ViewTransitionContext = {799 update: 'none' | 'auto' | string,800 enter: 'none' | 'auto' | string,801 exit: 'none' | 'auto' | string,802 share: 'none' | 'auto' | string,803 name: 'auto' | string,804 autoName: string, // a name that can be used if an explicit one is not defined.805 nameIdx: number, // keeps track of how many duplicates of this name we've emitted.806};807808// Lets us keep track of contextual state and pick it back up after suspending.809export type FormatContext = {810 insertionMode: InsertionMode, // root/svg/html/mathml/table811 selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>812 tagScope: number,813 viewTransition: null | ViewTransitionContext, // tracks if we're inside a ViewTransition outside the first DOM node814};815816function createFormatContext(817 insertionMode: InsertionMode,818 selectedValue: null | string | Array<string>,819 tagScope: number,820 viewTransition: null | ViewTransitionContext,821): FormatContext {822 return {823 insertionMode,824 selectedValue,825 tagScope,826 viewTransition,827 };828}829830export function canHavePreamble(formatContext: FormatContext): boolean {831 return formatContext.insertionMode < HTML_MODE;832}833834export function createRootFormatContext(namespaceURI?: string): FormatContext {835 const insertionMode =836 namespaceURI === 'http://www.w3.org/2000/svg'837 ? SVG_MODE838 : namespaceURI === 'http://www.w3.org/1998/Math/MathML'839 ? MATHML_MODE840 : ROOT_HTML_MODE;841 return createFormatContext(insertionMode, null, NO_SCOPE, null);842}843844export function getChildFormatContext(845 parentContext: FormatContext,846 type: string,847 props: Object,848): FormatContext {849 const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;850 switch (type) {851 case 'noscript':852 return createFormatContext(853 HTML_MODE,854 null,855 subtreeScope | NOSCRIPT_SCOPE,856 null,857 );858 case 'select':859 return createFormatContext(860 HTML_MODE,861 props.value != null ? props.value : props.defaultValue,862 subtreeScope,863 null,864 );865 case 'svg':866 return createFormatContext(SVG_MODE, null, subtreeScope, null);867 case 'picture':868 return createFormatContext(869 HTML_MODE,870 null,871 subtreeScope | PICTURE_SCOPE,872 null,873 );874 case 'math':875 return createFormatContext(MATHML_MODE, null, subtreeScope, null);876 case 'foreignObject':877 return createFormatContext(HTML_MODE, null, subtreeScope, null);878 // Table parents are special in that their children can only be created at all if they're879 // wrapped in a table parent. So we need to encode that we're entering this mode.880 case 'table':881 return createFormatContext(HTML_TABLE_MODE, null, subtreeScope, null);882 case 'thead':883 case 'tbody':884 case 'tfoot':885 return createFormatContext(886 HTML_TABLE_BODY_MODE,887 null,888 subtreeScope,889 null,890 );891 case 'colgroup':892 return createFormatContext(HTML_COLGROUP_MODE, null, subtreeScope, null);893 case 'tr':894 return createFormatContext(HTML_TABLE_ROW_MODE, null, subtreeScope, null);895 case 'head':896 if (parentContext.insertionMode < HTML_MODE) {897 // We are either at the root or inside the <html> tag and can enter898 // the <head> scope899 return createFormatContext(HTML_HEAD_MODE, null, subtreeScope, null);900 }901 break;902 case 'html':903 if (parentContext.insertionMode === ROOT_HTML_MODE) {904 return createFormatContext(HTML_HTML_MODE, null, subtreeScope, null);905 }906 break;907 }908 if (parentContext.insertionMode >= HTML_TABLE_MODE) {909 // Whatever tag this was, it wasn't a table parent or other special parent, so we must have910 // entered plain HTML again.911 return createFormatContext(HTML_MODE, null, subtreeScope, null);912 }913 if (parentContext.insertionMode < HTML_MODE) {914 return createFormatContext(HTML_MODE, null, subtreeScope, null);915 }916 if (enableViewTransition) {917 if (parentContext.viewTransition !== null) {918 // If we're inside a view transition, regardless what element we were in, it consumes919 // the view transition context.920 return createFormatContext(921 parentContext.insertionMode,922 parentContext.selectedValue,923 subtreeScope,924 null,925 );926 }927 }928 if (parentContext.tagScope !== subtreeScope) {929 return createFormatContext(930 parentContext.insertionMode,931 parentContext.selectedValue,932 subtreeScope,933 null,934 );935 }936 return parentContext;937}938939function getSuspenseViewTransition(940 parentViewTransition: null | ViewTransitionContext,941): null | ViewTransitionContext {942 if (parentViewTransition === null) {943 return null;944 }945 // If a ViewTransition wraps a Suspense boundary it applies to the children Instances946 // in both the fallback and the content.947 // Since we only have a representation of ViewTransitions on the Instances themselves948 // we cannot model the parent ViewTransition activating "enter", "exit" or "share"949 // since those would be ambiguous with the Suspense boundary changing states and950 // affecting the same Instances.951 // We also can't model an "update" when that update is fallback nodes swapping for952 // content nodes. However, we can model is as a "share" from the fallback nodes to953 // the content nodes using the same name. We just have to assign the same name that954 // we would've used (the parent ViewTransition name or auto-assign one).955 const viewTransition: ViewTransitionContext = {956 update: parentViewTransition.update, // For deep updates.957 enter: 'none',958 exit: 'none',959 share: parentViewTransition.update, // For exit or enter of reveals.960 name: parentViewTransition.autoName,961 autoName: parentViewTransition.autoName,962 // TOOD: If we have more than just this Suspense boundary as a child of the ViewTransition963 // then the parent needs to isolate the names so that they don't conflict.964 nameIdx: 0,965 };966 return viewTransition;967}968969export function getSuspenseFallbackFormatContext(970 resumableState: ResumableState,971 parentContext: FormatContext,972): FormatContext {973 if (parentContext.tagScope & UPDATE_SCOPE) {974 // If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,975 // which hasn't disabled updates, then revealing it might animate the parent so we need976 // the ViewTransition instructions.977 resumableState.instructions |= NeedUpgradeToViewTransitions;978 }979 return createFormatContext(980 parentContext.insertionMode,981 parentContext.selectedValue,982 parentContext.tagScope | FALLBACK_SCOPE | EXIT_SCOPE,983 getSuspenseViewTransition(parentContext.viewTransition),984 );985}986987export function getSuspenseContentFormatContext(988 resumableState: ResumableState,989 parentContext: FormatContext,990): FormatContext {991 const viewTransition = getSuspenseViewTransition(992 parentContext.viewTransition,993 );994 let subtreeScope = parentContext.tagScope | ENTER_SCOPE;995 if (viewTransition !== null && viewTransition.share !== 'none') {996 // If we have a ViewTransition wrapping Suspense then the appearing animation997 // will be applied just like an "enter" below. Mark it as animating.998 subtreeScope |= APPEARING_SCOPE;999 }1000 return createFormatContext(1001 parentContext.insertionMode,1002 parentContext.selectedValue,1003 subtreeScope,1004 viewTransition,1005 );1006}10071008export function getViewTransitionFormatContext(1009 resumableState: ResumableState,1010 parentContext: FormatContext,1011 update: ?string,1012 enter: ?string,1013 exit: ?string,1014 share: ?string,1015 name: ?string,1016 autoName: string, // name or an autogenerated unique name1017): FormatContext {1018 // We're entering a <ViewTransition>. Normalize props.1019 if (update == null) {1020 update = 'auto';1021 }1022 if (enter == null) {1023 enter = 'auto';1024 }1025 if (exit == null) {1026 exit = 'auto';1027 }1028 if (name == null) {1029 const parentViewTransition = parentContext.viewTransition;1030 if (parentViewTransition !== null) {1031 // If we have multiple nested ViewTransition and the parent has a "share"1032 // but the child doesn't, then the parent ViewTransition can still activate1033 // a share scenario so we reuse the name and share from the parent.1034 name = parentViewTransition.name;1035 share = parentViewTransition.share;1036 } else {1037 name = 'auto';1038 share = 'none'; // share is only relevant if there's an explicit name1039 }1040 } else {1041 if (share == null) {1042 share = 'auto';1043 }1044 if (parentContext.tagScope & FALLBACK_SCOPE) {1045 // If we have an explicit name and share is not disabled, and we're inside1046 // a fallback, then that fallback might pair with content and so we might need1047 // the ViewTransition instructions to animate between them.1048 resumableState.instructions |= NeedUpgradeToViewTransitions;1049 }1050 }1051 if (!(parentContext.tagScope & EXIT_SCOPE)) {1052 exit = 'none'; // exit is only relevant for the first ViewTransition inside fallback1053 } else {1054 resumableState.instructions |= NeedUpgradeToViewTransitions;1055 }1056 if (!(parentContext.tagScope & ENTER_SCOPE)) {1057 enter = 'none'; // enter is only relevant for the first ViewTransition inside content1058 } else {1059 resumableState.instructions |= NeedUpgradeToViewTransitions;1060 }1061 const viewTransition: ViewTransitionContext = {1062 update,1063 enter,1064 exit,1065 share,1066 name,1067 autoName,1068 nameIdx: 0,1069 };1070 let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;1071 if (update !== 'none') {1072 subtreeScope |= UPDATE_SCOPE;1073 } else {1074 subtreeScope &= ~UPDATE_SCOPE;1075 }1076 if (enter !== 'none') {1077 subtreeScope |= APPEARING_SCOPE;1078 }1079 return createFormatContext(1080 parentContext.insertionMode,1081 parentContext.selectedValue,1082 subtreeScope,1083 viewTransition,1084 );1085}10861087export function isPreambleContext(formatContext: FormatContext): boolean {1088 return formatContext.insertionMode === HTML_HEAD_MODE;1089}10901091export function makeId(1092 resumableState: ResumableState,1093 treeId: string,1094 localId: number,1095): string {1096 const idPrefix = resumableState.idPrefix;10971098 let id = '_' + idPrefix + 'R_' + treeId;10991100 // Unless this is the first id at this level, append a number at the end1101 // that represents the position of this useId hook among all the useId1102 // hooks for this fiber.1103 if (localId > 0) {1104 id += 'H' + localId.toString(32);1105 }11061107 return id + '_';1108}11091110function encodeHTMLTextNode(text: string): string {1111 return escapeTextForBrowser(text);1112}11131114const textSeparator = stringToPrecomputedChunk('<!-- -->');11151116export function pushTextInstance(1117 target: Array<Chunk | PrecomputedChunk>,1118 text: string,1119 renderState: RenderState,1120 textEmbedded: boolean,1121): boolean {1122 if (text === '') {1123 // Empty text doesn't have a DOM node representation and the hydration is aware of this.1124 return textEmbedded;1125 }1126 if (textEmbedded) {1127 target.push(textSeparator);1128 }1129 target.push(stringToChunk(encodeHTMLTextNode(text)));1130 return true;1131}11321133// Called when Fizz is done with a Segment. Currently the only purpose is to conditionally1134// emit a text separator when we don't know for sure it is safe to omit1135export function pushSegmentFinale(1136 target: Array<Chunk | PrecomputedChunk>,1137 renderState: RenderState,1138 lastPushedText: boolean,1139 textEmbedded: boolean,1140): void {1141 if (lastPushedText && textEmbedded) {1142 target.push(textSeparator);1143 }1144}11451146function pushViewTransitionAttributes(1147 target: Array<Chunk | PrecomputedChunk>,1148 formatContext: FormatContext,1149): void {1150 if (!enableViewTransition) {1151 return;1152 }1153 const viewTransition = formatContext.viewTransition;1154 if (viewTransition === null) {1155 return;1156 }1157 if (viewTransition.name !== 'auto') {1158 pushStringAttribute(1159 target,1160 'vt-name',1161 viewTransition.nameIdx === 01162 ? viewTransition.name1163 : viewTransition.name + '_' + viewTransition.nameIdx,1164 );1165 // Increment the index in case we have multiple children to the same ViewTransition.1166 // Because this is a side-effect in render, we should ideally call pushViewTransitionAttributes1167 // after we've suspended (like forms do), so that we don't increment each attempt.1168 // TODO: Make this deterministic.1169 viewTransition.nameIdx++;1170 }1171 pushStringAttribute(target, 'vt-update', viewTransition.update);1172 if (viewTransition.enter !== 'none') {1173 pushStringAttribute(target, 'vt-enter', viewTransition.enter);1174 }1175 if (viewTransition.exit !== 'none') {1176 pushStringAttribute(target, 'vt-exit', viewTransition.exit);1177 }1178 if (viewTransition.share !== 'none') {1179 pushStringAttribute(target, 'vt-share', viewTransition.share);1180 }1181}11821183const styleNameCache: Map<string, PrecomputedChunk> = new Map();1184function processStyleName(styleName: string): PrecomputedChunk {1185 const chunk = styleNameCache.get(styleName);1186 if (chunk !== undefined) {1187 return chunk;1188 }1189 const result = stringToPrecomputedChunk(1190 escapeTextForBrowser(hyphenateStyleName(styleName)),1191 );1192 styleNameCache.set(styleName, result);1193 return result;1194}11951196const styleAttributeStart = stringToPrecomputedChunk(' style="');1197const styleAssign = stringToPrecomputedChunk(':');1198const styleSeparator = stringToPrecomputedChunk(';');11991200function pushStyleAttribute(1201 target: Array<Chunk | PrecomputedChunk>,1202 style: Object,1203): void {1204 if (typeof style !== 'object') {1205 throw new Error(1206 'The `style` prop expects a mapping from style properties to values, ' +1207 "not a string. For example, style={{marginRight: spacing + 'em'}} when " +1208 'using JSX.',1209 );1210 }12111212 let isFirst = true;1213 for (const styleName in style) {1214 if (!hasOwnProperty.call(style, styleName)) {1215 continue;1216 }1217 // If you provide unsafe user data here they can inject arbitrary CSS1218 // which may be problematic (I couldn't repro this):1219 // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet1220 // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/1221 // This is not an XSS hole but instead a potential CSS injection issue1222 // which has lead to a greater discussion about how we're going to1223 // trust URLs moving forward. See #21159011224 const styleValue = style[styleName];1225 if (1226 styleValue == null ||1227 typeof styleValue === 'boolean' ||1228 styleValue === ''1229 ) {1230 // TODO: We used to set empty string as a style with an empty value. Does that ever make sense?1231 continue;1232 }12331234 let nameChunk;1235 let valueChunk;1236 const isCustomProperty = styleName.indexOf('--') === 0;1237 if (isCustomProperty) {1238 nameChunk = stringToChunk(escapeTextForBrowser(styleName));1239 if (__DEV__) {1240 checkCSSPropertyStringCoercion(styleValue, styleName);1241 }1242 valueChunk = stringToChunk(1243 escapeTextForBrowser(('' + styleValue).trim()),1244 );1245 } else {1246 if (__DEV__) {1247 warnValidStyle(styleName, styleValue);1248 }12491250 nameChunk = processStyleName(styleName);1251 if (typeof styleValue === 'number') {1252 if (styleValue !== 0 && !isUnitlessNumber(styleName)) {1253 valueChunk = stringToChunk(styleValue + 'px'); // Presumes implicit 'px' suffix for unitless numbers1254 } else {1255 valueChunk = stringToChunk('' + styleValue);1256 }1257 } else {1258 if (__DEV__) {1259 checkCSSPropertyStringCoercion(styleValue, styleName);1260 }1261 valueChunk = stringToChunk(1262 escapeTextForBrowser(('' + styleValue).trim()),1263 );1264 }1265 }1266 if (isFirst) {1267 isFirst = false;1268 // If it's first, we don't need any separators prefixed.1269 target.push(styleAttributeStart, nameChunk, styleAssign, valueChunk);1270 } else {1271 target.push(styleSeparator, nameChunk, styleAssign, valueChunk);1272 }1273 }1274 if (!isFirst) {1275 target.push(attributeEnd);1276 }1277}12781279const attributeSeparator = stringToPrecomputedChunk(' ');1280const attributeAssign = stringToPrecomputedChunk('="');1281const attributeEnd = stringToPrecomputedChunk('"');1282const attributeEmptyString = stringToPrecomputedChunk('=""');12831284function pushBooleanAttribute(1285 target: Array<Chunk | PrecomputedChunk>,1286 name: string,1287 value: string | boolean | number | Function | Object, // not null or undefined1288): void {1289 if (value && typeof value !== 'function' && typeof value !== 'symbol') {1290 target.push(attributeSeparator, stringToChunk(name), attributeEmptyString);1291 }1292}12931294function pushStringAttribute(1295 target: Array<Chunk | PrecomputedChunk>,1296 name: string,1297 value: string | boolean | number | Function | Object, // not null or undefined1298): void {1299 if (1300 typeof value !== 'function' &&1301 typeof value !== 'symbol' &&1302 typeof value !== 'boolean'1303 ) {1304 target.push(1305 attributeSeparator,1306 stringToChunk(name),1307 attributeAssign,1308 stringToChunk(escapeTextForBrowser(value)),1309 attributeEnd,1310 );1311 }1312}13131314function makeFormFieldPrefix(resumableState: ResumableState): string {1315 // TODO: Make this deterministic.1316 const id = resumableState.nextFormID++;1317 return resumableState.idPrefix + id;1318}13191320// Since this will likely be repeated a lot in the HTML, we use a more concise message1321// than on the client and hopefully it's googleable.1322const actionJavaScriptURL = stringToPrecomputedChunk(1323 escapeTextForBrowser(1324 // eslint-disable-next-line no-script-url1325 "javascript:throw new Error('React form unexpectedly submitted.')",1326 ),1327);13281329const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');13301331function pushAdditionalFormField(1332 this: Array<Chunk | PrecomputedChunk>,1333 value: string | File,1334 key: string,1335): void {1336 const target: Array<Chunk | PrecomputedChunk> = this;1337 target.push(startHiddenInputChunk);1338 validateAdditionalFormField(value, key);1339 pushStringAttribute(target, 'name', key);1340 pushStringAttribute(target, 'value', value);1341 target.push(endOfStartTagSelfClosing);1342}13431344function pushAdditionalFormFields(1345 target: Array<Chunk | PrecomputedChunk>,1346 formData: void | null | FormData,1347) {1348 if (formData != null) {1349 // $FlowFixMe[prop-missing]: FormData has forEach.1350 formData.forEach(pushAdditionalFormField, target);1351 }1352}13531354function validateAdditionalFormField(value: string | File, key: string): void {1355 if (typeof value !== 'string') {1356 throw new Error(1357 'File/Blob fields are not yet supported in progressive forms. ' +1358 'Will fallback to client hydration.',1359 );1360 }1361}13621363function validateAdditionalFormFields(formData: void | null | FormData) {1364 if (formData != null) {1365 // $FlowFixMe[prop-missing]: FormData has forEach.1366 formData.forEach(validateAdditionalFormField);1367 }1368 return formData;1369}13701371function getCustomFormFields(1372 resumableState: ResumableState,1373 formAction: any,1374): null | ReactCustomFormAction {1375 const customAction = formAction.$$FORM_ACTION;1376 if (typeof customAction === 'function') {1377 const prefix = makeFormFieldPrefix(resumableState);1378 try {1379 const customFields = formAction.$$FORM_ACTION(prefix);1380 if (customFields) {1381 validateAdditionalFormFields(customFields.data);1382 }1383 return customFields;1384 } catch (x) {1385 if (typeof x === 'object' && x !== null && typeof x.then === 'function') {1386 // Rethrow suspense.1387 throw x;1388 }1389 // If we fail to encode the form action for progressive enhancement for some reason,1390 // fallback to trying replaying on the client instead of failing the page. It might1391 // work there.1392 if (__DEV__) {1393 // TODO: Should this be some kind of recoverable error?1394 console.error(1395 'Failed to serialize an action for progressive enhancement:\n%s',1396 x,1397 );1398 }1399 }1400 }1401 return null;1402}14031404function pushFormActionAttribute(1405 target: Array<Chunk | PrecomputedChunk>,1406 resumableState: ResumableState,1407 renderState: RenderState,1408 formAction: any,1409 formEncType: any,1410 formMethod: any,1411 formTarget: any,1412 name: any,1413): void | null | FormData {1414 let formData = null;1415 if (typeof formAction === 'function') {1416 // Function form actions cannot control the form properties1417 if (__DEV__) {1418 if (name !== null && !didWarnFormActionName) {1419 didWarnFormActionName = true;1420 console.error(1421 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +1422 'React needs it to encode which action should be invoked. It will get overridden.',1423 );1424 }1425 if (1426 (formEncType !== null || formMethod !== null) &&1427 !didWarnFormActionMethod1428 ) {1429 didWarnFormActionMethod = true;1430 console.error(1431 'Cannot specify a formEncType or formMethod for a button that specifies a ' +1432 'function as a formAction. React provides those automatically. They will get overridden.',1433 );1434 }1435 if (formTarget !== null && !didWarnFormActionTarget) {1436 didWarnFormActionTarget = true;1437 console.error(1438 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +1439 'The function will always be executed in the same window.',1440 );1441 }1442 }1443 const customFields = getCustomFormFields(resumableState, formAction);1444 if (customFields !== null) {1445 // This action has a custom progressive enhancement form that can submit the form1446 // back to the server if it's invoked before hydration. Such as a Server Action.1447 name = customFields.name;1448 formAction = customFields.action || '';1449 formEncType = customFields.encType;1450 formMethod = customFields.method;1451 formTarget = customFields.target;1452 formData = customFields.data;1453 } else {1454 // Set a javascript URL that doesn't do anything. We don't expect this to be invoked1455 // because we'll preventDefault in the Fizz runtime, but it can happen if a form is1456 // manually submitted or if someone calls stopPropagation before React gets the event.1457 // If CSP is used to block javascript: URLs that's fine too. It just won't show this1458 // error message but the URL will be logged.1459 target.push(1460 attributeSeparator,1461 stringToChunk('formAction'),1462 attributeAssign,1463 actionJavaScriptURL,1464 attributeEnd,1465 );1466 name = null;1467 formAction = null;1468 formEncType = null;1469 formMethod = null;1470 formTarget = null;1471 injectFormReplayingRuntime(resumableState, renderState);1472 }1473 }1474 if (name != null) {1475 pushAttribute(target, 'name', name);1476 }1477 if (formAction != null) {1478 pushAttribute(target, 'formAction', formAction);1479 }1480 if (formEncType != null) {1481 pushAttribute(target, 'formEncType', formEncType);1482 }1483 if (formMethod != null) {1484 pushAttribute(target, 'formMethod', formMethod);1485 }1486 if (formTarget != null) {1487 pushAttribute(target, 'formTarget', formTarget);1488 }1489 return formData;1490}14911492let blobCache: null | WeakMap<Blob, Thenable<string>> = null;14931494function pushSrcObjectAttribute(1495 target: Array<Chunk | PrecomputedChunk>,1496 blob: Blob,1497): void {1498 // Throwing a Promise style suspense read of the Blob content.1499 if (blobCache === null) {1500 blobCache = new WeakMap();1501 }1502 const suspenseCache: WeakMap<Blob, Thenable<string>> = blobCache;1503 let thenable = suspenseCache.get(blob);1504 if (thenable === undefined) {1505 thenable = readAsDataURL(blob) as any as Thenable<string>;1506 thenable.then(1507 result => {1508 (thenable as any).status = 'fulfilled';1509 (thenable as any).value = result;1510 },1511 error => {1512 (thenable as any).status = 'rejected';1513 (thenable as any).reason = error;1514 },1515 );1516 suspenseCache.set(blob, thenable);1517 }1518 if (thenable.status === 'rejected') {1519 throw thenable.reason;1520 } else if (thenable.status !== 'fulfilled') {1521 throw thenable;1522 }1523 const url = thenable.value;1524 target.push(1525 attributeSeparator,1526 stringToChunk('src'),1527 attributeAssign,1528 stringToChunk(escapeTextForBrowser(url)),1529 attributeEnd,1530 );1531}15321533function pushAttribute(1534 target: Array<Chunk | PrecomputedChunk>,1535 name: string,1536 value: string | boolean | number | Function | Object, // not null or undefined1537): void {1538 switch (name) {1539 // These are very common props and therefore are in the beginning of the switch.1540 // TODO: aria-label is a very common prop but allows booleans so is not like the others1541 // but should ideally go in this list too.1542 case 'className': {1543 pushStringAttribute(target, 'class', value);1544 break;1545 }1546 case 'tabIndex': {1547 pushStringAttribute(target, 'tabindex', value);1548 break;1549 }1550 case 'dir':1551 case 'role':1552 case 'viewBox':1553 case 'width':1554 case 'height': {1555 pushStringAttribute(target, name, value);1556 break;1557 }1558 case 'style': {1559 pushStyleAttribute(target, value);1560 return;1561 }1562 case 'src': {1563 // $FlowFixMe[invalid-compare]1564 if (enableSrcObject && typeof value === 'object' && value !== null) {1565 if (typeof Blob === 'function' && value instanceof Blob) {1566 pushSrcObjectAttribute(target, value);1567 return;1568 }1569 }1570 // Fallthrough to general urls1571 }1572 case 'href': {1573 if (value === '') {1574 if (__DEV__) {1575 if (name === 'src') {1576 console.error(1577 'An empty string ("") was passed to the %s attribute. ' +1578 'This may cause the browser to download the whole page again over the network. ' +1579 'To fix this, either do not render the element at all ' +1580 'or pass null to %s instead of an empty string.',1581 name,1582 name,1583 );1584 } else {1585 console.error(1586 'An empty string ("") was passed to the %s attribute. ' +1587 'To fix this, either do not render the element at all ' +1588 'or pass null to %s instead of an empty string.',1589 name,1590 name,1591 );1592 }1593 }1594 return;1595 }1596 }1597 // Fall through to the last case which shouldn't remove empty strings.1598 case 'action':1599 case 'formAction': {1600 // TODO: Consider only special casing these for each tag.1601 if (1602 value == null ||1603 typeof value === 'function' ||1604 typeof value === 'symbol' ||1605 typeof value === 'boolean'1606 ) {1607 return;1608 }1609 if (__DEV__) {1610 checkAttributeStringCoercion(value, name);1611 }1612 const sanitizedValue = sanitizeURL('' + value);1613 target.push(1614 attributeSeparator,1615 stringToChunk(name),1616 attributeAssign,1617 stringToChunk(escapeTextForBrowser(sanitizedValue)),1618 attributeEnd,1619 );1620 return;1621 }1622 case 'defaultValue':1623 case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements.1624 case 'innerHTML': // Must use dangerouslySetInnerHTML instead.1625 case 'suppressContentEditableWarning':1626 case 'suppressHydrationWarning':1627 case 'ref':1628 // Ignored. These are built-in to React on the client.1629 return;1630 case 'autoFocus':1631 case 'multiple':1632 case 'muted': {1633 pushBooleanAttribute(target, name.toLowerCase(), value);1634 return;1635 }1636 case 'xlinkHref': {1637 if (1638 typeof value === 'function' ||1639 typeof value === 'symbol' ||1640 typeof value === 'boolean'1641 ) {1642 return;1643 }1644 if (__DEV__) {1645 checkAttributeStringCoercion(value, name);1646 }1647 const sanitizedValue = sanitizeURL('' + value);1648 target.push(1649 attributeSeparator,1650 stringToChunk('xlink:href'),1651 attributeAssign,1652 stringToChunk(escapeTextForBrowser(sanitizedValue)),1653 attributeEnd,1654 );1655 return;1656 }1657 case 'contentEditable':1658 case 'spellCheck':1659 case 'draggable':1660 case 'value':1661 case 'autoReverse':1662 case 'externalResourcesRequired':1663 case 'focusable':1664 case 'preserveAlpha': {1665 // Booleanish String1666 // These are "enumerated" attributes that accept "true" and "false".1667 // In React, we let users pass `true` and `false` even though technically1668 // these aren't boolean attributes (they are coerced to strings).1669 if (typeof value !== 'function' && typeof value !== 'symbol') {1670 target.push(1671 attributeSeparator,1672 stringToChunk(name),1673 attributeAssign,1674 stringToChunk(escapeTextForBrowser(value)),1675 attributeEnd,1676 );1677 }1678 return;1679 }1680 case 'inert': {1681 if (__DEV__) {1682 if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[name]) {1683 didWarnForNewBooleanPropsWithEmptyValue[name] = true;1684 console.error(1685 'Received an empty string for a boolean attribute `%s`. ' +1686 'This will treat the attribute as if it were false. ' +1687 'Either pass `false` to silence this warning, or ' +1688 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',1689 name,1690 );1691 }1692 }1693 }1694 // Fallthrough for boolean props that don't have a warning for empty strings.1695 case 'allowFullScreen':1696 case 'async':1697 case 'autoPlay':1698 case 'controls':1699 case 'credentialless':1700 case 'default':1701 case 'defer':1702 case 'disabled':1703 case 'disablePictureInPicture':1704 case 'disableRemotePlayback':1705 case 'formNoValidate':1706 case 'hidden':1707 case 'loop':1708 case 'noModule':1709 case 'noValidate':1710 case 'open':1711 case 'playsInline':1712 case 'readOnly':1713 case 'required':1714 case 'reversed':1715 case 'scoped':1716 case 'seamless':1717 case 'itemScope': {1718 // Boolean1719 if (value && typeof value !== 'function' && typeof value !== 'symbol') {1720 target.push(1721 attributeSeparator,1722 stringToChunk(name),1723 attributeEmptyString,1724 );1725 }1726 return;1727 }1728 case 'capture':1729 case 'download': {1730 // Overloaded Boolean1731 if (value === true) {1732 target.push(1733 attributeSeparator,1734 stringToChunk(name),1735 attributeEmptyString,1736 );1737 } else if (value === false) {1738 // Ignored1739 } else if (typeof value !== 'function' && typeof value !== 'symbol') {1740 target.push(1741 attributeSeparator,1742 stringToChunk(name),1743 attributeAssign,1744 stringToChunk(escapeTextForBrowser(value)),1745 attributeEnd,1746 );1747 }1748 return;1749 }1750 case 'cols':1751 case 'rows':1752 case 'size':1753 case 'span': {1754 // These are HTML attributes that must be positive numbers.1755 if (1756 typeof value !== 'function' &&1757 typeof value !== 'symbol' &&1758 !isNaN(value) &&1759 (value as any) >= 11760 ) {1761 target.push(1762 attributeSeparator,1763 stringToChunk(name),1764 attributeAssign,1765 stringToChunk(escapeTextForBrowser(value)),1766 attributeEnd,1767 );1768 }1769 return;1770 }1771 case 'rowSpan':1772 case 'start': {1773 // These are HTML attributes that must be numbers.1774 if (1775 typeof value !== 'function' &&1776 typeof value !== 'symbol' &&1777 !isNaN(value)1778 ) {1779 target.push(1780 attributeSeparator,1781 stringToChunk(name),1782 attributeAssign,1783 stringToChunk(escapeTextForBrowser(value)),1784 attributeEnd,1785 );1786 }1787 return;1788 }1789 case 'xlinkActuate':1790 pushStringAttribute(target, 'xlink:actuate', value);1791 return;1792 case 'xlinkArcrole':1793 pushStringAttribute(target, 'xlink:arcrole', value);1794 return;1795 case 'xlinkRole':1796 pushStringAttribute(target, 'xlink:role', value);1797 return;1798 case 'xlinkShow':1799 pushStringAttribute(target, 'xlink:show', value);1800 return;1801 case 'xlinkTitle':1802 pushStringAttribute(target, 'xlink:title', value);1803 return;1804 case 'xlinkType':1805 pushStringAttribute(target, 'xlink:type', value);1806 return;1807 case 'xmlBase':1808 pushStringAttribute(target, 'xml:base', value);1809 return;1810 case 'xmlLang':1811 pushStringAttribute(target, 'xml:lang', value);1812 return;1813 case 'xmlSpace':1814 pushStringAttribute(target, 'xml:space', value);1815 return;1816 default:1817 if (1818 // shouldIgnoreAttribute1819 // We have already filtered out null/undefined and reserved words.1820 name.length > 2 &&1821 (name[0] === 'o' || name[0] === 'O') &&1822 (name[1] === 'n' || name[1] === 'N')1823 ) {1824 return;1825 }18261827 const attributeName = getAttributeAlias(name);1828 if (isAttributeNameSafe(attributeName)) {1829 // shouldRemoveAttribute1830 switch (typeof value) {1831 case 'function':1832 case 'symbol':1833 return;1834 case 'boolean': {1835 const prefix = attributeName.toLowerCase().slice(0, 5);1836 if (prefix !== 'data-' && prefix !== 'aria-') {1837 return;1838 }1839 }1840 }1841 target.push(1842 attributeSeparator,1843 stringToChunk(attributeName),1844 attributeAssign,1845 stringToChunk(escapeTextForBrowser(value)),1846 attributeEnd,1847 );1848 }1849 }1850}18511852const endOfStartTag = stringToPrecomputedChunk('>');1853const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');18541855function pushInnerHTML(1856 target: Array<Chunk | PrecomputedChunk>,1857 innerHTML: any,1858 children: any,1859) {1860 if (innerHTML != null) {1861 if (children != null) {1862 throw new Error(1863 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',1864 );1865 }18661867 if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {1868 throw new Error(1869 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +1870 'Please visit https://react.dev/link/dangerously-set-inner-html ' +1871 'for more information.',1872 );1873 }18741875 const html = innerHTML.__html;1876 if (html !== null && html !== undefined) {1877 if (__DEV__) {1878 checkHtmlStringCoercion(html);1879 }1880 target.push(stringToChunk('' + html));1881 }1882 }1883}18841885// TODO: Move these to RenderState so that we warn for every request.1886// It would help debugging in stateful servers (e.g. service worker).1887let didWarnDefaultInputValue = false;1888let didWarnDefaultChecked = false;1889let didWarnDefaultSelectValue = false;1890let didWarnDefaultTextareaValue = false;1891let didWarnInvalidOptionChildren = false;1892let didWarnInvalidOptionInnerHTML = false;1893let didWarnSelectedSetOnOption = false;1894let didWarnFormActionType = false;1895let didWarnFormActionName = false;1896let didWarnFormActionTarget = false;1897let didWarnFormActionMethod = false;18981899function checkSelectProp(props: any, propName: string) {1900 if (__DEV__) {1901 const value = props[propName];1902 if (value != null) {1903 const array = isArray(value);1904 if (props.multiple && !array) {1905 console.error(1906 'The `%s` prop supplied to <select> must be an array if ' +1907 '`multiple` is true.',1908 propName,1909 );1910 } else if (!props.multiple && array) {1911 console.error(1912 'The `%s` prop supplied to <select> must be a scalar ' +1913 'value if `multiple` is false.',1914 propName,1915 );1916 }1917 }1918 }1919}19201921function pushStartAnchor(1922 target: Array<Chunk | PrecomputedChunk>,1923 props: Object,1924 formatContext: FormatContext,1925): ReactNodeList {1926 target.push(startChunkForTag('a'));19271928 let children = null;1929 let innerHTML = null;1930 for (const propKey in props) {1931 if (hasOwnProperty.call(props, propKey)) {1932 const propValue = props[propKey];1933 if (propValue == null) {1934 continue;1935 }1936 switch (propKey) {1937 case 'children':1938 children = propValue;1939 break;1940 case 'dangerouslySetInnerHTML':1941 innerHTML = propValue;1942 break;1943 case 'href':1944 if (propValue === '') {1945 // Empty `href` is special on anchors so we're short-circuiting here.1946 // On other tags it should trigger a warning1947 pushStringAttribute(target, 'href', '');1948 } else {1949 pushAttribute(target, propKey, propValue);1950 }1951 break;1952 default:1953 pushAttribute(target, propKey, propValue);1954 break;1955 }1956 }1957 }19581959 pushViewTransitionAttributes(target, formatContext);19601961 target.push(endOfStartTag);1962 pushInnerHTML(target, innerHTML, children);1963 if (typeof children === 'string') {1964 // Special case children as a string to avoid the unnecessary comment.1965 // TODO: Remove this special case after the general optimization is in place.1966 target.push(stringToChunk(encodeHTMLTextNode(children)));1967 return null;1968 }1969 return children;1970}19711972function pushStartObject(1973 target: Array<Chunk | PrecomputedChunk>,1974 props: Object,1975 formatContext: FormatContext,1976): ReactNodeList {1977 target.push(startChunkForTag('object'));19781979 let children = null;1980 let innerHTML = null;1981 for (const propKey in props) {1982 if (hasOwnProperty.call(props, propKey)) {1983 const propValue = props[propKey];1984 if (propValue == null) {1985 continue;1986 }1987 switch (propKey) {1988 case 'children':1989 children = propValue;1990 break;1991 case 'dangerouslySetInnerHTML':1992 innerHTML = propValue;1993 break;1994 case 'data': {1995 if (__DEV__) {1996 checkAttributeStringCoercion(propValue, 'data');1997 }1998 const sanitizedValue = sanitizeURL('' + propValue);1999 if (sanitizedValue === '') {2000 if (__DEV__) {
Findings
✓ No findings reported for this file.