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 enableViewTransitionParentEnterExit,40} from 'shared/ReactFeatureFlags';4142import type {43 Destination,44 Chunk,45 PrecomputedChunk,46} from 'react-server/src/ReactServerStreamConfig';4748import type {FormStatus} from '../shared/ReactDOMFormActions';4950import {51 writeChunk,52 writeChunkAndReturn,53 stringToChunk,54 stringToPrecomputedChunk,55 readAsDataURL,56} from 'react-server/src/ReactServerStreamConfig';57import {58 resolveRequest,59 getResumableState,60 getRenderState,61 flushResources,62} from 'react-server/src/ReactFizzServer';6364import isAttributeNameSafe from '../shared/isAttributeNameSafe';65import isUnitlessNumber from '../shared/isUnitlessNumber';66import getAttributeAlias from '../shared/getAttributeAlias';6768import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';69import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';70import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';71import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';72import warnValidStyle from '../shared/warnValidStyle';73import {getCrossOriginString} from '../shared/crossOriginStrings';7475import escapeTextForBrowser from './escapeTextForBrowser';76import hyphenateStyleName from '../shared/hyphenateStyleName';77import hasOwnProperty from 'shared/hasOwnProperty';78import sanitizeURL from '../shared/sanitizeURL';79import isArray from 'shared/isArray';8081import {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';9091import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMResourceValidation';9293import {NotPending} from '../shared/ReactDOMFormActions';9495import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';9697const previousDispatcher =98 ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */99ReactDOMSharedInternals.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};110111// We make every property of the descriptor optional because it is not a contract that112// the headers provided by onHeaders has any particular header types.113export type HeadersDescriptor = {114 Link?: string,115};116117// 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.119export const isPrimaryRenderer = true;120121export const supportsClientAPIs = true;122123export type StreamingFormat = 0 | 1;124const ScriptStreamingFormat: StreamingFormat = 0;125const DataStreamingFormat: StreamingFormat = 1;126127export type InstructionState = number;128const NothingSent /* */ = 0b000000000;129const SentCompleteSegmentFunction /* */ = 0b000000001;130const SentCompleteBoundaryFunction /* */ = 0b000000010;131const SentClientRenderFunction /* */ = 0b000000100;132const SentStyleInsertionFunction /* */ = 0b000001000;133const SentFormReplayingRuntime /* */ = 0b000010000;134const SentCompletedShellId /* */ = 0b000100000;135const SentMarkShellTime /* */ = 0b001000000;136const NeedUpgradeToViewTransitions /* */ = 0b010000000;137const SentUpgradeToViewTransitions /* */ = 0b100000000;138139type NonceOption =140 | string141 | {142 script?: string,143 style?: string,144 };145146// Per request, global state that is not contextual to the rendering subtree.147// This cannot be resumed and therefore should only contain things that are148// temporary working state or are never used in the prerender pass.149export type RenderState = {150 // These can be recreated from resumable state.151 placeholderPrefix: PrecomputedChunk,152 segmentPrefix: PrecomputedChunk,153 boundaryPrefix: PrecomputedChunk,154155 // inline script streaming format, unused if using external runtime / data156 startInlineScript: PrecomputedChunk,157158 startInlineStyle: PrecomputedChunk,159160 // the preamble must always flush before resuming, so all these chunks must161 // be null or empty when resuming.162163 // preamble chunks164 preamble: PreambleState,165166 // external runtime script chunks167 externalRuntimeScript: null | ExternalRuntimeScript,168 bootstrapChunks: Array<Chunk | PrecomputedChunk>,169 importMapChunks: Array<Chunk | PrecomputedChunk>,170171 // Hoistable chunks172 charsetChunks: Array<Chunk | PrecomputedChunk>,173 viewportChunks: Array<Chunk | PrecomputedChunk>,174 hoistableChunks: Array<Chunk | PrecomputedChunk>,175176 // Headers queues for Resources that can flush early177 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 },203204 // Flushing queues for Resource dependencies205 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>,213214 // 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 },221222 nonce: {223 script: string | void,224 style: string | void,225 },226227 // Module-global-like reference for flushing/hoisting state of style resources228 // We need to track whether the current request has flushed any style resources229 // without sending an instruction to hoist them. we do that here230 stylesToHoist: boolean,231232 // We allow the legacy renderer to extend this object.233234 ...235};236237type Exists = null;238type Preloaded = [];239// Credentials here are things that affect whether a browser will make a request240// 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 preload242// will be wasted.243// We investigated whether referrerPolicy should be included here but from experimentation244// it seems that browsers do not treat this as part of the http cache key and does not affect245// which connection is used.246type PreloadedWithCredentials = [247 /* crossOrigin */ ?CrossOriginEnum,248 /* integrity */ ?string,249];250251const EXISTS: Exists = null;252// This constant is to mark preloads that have no unique credentials253// to convey. It should never be checked by identity and we should not254// assume Preload values in ResumableState equal this value because they255// will have come from some parsed input.256const PRELOAD_NO_CREDS: Preloaded = [];257if (__DEV__) {258 Object.freeze(PRELOAD_NO_CREDS);259}260261// Per response, global state that is not contextual to the rendering subtree.262// This is resumable and therefore should be serializable.263export type ResumableState = {264 idPrefix: string,265 nextFormID: number,266 streamingFormat: StreamingFormat,267268 // We carry the bootstrap intializers in resumable state in case we postpone in the shell269 // 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 state271 bootstrapScriptContent?: string | void,272 bootstrapScripts?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,273 bootstrapModules?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,274275 // state for script streaming format, unused if using external runtime / data276 instructions: InstructionState,277278 // postamble state279 hasBody: boolean,280 hasHtml: boolean,281282 // Resources - Request local cache283 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};312313let currentlyFlushingRenderState: RenderState | null = null;314315const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');316317const startInlineScript = stringToPrecomputedChunk('<script');318const endInlineScript = stringToPrecomputedChunk('</script>');319320const startScriptSrc = stringToPrecomputedChunk('<script src="');321const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');322const scriptNonce = stringToPrecomputedChunk(' nonce="');323const scriptIntegirty = stringToPrecomputedChunk(' integrity="');324const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="');325const endAsyncScript = stringToPrecomputedChunk(' async=""></script>');326327const startInlineStyle = stringToPrecomputedChunk('<style');328329/**330 * This escaping function is designed to work with with inline scripts where the entire331 * contents are escaped. Because we know we are escaping the entire script we can avoid for instance332 * escaping html comment string sequences that are valid javascript as well because333 * if there are no sebsequent <script sequences the html parser will never enter334 * 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 will337 * ensure that the script cannot be early terminated or never terminated state338 */339function escapeEntireInlineScriptContent(scriptText: string) {340 if (__DEV__) {341 checkHtmlStringCoercion(scriptText);342 }343 return ('' + scriptText).replace(scriptRegex, scriptReplacer);344}345const scriptRegex = /(<\/|<)(s)(cript)/gi;346const scriptReplacer = (347 match: string,348 prefix: string,349 s: string,350 suffix: string,351) => `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;352353export type BootstrapScriptDescriptor = {354 src: string,355 integrity?: string,356 crossOrigin?: string,357};358export type ExternalRuntimeScript = {359 src: string,360 chunks: Array<Chunk | PrecomputedChunk>,361};362363const importMapScriptStart = stringToPrecomputedChunk(364 '<script type="importmap">',365);366const importMapScriptEnd = stringToPrecomputedChunk('</script>');367368// Since we store headers as strings we deal with their length in utf16 code units369// rather than visual characters or the utf8 encoding that is used for most binary370// 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 however372// 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 will374// allow one more header to be captured which means in practice if the limit is approached it will be exceeded375const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;376377let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};378if (__DEV__) {379 didWarnForNewBooleanPropsWithEmptyValue = {};380}381382// 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 flag384// is set, the server will send instructions via data attributes (instead of inline scripts)385export function createRenderState(386 resumableState: ResumableState,387 nonce:388 | string389 | {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 === undefined402 ? startInlineScript403 : stringToPrecomputedChunk(404 '<script nonce="' + escapeTextForBrowser(nonceScript) + '"',405 );406 const nonceStyle =407 typeof nonce === 'string' ? undefined : nonce && nonce.style;408 const inlineStyleWithNonce =409 nonceStyle === undefined410 ? startInlineStyle411 : stringToPrecomputedChunk(412 '<style nonce="' + escapeTextForBrowser(nonceStyle) + '"',413 );414 const idPrefix = resumableState.idPrefix;415416 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 }456457 const importMapChunks: Array<Chunk | PrecomputedChunk> = [];458 if (importMap !== undefined) {459 const map = importMap;460 importMapChunks.push(461 nonceScript === undefined462 ? importMapScriptStart463 : stringToPrecomputedChunk(464 '<script type="importmap" nonce="' +465 escapeTextForBrowser(nonceScript) +466 '">',467 ),468 );469 importMapChunks.push(470 stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map))),471 );472 importMapChunks.push(importMapScriptEnd);473 }474 if (__DEV__) {475 if (onHeaders && typeof maxHeadersLength === 'number') {476 if (maxHeadersLength <= 0) {477 console.error(478 '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.',479 maxHeadersLength === 0 ? 'zero' : maxHeadersLength,480 );481 }482 }483 }484 const headers = onHeaders485 ? {486 preconnects: '',487 fontPreloads: '',488 highImagePreloads: '',489 remainingCapacity:490 // We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity491 // we always assume we are inserting an interstitial ", " however the first header does not actually492 // consume these two extra bytes.493 2 +494 (typeof maxHeadersLength === 'number'495 ? maxHeadersLength496 : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS),497 }498 : null;499 const renderState: RenderState = {500 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),501 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),502 boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),503 startInlineScript: inlineScriptWithNonce,504 startInlineStyle: inlineStyleWithNonce,505 preamble: createPreambleState(),506507 externalRuntimeScript: externalRuntimeScript,508 bootstrapChunks: bootstrapChunks,509 importMapChunks,510511 onHeaders,512 headers,513 resets: {514 font: {},515 dns: {},516 connect: {517 default: {},518 anonymous: {},519 credentials: {},520 },521 image: {},522 style: {},523 },524525 charsetChunks: [],526 viewportChunks: [],527 hoistableChunks: [],528529 // cleared on flush530 preconnects: new Set(),531 fontPreloads: new Set(),532 highImagePreloads: new Set(),533 // usedImagePreloads: new Set(),534 styles: new Map(),535 bootstrapScripts: new Set(),536 scripts: new Set(),537 bulkPreloads: new Set(),538539 preloads: {540 images: new Map(),541 stylesheets: new Map(),542 scripts: new Map(),543 moduleScripts: new Map(),544 },545546 nonce: {547 script: nonceScript,548 style: nonceStyle,549 },550 // like a module global for currently rendering boundary551 hoistableState: null,552 stylesToHoist: false,553 };554555 if (bootstrapScripts !== undefined) {556 for (let i = 0; i < bootstrapScripts.length; i++) {557 const scriptConfig = bootstrapScripts[i];558 let src, crossOrigin, integrity;559 const props: PreloadAsProps = {560 rel: 'preload',561 as: 'script',562 fetchPriority: 'low',563 nonce,564 } as any;565 if (typeof scriptConfig === 'string') {566 props.href = src = scriptConfig;567 } else {568 props.href = src = scriptConfig.src;569 props.integrity = integrity =570 typeof scriptConfig.integrity === 'string'571 ? scriptConfig.integrity572 : undefined;573 props.crossOrigin = crossOrigin =574 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null575 ? undefined576 : scriptConfig.crossOrigin === 'use-credentials'577 ? 'use-credentials'578 : '';579 }580581 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);582583 bootstrapChunks.push(584 startScriptSrc,585 stringToChunk(escapeTextForBrowser(src)),586 attributeEnd,587 );588 if (nonceScript) {589 bootstrapChunks.push(590 scriptNonce,591 stringToChunk(escapeTextForBrowser(nonceScript)),592 attributeEnd,593 );594 }595 if (typeof integrity === 'string') {596 bootstrapChunks.push(597 scriptIntegirty,598 stringToChunk(escapeTextForBrowser(integrity)),599 attributeEnd,600 );601 }602 if (typeof crossOrigin === 'string') {603 bootstrapChunks.push(604 scriptCrossOrigin,605 stringToChunk(escapeTextForBrowser(crossOrigin)),606 attributeEnd,607 );608 }609 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);610 bootstrapChunks.push(endAsyncScript);611 }612 }613 if (bootstrapModules !== undefined) {614 for (let i = 0; i < bootstrapModules.length; i++) {615 const scriptConfig = bootstrapModules[i];616 let src, crossOrigin, integrity;617 const props: PreloadModuleProps = {618 rel: 'modulepreload',619 fetchPriority: 'low',620 nonce: nonceScript,621 } as any;622 if (typeof scriptConfig === 'string') {623 props.href = src = scriptConfig;624 } else {625 props.href = src = scriptConfig.src;626 props.integrity = integrity =627 typeof scriptConfig.integrity === 'string'628 ? scriptConfig.integrity629 : undefined;630 props.crossOrigin = crossOrigin =631 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null632 ? undefined633 : scriptConfig.crossOrigin === 'use-credentials'634 ? 'use-credentials'635 : '';636 }637638 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);639640 bootstrapChunks.push(641 startModuleSrc,642 stringToChunk(escapeTextForBrowser(src)),643 attributeEnd,644 );645 if (nonceScript) {646 bootstrapChunks.push(647 scriptNonce,648 stringToChunk(escapeTextForBrowser(nonceScript)),649 attributeEnd,650 );651 }652 if (typeof integrity === 'string') {653 bootstrapChunks.push(654 scriptIntegirty,655 stringToChunk(escapeTextForBrowser(integrity)),656 attributeEnd,657 );658 }659 if (typeof crossOrigin === 'string') {660 bootstrapChunks.push(661 scriptCrossOrigin,662 stringToChunk(escapeTextForBrowser(crossOrigin)),663 attributeEnd,664 );665 }666 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);667 bootstrapChunks.push(endAsyncScript);668 }669 }670671 return renderState;672}673674export function resumeRenderState(675 resumableState: ResumableState,676 nonce: NonceOption | void,677): RenderState {678 return createRenderState(679 resumableState,680 nonce,681 undefined,682 undefined,683 undefined,684 undefined,685 );686}687688export function createResumableState(689 identifierPrefix: string | void,690 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,691 bootstrapScriptContent: string | void,692 bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,693 bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,694): ResumableState {695 const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;696697 let streamingFormat = ScriptStreamingFormat;698 if (enableFizzExternalRuntime) {699 if (externalRuntimeConfig !== undefined) {700 streamingFormat = DataStreamingFormat;701 }702 }703 return {704 idPrefix: idPrefix,705 nextFormID: 0,706 streamingFormat,707 bootstrapScriptContent,708 bootstrapScripts,709 bootstrapModules,710 instructions: NothingSent,711 hasBody: false,712 hasHtml: false,713714 // @TODO add bootstrap script to implicit preloads715716 // persistent717 unknownResources: {},718 dnsResources: {},719 connectResources: {720 default: {},721 anonymous: {},722 credentials: {},723 },724 imageResources: {},725 styleResources: {},726 scriptResources: {},727 moduleUnknownResources: {},728 moduleScriptResources: {},729 };730}731732export function resetResumableState(733 resumableState: ResumableState,734 renderState: RenderState,735): void {736 // Resets the resumable state based on what didn't manage to fully flush in the render state.737 // This currently assumes nothing was flushed.738 resumableState.nextFormID = 0;739 resumableState.hasBody = false;740 resumableState.hasHtml = false;741 resumableState.unknownResources = {742 font: renderState.resets.font,743 };744 resumableState.dnsResources = renderState.resets.dns;745 resumableState.connectResources = renderState.resets.connect;746 resumableState.imageResources = renderState.resets.image;747 resumableState.styleResources = renderState.resets.style;748 resumableState.scriptResources = {};749 resumableState.moduleUnknownResources = {};750 resumableState.moduleScriptResources = {};751 resumableState.instructions = NothingSent; // Nothing was flushed so no instructions could've flushed.752}753754export function completeResumableState(resumableState: ResumableState): void {755 // This function is called when we have completed a prerender and there is a shell.756 resumableState.bootstrapScriptContent = undefined;757 resumableState.bootstrapScripts = undefined;758 resumableState.bootstrapModules = undefined;759}760761export type PreambleState = {762 htmlChunks: null | Array<Chunk | PrecomputedChunk>,763 headChunks: null | Array<Chunk | PrecomputedChunk>,764 bodyChunks: null | Array<Chunk | PrecomputedChunk>,765};766export function createPreambleState(): PreambleState {767 return {768 htmlChunks: null,769 headChunks: null,770 bodyChunks: null,771 };772}773774// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion775// modes. We only include the variants as they matter for the sake of our purposes.776// We don't actually provide the namespace therefore we use constants instead of the string.777export const ROOT_HTML_MODE = 0; // Used for the root most element tag.778// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it779// still makes sense780const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.781const HTML_MODE = 2;782const HTML_HEAD_MODE = 3;783const SVG_MODE = 4;784const MATHML_MODE = 5;785const HTML_TABLE_MODE = 6;786const HTML_TABLE_BODY_MODE = 7;787const HTML_TABLE_ROW_MODE = 8;788const HTML_COLGROUP_MODE = 9;789// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it790// still makes sense791792type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;793794const NO_SCOPE = /* */ 0b0000000;795const NOSCRIPT_SCOPE = /* */ 0b0000001;796const PICTURE_SCOPE = /* */ 0b0000010;797const FALLBACK_SCOPE = /* */ 0b0000100;798const EXIT_SCOPE = /* */ 0b0001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"799const ENTER_SCOPE = /* */ 0b0010000; // A direct Instance below Suspense content is the only thing that can "enter"800const UPDATE_SCOPE = /* */ 0b0100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.801const APPEARING_SCOPE = /* */ 0b1000000; // Below Suspense content subtree which might appear in an "enter" animation or "shared" animation.802const PARENT_EXIT_SCOPE = /* */ 0b10000000; // Below a ViewTransition that is exiting.803const PARENT_ENTER_SCOPE = /* */ 0b100000000; // Below a ViewTransition that is entering.804805// Everything not listed here are tracked for the whole subtree as opposed to just806// until the next Instance.807const SUBTREE_SCOPE = ~(ENTER_SCOPE | EXIT_SCOPE);808809type ViewTransitionContext = {810 update: 'none' | 'auto' | string,811 enter: 'none' | 'auto' | string,812 exit: 'none' | 'auto' | string,813 share: 'none' | 'auto' | string,814 parentEnter: 'none' | 'auto' | string,815 parentExit: 'none' | 'auto' | string,816 name: 'auto' | string,817 autoName: string, // a name that can be used if an explicit one is not defined.818 nameIdx: number, // keeps track of how many duplicates of this name we've emitted.819};820821// Lets us keep track of contextual state and pick it back up after suspending.822export type FormatContext = {823 insertionMode: InsertionMode, // root/svg/html/mathml/table824 selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>825 tagScope: number,826 viewTransition: null | ViewTransitionContext, // tracks if we're inside a ViewTransition outside the first DOM node827};828829function createFormatContext(830 insertionMode: InsertionMode,831 selectedValue: null | string | Array<string>,832 tagScope: number,833 viewTransition: null | ViewTransitionContext,834): FormatContext {835 return {836 insertionMode,837 selectedValue,838 tagScope,839 viewTransition,840 };841}842843export function canHavePreamble(formatContext: FormatContext): boolean {844 return formatContext.insertionMode < HTML_MODE;845}846847export function createRootFormatContext(namespaceURI?: string): FormatContext {848 const insertionMode =849 namespaceURI === 'http://www.w3.org/2000/svg'850 ? SVG_MODE851 : namespaceURI === 'http://www.w3.org/1998/Math/MathML'852 ? MATHML_MODE853 : ROOT_HTML_MODE;854 return createFormatContext(insertionMode, null, NO_SCOPE, null);855}856857export function getChildFormatContext(858 parentContext: FormatContext,859 type: string,860 props: Object,861): FormatContext {862 const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;863 switch (type) {864 case 'noscript':865 return createFormatContext(866 HTML_MODE,867 null,868 subtreeScope | NOSCRIPT_SCOPE,869 null,870 );871 case 'select':872 return createFormatContext(873 HTML_MODE,874 props.value != null ? props.value : props.defaultValue,875 subtreeScope,876 null,877 );878 case 'svg':879 return createFormatContext(SVG_MODE, null, subtreeScope, null);880 case 'picture':881 return createFormatContext(882 HTML_MODE,883 null,884 subtreeScope | PICTURE_SCOPE,885 null,886 );887 case 'math':888 return createFormatContext(MATHML_MODE, null, subtreeScope, null);889 case 'foreignObject':890 return createFormatContext(HTML_MODE, null, subtreeScope, null);891 // Table parents are special in that their children can only be created at all if they're892 // wrapped in a table parent. So we need to encode that we're entering this mode.893 case 'table':894 return createFormatContext(HTML_TABLE_MODE, null, subtreeScope, null);895 case 'thead':896 case 'tbody':897 case 'tfoot':898 return createFormatContext(899 HTML_TABLE_BODY_MODE,900 null,901 subtreeScope,902 null,903 );904 case 'colgroup':905 return createFormatContext(HTML_COLGROUP_MODE, null, subtreeScope, null);906 case 'tr':907 return createFormatContext(HTML_TABLE_ROW_MODE, null, subtreeScope, null);908 case 'head':909 if (parentContext.insertionMode < HTML_MODE) {910 // We are either at the root or inside the <html> tag and can enter911 // the <head> scope912 return createFormatContext(HTML_HEAD_MODE, null, subtreeScope, null);913 }914 break;915 case 'html':916 if (parentContext.insertionMode === ROOT_HTML_MODE) {917 return createFormatContext(HTML_HTML_MODE, null, subtreeScope, null);918 }919 break;920 }921 if (parentContext.insertionMode >= HTML_TABLE_MODE) {922 // Whatever tag this was, it wasn't a table parent or other special parent, so we must have923 // entered plain HTML again.924 return createFormatContext(HTML_MODE, null, subtreeScope, null);925 }926 if (parentContext.insertionMode < HTML_MODE) {927 return createFormatContext(HTML_MODE, null, subtreeScope, null);928 }929 if (enableViewTransition) {930 if (parentContext.viewTransition !== null) {931 // If we're inside a view transition, regardless what element we were in, it consumes932 // the view transition context.933 return createFormatContext(934 parentContext.insertionMode,935 parentContext.selectedValue,936 subtreeScope,937 null,938 );939 }940 }941 if (parentContext.tagScope !== subtreeScope) {942 return createFormatContext(943 parentContext.insertionMode,944 parentContext.selectedValue,945 subtreeScope,946 null,947 );948 }949 return parentContext;950}951952function getSuspenseViewTransition(953 parentViewTransition: null | ViewTransitionContext,954): null | ViewTransitionContext {955 if (parentViewTransition === null) {956 return null;957 }958 // If a ViewTransition wraps a Suspense boundary it applies to the children Instances959 // in both the fallback and the content.960 // Since we only have a representation of ViewTransitions on the Instances themselves961 // we cannot model the parent ViewTransition activating "enter", "exit" or "share"962 // since those would be ambiguous with the Suspense boundary changing states and963 // affecting the same Instances.964 // We also can't model an "update" when that update is fallback nodes swapping for965 // content nodes. However, we can model is as a "share" from the fallback nodes to966 // the content nodes using the same name. We just have to assign the same name that967 // we would've used (the parent ViewTransition name or auto-assign one).968 const viewTransition: ViewTransitionContext = {969 update: parentViewTransition.update, // For deep updates.970 enter: 'none',971 exit: 'none',972 share: parentViewTransition.update, // For exit or enter of reveals.973 parentEnter: 'none',974 parentExit: 'none',975 name: parentViewTransition.autoName,976 autoName: parentViewTransition.autoName,977 // TOOD: If we have more than just this Suspense boundary as a child of the ViewTransition978 // then the parent needs to isolate the names so that they don't conflict.979 nameIdx: 0,980 };981 return viewTransition;982}983984export function getSuspenseFallbackFormatContext(985 resumableState: ResumableState,986 parentContext: FormatContext,987): FormatContext {988 if (parentContext.tagScope & UPDATE_SCOPE) {989 // If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,990 // which hasn't disabled updates, then revealing it might animate the parent so we need991 // the ViewTransition instructions.992 resumableState.instructions |= NeedUpgradeToViewTransitions;993 }994 return createFormatContext(995 parentContext.insertionMode,996 parentContext.selectedValue,997 parentContext.tagScope | FALLBACK_SCOPE | EXIT_SCOPE,998 getSuspenseViewTransition(parentContext.viewTransition),999 );1000}10011002export function getSuspenseContentFormatContext(1003 resumableState: ResumableState,1004 parentContext: FormatContext,1005): FormatContext {1006 const viewTransition = getSuspenseViewTransition(1007 parentContext.viewTransition,1008 );1009 let subtreeScope = parentContext.tagScope | ENTER_SCOPE;1010 if (viewTransition !== null && viewTransition.share !== 'none') {1011 // If we have a ViewTransition wrapping Suspense then the appearing animation1012 // will be applied just like an "enter" below. Mark it as animating.1013 subtreeScope |= APPEARING_SCOPE;1014 }1015 return createFormatContext(1016 parentContext.insertionMode,1017 parentContext.selectedValue,1018 subtreeScope,1019 viewTransition,1020 );1021}10221023export function getViewTransitionFormatContext(1024 resumableState: ResumableState,1025 parentContext: FormatContext,1026 update: ?string,1027 enter: ?string,1028 exit: ?string,1029 share: ?string,1030 parentEnter: ?string,1031 parentExit: ?string,1032 hasParentEnterHandler: boolean,1033 hasParentExitHandler: boolean,1034 name: ?string,1035 autoName: string, // name or an autogenerated unique name1036): FormatContext {1037 // We're entering a <ViewTransition>. Normalize props.1038 if (update == null) {1039 update = 'auto';1040 }1041 if (enter == null) {1042 enter = 'auto';1043 }1044 if (exit == null) {1045 exit = 'auto';1046 }1047 if (name == null) {1048 const parentViewTransition = parentContext.viewTransition;1049 if (parentViewTransition !== null) {1050 // If we have multiple nested ViewTransition and the parent has a "share"1051 // but the child doesn't, then the parent ViewTransition can still activate1052 // a share scenario so we reuse the name and share from the parent.1053 name = parentViewTransition.name;1054 share = parentViewTransition.share;1055 } else {1056 name = 'auto';1057 share = 'none'; // share is only relevant if there's an explicit name1058 }1059 } else {1060 if (share == null) {1061 share = 'auto';1062 }1063 if (parentContext.tagScope & FALLBACK_SCOPE) {1064 // If we have an explicit name and share is not disabled, and we're inside1065 // a fallback, then that fallback might pair with content and so we might need1066 // the ViewTransition instructions to animate between them.1067 resumableState.instructions |= NeedUpgradeToViewTransitions;1068 }1069 }1070 if (!(parentContext.tagScope & EXIT_SCOPE)) {1071 exit = 'none'; // exit is only relevant for the first ViewTransition inside fallback1072 } else {1073 resumableState.instructions |= NeedUpgradeToViewTransitions;1074 }1075 if (!(parentContext.tagScope & ENTER_SCOPE)) {1076 enter = 'none'; // enter is only relevant for the first ViewTransition inside content1077 } else {1078 resumableState.instructions |= NeedUpgradeToViewTransitions;1079 }1080 let resolvedParentEnter = 'none';1081 let resolvedParentExit = 'none';1082 if (enableViewTransitionParentEnterExit) {1083 if (1084 parentEnter != null &&1085 (parentContext.tagScope & PARENT_ENTER_SCOPE) !== 01086 ) {1087 resolvedParentEnter = parentEnter;1088 }1089 if (1090 parentExit != null &&1091 (parentContext.tagScope & PARENT_EXIT_SCOPE) !== 01092 ) {1093 resolvedParentExit = parentExit;1094 }1095 }1096 const viewTransition: ViewTransitionContext = {1097 update,1098 enter,1099 exit,1100 share,1101 parentEnter: resolvedParentEnter,1102 parentExit: resolvedParentExit,1103 name,1104 autoName,1105 nameIdx: 0,1106 };1107 let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;1108 if (update !== 'none') {1109 subtreeScope |= UPDATE_SCOPE;1110 } else {1111 subtreeScope &= ~UPDATE_SCOPE;1112 }1113 if (enter !== 'none') {1114 subtreeScope |= APPEARING_SCOPE;1115 }1116 if (enableViewTransitionParentEnterExit) {1117 // Parent enter relay: a ViewTransition that is itself entering starts a relay1118 // for its subtree. A nested ViewTransition continues that relay as long as it1119 // opts in with a parentEnter prop that doesn't resolve to "none", or with an1120 // onParentEnter handler. A missing parentEnter (with no handler) or an1121 // explicit "none" stops the relay so descendants below it don't participate.1122 // This mirrors commitParentEnterViewTransitions.1123 if (enter !== 'none') {1124 subtreeScope |= PARENT_ENTER_SCOPE;1125 } else if (1126 (parentContext.tagScope & PARENT_ENTER_SCOPE) !== 0 &&1127 (parentEnter === 'none' ||1128 (parentEnter === undefined && !hasParentEnterHandler))1129 ) {1130 subtreeScope &= ~PARENT_ENTER_SCOPE;1131 }1132 // Parent exit relay: mirror of the enter relay above.1133 if (exit !== 'none') {1134 subtreeScope |= PARENT_EXIT_SCOPE;1135 } else if (1136 (parentContext.tagScope & PARENT_EXIT_SCOPE) !== 0 &&1137 (parentExit === 'none' ||1138 (parentExit === undefined && !hasParentExitHandler))1139 ) {1140 subtreeScope &= ~PARENT_EXIT_SCOPE;1141 }1142 }1143 return createFormatContext(1144 parentContext.insertionMode,1145 parentContext.selectedValue,1146 subtreeScope,1147 viewTransition,1148 );1149}11501151export function isPreambleContext(formatContext: FormatContext): boolean {1152 return formatContext.insertionMode === HTML_HEAD_MODE;1153}11541155export function makeId(1156 resumableState: ResumableState,1157 treeId: string,1158 localId: number,1159): string {1160 const idPrefix = resumableState.idPrefix;11611162 let id = '_' + idPrefix + 'R_' + treeId;11631164 // Unless this is the first id at this level, append a number at the end1165 // that represents the position of this useId hook among all the useId1166 // hooks for this fiber.1167 if (localId > 0) {1168 id += 'H' + localId.toString(32);1169 }11701171 return id + '_';1172}11731174function encodeHTMLTextNode(text: string): string {1175 return escapeTextForBrowser(text);1176}11771178const textSeparator = stringToPrecomputedChunk('<!-- -->');11791180export function pushTextInstance(1181 target: Array<Chunk | PrecomputedChunk>,1182 text: string,1183 renderState: RenderState,1184 textEmbedded: boolean,1185): boolean {1186 if (text === '') {1187 // Empty text doesn't have a DOM node representation and the hydration is aware of this.1188 return textEmbedded;1189 }1190 if (textEmbedded) {1191 target.push(textSeparator);1192 }1193 target.push(stringToChunk(encodeHTMLTextNode(text)));1194 return true;1195}11961197// Called when Fizz is done with a Segment. Currently the only purpose is to conditionally1198// emit a text separator when we don't know for sure it is safe to omit1199export function pushSegmentFinale(1200 target: Array<Chunk | PrecomputedChunk>,1201 renderState: RenderState,1202 lastPushedText: boolean,1203 textEmbedded: boolean,1204): void {1205 if (lastPushedText && textEmbedded) {1206 target.push(textSeparator);1207 }1208}12091210function pushViewTransitionAttributes(1211 target: Array<Chunk | PrecomputedChunk>,1212 formatContext: FormatContext,1213): void {1214 if (!enableViewTransition) {1215 return;1216 }1217 const viewTransition = formatContext.viewTransition;1218 if (viewTransition === null) {1219 return;1220 }1221 if (viewTransition.name !== 'auto') {1222 pushStringAttribute(1223 target,1224 'vt-name',1225 viewTransition.nameIdx === 01226 ? viewTransition.name1227 : viewTransition.name + '_' + viewTransition.nameIdx,1228 );1229 // Increment the index in case we have multiple children to the same ViewTransition.1230 // Because this is a side-effect in render, we should ideally call pushViewTransitionAttributes1231 // after we've suspended (like forms do), so that we don't increment each attempt.1232 // TODO: Make this deterministic.1233 viewTransition.nameIdx++;1234 }1235 pushStringAttribute(target, 'vt-update', viewTransition.update);1236 if (viewTransition.enter !== 'none') {1237 pushStringAttribute(target, 'vt-enter', viewTransition.enter);1238 }1239 if (viewTransition.exit !== 'none') {1240 pushStringAttribute(target, 'vt-exit', viewTransition.exit);1241 }1242 if (viewTransition.share !== 'none') {1243 pushStringAttribute(target, 'vt-share', viewTransition.share);1244 }1245 if (1246 enableViewTransitionParentEnterExit &&1247 viewTransition.parentEnter !== 'none'1248 ) {1249 pushStringAttribute(target, 'vt-parent-enter', viewTransition.parentEnter);1250 }1251 if (1252 enableViewTransitionParentEnterExit &&1253 viewTransition.parentExit !== 'none'1254 ) {1255 pushStringAttribute(target, 'vt-parent-exit', viewTransition.parentExit);1256 }1257}12581259const styleNameCache: Map<string, PrecomputedChunk> = new Map();1260function processStyleName(styleName: string): PrecomputedChunk {1261 const chunk = styleNameCache.get(styleName);1262 if (chunk !== undefined) {1263 return chunk;1264 }1265 const result = stringToPrecomputedChunk(1266 escapeTextForBrowser(hyphenateStyleName(styleName)),1267 );1268 styleNameCache.set(styleName, result);1269 return result;1270}12711272const styleAttributeStart = stringToPrecomputedChunk(' style="');1273const styleAssign = stringToPrecomputedChunk(':');1274const styleSeparator = stringToPrecomputedChunk(';');12751276function pushStyleAttribute(1277 target: Array<Chunk | PrecomputedChunk>,1278 style: Object,1279): void {1280 if (typeof style !== 'object') {1281 throw new Error(1282 'The `style` prop expects a mapping from style properties to values, ' +1283 "not a string. For example, style={{marginRight: spacing + 'em'}} when " +1284 'using JSX.',1285 );1286 }12871288 let isFirst = true;1289 for (const styleName in style) {1290 if (!hasOwnProperty.call(style, styleName)) {1291 continue;1292 }1293 // If you provide unsafe user data here they can inject arbitrary CSS1294 // which may be problematic (I couldn't repro this):1295 // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet1296 // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/1297 // This is not an XSS hole but instead a potential CSS injection issue1298 // which has lead to a greater discussion about how we're going to1299 // trust URLs moving forward. See #21159011300 const styleValue = style[styleName];1301 if (1302 styleValue == null ||1303 typeof styleValue === 'boolean' ||1304 styleValue === ''1305 ) {1306 // TODO: We used to set empty string as a style with an empty value. Does that ever make sense?1307 continue;1308 }13091310 let nameChunk;1311 let valueChunk;1312 const isCustomProperty = styleName.indexOf('--') === 0;1313 if (isCustomProperty) {1314 nameChunk = stringToChunk(escapeTextForBrowser(styleName));1315 if (__DEV__) {1316 checkCSSPropertyStringCoercion(styleValue, styleName);1317 }1318 valueChunk = stringToChunk(1319 escapeTextForBrowser(('' + styleValue).trim()),1320 );1321 } else {1322 if (__DEV__) {1323 warnValidStyle(styleName, styleValue);1324 }13251326 nameChunk = processStyleName(styleName);1327 if (typeof styleValue === 'number') {1328 if (styleValue !== 0 && !isUnitlessNumber(styleName)) {1329 valueChunk = stringToChunk(styleValue + 'px'); // Presumes implicit 'px' suffix for unitless numbers1330 } else {1331 valueChunk = stringToChunk('' + styleValue);1332 }1333 } else {1334 if (__DEV__) {1335 checkCSSPropertyStringCoercion(styleValue, styleName);1336 }1337 valueChunk = stringToChunk(1338 escapeTextForBrowser(('' + styleValue).trim()),1339 );1340 }1341 }1342 if (isFirst) {1343 isFirst = false;1344 // If it's first, we don't need any separators prefixed.1345 target.push(styleAttributeStart, nameChunk, styleAssign, valueChunk);1346 } else {1347 target.push(styleSeparator, nameChunk, styleAssign, valueChunk);1348 }1349 }1350 if (!isFirst) {1351 target.push(attributeEnd);1352 }1353}13541355const attributeSeparator = stringToPrecomputedChunk(' ');1356const attributeAssign = stringToPrecomputedChunk('="');1357const attributeEnd = stringToPrecomputedChunk('"');1358const attributeEmptyString = stringToPrecomputedChunk('=""');13591360function pushBooleanAttribute(1361 target: Array<Chunk | PrecomputedChunk>,1362 name: string,1363 value: string | boolean | number | Function | Object, // not null or undefined1364): void {1365 if (value && typeof value !== 'function' && typeof value !== 'symbol') {1366 target.push(attributeSeparator, stringToChunk(name), attributeEmptyString);1367 }1368}13691370function pushStringAttribute(1371 target: Array<Chunk | PrecomputedChunk>,1372 name: string,1373 value: string | boolean | number | Function | Object, // not null or undefined1374): void {1375 if (1376 typeof value !== 'function' &&1377 typeof value !== 'symbol' &&1378 typeof value !== 'boolean'1379 ) {1380 target.push(1381 attributeSeparator,1382 stringToChunk(name),1383 attributeAssign,1384 stringToChunk(escapeTextForBrowser(value)),1385 attributeEnd,1386 );1387 }1388}13891390function makeFormFieldPrefix(resumableState: ResumableState): string {1391 // TODO: Make this deterministic.1392 const id = resumableState.nextFormID++;1393 return resumableState.idPrefix + id;1394}13951396// Since this will likely be repeated a lot in the HTML, we use a more concise message1397// than on the client and hopefully it's googleable.1398const actionJavaScriptURL = stringToPrecomputedChunk(1399 escapeTextForBrowser(1400 // eslint-disable-next-line no-script-url1401 "javascript:throw new Error('React form unexpectedly submitted.')",1402 ),1403);14041405const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');14061407function pushAdditionalFormField(1408 this: Array<Chunk | PrecomputedChunk>,1409 value: string | File,1410 key: string,1411): void {1412 const target: Array<Chunk | PrecomputedChunk> = this;1413 target.push(startHiddenInputChunk);1414 validateAdditionalFormField(value, key);1415 pushStringAttribute(target, 'name', key);1416 pushStringAttribute(target, 'value', value);1417 target.push(endOfStartTagSelfClosing);1418}14191420function pushAdditionalFormFields(1421 target: Array<Chunk | PrecomputedChunk>,1422 formData: void | null | FormData,1423) {1424 if (formData != null) {1425 // $FlowFixMe[prop-missing]: FormData has forEach.1426 formData.forEach(pushAdditionalFormField, target);1427 }1428}14291430function validateAdditionalFormField(value: string | File, key: string): void {1431 if (typeof value !== 'string') {1432 throw new Error(1433 'File/Blob fields are not yet supported in progressive forms. ' +1434 'Will fallback to client hydration.',1435 );1436 }1437}14381439function validateAdditionalFormFields(formData: void | null | FormData) {1440 if (formData != null) {1441 // $FlowFixMe[prop-missing]: FormData has forEach.1442 formData.forEach(validateAdditionalFormField);1443 }1444 return formData;1445}14461447function getCustomFormFields(1448 resumableState: ResumableState,1449 formAction: any,1450): null | ReactCustomFormAction {1451 const customAction = formAction.$$FORM_ACTION;1452 if (typeof customAction === 'function') {1453 const prefix = makeFormFieldPrefix(resumableState);1454 try {1455 const customFields = formAction.$$FORM_ACTION(prefix);1456 if (customFields) {1457 validateAdditionalFormFields(customFields.data);1458 }1459 return customFields;1460 } catch (x) {1461 if (typeof x === 'object' && x !== null && typeof x.then === 'function') {1462 // Rethrow suspense.1463 throw x;1464 }1465 // If we fail to encode the form action for progressive enhancement for some reason,1466 // fallback to trying replaying on the client instead of failing the page. It might1467 // work there.1468 if (__DEV__) {1469 // TODO: Should this be some kind of recoverable error?1470 console.error(1471 'Failed to serialize an action for progressive enhancement:\n%s',1472 x,1473 );1474 }1475 }1476 }1477 return null;1478}14791480function pushFormActionAttribute(1481 target: Array<Chunk | PrecomputedChunk>,1482 resumableState: ResumableState,1483 renderState: RenderState,1484 formAction: any,1485 formEncType: any,1486 formMethod: any,1487 formTarget: any,1488 name: any,1489): void | null | FormData {1490 let formData = null;1491 if (typeof formAction === 'function') {1492 // Function form actions cannot control the form properties1493 if (__DEV__) {1494 if (name !== null && !didWarnFormActionName) {1495 didWarnFormActionName = true;1496 console.error(1497 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +1498 'React needs it to encode which action should be invoked. It will get overridden.',1499 );1500 }1501 if (1502 (formEncType !== null || formMethod !== null) &&1503 !didWarnFormActionMethod1504 ) {1505 didWarnFormActionMethod = true;1506 console.error(1507 'Cannot specify a formEncType or formMethod for a button that specifies a ' +1508 'function as a formAction. React provides those automatically. They will get overridden.',1509 );1510 }1511 if (formTarget !== null && !didWarnFormActionTarget) {1512 didWarnFormActionTarget = true;1513 console.error(1514 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +1515 'The function will always be executed in the same window.',1516 );1517 }1518 }1519 const customFields = getCustomFormFields(resumableState, formAction);1520 if (customFields !== null) {1521 // This action has a custom progressive enhancement form that can submit the form1522 // back to the server if it's invoked before hydration. Such as a Server Action.1523 name = customFields.name;1524 formAction = customFields.action || '';1525 formEncType = customFields.encType;1526 formMethod = customFields.method;1527 formTarget = customFields.target;1528 formData = customFields.data;1529 } else {1530 // Set a javascript URL that doesn't do anything. We don't expect this to be invoked1531 // because we'll preventDefault in the Fizz runtime, but it can happen if a form is1532 // manually submitted or if someone calls stopPropagation before React gets the event.1533 // If CSP is used to block javascript: URLs that's fine too. It just won't show this1534 // error message but the URL will be logged.1535 target.push(1536 attributeSeparator,1537 stringToChunk('formAction'),1538 attributeAssign,1539 actionJavaScriptURL,1540 attributeEnd,1541 );1542 name = null;1543 formAction = null;1544 formEncType = null;1545 formMethod = null;1546 formTarget = null;1547 injectFormReplayingRuntime(resumableState, renderState);1548 }1549 }1550 if (name != null) {1551 pushAttribute(target, 'name', name);1552 }1553 if (formAction != null) {1554 pushAttribute(target, 'formAction', formAction);1555 }1556 if (formEncType != null) {1557 pushAttribute(target, 'formEncType', formEncType);1558 }1559 if (formMethod != null) {1560 pushAttribute(target, 'formMethod', formMethod);1561 }1562 if (formTarget != null) {1563 pushAttribute(target, 'formTarget', formTarget);1564 }1565 return formData;1566}15671568let blobCache: null | WeakMap<Blob, Thenable<string>> = null;15691570function pushSrcObjectAttribute(1571 target: Array<Chunk | PrecomputedChunk>,1572 blob: Blob,1573): void {1574 // Throwing a Promise style suspense read of the Blob content.1575 if (blobCache === null) {1576 blobCache = new WeakMap();1577 }1578 const suspenseCache: WeakMap<Blob, Thenable<string>> = blobCache;1579 let thenable = suspenseCache.get(blob);1580 if (thenable === undefined) {1581 thenable = readAsDataURL(blob) as any as Thenable<string>;1582 thenable.then(1583 result => {1584 (thenable as any).status = 'fulfilled';1585 (thenable as any).value = result;1586 },1587 error => {1588 (thenable as any).status = 'rejected';1589 (thenable as any).reason = error;1590 },1591 );1592 suspenseCache.set(blob, thenable);1593 }1594 if (thenable.status === 'rejected') {1595 throw thenable.reason;1596 } else if (thenable.status !== 'fulfilled') {1597 throw thenable;1598 }1599 const url = thenable.value;1600 target.push(1601 attributeSeparator,1602 stringToChunk('src'),1603 attributeAssign,1604 stringToChunk(escapeTextForBrowser(url)),1605 attributeEnd,1606 );1607}16081609function pushAttribute(1610 target: Array<Chunk | PrecomputedChunk>,1611 name: string,1612 value: string | boolean | number | Function | Object, // not null or undefined1613): void {1614 switch (name) {1615 // These are very common props and therefore are in the beginning of the switch.1616 // TODO: aria-label is a very common prop but allows booleans so is not like the others1617 // but should ideally go in this list too.1618 case 'className': {1619 pushStringAttribute(target, 'class', value);1620 break;1621 }1622 case 'tabIndex': {1623 pushStringAttribute(target, 'tabindex', value);1624 break;1625 }1626 case 'dir':1627 case 'role':1628 case 'viewBox':1629 case 'width':1630 case 'height': {1631 pushStringAttribute(target, name, value);1632 break;1633 }1634 case 'style': {1635 pushStyleAttribute(target, value);1636 return;1637 }1638 case 'src': {1639 // $FlowFixMe[invalid-compare]1640 if (enableSrcObject && typeof value === 'object' && value !== null) {1641 if (typeof Blob === 'function' && value instanceof Blob) {1642 pushSrcObjectAttribute(target, value);1643 return;1644 }1645 }1646 // Fallthrough to general urls1647 }1648 case 'href': {1649 if (value === '') {1650 if (__DEV__) {1651 if (name === 'src') {1652 console.error(1653 'An empty string ("") was passed to the %s attribute. ' +1654 'This may cause the browser to download the whole page again over the network. ' +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 } else {1661 console.error(1662 'An empty string ("") was passed to the %s attribute. ' +1663 'To fix this, either do not render the element at all ' +1664 'or pass null to %s instead of an empty string.',1665 name,1666 name,1667 );1668 }1669 }1670 return;1671 }1672 }1673 // Fall through to the last case which shouldn't remove empty strings.1674 case 'action':1675 case 'formAction': {1676 // TODO: Consider only special casing these for each tag.1677 if (1678 value == null ||1679 typeof value === 'function' ||1680 typeof value === 'symbol' ||1681 typeof value === 'boolean'1682 ) {1683 return;1684 }1685 if (__DEV__) {1686 checkAttributeStringCoercion(value, name);1687 }1688 const sanitizedValue = sanitizeURL('' + value);1689 target.push(1690 attributeSeparator,1691 stringToChunk(name),1692 attributeAssign,1693 stringToChunk(escapeTextForBrowser(sanitizedValue)),1694 attributeEnd,1695 );1696 return;1697 }1698 case 'defaultValue':1699 case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements.1700 case 'innerHTML': // Must use dangerouslySetInnerHTML instead.1701 case 'suppressContentEditableWarning':1702 case 'suppressHydrationWarning':1703 case 'ref':1704 // Ignored. These are built-in to React on the client.1705 return;1706 case 'autoFocus':1707 case 'multiple':1708 case 'muted': {1709 pushBooleanAttribute(target, name.toLowerCase(), value);1710 return;1711 }1712 case 'xlinkHref': {1713 if (1714 typeof value === 'function' ||1715 typeof value === 'symbol' ||1716 typeof value === 'boolean'1717 ) {1718 return;1719 }1720 if (__DEV__) {1721 checkAttributeStringCoercion(value, name);1722 }1723 const sanitizedValue = sanitizeURL('' + value);1724 target.push(1725 attributeSeparator,1726 stringToChunk('xlink:href'),1727 attributeAssign,1728 stringToChunk(escapeTextForBrowser(sanitizedValue)),1729 attributeEnd,1730 );1731 return;1732 }1733 case 'contentEditable':1734 case 'spellCheck':1735 case 'draggable':1736 case 'value':1737 case 'autoReverse':1738 case 'externalResourcesRequired':1739 case 'focusable':1740 case 'preserveAlpha': {1741 // Booleanish String1742 // These are "enumerated" attributes that accept "true" and "false".1743 // In React, we let users pass `true` and `false` even though technically1744 // these aren't boolean attributes (they are coerced to strings).1745 if (typeof value !== 'function' && typeof value !== 'symbol') {1746 target.push(1747 attributeSeparator,1748 stringToChunk(name),1749 attributeAssign,1750 stringToChunk(escapeTextForBrowser(value)),1751 attributeEnd,1752 );1753 }1754 return;1755 }1756 case 'inert': {1757 if (__DEV__) {1758 if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[name]) {1759 didWarnForNewBooleanPropsWithEmptyValue[name] = true;1760 console.error(1761 'Received an empty string for a boolean attribute `%s`. ' +1762 'This will treat the attribute as if it were false. ' +1763 'Either pass `false` to silence this warning, or ' +1764 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',1765 name,1766 );1767 }1768 }1769 }1770 // Fallthrough for boolean props that don't have a warning for empty strings.1771 case 'allowFullScreen':1772 case 'async':1773 case 'autoPlay':1774 case 'controls':1775 case 'credentialless':1776 case 'default':1777 case 'defer':1778 case 'disabled':1779 case 'disablePictureInPicture':1780 case 'disableRemotePlayback':1781 case 'formNoValidate':1782 case 'hidden':1783 case 'loop':1784 case 'noModule':1785 case 'noValidate':1786 case 'open':1787 case 'playsInline':1788 case 'readOnly':1789 case 'required':1790 case 'reversed':1791 case 'scoped':1792 case 'seamless':1793 case 'itemScope': {1794 // Boolean1795 if (value && typeof value !== 'function' && typeof value !== 'symbol') {1796 target.push(1797 attributeSeparator,1798 stringToChunk(name),1799 attributeEmptyString,1800 );1801 }1802 return;1803 }1804 case 'capture':1805 case 'download': {1806 // Overloaded Boolean1807 if (value === true) {1808 target.push(1809 attributeSeparator,1810 stringToChunk(name),1811 attributeEmptyString,1812 );1813 } else if (value === false) {1814 // Ignored1815 } else if (typeof value !== 'function' && typeof value !== 'symbol') {1816 target.push(1817 attributeSeparator,1818 stringToChunk(name),1819 attributeAssign,1820 stringToChunk(escapeTextForBrowser(value)),1821 attributeEnd,1822 );1823 }1824 return;1825 }1826 case 'cols':1827 case 'rows':1828 case 'size':1829 case 'span': {1830 // These are HTML attributes that must be positive numbers.1831 if (1832 typeof value !== 'function' &&1833 typeof value !== 'symbol' &&1834 !isNaN(value) &&1835 (value as any) >= 11836 ) {1837 target.push(1838 attributeSeparator,1839 stringToChunk(name),1840 attributeAssign,1841 stringToChunk(escapeTextForBrowser(value)),1842 attributeEnd,1843 );1844 }1845 return;1846 }1847 case 'rowSpan':1848 case 'start': {1849 // These are HTML attributes that must be numbers.1850 if (1851 typeof value !== 'function' &&1852 typeof value !== 'symbol' &&1853 !isNaN(value)1854 ) {1855 target.push(1856 attributeSeparator,1857 stringToChunk(name),1858 attributeAssign,1859 stringToChunk(escapeTextForBrowser(value)),1860 attributeEnd,1861 );1862 }1863 return;1864 }1865 case 'xlinkActuate':1866 pushStringAttribute(target, 'xlink:actuate', value);1867 return;1868 case 'xlinkArcrole':1869 pushStringAttribute(target, 'xlink:arcrole', value);1870 return;1871 case 'xlinkRole':1872 pushStringAttribute(target, 'xlink:role', value);1873 return;1874 case 'xlinkShow':1875 pushStringAttribute(target, 'xlink:show', value);1876 return;1877 case 'xlinkTitle':1878 pushStringAttribute(target, 'xlink:title', value);1879 return;1880 case 'xlinkType':1881 pushStringAttribute(target, 'xlink:type', value);1882 return;1883 case 'xmlBase':1884 pushStringAttribute(target, 'xml:base', value);1885 return;1886 case 'xmlLang':1887 pushStringAttribute(target, 'xml:lang', value);1888 return;1889 case 'xmlSpace':1890 pushStringAttribute(target, 'xml:space', value);1891 return;1892 default:1893 if (1894 // shouldIgnoreAttribute1895 // We have already filtered out null/undefined and reserved words.1896 name.length > 2 &&1897 (name[0] === 'o' || name[0] === 'O') &&1898 (name[1] === 'n' || name[1] === 'N')1899 ) {1900 return;1901 }19021903 const attributeName = getAttributeAlias(name);1904 if (isAttributeNameSafe(attributeName)) {1905 // shouldRemoveAttribute1906 switch (typeof value) {1907 case 'function':1908 case 'symbol':1909 return;1910 case 'boolean': {1911 const prefix = attributeName.toLowerCase().slice(0, 5);1912 if (prefix !== 'data-' && prefix !== 'aria-') {1913 return;1914 }1915 }1916 }1917 target.push(1918 attributeSeparator,1919 stringToChunk(attributeName),1920 attributeAssign,1921 stringToChunk(escapeTextForBrowser(value)),1922 attributeEnd,1923 );1924 }1925 }1926}19271928const endOfStartTag = stringToPrecomputedChunk('>');1929const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');19301931function pushInnerHTML(1932 target: Array<Chunk | PrecomputedChunk>,1933 innerHTML: any,1934 children: any,1935) {1936 if (innerHTML != null) {1937 if (children != null) {1938 throw new Error(1939 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',1940 );1941 }19421943 if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {1944 throw new Error(1945 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +1946 'Please visit https://react.dev/link/dangerously-set-inner-html ' +1947 'for more information.',1948 );1949 }19501951 const html = innerHTML.__html;1952 if (html !== null && html !== undefined) {1953 if (__DEV__) {1954 checkHtmlStringCoercion(html);1955 }1956 target.push(stringToChunk('' + html));1957 }1958 }1959}19601961// TODO: Move these to RenderState so that we warn for every request.1962// It would help debugging in stateful servers (e.g. service worker).1963let didWarnDefaultInputValue = false;1964let didWarnDefaultChecked = false;1965let didWarnDefaultSelectValue = false;1966let didWarnDefaultTextareaValue = false;1967let didWarnInvalidOptionChildren = false;1968let didWarnInvalidOptionInnerHTML = false;1969let didWarnSelectedSetOnOption = false;1970let didWarnFormActionType = false;1971let didWarnFormActionName = false;1972let didWarnFormActionTarget = false;1973let didWarnFormActionMethod = false;19741975function checkSelectProp(props: any, propName: string) {1976 if (__DEV__) {1977 const value = props[propName];1978 if (value != null) {1979 const array = isArray(value);1980 if (props.multiple && !array) {1981 console.error(1982 'The `%s` prop supplied to <select> must be an array if ' +1983 '`multiple` is true.',1984 propName,1985 );1986 } else if (!props.multiple && array) {1987 console.error(1988 'The `%s` prop supplied to <select> must be a scalar ' +1989 'value if `multiple` is false.',1990 propName,1991 );1992 }1993 }1994 }1995}19961997function pushStartAnchor(1998 target: Array<Chunk | PrecomputedChunk>,1999 props: Object,2000 formatContext: FormatContext,
Findings
✓ No findings reported for this file.