Throw an error during module initialization if the version of the "react-dom" package does not match the version of "react". We used to be more relaxed about this, because the "react" package changed so infrequently. However, we now have many more features that rely on an internal protocol between the two packages, including Hooks, Float, and the compiler runtime. So it's important that both packages are versioned in lockstep. Before this change, a version mismatch would often result in a cryptic internal error with no indication of the root cause. Instead, we will now compare the versions during module initialization and immediately throw an error to catch mistakes as early as possible and provide a clear error message.
226 lines
6.5 KiB
JavaScript
226 lines
6.5 KiB
JavaScript
/**
|
|
* 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 {
|
|
Request,
|
|
PostponedState,
|
|
ErrorInfo,
|
|
PostponeInfo,
|
|
} from 'react-server/src/ReactFizzServer';
|
|
import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
|
|
import type {Writable} from 'stream';
|
|
import type {
|
|
BootstrapScriptDescriptor,
|
|
HeadersDescriptor,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
|
|
import type {ImportMap} from '../shared/ReactDOMTypes';
|
|
|
|
import ReactVersion from 'shared/ReactVersion';
|
|
|
|
import {
|
|
createRequest,
|
|
resumeRequest,
|
|
startWork,
|
|
startFlowing,
|
|
stopFlowing,
|
|
abort,
|
|
prepareForStartFlowingIfBeforeAllReady,
|
|
} from 'react-server/src/ReactFizzServer';
|
|
|
|
import {
|
|
createResumableState,
|
|
createRenderState,
|
|
resumeRenderState,
|
|
createRootFormatContext,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
|
|
import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
|
|
ensureCorrectIsomorphicReactVersion();
|
|
|
|
function createDrainHandler(destination: Destination, request: Request) {
|
|
return () => startFlowing(request, destination);
|
|
}
|
|
|
|
function createCancelHandler(request: Request, reason: string) {
|
|
return () => {
|
|
stopFlowing(request);
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
abort(request, new Error(reason));
|
|
};
|
|
}
|
|
|
|
type Options = {
|
|
identifierPrefix?: string,
|
|
namespaceURI?: string,
|
|
nonce?: string,
|
|
bootstrapScriptContent?: string,
|
|
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
|
|
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
|
|
progressiveChunkSize?: number,
|
|
onShellReady?: () => void,
|
|
onShellError?: (error: mixed) => void,
|
|
onAllReady?: () => void,
|
|
onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
|
|
onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
|
|
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
|
|
importMap?: ImportMap,
|
|
formState?: ReactFormState<any, any> | null,
|
|
onHeaders?: (headers: HeadersDescriptor) => void,
|
|
maxHeadersLength?: number,
|
|
};
|
|
|
|
type ResumeOptions = {
|
|
nonce?: string,
|
|
onShellReady?: () => void,
|
|
onShellError?: (error: mixed) => void,
|
|
onAllReady?: () => void,
|
|
onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
|
|
onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void,
|
|
};
|
|
|
|
type PipeableStream = {
|
|
// Cancel any pending I/O and put anything remaining into
|
|
// client rendered mode.
|
|
abort(reason: mixed): void,
|
|
pipe<T: Writable>(destination: T): T,
|
|
};
|
|
|
|
function createRequestImpl(children: ReactNodeList, options: void | Options) {
|
|
const resumableState = createResumableState(
|
|
options ? options.identifierPrefix : undefined,
|
|
options ? options.unstable_externalRuntimeSrc : undefined,
|
|
options ? options.bootstrapScriptContent : undefined,
|
|
options ? options.bootstrapScripts : undefined,
|
|
options ? options.bootstrapModules : undefined,
|
|
);
|
|
return createRequest(
|
|
children,
|
|
resumableState,
|
|
createRenderState(
|
|
resumableState,
|
|
options ? options.nonce : undefined,
|
|
options ? options.unstable_externalRuntimeSrc : undefined,
|
|
options ? options.importMap : undefined,
|
|
options ? options.onHeaders : undefined,
|
|
options ? options.maxHeadersLength : undefined,
|
|
),
|
|
createRootFormatContext(options ? options.namespaceURI : undefined),
|
|
options ? options.progressiveChunkSize : undefined,
|
|
options ? options.onError : undefined,
|
|
options ? options.onAllReady : undefined,
|
|
options ? options.onShellReady : undefined,
|
|
options ? options.onShellError : undefined,
|
|
undefined,
|
|
options ? options.onPostpone : undefined,
|
|
options ? options.formState : undefined,
|
|
);
|
|
}
|
|
|
|
function renderToPipeableStream(
|
|
children: ReactNodeList,
|
|
options?: Options,
|
|
): PipeableStream {
|
|
const request = createRequestImpl(children, options);
|
|
let hasStartedFlowing = false;
|
|
startWork(request);
|
|
return {
|
|
pipe<T: Writable>(destination: T): T {
|
|
if (hasStartedFlowing) {
|
|
throw new Error(
|
|
'React currently only supports piping to one writable stream.',
|
|
);
|
|
}
|
|
hasStartedFlowing = true;
|
|
prepareForStartFlowingIfBeforeAllReady(request);
|
|
startFlowing(request, destination);
|
|
destination.on('drain', createDrainHandler(destination, request));
|
|
destination.on(
|
|
'error',
|
|
createCancelHandler(
|
|
request,
|
|
'The destination stream errored while writing data.',
|
|
),
|
|
);
|
|
destination.on(
|
|
'close',
|
|
createCancelHandler(request, 'The destination stream closed early.'),
|
|
);
|
|
return destination;
|
|
},
|
|
abort(reason: mixed) {
|
|
abort(request, reason);
|
|
},
|
|
};
|
|
}
|
|
|
|
function resumeRequestImpl(
|
|
children: ReactNodeList,
|
|
postponedState: PostponedState,
|
|
options: void | ResumeOptions,
|
|
) {
|
|
return resumeRequest(
|
|
children,
|
|
postponedState,
|
|
resumeRenderState(
|
|
postponedState.resumableState,
|
|
options ? options.nonce : undefined,
|
|
),
|
|
options ? options.onError : undefined,
|
|
options ? options.onAllReady : undefined,
|
|
options ? options.onShellReady : undefined,
|
|
options ? options.onShellError : undefined,
|
|
undefined,
|
|
options ? options.onPostpone : undefined,
|
|
);
|
|
}
|
|
|
|
function resumeToPipeableStream(
|
|
children: ReactNodeList,
|
|
postponedState: PostponedState,
|
|
options?: ResumeOptions,
|
|
): PipeableStream {
|
|
const request = resumeRequestImpl(children, postponedState, options);
|
|
let hasStartedFlowing = false;
|
|
startWork(request);
|
|
return {
|
|
pipe<T: Writable>(destination: T): T {
|
|
if (hasStartedFlowing) {
|
|
throw new Error(
|
|
'React currently only supports piping to one writable stream.',
|
|
);
|
|
}
|
|
hasStartedFlowing = true;
|
|
startFlowing(request, destination);
|
|
destination.on('drain', createDrainHandler(destination, request));
|
|
destination.on(
|
|
'error',
|
|
createCancelHandler(
|
|
request,
|
|
'The destination stream errored while writing data.',
|
|
),
|
|
);
|
|
destination.on(
|
|
'close',
|
|
createCancelHandler(request, 'The destination stream closed early.'),
|
|
);
|
|
return destination;
|
|
},
|
|
abort(reason: mixed) {
|
|
abort(request, reason);
|
|
},
|
|
};
|
|
}
|
|
|
|
export {
|
|
renderToPipeableStream,
|
|
resumeToPipeableStream,
|
|
ReactVersion as version,
|
|
};
|