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(importMapScriptStart);461 importMapChunks.push(462 stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map))),463 );464 importMapChunks.push(importMapScriptEnd);465 }466 if (__DEV__) {467 if (onHeaders && typeof maxHeadersLength === 'number') {468 if (maxHeadersLength <= 0) {469 console.error(470 'React expected a positive non-zero `maxHeadersLength` option but found %s instead. When using the `onHeaders` option you may supply an optional `maxHeadersLength` option as well however, when setting this value to zero or less no headers will be captured.',471 maxHeadersLength === 0 ? 'zero' : maxHeadersLength,472 );473 }474 }475 }476 const headers = onHeaders477 ? {478 preconnects: '',479 fontPreloads: '',480 highImagePreloads: '',481 remainingCapacity:482 // We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity483 // we always assume we are inserting an interstitial ", " however the first header does not actually484 // consume these two extra bytes.485 2 +486 (typeof maxHeadersLength === 'number'487 ? maxHeadersLength488 : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS),489 }490 : null;491 const renderState: RenderState = {492 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),493 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),494 boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),495 startInlineScript: inlineScriptWithNonce,496 startInlineStyle: inlineStyleWithNonce,497 preamble: createPreambleState(),498499 externalRuntimeScript: externalRuntimeScript,500 bootstrapChunks: bootstrapChunks,501 importMapChunks,502503 onHeaders,504 headers,505 resets: {506 font: {},507 dns: {},508 connect: {509 default: {},510 anonymous: {},511 credentials: {},512 },513 image: {},514 style: {},515 },516517 charsetChunks: [],518 viewportChunks: [],519 hoistableChunks: [],520521 // cleared on flush522 preconnects: new Set(),523 fontPreloads: new Set(),524 highImagePreloads: new Set(),525 // usedImagePreloads: new Set(),526 styles: new Map(),527 bootstrapScripts: new Set(),528 scripts: new Set(),529 bulkPreloads: new Set(),530531 preloads: {532 images: new Map(),533 stylesheets: new Map(),534 scripts: new Map(),535 moduleScripts: new Map(),536 },537538 nonce: {539 script: nonceScript,540 style: nonceStyle,541 },542 // like a module global for currently rendering boundary543 hoistableState: null,544 stylesToHoist: false,545 };546547 if (bootstrapScripts !== undefined) {548 for (let i = 0; i < bootstrapScripts.length; i++) {549 const scriptConfig = bootstrapScripts[i];550 let src, crossOrigin, integrity;551 const props: PreloadAsProps = {552 rel: 'preload',553 as: 'script',554 fetchPriority: 'low',555 nonce,556 } as any;557 if (typeof scriptConfig === 'string') {558 props.href = src = scriptConfig;559 } else {560 props.href = src = scriptConfig.src;561 props.integrity = integrity =562 typeof scriptConfig.integrity === 'string'563 ? scriptConfig.integrity564 : undefined;565 props.crossOrigin = crossOrigin =566 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null567 ? undefined568 : scriptConfig.crossOrigin === 'use-credentials'569 ? 'use-credentials'570 : '';571 }572573 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);574575 bootstrapChunks.push(576 startScriptSrc,577 stringToChunk(escapeTextForBrowser(src)),578 attributeEnd,579 );580 if (nonceScript) {581 bootstrapChunks.push(582 scriptNonce,583 stringToChunk(escapeTextForBrowser(nonceScript)),584 attributeEnd,585 );586 }587 if (typeof integrity === 'string') {588 bootstrapChunks.push(589 scriptIntegirty,590 stringToChunk(escapeTextForBrowser(integrity)),591 attributeEnd,592 );593 }594 if (typeof crossOrigin === 'string') {595 bootstrapChunks.push(596 scriptCrossOrigin,597 stringToChunk(escapeTextForBrowser(crossOrigin)),598 attributeEnd,599 );600 }601 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);602 bootstrapChunks.push(endAsyncScript);603 }604 }605 if (bootstrapModules !== undefined) {606 for (let i = 0; i < bootstrapModules.length; i++) {607 const scriptConfig = bootstrapModules[i];608 let src, crossOrigin, integrity;609 const props: PreloadModuleProps = {610 rel: 'modulepreload',611 fetchPriority: 'low',612 nonce: nonceScript,613 } as any;614 if (typeof scriptConfig === 'string') {615 props.href = src = scriptConfig;616 } else {617 props.href = src = scriptConfig.src;618 props.integrity = integrity =619 typeof scriptConfig.integrity === 'string'620 ? scriptConfig.integrity621 : undefined;622 props.crossOrigin = crossOrigin =623 typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null624 ? undefined625 : scriptConfig.crossOrigin === 'use-credentials'626 ? 'use-credentials'627 : '';628 }629630 preloadBootstrapScriptOrModule(resumableState, renderState, src, props);631632 bootstrapChunks.push(633 startModuleSrc,634 stringToChunk(escapeTextForBrowser(src)),635 attributeEnd,636 );637 if (nonceScript) {638 bootstrapChunks.push(639 scriptNonce,640 stringToChunk(escapeTextForBrowser(nonceScript)),641 attributeEnd,642 );643 }644 if (typeof integrity === 'string') {645 bootstrapChunks.push(646 scriptIntegirty,647 stringToChunk(escapeTextForBrowser(integrity)),648 attributeEnd,649 );650 }651 if (typeof crossOrigin === 'string') {652 bootstrapChunks.push(653 scriptCrossOrigin,654 stringToChunk(escapeTextForBrowser(crossOrigin)),655 attributeEnd,656 );657 }658 pushCompletedShellIdAttribute(bootstrapChunks, resumableState);659 bootstrapChunks.push(endAsyncScript);660 }661 }662663 return renderState;664}665666export function resumeRenderState(667 resumableState: ResumableState,668 nonce: NonceOption | void,669): RenderState {670 return createRenderState(671 resumableState,672 nonce,673 undefined,674 undefined,675 undefined,676 undefined,677 );678}679680export function createResumableState(681 identifierPrefix: string | void,682 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,683 bootstrapScriptContent: string | void,684 bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,685 bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,686): ResumableState {687 const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;688689 let streamingFormat = ScriptStreamingFormat;690 if (enableFizzExternalRuntime) {691 if (externalRuntimeConfig !== undefined) {692 streamingFormat = DataStreamingFormat;693 }694 }695 return {696 idPrefix: idPrefix,697 nextFormID: 0,698 streamingFormat,699 bootstrapScriptContent,700 bootstrapScripts,701 bootstrapModules,702 instructions: NothingSent,703 hasBody: false,704 hasHtml: false,705706 // @TODO add bootstrap script to implicit preloads707708 // persistent709 unknownResources: {},710 dnsResources: {},711 connectResources: {712 default: {},713 anonymous: {},714 credentials: {},715 },716 imageResources: {},717 styleResources: {},718 scriptResources: {},719 moduleUnknownResources: {},720 moduleScriptResources: {},721 };722}723724export function resetResumableState(725 resumableState: ResumableState,726 renderState: RenderState,727): void {728 // Resets the resumable state based on what didn't manage to fully flush in the render state.729 // This currently assumes nothing was flushed.730 resumableState.nextFormID = 0;731 resumableState.hasBody = false;732 resumableState.hasHtml = false;733 resumableState.unknownResources = {734 font: renderState.resets.font,735 };736 resumableState.dnsResources = renderState.resets.dns;737 resumableState.connectResources = renderState.resets.connect;738 resumableState.imageResources = renderState.resets.image;739 resumableState.styleResources = renderState.resets.style;740 resumableState.scriptResources = {};741 resumableState.moduleUnknownResources = {};742 resumableState.moduleScriptResources = {};743 resumableState.instructions = NothingSent; // Nothing was flushed so no instructions could've flushed.744}745746export function completeResumableState(resumableState: ResumableState): void {747 // This function is called when we have completed a prerender and there is a shell.748 resumableState.bootstrapScriptContent = undefined;749 resumableState.bootstrapScripts = undefined;750 resumableState.bootstrapModules = undefined;751}752753export type PreambleState = {754 htmlChunks: null | Array<Chunk | PrecomputedChunk>,755 headChunks: null | Array<Chunk | PrecomputedChunk>,756 bodyChunks: null | Array<Chunk | PrecomputedChunk>,757};758export function createPreambleState(): PreambleState {759 return {760 htmlChunks: null,761 headChunks: null,762 bodyChunks: null,763 };764}765766// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion767// modes. We only include the variants as they matter for the sake of our purposes.768// We don't actually provide the namespace therefore we use constants instead of the string.769export const ROOT_HTML_MODE = 0; // Used for the root most element tag.770// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it771// still makes sense772const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.773const HTML_MODE = 2;774const HTML_HEAD_MODE = 3;775const SVG_MODE = 4;776const MATHML_MODE = 5;777const HTML_TABLE_MODE = 6;778const HTML_TABLE_BODY_MODE = 7;779const HTML_TABLE_ROW_MODE = 8;780const HTML_COLGROUP_MODE = 9;781// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it782// still makes sense783784type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;785786const NO_SCOPE = /* */ 0b0000000;787const NOSCRIPT_SCOPE = /* */ 0b0000001;788const PICTURE_SCOPE = /* */ 0b0000010;789const FALLBACK_SCOPE = /* */ 0b0000100;790const EXIT_SCOPE = /* */ 0b0001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"791const ENTER_SCOPE = /* */ 0b0010000; // A direct Instance below Suspense content is the only thing that can "enter"792const UPDATE_SCOPE = /* */ 0b0100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.793const APPEARING_SCOPE = /* */ 0b1000000; // Below Suspense content subtree which might appear in an "enter" animation or "shared" animation.794const PARENT_EXIT_SCOPE = /* */ 0b10000000; // Below a ViewTransition that is exiting.795const PARENT_ENTER_SCOPE = /* */ 0b100000000; // Below a ViewTransition that is entering.796797// Everything not listed here are tracked for the whole subtree as opposed to just798// until the next Instance.799const SUBTREE_SCOPE = ~(ENTER_SCOPE | EXIT_SCOPE);800801type ViewTransitionContext = {802 update: 'none' | 'auto' | string,803 enter: 'none' | 'auto' | string,804 exit: 'none' | 'auto' | string,805 share: 'none' | 'auto' | string,806 parentEnter: 'none' | 'auto' | string,807 parentExit: 'none' | 'auto' | string,808 name: 'auto' | string,809 autoName: string, // a name that can be used if an explicit one is not defined.810 nameIdx: number, // keeps track of how many duplicates of this name we've emitted.811};812813// Lets us keep track of contextual state and pick it back up after suspending.814export type FormatContext = {815 insertionMode: InsertionMode, // root/svg/html/mathml/table816 selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>817 tagScope: number,818 viewTransition: null | ViewTransitionContext, // tracks if we're inside a ViewTransition outside the first DOM node819};820821function createFormatContext(822 insertionMode: InsertionMode,823 selectedValue: null | string | Array<string>,824 tagScope: number,825 viewTransition: null | ViewTransitionContext,826): FormatContext {827 return {828 insertionMode,829 selectedValue,830 tagScope,831 viewTransition,832 };833}834835export function canHavePreamble(formatContext: FormatContext): boolean {836 return formatContext.insertionMode < HTML_MODE;837}838839export function createRootFormatContext(namespaceURI?: string): FormatContext {840 const insertionMode =841 namespaceURI === 'http://www.w3.org/2000/svg'842 ? SVG_MODE843 : namespaceURI === 'http://www.w3.org/1998/Math/MathML'844 ? MATHML_MODE845 : ROOT_HTML_MODE;846 return createFormatContext(insertionMode, null, NO_SCOPE, null);847}848849export function getChildFormatContext(850 parentContext: FormatContext,851 type: string,852 props: Object,853): FormatContext {854 const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;855 switch (type) {856 case 'noscript':857 return createFormatContext(858 HTML_MODE,859 null,860 subtreeScope | NOSCRIPT_SCOPE,861 null,862 );863 case 'select':864 return createFormatContext(865 HTML_MODE,866 props.value != null ? props.value : props.defaultValue,867 subtreeScope,868 null,869 );870 case 'svg':871 return createFormatContext(SVG_MODE, null, subtreeScope, null);872 case 'picture':873 return createFormatContext(874 HTML_MODE,875 null,876 subtreeScope | PICTURE_SCOPE,877 null,878 );879 case 'math':880 return createFormatContext(MATHML_MODE, null, subtreeScope, null);881 case 'foreignObject':882 return createFormatContext(HTML_MODE, null, subtreeScope, null);883 // Table parents are special in that their children can only be created at all if they're884 // wrapped in a table parent. So we need to encode that we're entering this mode.885 case 'table':886 return createFormatContext(HTML_TABLE_MODE, null, subtreeScope, null);887 case 'thead':888 case 'tbody':889 case 'tfoot':890 return createFormatContext(891 HTML_TABLE_BODY_MODE,892 null,893 subtreeScope,894 null,895 );896 case 'colgroup':897 return createFormatContext(HTML_COLGROUP_MODE, null, subtreeScope, null);898 case 'tr':899 return createFormatContext(HTML_TABLE_ROW_MODE, null, subtreeScope, null);900 case 'head':901 if (parentContext.insertionMode < HTML_MODE) {902 // We are either at the root or inside the <html> tag and can enter903 // the <head> scope904 return createFormatContext(HTML_HEAD_MODE, null, subtreeScope, null);905 }906 break;907 case 'html':908 if (parentContext.insertionMode === ROOT_HTML_MODE) {909 return createFormatContext(HTML_HTML_MODE, null, subtreeScope, null);910 }911 break;912 }913 if (parentContext.insertionMode >= HTML_TABLE_MODE) {914 // Whatever tag this was, it wasn't a table parent or other special parent, so we must have915 // entered plain HTML again.916 return createFormatContext(HTML_MODE, null, subtreeScope, null);917 }918 if (parentContext.insertionMode < HTML_MODE) {919 return createFormatContext(HTML_MODE, null, subtreeScope, null);920 }921 if (enableViewTransition) {922 if (parentContext.viewTransition !== null) {923 // If we're inside a view transition, regardless what element we were in, it consumes924 // the view transition context.925 return createFormatContext(926 parentContext.insertionMode,927 parentContext.selectedValue,928 subtreeScope,929 null,930 );931 }932 }933 if (parentContext.tagScope !== subtreeScope) {934 return createFormatContext(935 parentContext.insertionMode,936 parentContext.selectedValue,937 subtreeScope,938 null,939 );940 }941 return parentContext;942}943944function getSuspenseViewTransition(945 parentViewTransition: null | ViewTransitionContext,946): null | ViewTransitionContext {947 if (parentViewTransition === null) {948 return null;949 }950 // If a ViewTransition wraps a Suspense boundary it applies to the children Instances951 // in both the fallback and the content.952 // Since we only have a representation of ViewTransitions on the Instances themselves953 // we cannot model the parent ViewTransition activating "enter", "exit" or "share"954 // since those would be ambiguous with the Suspense boundary changing states and955 // affecting the same Instances.956 // We also can't model an "update" when that update is fallback nodes swapping for957 // content nodes. However, we can model is as a "share" from the fallback nodes to958 // the content nodes using the same name. We just have to assign the same name that959 // we would've used (the parent ViewTransition name or auto-assign one).960 const viewTransition: ViewTransitionContext = {961 update: parentViewTransition.update, // For deep updates.962 enter: 'none',963 exit: 'none',964 share: parentViewTransition.update, // For exit or enter of reveals.965 parentEnter: 'none',966 parentExit: 'none',967 name: parentViewTransition.autoName,968 autoName: parentViewTransition.autoName,969 // TOOD: If we have more than just this Suspense boundary as a child of the ViewTransition970 // then the parent needs to isolate the names so that they don't conflict.971 nameIdx: 0,972 };973 return viewTransition;974}975976export function getSuspenseFallbackFormatContext(977 resumableState: ResumableState,978 parentContext: FormatContext,979): FormatContext {980 if (parentContext.tagScope & UPDATE_SCOPE) {981 // If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,982 // which hasn't disabled updates, then revealing it might animate the parent so we need983 // the ViewTransition instructions.984 resumableState.instructions |= NeedUpgradeToViewTransitions;985 }986 return createFormatContext(987 parentContext.insertionMode,988 parentContext.selectedValue,989 parentContext.tagScope | FALLBACK_SCOPE | EXIT_SCOPE,990 getSuspenseViewTransition(parentContext.viewTransition),991 );992}993994export function getSuspenseContentFormatContext(995 resumableState: ResumableState,996 parentContext: FormatContext,997): FormatContext {998 const viewTransition = getSuspenseViewTransition(999 parentContext.viewTransition,1000 );1001 let subtreeScope = parentContext.tagScope | ENTER_SCOPE;1002 if (viewTransition !== null && viewTransition.share !== 'none') {1003 // If we have a ViewTransition wrapping Suspense then the appearing animation1004 // will be applied just like an "enter" below. Mark it as animating.1005 subtreeScope |= APPEARING_SCOPE;1006 }1007 return createFormatContext(1008 parentContext.insertionMode,1009 parentContext.selectedValue,1010 subtreeScope,1011 viewTransition,1012 );1013}10141015export function getViewTransitionFormatContext(1016 resumableState: ResumableState,1017 parentContext: FormatContext,1018 update: ?string,1019 enter: ?string,1020 exit: ?string,1021 share: ?string,1022 parentEnter: ?string,1023 parentExit: ?string,1024 hasParentEnterHandler: boolean,1025 hasParentExitHandler: boolean,1026 name: ?string,1027 autoName: string, // name or an autogenerated unique name1028): FormatContext {1029 // We're entering a <ViewTransition>. Normalize props.1030 if (update == null) {1031 update = 'auto';1032 }1033 if (enter == null) {1034 enter = 'auto';1035 }1036 if (exit == null) {1037 exit = 'auto';1038 }1039 if (name == null) {1040 const parentViewTransition = parentContext.viewTransition;1041 if (parentViewTransition !== null) {1042 // If we have multiple nested ViewTransition and the parent has a "share"1043 // but the child doesn't, then the parent ViewTransition can still activate1044 // a share scenario so we reuse the name and share from the parent.1045 name = parentViewTransition.name;1046 share = parentViewTransition.share;1047 } else {1048 name = 'auto';1049 share = 'none'; // share is only relevant if there's an explicit name1050 }1051 } else {1052 if (share == null) {1053 share = 'auto';1054 }1055 if (parentContext.tagScope & FALLBACK_SCOPE) {1056 // If we have an explicit name and share is not disabled, and we're inside1057 // a fallback, then that fallback might pair with content and so we might need1058 // the ViewTransition instructions to animate between them.1059 resumableState.instructions |= NeedUpgradeToViewTransitions;1060 }1061 }1062 if (!(parentContext.tagScope & EXIT_SCOPE)) {1063 exit = 'none'; // exit is only relevant for the first ViewTransition inside fallback1064 } else {1065 resumableState.instructions |= NeedUpgradeToViewTransitions;1066 }1067 if (!(parentContext.tagScope & ENTER_SCOPE)) {1068 enter = 'none'; // enter is only relevant for the first ViewTransition inside content1069 } else {1070 resumableState.instructions |= NeedUpgradeToViewTransitions;1071 }1072 let resolvedParentEnter = 'none';1073 let resolvedParentExit = 'none';1074 if (enableViewTransitionParentEnterExit) {1075 if (1076 parentEnter != null &&1077 (parentContext.tagScope & PARENT_ENTER_SCOPE) !== 01078 ) {1079 resolvedParentEnter = parentEnter;1080 }1081 if (1082 parentExit != null &&1083 (parentContext.tagScope & PARENT_EXIT_SCOPE) !== 01084 ) {1085 resolvedParentExit = parentExit;1086 }1087 }1088 const viewTransition: ViewTransitionContext = {1089 update,1090 enter,1091 exit,1092 share,1093 parentEnter: resolvedParentEnter,1094 parentExit: resolvedParentExit,1095 name,1096 autoName,1097 nameIdx: 0,1098 };1099 let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;1100 if (update !== 'none') {1101 subtreeScope |= UPDATE_SCOPE;1102 } else {1103 subtreeScope &= ~UPDATE_SCOPE;1104 }1105 if (enter !== 'none') {1106 subtreeScope |= APPEARING_SCOPE;1107 }1108 if (enableViewTransitionParentEnterExit) {1109 // Parent enter relay: a ViewTransition that is itself entering starts a relay1110 // for its subtree. A nested ViewTransition continues that relay as long as it1111 // opts in with a parentEnter prop that doesn't resolve to "none", or with an1112 // onParentEnter handler. A missing parentEnter (with no handler) or an1113 // explicit "none" stops the relay so descendants below it don't participate.1114 // This mirrors commitParentEnterViewTransitions.1115 if (enter !== 'none') {1116 subtreeScope |= PARENT_ENTER_SCOPE;1117 } else if (1118 (parentContext.tagScope & PARENT_ENTER_SCOPE) !== 0 &&1119 (parentEnter === 'none' ||1120 (parentEnter === undefined && !hasParentEnterHandler))1121 ) {1122 subtreeScope &= ~PARENT_ENTER_SCOPE;1123 }1124 // Parent exit relay: mirror of the enter relay above.1125 if (exit !== 'none') {1126 subtreeScope |= PARENT_EXIT_SCOPE;1127 } else if (1128 (parentContext.tagScope & PARENT_EXIT_SCOPE) !== 0 &&1129 (parentExit === 'none' ||1130 (parentExit === undefined && !hasParentExitHandler))1131 ) {1132 subtreeScope &= ~PARENT_EXIT_SCOPE;1133 }1134 }1135 return createFormatContext(1136 parentContext.insertionMode,1137 parentContext.selectedValue,1138 subtreeScope,1139 viewTransition,1140 );1141}11421143export function isPreambleContext(formatContext: FormatContext): boolean {1144 return formatContext.insertionMode === HTML_HEAD_MODE;1145}11461147export function makeId(1148 resumableState: ResumableState,1149 treeId: string,1150 localId: number,1151): string {1152 const idPrefix = resumableState.idPrefix;11531154 let id = '_' + idPrefix + 'R_' + treeId;11551156 // Unless this is the first id at this level, append a number at the end1157 // that represents the position of this useId hook among all the useId1158 // hooks for this fiber.1159 if (localId > 0) {1160 id += 'H' + localId.toString(32);1161 }11621163 return id + '_';1164}11651166function encodeHTMLTextNode(text: string): string {1167 return escapeTextForBrowser(text);1168}11691170const textSeparator = stringToPrecomputedChunk('<!-- -->');11711172export function pushTextInstance(1173 target: Array<Chunk | PrecomputedChunk>,1174 text: string,1175 renderState: RenderState,1176 textEmbedded: boolean,1177): boolean {1178 if (text === '') {1179 // Empty text doesn't have a DOM node representation and the hydration is aware of this.1180 return textEmbedded;1181 }1182 if (textEmbedded) {1183 target.push(textSeparator);1184 }1185 target.push(stringToChunk(encodeHTMLTextNode(text)));1186 return true;1187}11881189// Called when Fizz is done with a Segment. Currently the only purpose is to conditionally1190// emit a text separator when we don't know for sure it is safe to omit1191export function pushSegmentFinale(1192 target: Array<Chunk | PrecomputedChunk>,1193 renderState: RenderState,1194 lastPushedText: boolean,1195 textEmbedded: boolean,1196): void {1197 if (lastPushedText && textEmbedded) {1198 target.push(textSeparator);1199 }1200}12011202function pushViewTransitionAttributes(1203 target: Array<Chunk | PrecomputedChunk>,1204 formatContext: FormatContext,1205): void {1206 if (!enableViewTransition) {1207 return;1208 }1209 const viewTransition = formatContext.viewTransition;1210 if (viewTransition === null) {1211 return;1212 }1213 if (viewTransition.name !== 'auto') {1214 pushStringAttribute(1215 target,1216 'vt-name',1217 viewTransition.nameIdx === 01218 ? viewTransition.name1219 : viewTransition.name + '_' + viewTransition.nameIdx,1220 );1221 // Increment the index in case we have multiple children to the same ViewTransition.1222 // Because this is a side-effect in render, we should ideally call pushViewTransitionAttributes1223 // after we've suspended (like forms do), so that we don't increment each attempt.1224 // TODO: Make this deterministic.1225 viewTransition.nameIdx++;1226 }1227 pushStringAttribute(target, 'vt-update', viewTransition.update);1228 if (viewTransition.enter !== 'none') {1229 pushStringAttribute(target, 'vt-enter', viewTransition.enter);1230 }1231 if (viewTransition.exit !== 'none') {1232 pushStringAttribute(target, 'vt-exit', viewTransition.exit);1233 }1234 if (viewTransition.share !== 'none') {1235 pushStringAttribute(target, 'vt-share', viewTransition.share);1236 }1237 if (1238 enableViewTransitionParentEnterExit &&1239 viewTransition.parentEnter !== 'none'1240 ) {1241 pushStringAttribute(target, 'vt-parent-enter', viewTransition.parentEnter);1242 }1243 if (1244 enableViewTransitionParentEnterExit &&1245 viewTransition.parentExit !== 'none'1246 ) {1247 pushStringAttribute(target, 'vt-parent-exit', viewTransition.parentExit);1248 }1249}12501251const styleNameCache: Map<string, PrecomputedChunk> = new Map();1252function processStyleName(styleName: string): PrecomputedChunk {1253 const chunk = styleNameCache.get(styleName);1254 if (chunk !== undefined) {1255 return chunk;1256 }1257 const result = stringToPrecomputedChunk(1258 escapeTextForBrowser(hyphenateStyleName(styleName)),1259 );1260 styleNameCache.set(styleName, result);1261 return result;1262}12631264const styleAttributeStart = stringToPrecomputedChunk(' style="');1265const styleAssign = stringToPrecomputedChunk(':');1266const styleSeparator = stringToPrecomputedChunk(';');12671268function pushStyleAttribute(1269 target: Array<Chunk | PrecomputedChunk>,1270 style: Object,1271): void {1272 if (typeof style !== 'object') {1273 throw new Error(1274 'The `style` prop expects a mapping from style properties to values, ' +1275 "not a string. For example, style={{marginRight: spacing + 'em'}} when " +1276 'using JSX.',1277 );1278 }12791280 let isFirst = true;1281 for (const styleName in style) {1282 if (!hasOwnProperty.call(style, styleName)) {1283 continue;1284 }1285 // If you provide unsafe user data here they can inject arbitrary CSS1286 // which may be problematic (I couldn't repro this):1287 // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet1288 // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/1289 // This is not an XSS hole but instead a potential CSS injection issue1290 // which has lead to a greater discussion about how we're going to1291 // trust URLs moving forward. See #21159011292 const styleValue = style[styleName];1293 if (1294 styleValue == null ||1295 typeof styleValue === 'boolean' ||1296 styleValue === ''1297 ) {1298 // TODO: We used to set empty string as a style with an empty value. Does that ever make sense?1299 continue;1300 }13011302 let nameChunk;1303 let valueChunk;1304 const isCustomProperty = styleName.indexOf('--') === 0;1305 if (isCustomProperty) {1306 nameChunk = stringToChunk(escapeTextForBrowser(styleName));1307 if (__DEV__) {1308 checkCSSPropertyStringCoercion(styleValue, styleName);1309 }1310 valueChunk = stringToChunk(1311 escapeTextForBrowser(('' + styleValue).trim()),1312 );1313 } else {1314 if (__DEV__) {1315 warnValidStyle(styleName, styleValue);1316 }13171318 nameChunk = processStyleName(styleName);1319 if (typeof styleValue === 'number') {1320 if (styleValue !== 0 && !isUnitlessNumber(styleName)) {1321 valueChunk = stringToChunk(styleValue + 'px'); // Presumes implicit 'px' suffix for unitless numbers1322 } else {1323 valueChunk = stringToChunk('' + styleValue);1324 }1325 } else {1326 if (__DEV__) {1327 checkCSSPropertyStringCoercion(styleValue, styleName);1328 }1329 valueChunk = stringToChunk(1330 escapeTextForBrowser(('' + styleValue).trim()),1331 );1332 }1333 }1334 if (isFirst) {1335 isFirst = false;1336 // If it's first, we don't need any separators prefixed.1337 target.push(styleAttributeStart, nameChunk, styleAssign, valueChunk);1338 } else {1339 target.push(styleSeparator, nameChunk, styleAssign, valueChunk);1340 }1341 }1342 if (!isFirst) {1343 target.push(attributeEnd);1344 }1345}13461347const attributeSeparator = stringToPrecomputedChunk(' ');1348const attributeAssign = stringToPrecomputedChunk('="');1349const attributeEnd = stringToPrecomputedChunk('"');1350const attributeEmptyString = stringToPrecomputedChunk('=""');13511352function pushBooleanAttribute(1353 target: Array<Chunk | PrecomputedChunk>,1354 name: string,1355 value: string | boolean | number | Function | Object, // not null or undefined1356): void {1357 if (value && typeof value !== 'function' && typeof value !== 'symbol') {1358 target.push(attributeSeparator, stringToChunk(name), attributeEmptyString);1359 }1360}13611362function pushStringAttribute(1363 target: Array<Chunk | PrecomputedChunk>,1364 name: string,1365 value: string | boolean | number | Function | Object, // not null or undefined1366): void {1367 if (1368 typeof value !== 'function' &&1369 typeof value !== 'symbol' &&1370 typeof value !== 'boolean'1371 ) {1372 target.push(1373 attributeSeparator,1374 stringToChunk(name),1375 attributeAssign,1376 stringToChunk(escapeTextForBrowser(value)),1377 attributeEnd,1378 );1379 }1380}13811382function makeFormFieldPrefix(resumableState: ResumableState): string {1383 // TODO: Make this deterministic.1384 const id = resumableState.nextFormID++;1385 return resumableState.idPrefix + id;1386}13871388// Since this will likely be repeated a lot in the HTML, we use a more concise message1389// than on the client and hopefully it's googleable.1390const actionJavaScriptURL = stringToPrecomputedChunk(1391 escapeTextForBrowser(1392 // eslint-disable-next-line no-script-url1393 "javascript:throw new Error('React form unexpectedly submitted.')",1394 ),1395);13961397const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');13981399function pushAdditionalFormField(1400 this: Array<Chunk | PrecomputedChunk>,1401 value: string | File,1402 key: string,1403): void {1404 const target: Array<Chunk | PrecomputedChunk> = this;1405 target.push(startHiddenInputChunk);1406 validateAdditionalFormField(value, key);1407 pushStringAttribute(target, 'name', key);1408 pushStringAttribute(target, 'value', value);1409 target.push(endOfStartTagSelfClosing);1410}14111412function pushAdditionalFormFields(1413 target: Array<Chunk | PrecomputedChunk>,1414 formData: void | null | FormData,1415) {1416 if (formData != null) {1417 // $FlowFixMe[prop-missing]: FormData has forEach.1418 formData.forEach(pushAdditionalFormField, target);1419 }1420}14211422function validateAdditionalFormField(value: string | File, key: string): void {1423 if (typeof value !== 'string') {1424 throw new Error(1425 'File/Blob fields are not yet supported in progressive forms. ' +1426 'Will fallback to client hydration.',1427 );1428 }1429}14301431function validateAdditionalFormFields(formData: void | null | FormData) {1432 if (formData != null) {1433 // $FlowFixMe[prop-missing]: FormData has forEach.1434 formData.forEach(validateAdditionalFormField);1435 }1436 return formData;1437}14381439function getCustomFormFields(1440 resumableState: ResumableState,1441 formAction: any,1442): null | ReactCustomFormAction {1443 const customAction = formAction.$$FORM_ACTION;1444 if (typeof customAction === 'function') {1445 const prefix = makeFormFieldPrefix(resumableState);1446 try {1447 const customFields = formAction.$$FORM_ACTION(prefix);1448 if (customFields) {1449 validateAdditionalFormFields(customFields.data);1450 }1451 return customFields;1452 } catch (x) {1453 if (typeof x === 'object' && x !== null && typeof x.then === 'function') {1454 // Rethrow suspense.1455 throw x;1456 }1457 // If we fail to encode the form action for progressive enhancement for some reason,1458 // fallback to trying replaying on the client instead of failing the page. It might1459 // work there.1460 if (__DEV__) {1461 // TODO: Should this be some kind of recoverable error?1462 console.error(1463 'Failed to serialize an action for progressive enhancement:\n%s',1464 x,1465 );1466 }1467 }1468 }1469 return null;1470}14711472function pushFormActionAttribute(1473 target: Array<Chunk | PrecomputedChunk>,1474 resumableState: ResumableState,1475 renderState: RenderState,1476 formAction: any,1477 formEncType: any,1478 formMethod: any,1479 formTarget: any,1480 name: any,1481): void | null | FormData {1482 let formData = null;1483 if (typeof formAction === 'function') {1484 // Function form actions cannot control the form properties1485 if (__DEV__) {1486 if (name !== null && !didWarnFormActionName) {1487 didWarnFormActionName = true;1488 console.error(1489 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +1490 'React needs it to encode which action should be invoked. It will get overridden.',1491 );1492 }1493 if (1494 (formEncType !== null || formMethod !== null) &&1495 !didWarnFormActionMethod1496 ) {1497 didWarnFormActionMethod = true;1498 console.error(1499 'Cannot specify a formEncType or formMethod for a button that specifies a ' +1500 'function as a formAction. React provides those automatically. They will get overridden.',1501 );1502 }1503 if (formTarget !== null && !didWarnFormActionTarget) {1504 didWarnFormActionTarget = true;1505 console.error(1506 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +1507 'The function will always be executed in the same window.',1508 );1509 }1510 }1511 const customFields = getCustomFormFields(resumableState, formAction);1512 if (customFields !== null) {1513 // This action has a custom progressive enhancement form that can submit the form1514 // back to the server if it's invoked before hydration. Such as a Server Action.1515 name = customFields.name;1516 formAction = customFields.action || '';1517 formEncType = customFields.encType;1518 formMethod = customFields.method;1519 formTarget = customFields.target;1520 formData = customFields.data;1521 } else {1522 // Set a javascript URL that doesn't do anything. We don't expect this to be invoked1523 // because we'll preventDefault in the Fizz runtime, but it can happen if a form is1524 // manually submitted or if someone calls stopPropagation before React gets the event.1525 // If CSP is used to block javascript: URLs that's fine too. It just won't show this1526 // error message but the URL will be logged.1527 target.push(1528 attributeSeparator,1529 stringToChunk('formAction'),1530 attributeAssign,1531 actionJavaScriptURL,1532 attributeEnd,1533 );1534 name = null;1535 formAction = null;1536 formEncType = null;1537 formMethod = null;1538 formTarget = null;1539 injectFormReplayingRuntime(resumableState, renderState);1540 }1541 }1542 if (name != null) {1543 pushAttribute(target, 'name', name);1544 }1545 if (formAction != null) {1546 pushAttribute(target, 'formAction', formAction);1547 }1548 if (formEncType != null) {1549 pushAttribute(target, 'formEncType', formEncType);1550 }1551 if (formMethod != null) {1552 pushAttribute(target, 'formMethod', formMethod);1553 }1554 if (formTarget != null) {1555 pushAttribute(target, 'formTarget', formTarget);1556 }1557 return formData;1558}15591560let blobCache: null | WeakMap<Blob, Thenable<string>> = null;15611562function pushSrcObjectAttribute(1563 target: Array<Chunk | PrecomputedChunk>,1564 blob: Blob,1565): void {1566 // Throwing a Promise style suspense read of the Blob content.1567 if (blobCache === null) {1568 blobCache = new WeakMap();1569 }1570 const suspenseCache: WeakMap<Blob, Thenable<string>> = blobCache;1571 let thenable = suspenseCache.get(blob);1572 if (thenable === undefined) {1573 thenable = readAsDataURL(blob) as any as Thenable<string>;1574 thenable.then(1575 result => {1576 (thenable as any).status = 'fulfilled';1577 (thenable as any).value = result;1578 },1579 error => {1580 (thenable as any).status = 'rejected';1581 (thenable as any).reason = error;1582 },1583 );1584 suspenseCache.set(blob, thenable);1585 }1586 if (thenable.status === 'rejected') {1587 throw thenable.reason;1588 } else if (thenable.status !== 'fulfilled') {1589 throw thenable;1590 }1591 const url = thenable.value;1592 target.push(1593 attributeSeparator,1594 stringToChunk('src'),1595 attributeAssign,1596 stringToChunk(escapeTextForBrowser(url)),1597 attributeEnd,1598 );1599}16001601function pushAttribute(1602 target: Array<Chunk | PrecomputedChunk>,1603 name: string,1604 value: string | boolean | number | Function | Object, // not null or undefined1605): void {1606 switch (name) {1607 // These are very common props and therefore are in the beginning of the switch.1608 // TODO: aria-label is a very common prop but allows booleans so is not like the others1609 // but should ideally go in this list too.1610 case 'className': {1611 pushStringAttribute(target, 'class', value);1612 break;1613 }1614 case 'tabIndex': {1615 pushStringAttribute(target, 'tabindex', value);1616 break;1617 }1618 case 'dir':1619 case 'role':1620 case 'viewBox':1621 case 'width':1622 case 'height': {1623 pushStringAttribute(target, name, value);1624 break;1625 }1626 case 'style': {1627 pushStyleAttribute(target, value);1628 return;1629 }1630 case 'src': {1631 // $FlowFixMe[invalid-compare]1632 if (enableSrcObject && typeof value === 'object' && value !== null) {1633 if (typeof Blob === 'function' && value instanceof Blob) {1634 pushSrcObjectAttribute(target, value);1635 return;1636 }1637 }1638 // Fallthrough to general urls1639 }1640 case 'href': {1641 if (value === '') {1642 if (__DEV__) {1643 if (name === 'src') {1644 console.error(1645 'An empty string ("") was passed to the %s attribute. ' +1646 'This may cause the browser to download the whole page again over the network. ' +1647 'To fix this, either do not render the element at all ' +1648 'or pass null to %s instead of an empty string.',1649 name,1650 name,1651 );1652 } else {1653 console.error(1654 'An empty string ("") was passed to the %s attribute. ' +1655 'To fix this, either do not render the element at all ' +1656 'or pass null to %s instead of an empty string.',1657 name,1658 name,1659 );1660 }1661 }1662 return;1663 }1664 }1665 // Fall through to the last case which shouldn't remove empty strings.1666 case 'action':1667 case 'formAction': {1668 // TODO: Consider only special casing these for each tag.1669 if (1670 value == null ||1671 typeof value === 'function' ||1672 typeof value === 'symbol' ||1673 typeof value === 'boolean'1674 ) {1675 return;1676 }1677 if (__DEV__) {1678 checkAttributeStringCoercion(value, name);1679 }1680 const sanitizedValue = sanitizeURL('' + value);1681 target.push(1682 attributeSeparator,1683 stringToChunk(name),1684 attributeAssign,1685 stringToChunk(escapeTextForBrowser(sanitizedValue)),1686 attributeEnd,1687 );1688 return;1689 }1690 case 'defaultValue':1691 case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements.1692 case 'innerHTML': // Must use dangerouslySetInnerHTML instead.1693 case 'suppressContentEditableWarning':1694 case 'suppressHydrationWarning':1695 case 'ref':1696 // Ignored. These are built-in to React on the client.1697 return;1698 case 'autoFocus':1699 case 'multiple':1700 case 'muted': {1701 pushBooleanAttribute(target, name.toLowerCase(), value);1702 return;1703 }1704 case 'xlinkHref': {1705 if (1706 typeof value === 'function' ||1707 typeof value === 'symbol' ||1708 typeof value === 'boolean'1709 ) {1710 return;1711 }1712 if (__DEV__) {1713 checkAttributeStringCoercion(value, name);1714 }1715 const sanitizedValue = sanitizeURL('' + value);1716 target.push(1717 attributeSeparator,1718 stringToChunk('xlink:href'),1719 attributeAssign,1720 stringToChunk(escapeTextForBrowser(sanitizedValue)),1721 attributeEnd,1722 );1723 return;1724 }1725 case 'contentEditable':1726 case 'spellCheck':1727 case 'draggable':1728 case 'value':1729 case 'autoReverse':1730 case 'externalResourcesRequired':1731 case 'focusable':1732 case 'preserveAlpha': {1733 // Booleanish String1734 // These are "enumerated" attributes that accept "true" and "false".1735 // In React, we let users pass `true` and `false` even though technically1736 // these aren't boolean attributes (they are coerced to strings).1737 if (typeof value !== 'function' && typeof value !== 'symbol') {1738 target.push(1739 attributeSeparator,1740 stringToChunk(name),1741 attributeAssign,1742 stringToChunk(escapeTextForBrowser(value)),1743 attributeEnd,1744 );1745 }1746 return;1747 }1748 case 'inert': {1749 if (__DEV__) {1750 if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[name]) {1751 didWarnForNewBooleanPropsWithEmptyValue[name] = true;1752 console.error(1753 'Received an empty string for a boolean attribute `%s`. ' +1754 'This will treat the attribute as if it were false. ' +1755 'Either pass `false` to silence this warning, or ' +1756 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',1757 name,1758 );1759 }1760 }1761 }1762 // Fallthrough for boolean props that don't have a warning for empty strings.1763 case 'allowFullScreen':1764 case 'async':1765 case 'autoPlay':1766 case 'controls':1767 case 'credentialless':1768 case 'default':1769 case 'defer':1770 case 'disabled':1771 case 'disablePictureInPicture':1772 case 'disableRemotePlayback':1773 case 'formNoValidate':1774 case 'hidden':1775 case 'loop':1776 case 'noModule':1777 case 'noValidate':1778 case 'open':1779 case 'playsInline':1780 case 'readOnly':1781 case 'required':1782 case 'reversed':1783 case 'scoped':1784 case 'seamless':1785 case 'itemScope': {1786 // Boolean1787 if (value && typeof value !== 'function' && typeof value !== 'symbol') {1788 target.push(1789 attributeSeparator,1790 stringToChunk(name),1791 attributeEmptyString,1792 );1793 }1794 return;1795 }1796 case 'capture':1797 case 'download': {1798 // Overloaded Boolean1799 if (value === true) {1800 target.push(1801 attributeSeparator,1802 stringToChunk(name),1803 attributeEmptyString,1804 );1805 } else if (value === false) {1806 // Ignored1807 } else if (typeof value !== 'function' && typeof value !== 'symbol') {1808 target.push(1809 attributeSeparator,1810 stringToChunk(name),1811 attributeAssign,1812 stringToChunk(escapeTextForBrowser(value)),1813 attributeEnd,1814 );1815 }1816 return;1817 }1818 case 'cols':1819 case 'rows':1820 case 'size':1821 case 'span': {1822 // These are HTML attributes that must be positive numbers.1823 if (1824 typeof value !== 'function' &&1825 typeof value !== 'symbol' &&1826 !isNaN(value) &&1827 (value as any) >= 11828 ) {1829 target.push(1830 attributeSeparator,1831 stringToChunk(name),1832 attributeAssign,1833 stringToChunk(escapeTextForBrowser(value)),1834 attributeEnd,1835 );1836 }1837 return;1838 }1839 case 'rowSpan':1840 case 'start': {1841 // These are HTML attributes that must be numbers.1842 if (1843 typeof value !== 'function' &&1844 typeof value !== 'symbol' &&1845 !isNaN(value)1846 ) {1847 target.push(1848 attributeSeparator,1849 stringToChunk(name),1850 attributeAssign,1851 stringToChunk(escapeTextForBrowser(value)),1852 attributeEnd,1853 );1854 }1855 return;1856 }1857 case 'xlinkActuate':1858 pushStringAttribute(target, 'xlink:actuate', value);1859 return;1860 case 'xlinkArcrole':1861 pushStringAttribute(target, 'xlink:arcrole', value);1862 return;1863 case 'xlinkRole':1864 pushStringAttribute(target, 'xlink:role', value);1865 return;1866 case 'xlinkShow':1867 pushStringAttribute(target, 'xlink:show', value);1868 return;1869 case 'xlinkTitle':1870 pushStringAttribute(target, 'xlink:title', value);1871 return;1872 case 'xlinkType':1873 pushStringAttribute(target, 'xlink:type', value);1874 return;1875 case 'xmlBase':1876 pushStringAttribute(target, 'xml:base', value);1877 return;1878 case 'xmlLang':1879 pushStringAttribute(target, 'xml:lang', value);1880 return;1881 case 'xmlSpace':1882 pushStringAttribute(target, 'xml:space', value);1883 return;1884 default:1885 if (1886 // shouldIgnoreAttribute1887 // We have already filtered out null/undefined and reserved words.1888 name.length > 2 &&1889 (name[0] === 'o' || name[0] === 'O') &&1890 (name[1] === 'n' || name[1] === 'N')1891 ) {1892 return;1893 }18941895 const attributeName = getAttributeAlias(name);1896 if (isAttributeNameSafe(attributeName)) {1897 // shouldRemoveAttribute1898 switch (typeof value) {1899 case 'function':1900 case 'symbol':1901 return;1902 case 'boolean': {1903 const prefix = attributeName.toLowerCase().slice(0, 5);1904 if (prefix !== 'data-' && prefix !== 'aria-') {1905 return;1906 }1907 }1908 }1909 target.push(1910 attributeSeparator,1911 stringToChunk(attributeName),1912 attributeAssign,1913 stringToChunk(escapeTextForBrowser(value)),1914 attributeEnd,1915 );1916 }1917 }1918}19191920const endOfStartTag = stringToPrecomputedChunk('>');1921const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');19221923function pushInnerHTML(1924 target: Array<Chunk | PrecomputedChunk>,1925 innerHTML: any,1926 children: any,1927) {1928 if (innerHTML != null) {1929 if (children != null) {1930 throw new Error(1931 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',1932 );1933 }19341935 if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {1936 throw new Error(1937 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +1938 'Please visit https://react.dev/link/dangerously-set-inner-html ' +1939 'for more information.',1940 );1941 }19421943 const html = innerHTML.__html;1944 if (html !== null && html !== undefined) {1945 if (__DEV__) {1946 checkHtmlStringCoercion(html);1947 }1948 target.push(stringToChunk('' + html));1949 }1950 }1951}19521953// TODO: Move these to RenderState so that we warn for every request.1954// It would help debugging in stateful servers (e.g. service worker).1955let didWarnDefaultInputValue = false;1956let didWarnDefaultChecked = false;1957let didWarnDefaultSelectValue = false;1958let didWarnDefaultTextareaValue = false;1959let didWarnInvalidOptionChildren = false;1960let didWarnInvalidOptionInnerHTML = false;1961let didWarnSelectedSetOnOption = false;1962let didWarnFormActionType = false;1963let didWarnFormActionName = false;1964let didWarnFormActionTarget = false;1965let didWarnFormActionMethod = false;19661967function checkSelectProp(props: any, propName: string) {1968 if (__DEV__) {1969 const value = props[propName];1970 if (value != null) {1971 const array = isArray(value);1972 if (props.multiple && !array) {1973 console.error(1974 'The `%s` prop supplied to <select> must be an array if ' +1975 '`multiple` is true.',1976 propName,1977 );1978 } else if (!props.multiple && array) {1979 console.error(1980 'The `%s` prop supplied to <select> must be a scalar ' +1981 'value if `multiple` is false.',1982 propName,1983 );1984 }1985 }1986 }1987}19881989function pushStartAnchor(1990 target: Array<Chunk | PrecomputedChunk>,1991 props: Object,1992 formatContext: FormatContext,1993): ReactNodeList {1994 target.push(startChunkForTag('a'));19951996 let children = null;1997 let innerHTML = null;1998 for (const propKey in props) {1999 if (hasOwnProperty.call(props, propKey)) {2000 const propValue = props[propKey];
Findings
✓ No findings reported for this file.