/**
* 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 BoundaryResources = 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 {},
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() {},
writePostamble() {},
createBoundaryResources(): BoundaryResources {
return null;
},
setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
prepareHostDispatcher() {},
});
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};