mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
c1ac052158
* Shim support for more element types * Shim commonly used Hooks that are safe * Flow * Oopsie
452 lines
12 KiB
JavaScript
452 lines
12 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
|
|
import type {
|
|
Destination,
|
|
Chunk,
|
|
BundlerConfig,
|
|
ModuleMetaData,
|
|
ModuleReference,
|
|
} from './ReactFlightServerConfig';
|
|
|
|
import {
|
|
scheduleWork,
|
|
beginWriting,
|
|
writeChunk,
|
|
completeWriting,
|
|
flushBuffered,
|
|
close,
|
|
processModelChunk,
|
|
processErrorChunk,
|
|
resolveModuleMetaData,
|
|
} from './ReactFlightServerConfig';
|
|
|
|
import {
|
|
REACT_BLOCK_TYPE,
|
|
REACT_ELEMENT_TYPE,
|
|
REACT_DEBUG_TRACING_MODE_TYPE,
|
|
REACT_FORWARD_REF_TYPE,
|
|
REACT_FRAGMENT_TYPE,
|
|
REACT_LAZY_TYPE,
|
|
REACT_LEGACY_HIDDEN_TYPE,
|
|
REACT_MEMO_TYPE,
|
|
REACT_OFFSCREEN_TYPE,
|
|
REACT_PROFILER_TYPE,
|
|
REACT_SCOPE_TYPE,
|
|
REACT_SERVER_BLOCK_TYPE,
|
|
REACT_STRICT_MODE_TYPE,
|
|
REACT_SUSPENSE_TYPE,
|
|
REACT_SUSPENSE_LIST_TYPE,
|
|
} from 'shared/ReactSymbols';
|
|
|
|
import * as React from 'react';
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
import invariant from 'shared/invariant';
|
|
|
|
type ReactJSONValue =
|
|
| string
|
|
| boolean
|
|
| number
|
|
| null
|
|
| $ReadOnlyArray<ReactJSONValue>
|
|
| ReactModelObject;
|
|
|
|
export type ReactModel =
|
|
| React$Element<any>
|
|
| string
|
|
| boolean
|
|
| number
|
|
| null
|
|
| Iterable<ReactModel>
|
|
| ReactModelObject;
|
|
|
|
type ReactModelObject = {+[key: string]: ReactModel};
|
|
|
|
type Segment = {
|
|
id: number,
|
|
query: () => ReactModel,
|
|
ping: () => void,
|
|
};
|
|
|
|
export type Request = {
|
|
destination: Destination,
|
|
bundlerConfig: BundlerConfig,
|
|
nextChunkId: number,
|
|
pendingChunks: number,
|
|
pingedSegments: Array<Segment>,
|
|
completedJSONChunks: Array<Chunk>,
|
|
completedErrorChunks: Array<Chunk>,
|
|
flowing: boolean,
|
|
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
|
|
};
|
|
|
|
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
|
|
|
export function createRequest(
|
|
model: ReactModel,
|
|
destination: Destination,
|
|
bundlerConfig: BundlerConfig,
|
|
): Request {
|
|
const pingedSegments = [];
|
|
const request = {
|
|
destination,
|
|
bundlerConfig,
|
|
nextChunkId: 0,
|
|
pendingChunks: 0,
|
|
pingedSegments: pingedSegments,
|
|
completedJSONChunks: [],
|
|
completedErrorChunks: [],
|
|
flowing: false,
|
|
toJSON: function(key: string, value: ReactModel): ReactJSONValue {
|
|
return resolveModelToJSON(request, this, key, value);
|
|
},
|
|
};
|
|
request.pendingChunks++;
|
|
const rootSegment = createSegment(request, () => model);
|
|
pingedSegments.push(rootSegment);
|
|
return request;
|
|
}
|
|
|
|
function attemptResolveElement(element: React$Element<any>): ReactModel {
|
|
const type = element.type;
|
|
const props = element.props;
|
|
if (typeof type === 'function') {
|
|
// This is a server-side component.
|
|
return type(props);
|
|
} else if (typeof type === 'string') {
|
|
// This is a host element. E.g. HTML.
|
|
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
|
|
} else if (type[0] === REACT_SERVER_BLOCK_TYPE) {
|
|
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
|
|
} else if (
|
|
type === REACT_FRAGMENT_TYPE ||
|
|
type === REACT_STRICT_MODE_TYPE ||
|
|
type === REACT_PROFILER_TYPE ||
|
|
type === REACT_SCOPE_TYPE ||
|
|
type === REACT_DEBUG_TRACING_MODE_TYPE ||
|
|
type === REACT_LEGACY_HIDDEN_TYPE ||
|
|
type === REACT_OFFSCREEN_TYPE ||
|
|
// TODO: These are temporary shims
|
|
// and we'll want a different behavior.
|
|
type === REACT_SUSPENSE_TYPE ||
|
|
type === REACT_SUSPENSE_LIST_TYPE
|
|
) {
|
|
return element.props.children;
|
|
} else if (type != null && typeof type === 'object') {
|
|
switch (type.$$typeof) {
|
|
case REACT_FORWARD_REF_TYPE: {
|
|
const render = type.render;
|
|
return render(props, undefined);
|
|
}
|
|
case REACT_MEMO_TYPE: {
|
|
const nextChildren = React.createElement(type.type, element.props);
|
|
return attemptResolveElement(nextChildren);
|
|
}
|
|
}
|
|
}
|
|
invariant(false, 'Unsupported type.');
|
|
}
|
|
|
|
function pingSegment(request: Request, segment: Segment): void {
|
|
const pingedSegments = request.pingedSegments;
|
|
pingedSegments.push(segment);
|
|
if (pingedSegments.length === 1) {
|
|
scheduleWork(() => performWork(request));
|
|
}
|
|
}
|
|
|
|
function createSegment(request: Request, query: () => ReactModel): Segment {
|
|
const id = request.nextChunkId++;
|
|
const segment = {
|
|
id,
|
|
query,
|
|
ping: () => pingSegment(request, segment),
|
|
};
|
|
return segment;
|
|
}
|
|
|
|
function serializeIDRef(id: number): string {
|
|
return '$' + id.toString(16);
|
|
}
|
|
|
|
function escapeStringValue(value: string): string {
|
|
if (value[0] === '$' || value[0] === '@') {
|
|
// We need to escape $ or @ prefixed strings since we use those to encode
|
|
// references to IDs and as special symbol values.
|
|
return '$' + value;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export function resolveModelToJSON(
|
|
request: Request,
|
|
parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray<ReactModel>,
|
|
key: string,
|
|
value: ReactModel,
|
|
): ReactJSONValue {
|
|
// Special Symbols
|
|
switch (value) {
|
|
case REACT_ELEMENT_TYPE:
|
|
return '$';
|
|
case REACT_SERVER_BLOCK_TYPE:
|
|
return '@';
|
|
case REACT_LAZY_TYPE:
|
|
case REACT_BLOCK_TYPE:
|
|
invariant(
|
|
false,
|
|
'React Blocks (and Lazy Components) are expected to be replaced by a ' +
|
|
'compiler on the server. Try configuring your compiler set up and avoid ' +
|
|
'using React.lazy inside of Blocks.',
|
|
);
|
|
}
|
|
|
|
if (parent[0] === REACT_SERVER_BLOCK_TYPE) {
|
|
// We're currently encoding part of a Block. Look up which key.
|
|
switch (key) {
|
|
case '1': {
|
|
// Module reference
|
|
const moduleReference: ModuleReference<any> = (value: any);
|
|
try {
|
|
const moduleMetaData: ModuleMetaData = resolveModuleMetaData(
|
|
request.bundlerConfig,
|
|
moduleReference,
|
|
);
|
|
return (moduleMetaData: ReactJSONValue);
|
|
} catch (x) {
|
|
request.pendingChunks++;
|
|
const errorId = request.nextChunkId++;
|
|
emitErrorChunk(request, errorId, x);
|
|
return serializeIDRef(errorId);
|
|
}
|
|
}
|
|
case '2': {
|
|
// Load function
|
|
const load: () => ReactModel = (value: any);
|
|
try {
|
|
// Attempt to resolve the data.
|
|
return load();
|
|
} catch (x) {
|
|
if (
|
|
typeof x === 'object' &&
|
|
x !== null &&
|
|
typeof x.then === 'function'
|
|
) {
|
|
// Something suspended, we'll need to create a new segment and resolve it later.
|
|
request.pendingChunks++;
|
|
const newSegment = createSegment(request, load);
|
|
const ping = newSegment.ping;
|
|
x.then(ping, ping);
|
|
return serializeIDRef(newSegment.id);
|
|
} else {
|
|
// This load failed, encode the error as a separate row and reference that.
|
|
request.pendingChunks++;
|
|
const errorId = request.nextChunkId++;
|
|
emitErrorChunk(request, errorId, x);
|
|
return serializeIDRef(errorId);
|
|
}
|
|
}
|
|
}
|
|
default: {
|
|
invariant(
|
|
false,
|
|
'A server block should never encode any other slots. This is a bug in React.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
return escapeStringValue(value);
|
|
}
|
|
|
|
// Resolve server components.
|
|
while (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
value.$$typeof === REACT_ELEMENT_TYPE
|
|
) {
|
|
const prevDispatcher = ReactCurrentDispatcher.current;
|
|
// TODO: Concatenate keys of parents onto children.
|
|
const element: React$Element<any> = (value: any);
|
|
try {
|
|
ReactCurrentDispatcher.current = Dispatcher;
|
|
// Attempt to render the server component.
|
|
value = attemptResolveElement(element);
|
|
} catch (x) {
|
|
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
|
|
// Something suspended, we'll need to create a new segment and resolve it later.
|
|
request.pendingChunks++;
|
|
const newSegment = createSegment(request, () => value);
|
|
const ping = newSegment.ping;
|
|
x.then(ping, ping);
|
|
return serializeIDRef(newSegment.id);
|
|
} else {
|
|
// Something errored. Don't bother encoding anything up to here.
|
|
throw x;
|
|
}
|
|
} finally {
|
|
ReactCurrentDispatcher.current = prevDispatcher;
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function emitErrorChunk(request: Request, id: number, error: mixed): void {
|
|
// TODO: We should not leak error messages to the client in prod.
|
|
// Give this an error code instead and log on the server.
|
|
// We can serialize the error in DEV as a convenience.
|
|
let message;
|
|
let stack = '';
|
|
try {
|
|
if (error instanceof Error) {
|
|
message = '' + error.message;
|
|
stack = '' + error.stack;
|
|
} else {
|
|
message = 'Error: ' + (error: any);
|
|
}
|
|
} catch (x) {
|
|
message = 'An error occurred but serializing the error message failed.';
|
|
}
|
|
|
|
const processedChunk = processErrorChunk(request, id, message, stack);
|
|
request.completedErrorChunks.push(processedChunk);
|
|
}
|
|
|
|
function retrySegment(request: Request, segment: Segment): void {
|
|
const query = segment.query;
|
|
let value;
|
|
try {
|
|
value = query();
|
|
while (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
value.$$typeof === REACT_ELEMENT_TYPE
|
|
) {
|
|
// TODO: Concatenate keys of parents onto children.
|
|
const element: React$Element<any> = (value: any);
|
|
// Attempt to render the server component.
|
|
// Doing this here lets us reuse this same segment if the next component
|
|
// also suspends.
|
|
segment.query = () => value;
|
|
value = attemptResolveElement(element);
|
|
}
|
|
const processedChunk = processModelChunk(request, segment.id, value);
|
|
request.completedJSONChunks.push(processedChunk);
|
|
} catch (x) {
|
|
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
|
|
// Something suspended again, let's pick it back up later.
|
|
const ping = segment.ping;
|
|
x.then(ping, ping);
|
|
return;
|
|
} else {
|
|
// This errored, we need to serialize this error to the
|
|
emitErrorChunk(request, segment.id, x);
|
|
}
|
|
}
|
|
}
|
|
|
|
function performWork(request: Request): void {
|
|
const pingedSegments = request.pingedSegments;
|
|
request.pingedSegments = [];
|
|
for (let i = 0; i < pingedSegments.length; i++) {
|
|
const segment = pingedSegments[i];
|
|
retrySegment(request, segment);
|
|
}
|
|
if (request.flowing) {
|
|
flushCompletedChunks(request);
|
|
}
|
|
}
|
|
|
|
let reentrant = false;
|
|
function flushCompletedChunks(request: Request): void {
|
|
if (reentrant) {
|
|
return;
|
|
}
|
|
reentrant = true;
|
|
const destination = request.destination;
|
|
beginWriting(destination);
|
|
try {
|
|
const jsonChunks = request.completedJSONChunks;
|
|
let i = 0;
|
|
for (; i < jsonChunks.length; i++) {
|
|
request.pendingChunks--;
|
|
const chunk = jsonChunks[i];
|
|
if (!writeChunk(destination, chunk)) {
|
|
request.flowing = false;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
jsonChunks.splice(0, i);
|
|
const errorChunks = request.completedErrorChunks;
|
|
i = 0;
|
|
for (; i < errorChunks.length; i++) {
|
|
request.pendingChunks--;
|
|
const chunk = errorChunks[i];
|
|
if (!writeChunk(destination, chunk)) {
|
|
request.flowing = false;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
errorChunks.splice(0, i);
|
|
} finally {
|
|
reentrant = false;
|
|
completeWriting(destination);
|
|
}
|
|
flushBuffered(destination);
|
|
if (request.pendingChunks === 0) {
|
|
// We're done.
|
|
close(destination);
|
|
}
|
|
}
|
|
|
|
export function startWork(request: Request): void {
|
|
request.flowing = true;
|
|
scheduleWork(() => performWork(request));
|
|
}
|
|
|
|
export function startFlowing(request: Request): void {
|
|
request.flowing = true;
|
|
flushCompletedChunks(request);
|
|
}
|
|
|
|
function unsupportedHook(): void {
|
|
invariant(false, 'This Hook is not supported in Server Components.');
|
|
}
|
|
|
|
const Dispatcher: DispatcherType = {
|
|
useMemo<T>(nextCreate: () => T): T {
|
|
return nextCreate();
|
|
},
|
|
useCallback<T>(callback: T): T {
|
|
return callback;
|
|
},
|
|
useDebugValue(): void {},
|
|
useDeferredValue<T>(value: T): T {
|
|
return value;
|
|
},
|
|
useTransition(): [(callback: () => void) => void, boolean] {
|
|
return [() => {}, false];
|
|
},
|
|
readContext: (unsupportedHook: any),
|
|
useContext: (unsupportedHook: any),
|
|
useReducer: (unsupportedHook: any),
|
|
useRef: (unsupportedHook: any),
|
|
useState: (unsupportedHook: any),
|
|
useLayoutEffect: (unsupportedHook: any),
|
|
useImperativeHandle: (unsupportedHook: any),
|
|
useEffect: (unsupportedHook: any),
|
|
useOpaqueIdentifier: (unsupportedHook: any),
|
|
useMutableSource: (unsupportedHook: any),
|
|
};
|