Files
react/packages/react-noop-renderer/src/ReactNoopServer.js
T
Josh StoryandGitHub 113ab9af08 [Flight][Fizz][Fiber] Chain HostDispatcher implementations (#28488)
The idea here is that host dispatchers are not bound to renders so we
need to be able to dispatch to them at any time. This updates the
implementation to chain these dispatchers so that each renderer can
respond to the dispatch. Semantically we don't always want every
renderer to do this for instance if Fizz handles a float method we don't
want Fiber to as well so each dispatcher implementation can decide if it
makes sense to forward the call or not. For float methods server
disaptchers will handle the call if they can resolve a Request otherwise
they will forward. For client dispatchers they will handle the call and
always forward. The choice needs to be made for each dispatcher method
and may have implications on correct renderer import order. For now we
just live with the restriction that if you want to use server and client
together (such as renderToString in the browser) you need to import the
server renderer after the client renderer.
2024-03-04 12:27:15 -08:00

307 lines
7.9 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
*/
/**
* This is a renderer of React that doesn't have a render target output.
* It is useful to demonstrate the internals of the reconciler in isolation
* and for testing semantics of reconciliation separate from the host
* environment.
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import ReactFizzServer from 'react-server';
type Instance = {
type: string,
children: Array<Instance | TextInstance | SuspenseInstance>,
prop: any,
hidden: boolean,
};
type TextInstance = {
text: string,
hidden: boolean,
};
type SuspenseInstance = {
state: 'pending' | 'complete' | 'client-render',
children: Array<Instance | TextInstance | SuspenseInstance>,
};
type Placeholder = {
parent: Instance | SuspenseInstance,
index: number,
};
type Segment = {
children: null | Instance | TextInstance | SuspenseInstance,
};
type Destination = {
root: null | Instance | TextInstance | SuspenseInstance,
placeholders: Map<number, Placeholder>,
segments: Map<number, Segment>,
stack: Array<Segment | Instance | SuspenseInstance>,
};
type RenderState = null;
type HoistableState = null;
const POP = Buffer.from('/', 'utf8');
function write(destination: Destination, buffer: Uint8Array): void {
const stack = destination.stack;
if (buffer === POP) {
stack.pop();
return;
}
// We assume one chunk is one instance.
const instance = JSON.parse(Buffer.from((buffer: any)).toString('utf8'));
if (stack.length === 0) {
destination.root = instance;
} else {
const parent = stack[stack.length - 1];
parent.children.push(instance);
}
stack.push(instance);
}
const ReactNoopServer = ReactFizzServer({
scheduleWork(callback: () => void) {
callback();
},
beginWriting(destination: Destination): void {},
writeChunk(destination: Destination, buffer: Uint8Array): void {
write(destination, buffer);
},
writeChunkAndReturn(destination: Destination, buffer: Uint8Array): boolean {
write(destination, buffer);
return true;
},
completeWriting(destination: Destination): void {},
close(destination: Destination): void {},
closeWithError(destination: Destination, error: mixed): void {},
flushBuffered(destination: Destination): void {},
getChildFormatContext(): null {
return null;
},
resetResumableState(): void {},
completeResumableState(): void {},
pushTextInstance(
target: Array<Uint8Array>,
text: string,
renderState: RenderState,
textEmbedded: boolean,
): boolean {
const textInstance: TextInstance = {
text,
hidden: false,
};
target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP);
return false;
},
pushStartInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): ReactNodeList {
const instance: Instance = {
type: type,
children: [],
prop: props.prop,
hidden: false,
};
target.push(Buffer.from(JSON.stringify(instance), 'utf8'));
return props.children;
},
pushEndInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
target.push(POP);
},
// This is a noop in ReactNoop
pushSegmentFinale(
target: Array<Uint8Array>,
renderState: RenderState,
lastPushedText: boolean,
textEmbedded: boolean,
): void {},
writeCompletedRoot(
destination: Destination,
renderState: RenderState,
): boolean {
return true;
},
writePlaceholder(
destination: Destination,
renderState: RenderState,
id: number,
): boolean {
const parent = destination.stack[destination.stack.length - 1];
destination.placeholders.set(id, {
parent: parent,
index: parent.children.length,
});
},
writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'complete';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeStartPendingSuspenseBoundary(
destination: Destination,
renderState: RenderState,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'pending';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeStartClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'client-render';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeEndCompletedSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
},
writeEndPendingSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
},
writeEndClientRenderedSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
},
writeStartSegment(
destination: Destination,
renderState: RenderState,
formatContext: null,
id: number,
): boolean {
const segment = {
children: [],
};
destination.segments.set(id, segment);
if (destination.stack.length > 0) {
throw new Error('Segments are only expected at the root of the stack.');
}
destination.stack.push(segment);
},
writeEndSegment(destination: Destination, formatContext: null): boolean {
destination.stack.pop();
},
writeCompletedSegmentInstruction(
destination: Destination,
renderState: RenderState,
contentSegmentID: number,
): boolean {
const segment = destination.segments.get(contentSegmentID);
if (!segment) {
throw new Error('Missing segment.');
}
const placeholder = destination.placeholders.get(contentSegmentID);
if (!placeholder) {
throw new Error('Missing placeholder.');
}
placeholder.parent.children.splice(
placeholder.index,
0,
...segment.children,
);
},
writeCompletedBoundaryInstruction(
destination: Destination,
renderState: RenderState,
boundary: SuspenseInstance,
contentSegmentID: number,
): boolean {
const segment = destination.segments.get(contentSegmentID);
if (!segment) {
throw new Error('Missing segment.');
}
boundary.children = segment.children;
boundary.state = 'complete';
},
writeClientRenderBoundaryInstruction(
destination: Destination,
renderState: RenderState,
boundary: SuspenseInstance,
): boolean {
boundary.status = 'client-render';
},
writePreamble() {},
writeHoistables() {},
writeHoistablesForBoundary() {},
writePostamble() {},
hoistHoistables(parent: HoistableState, child: HoistableState) {},
createHoistableState(): HoistableState {
return null;
},
emitEarlyPreloads() {},
});
type Options = {
progressiveChunkSize?: number,
onShellReady?: () => void,
onAllReady?: () => void,
onError?: (error: mixed) => ?string,
};
function render(children: React$Element<any>, options?: Options): Destination {
const destination: Destination = {
root: null,
placeholders: new Map(),
segments: new Map(),
stack: [],
abort() {
ReactNoopServer.abort(request);
},
};
const request = ReactNoopServer.createRequest(
children,
null,
null,
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onAllReady : undefined,
options ? options.onShellReady : undefined,
);
ReactNoopServer.startWork(request);
ReactNoopServer.startFlowing(request, destination);
return destination;
}
export {render};