/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes'; import type { CrossOriginEnum, PreloadImplOptions, PreloadModuleImplOptions, PreinitStyleOptions, PreinitScriptOptions, PreinitModuleScriptOptions, ImportMap, } from 'react-dom/src/shared/ReactDOMTypes'; import { checkHtmlStringCoercion, checkCSSPropertyStringCoercion, checkAttributeStringCoercion, checkOptionStringCoercion, } from 'shared/CheckStringCoercion'; import {Children} from 'react'; import {enableFizzExternalRuntime} from 'shared/ReactFeatureFlags'; import type { Destination, Chunk, PrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import type {FormStatus} from '../shared/ReactDOMFormActions'; import { writeChunk, writeChunkAndReturn, stringToChunk, stringToPrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import { resolveRequest, getResumableState, getRenderState, flushResources, } from 'react-server/src/ReactFizzServer'; import isAttributeNameSafe from '../shared/isAttributeNameSafe'; import isUnitlessNumber from '../shared/isUnitlessNumber'; import getAttributeAlias from '../shared/getAttributeAlias'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import warnValidStyle from '../shared/warnValidStyle'; import {getCrossOriginString} from '../shared/crossOriginStrings'; import escapeTextForBrowser from './escapeTextForBrowser'; import hyphenateStyleName from '../shared/hyphenateStyleName'; import hasOwnProperty from 'shared/hasOwnProperty'; import sanitizeURL from '../shared/sanitizeURL'; import isArray from 'shared/isArray'; import { clientRenderBoundary as clientRenderFunction, completeBoundary as completeBoundaryFunction, completeBoundaryWithStyles as styleInsertionFunction, completeSegment as completeSegmentFunction, formReplaying as formReplayingRuntime, } from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings'; import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMResourceValidation'; import {NotPending} from '../shared/ReactDOMFormActions'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const previousDispatcher = ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */ ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */ = { f /* flushSyncWork */: previousDispatcher.f /* flushSyncWork */, r /* requestFormReset */: previousDispatcher.r /* requestFormReset */, D /* prefetchDNS */: prefetchDNS, C /* preconnect */: preconnect, L /* preload */: preload, m /* preloadModule */: preloadModule, X /* preinitScript */: preinitScript, S /* preinitStyle */: preinitStyle, M /* preinitModuleScript */: preinitModuleScript, }; // We make every property of the descriptor optional because it is not a contract that // the headers provided by onHeaders has any particular header types. export type HeadersDescriptor = { Link?: string, }; // Used to distinguish these contexts from ones used in other renderers. // E.g. this can be used to distinguish legacy renderers from this modern one. export const isPrimaryRenderer = true; export const supportsClientAPIs = true; export type StreamingFormat = 0 | 1; const ScriptStreamingFormat: StreamingFormat = 0; const DataStreamingFormat: StreamingFormat = 1; export type InstructionState = number; const NothingSent /* */ = 0b00000; const SentCompleteSegmentFunction /* */ = 0b00001; const SentCompleteBoundaryFunction /* */ = 0b00010; const SentClientRenderFunction /* */ = 0b00100; const SentStyleInsertionFunction /* */ = 0b01000; const SentFormReplayingRuntime /* */ = 0b10000; // Per request, global state that is not contextual to the rendering subtree. // This cannot be resumed and therefore should only contain things that are // temporary working state or are never used in the prerender pass. export type RenderState = { // These can be recreated from resumable state. placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: PrecomputedChunk, // inline script streaming format, unused if using external runtime / data startInlineScript: PrecomputedChunk, // the preamble must always flush before resuming, so all these chunks must // be null or empty when resuming. // preamble chunks preamble: PreambleState, // external runtime script chunks externalRuntimeScript: null | ExternalRuntimeScript, bootstrapChunks: Array, importMapChunks: Array, // Hoistable chunks charsetChunks: Array, viewportChunks: Array, hoistableChunks: Array, // Headers queues for Resources that can flush early onHeaders: void | ((headers: HeadersDescriptor) => void), headers: null | { preconnects: string, fontPreloads: string, highImagePreloads: string, remainingCapacity: number, }, resets: { // corresponds to ResumableState.unknownResources["font"] font: { [href: string]: Preloaded, }, // the rest correspond to ResumableState[<...>Resources] dns: {[key: string]: Exists}, connect: { default: {[key: string]: Exists}, anonymous: {[key: string]: Exists}, credentials: {[key: string]: Exists}, }, image: { [key: string]: Preloaded, }, style: { [key: string]: Exists | Preloaded | PreloadedWithCredentials, }, }, // Flushing queues for Resource dependencies preconnects: Set, fontPreloads: Set, highImagePreloads: Set, // usedImagePreloads: Set, styles: Map, bootstrapScripts: Set, scripts: Set, bulkPreloads: Set, // Temporarily keeps track of key to preload resources before shell flushes. preloads: { images: Map, stylesheets: Map, scripts: Map, moduleScripts: Map, }, // Module-global-like reference for flushing/hoisting state of style resources // We need to track whether the current request has flushed any style resources // without sending an instruction to hoist them. we do that here stylesToHoist: boolean, // We allow the legacy renderer to extend this object. ... }; type Exists = null; type Preloaded = []; // Credentials here are things that affect whether a browser will make a request // as well as things that affect which connection the browser will use for that request. // We want these to be aligned across preloads and resources because otherwise the preload // will be wasted. // We investigated whether referrerPolicy should be included here but from experimentation // it seems that browsers do not treat this as part of the http cache key and does not affect // which connection is used. type PreloadedWithCredentials = [ /* crossOrigin */ ?CrossOriginEnum, /* integrity */ ?string, ]; const EXISTS: Exists = null; // This constant is to mark preloads that have no unique credentials // to convey. It should never be checked by identity and we should not // assume Preload values in ResumableState equal this value because they // will have come from some parsed input. const PRELOAD_NO_CREDS: Preloaded = []; if (__DEV__) { Object.freeze(PRELOAD_NO_CREDS); } // Per response, global state that is not contextual to the rendering subtree. // This is resumable and therefore should be serializable. export type ResumableState = { idPrefix: string, nextFormID: number, streamingFormat: StreamingFormat, // We carry the bootstrap intializers in resumable state in case we postpone in the shell // of a prerender. On resume we will reinitialize the bootstrap scripts if necessary. // If we end up flushing the bootstrap scripts we void these on the resumable state bootstrapScriptContent?: string | void, bootstrapScripts?: $ReadOnlyArray | void, bootstrapModules?: $ReadOnlyArray | void, // state for script streaming format, unused if using external runtime / data instructions: InstructionState, // postamble state hasBody: boolean, hasHtml: boolean, // Resources - Request local cache unknownResources: { [asType: string]: { [href: string]: Preloaded, }, }, dnsResources: {[key: string]: Exists}, connectResources: { default: {[key: string]: Exists}, anonymous: {[key: string]: Exists}, credentials: {[key: string]: Exists}, }, imageResources: { [key: string]: Preloaded, }, styleResources: { [key: string]: Exists | Preloaded | PreloadedWithCredentials, }, scriptResources: { [key: string]: Exists | Preloaded | PreloadedWithCredentials, }, moduleUnknownResources: { [asType: string]: { [href: string]: Preloaded, }, }, moduleScriptResources: { [key: string]: Exists | Preloaded | PreloadedWithCredentials, }, }; const dataElementQuotedEnd = stringToPrecomputedChunk('">'); const startInlineScript = stringToPrecomputedChunk(''); const startScriptSrc = stringToPrecomputedChunk(''); /** * This escaping function is designed to work with with inline scripts where the entire * contents are escaped. Because we know we are escaping the entire script we can avoid for instance * escaping html comment string sequences that are valid javascript as well because * if there are no sebsequent '); // Since we store headers as strings we deal with their length in utf16 code units // rather than visual characters or the utf8 encoding that is used for most binary // serialization. Some common HTTP servers only allow for headers to be 4kB in length. // We choose a default length that is likely to be well under this already limited length however // pathological cases may still cause the utf-8 encoding of the headers to approach this limit. // It should also be noted that this maximum is a soft maximum. we have not reached the limit we will // allow one more header to be captured which means in practice if the limit is approached it will be exceeded const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000; let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean}; if (__DEV__) { didWarnForNewBooleanPropsWithEmptyValue = {}; } // Allows us to keep track of what we've already written so we can refer back to it. // if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag // is set, the server will send instructions via data attributes (instead of inline scripts) export function createRenderState( resumableState: ResumableState, nonce: string | void, externalRuntimeConfig: string | BootstrapScriptDescriptor | void, importMap: ImportMap | void, onHeaders: void | ((headers: HeadersDescriptor) => void), maxHeadersLength: void | number, ): RenderState { const inlineScriptWithNonce = nonce === undefined ? startInlineScript : stringToPrecomputedChunk( ''); const completeSegmentData1 = stringToPrecomputedChunk( '