mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
FB-specific builds of Flight Server, Flight Client, and React Shared Subset (#27579)
This PR adds a new FB-specific configuration of Flight. We also need to bundle a version of ReactSharedSubset that will be used for running Flight on the server. This initial implementation does not support server actions yet. The FB-Flight still uses the text protocol on the server (the flag `enableBinaryFlight` is set to false). It looks like we need some changes in Hermes to properly support this binary format.
This commit is contained in:
parent
6c7b41da3d
commit
c17a27ef49
|
|
@ -327,6 +327,7 @@ module.exports = {
|
|||
'packages/react-server-dom-esm/**/*.js',
|
||||
'packages/react-server-dom-webpack/**/*.js',
|
||||
'packages/react-server-dom-turbopack/**/*.js',
|
||||
'packages/react-server-dom-fb/**/*.js',
|
||||
'packages/react-test-renderer/**/*.js',
|
||||
'packages/react-debug-tools/**/*.js',
|
||||
'packages/react-devtools-extensions/**/*.js',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export * from 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler';
|
||||
|
||||
export const usedWithSSR = false;
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
// This client file is in the shared folder because it applies to both SSR and browser contexts.
|
||||
// It is the configuraiton of the FlightClient behavior which can run in either environment.
|
||||
// It is the configuration of the FlightClient behavior which can run in either environment.
|
||||
|
||||
import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM';
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ export function dispatchHint<Code: HintCode>(
|
|||
}
|
||||
}
|
||||
|
||||
// Flow is having troulbe refining the HintModels so we help it a bit.
|
||||
// Flow is having trouble refining the HintModels so we help it a bit.
|
||||
// This should be compiled out in the production build.
|
||||
function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
|
||||
return model;
|
||||
|
|
|
|||
112
packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js
vendored
Normal file
112
packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* 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 {
|
||||
Thenable,
|
||||
FulfilledThenable,
|
||||
RejectedThenable,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
export type ModuleLoading = mixed;
|
||||
|
||||
type ResolveClientReferenceFn<T> =
|
||||
ClientReferenceMetadata => ClientReference<T>;
|
||||
|
||||
export type SSRModuleMap = {
|
||||
resolveClientReference?: ResolveClientReferenceFn<any>,
|
||||
};
|
||||
export type ServerManifest = string;
|
||||
export type {
|
||||
ClientManifest,
|
||||
ServerReferenceId,
|
||||
ClientReferenceMetadata,
|
||||
} from './ReactFlightReferencesFB';
|
||||
|
||||
import type {
|
||||
ServerReferenceId,
|
||||
ClientReferenceMetadata,
|
||||
} from './ReactFlightReferencesFB';
|
||||
|
||||
export type ClientReference<T> = {
|
||||
getModuleId: () => string,
|
||||
load: () => Thenable<T>,
|
||||
};
|
||||
|
||||
export function prepareDestinationForModule(
|
||||
moduleLoading: ModuleLoading,
|
||||
nonce: ?string,
|
||||
metadata: ClientReferenceMetadata,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
export function resolveClientReference<T>(
|
||||
moduleMap: SSRModuleMap,
|
||||
metadata: ClientReferenceMetadata,
|
||||
): ClientReference<T> {
|
||||
if (typeof moduleMap.resolveClientReference === 'function') {
|
||||
return moduleMap.resolveClientReference(metadata);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Expected `resolveClientReference` to be defined on the moduleMap.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveServerReference<T>(
|
||||
config: ServerManifest,
|
||||
id: ServerReferenceId,
|
||||
): ClientReference<T> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
const asyncModuleCache: Map<string, Thenable<any>> = new Map();
|
||||
|
||||
export function preloadModule<T>(
|
||||
clientReference: ClientReference<T>,
|
||||
): null | Thenable<any> {
|
||||
const existingPromise = asyncModuleCache.get(clientReference.getModuleId());
|
||||
if (existingPromise) {
|
||||
if (existingPromise.status === 'fulfilled') {
|
||||
return null;
|
||||
}
|
||||
return existingPromise;
|
||||
} else {
|
||||
const modulePromise: Thenable<T> = clientReference.load();
|
||||
modulePromise.then(
|
||||
value => {
|
||||
const fulfilledThenable: FulfilledThenable<mixed> =
|
||||
(modulePromise: any);
|
||||
fulfilledThenable.status = 'fulfilled';
|
||||
fulfilledThenable.value = value;
|
||||
},
|
||||
reason => {
|
||||
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
|
||||
rejectedThenable.status = 'rejected';
|
||||
rejectedThenable.reason = reason;
|
||||
},
|
||||
);
|
||||
asyncModuleCache.set(clientReference.getModuleId(), modulePromise);
|
||||
return modulePromise;
|
||||
}
|
||||
}
|
||||
|
||||
export function requireModule<T>(clientReference: ClientReference<T>): T {
|
||||
let module;
|
||||
// We assume that preloadModule has been called before, which
|
||||
// should have added something to the module cache.
|
||||
const promise: any = asyncModuleCache.get(clientReference.getModuleId());
|
||||
if (promise.status === 'fulfilled') {
|
||||
module = promise.value;
|
||||
} else {
|
||||
throw promise.reason;
|
||||
}
|
||||
// We are currently only support default exports for client components
|
||||
return module;
|
||||
}
|
||||
91
packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js
vendored
Normal file
91
packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* 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 {enableBinaryFlight} from 'shared/ReactFeatureFlags';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import {
|
||||
createResponse,
|
||||
getRoot,
|
||||
reportGlobalError,
|
||||
processBinaryChunk,
|
||||
close,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {SSRModuleMap} from './ReactFlightClientConfigFBBundler';
|
||||
|
||||
type Options = {
|
||||
moduleMap: SSRModuleMap,
|
||||
};
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
const moduleMap = options && options.moduleMap;
|
||||
if (moduleMap == null) {
|
||||
throw new Error('Expected `moduleMap` to be defined.');
|
||||
}
|
||||
|
||||
return createResponse(moduleMap, null, undefined, undefined);
|
||||
}
|
||||
|
||||
function processChunk(response: FlightResponse, chunk: string | Uint8Array) {
|
||||
if (enableBinaryFlight) {
|
||||
if (typeof chunk === 'string') {
|
||||
throw new Error(
|
||||
'`enableBinaryFlight` flag is enabled, expected a Uint8Array as input, got string.',
|
||||
);
|
||||
}
|
||||
}
|
||||
const buffer = typeof chunk !== 'string' ? chunk : encodeString(chunk);
|
||||
|
||||
processBinaryChunk(response, buffer);
|
||||
}
|
||||
|
||||
function encodeString(string: string) {
|
||||
const textEncoder = new TextEncoder();
|
||||
return textEncoder.encode(string);
|
||||
}
|
||||
|
||||
function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
if (done) {
|
||||
close(response);
|
||||
return;
|
||||
}
|
||||
const buffer: Uint8Array = (value: any);
|
||||
processChunk(response, buffer);
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
reportGlobalError(response, e);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function createFromReadableStream<T>(
|
||||
stream: ReadableStream,
|
||||
options?: Options,
|
||||
): Thenable<T> {
|
||||
const response: FlightResponse = createResponseFromOptions(options);
|
||||
startReadingFromStream(response, stream);
|
||||
return getRoot(response);
|
||||
}
|
||||
|
||||
export {createFromReadableStream};
|
||||
68
packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js
vendored
Normal file
68
packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* 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 {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Destination,
|
||||
Chunk,
|
||||
PrecomputedChunk,
|
||||
} from 'react-server/src/ReactServerStreamConfig';
|
||||
import type {ClientManifest} from './ReactFlightReferencesFB';
|
||||
|
||||
import {
|
||||
createRequest,
|
||||
startWork,
|
||||
startFlowing,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {setByteLengthOfChunkImplementation} from 'react-server/src/ReactServerStreamConfig';
|
||||
|
||||
export {
|
||||
registerClientReference,
|
||||
registerServerReference,
|
||||
getRequestedClientReferencesKeys,
|
||||
clearRequestedClientReferencesKeysSet,
|
||||
} from './ReactFlightReferencesFB';
|
||||
|
||||
type Options = {
|
||||
onError?: (error: mixed) => void,
|
||||
};
|
||||
|
||||
function renderToDestination(
|
||||
destination: Destination,
|
||||
model: ReactClientValue,
|
||||
bundlerConfig: ClientManifest,
|
||||
options?: Options,
|
||||
): void {
|
||||
if (!configured) {
|
||||
throw new Error(
|
||||
'Please make sure to call `setConfig(...)` before calling `renderToDestination`.',
|
||||
);
|
||||
}
|
||||
const request = createRequest(
|
||||
model,
|
||||
bundlerConfig,
|
||||
options ? options.onError : undefined,
|
||||
);
|
||||
startWork(request);
|
||||
startFlowing(request, destination);
|
||||
}
|
||||
|
||||
type Config = {
|
||||
byteLength: (chunk: Chunk | PrecomputedChunk) => number,
|
||||
};
|
||||
|
||||
let configured = false;
|
||||
|
||||
function setConfig(config: Config): void {
|
||||
setByteLengthOfChunkImplementation(config.byteLength);
|
||||
configured = true;
|
||||
}
|
||||
|
||||
export {renderToDestination, setConfig};
|
||||
98
packages/react-server-dom-fb/src/ReactFlightReferencesFB.js
vendored
Normal file
98
packages/react-server-dom-fb/src/ReactFlightReferencesFB.js
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export opaque type ClientManifest = mixed;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export type ServerReference<T> = string;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export type ClientReference<T> = string;
|
||||
|
||||
const registeredClientReferences = new Map<mixed, ClientReferenceMetadata>();
|
||||
const requestedClientReferencesKeys = new Set<ClientReferenceKey>();
|
||||
|
||||
export type ClientReferenceKey = string;
|
||||
export type ClientReferenceMetadata = {
|
||||
moduleId: ClientReferenceKey,
|
||||
exportName: string,
|
||||
};
|
||||
|
||||
export type ServerReferenceId = string;
|
||||
|
||||
export function registerClientReference<T>(
|
||||
clientReference: ClientReference<T>,
|
||||
moduleId: ClientReferenceKey,
|
||||
): ClientReference<T> {
|
||||
const exportName = 'default'; // Currently, we only support modules with `default` export
|
||||
registeredClientReferences.set(clientReference, {
|
||||
moduleId,
|
||||
exportName,
|
||||
});
|
||||
|
||||
return clientReference;
|
||||
}
|
||||
|
||||
export function isClientReference<T>(reference: T): boolean {
|
||||
return registeredClientReferences.has(reference);
|
||||
}
|
||||
|
||||
export function getClientReferenceKey<T>(
|
||||
clientReference: ClientReference<T>,
|
||||
): ClientReferenceKey {
|
||||
const reference = registeredClientReferences.get(clientReference);
|
||||
if (reference != null) {
|
||||
requestedClientReferencesKeys.add(reference.moduleId);
|
||||
return reference.moduleId;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Expected client reference ' + clientReference + ' to be registered.',
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveClientReferenceMetadata<T>(
|
||||
config: ClientManifest,
|
||||
clientReference: ClientReference<T>,
|
||||
): ClientReferenceMetadata {
|
||||
const metadata = registeredClientReferences.get(clientReference);
|
||||
if (metadata != null) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Expected client reference ' + clientReference + ' to be registered.',
|
||||
);
|
||||
}
|
||||
|
||||
export function registerServerReference<T>(
|
||||
serverReference: ServerReference<T>,
|
||||
exportName: string,
|
||||
): ServerReference<T> {
|
||||
throw new Error('registerServerReference: Not Implemented.');
|
||||
}
|
||||
|
||||
export function isServerReference<T>(reference: T): boolean {
|
||||
throw new Error('isServerReference: Not Implemented.');
|
||||
}
|
||||
|
||||
export function getServerReferenceId<T>(
|
||||
config: ClientManifest,
|
||||
serverReference: ServerReference<T>,
|
||||
): ServerReferenceId {
|
||||
throw new Error('getServerReferenceId: Not Implemented.');
|
||||
}
|
||||
|
||||
export function getRequestedClientReferencesKeys(): $ReadOnlyArray<ClientReferenceKey> {
|
||||
return Array.from(requestedClientReferencesKeys);
|
||||
}
|
||||
|
||||
export function clearRequestedClientReferencesKeysSet(): void {
|
||||
requestedClientReferencesKeys.clear();
|
||||
}
|
||||
36
packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js
vendored
Normal file
36
packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import type {
|
||||
ClientManifest,
|
||||
ClientReference,
|
||||
ServerReference,
|
||||
} from './ReactFlightReferencesFB';
|
||||
|
||||
export type {ClientManifest, ClientReference, ServerReference};
|
||||
|
||||
export {
|
||||
ClientReferenceKey,
|
||||
ClientReferenceMetadata,
|
||||
getClientReferenceKey,
|
||||
isClientReference,
|
||||
resolveClientReferenceMetadata,
|
||||
isServerReference,
|
||||
ServerReferenceId,
|
||||
getServerReferenceId,
|
||||
} from './ReactFlightReferencesFB';
|
||||
|
||||
export function getServerReferenceBoundArguments<T>(
|
||||
config: ClientManifest,
|
||||
serverReference: ServerReference<T>,
|
||||
): null | Array<ReactClientValue> {
|
||||
throw new Error('getServerReferenceBoundArguments: Not Implemented.');
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
// Don't wait before processing work on the server.
|
||||
// TODO: we can replace this with FlightServer.act().
|
||||
global.setImmediate = cb => cb();
|
||||
|
||||
let act;
|
||||
let use;
|
||||
let clientExports;
|
||||
let moduleMap;
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
let Suspense;
|
||||
let registerClientReference;
|
||||
|
||||
class Destination {
|
||||
#buffer = '';
|
||||
#controller = null;
|
||||
constructor() {
|
||||
const self = this;
|
||||
this.stream = new ReadableStream({
|
||||
start(controller) {
|
||||
self.#controller = controller;
|
||||
},
|
||||
});
|
||||
}
|
||||
write(chunk) {
|
||||
this.#buffer += chunk;
|
||||
}
|
||||
beginWriting() {}
|
||||
completeWriting() {}
|
||||
flushBuffered() {
|
||||
if (!this.#controller) {
|
||||
throw new Error('Expected a controller.');
|
||||
}
|
||||
this.#controller.enqueue(this.#buffer);
|
||||
this.#buffer = '';
|
||||
}
|
||||
close() {}
|
||||
onError() {}
|
||||
}
|
||||
|
||||
describe('ReactFlightDOM for FB', () => {
|
||||
beforeEach(() => {
|
||||
// For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
|
||||
// This can be thought of as essentially being the React Server Components scope with react-server
|
||||
// condition
|
||||
jest.resetModules();
|
||||
registerClientReference =
|
||||
require('../ReactFlightReferencesFB').registerClientReference;
|
||||
|
||||
jest.mock('react', () => require('react/src/ReactSharedSubsetFB'));
|
||||
|
||||
jest.mock('shared/ReactFeatureFlags', () => {
|
||||
jest.mock(
|
||||
'ReactFeatureFlags',
|
||||
() => jest.requireActual('shared/forks/ReactFeatureFlags.www-dynamic'),
|
||||
{virtual: true},
|
||||
);
|
||||
return jest.requireActual('shared/forks/ReactFeatureFlags.www');
|
||||
});
|
||||
|
||||
clientExports = value => {
|
||||
registerClientReference(value, value.name);
|
||||
return value;
|
||||
};
|
||||
|
||||
moduleMap = {
|
||||
resolveClientReference(metadata) {
|
||||
throw new Error('Do not expect to load client components.');
|
||||
},
|
||||
};
|
||||
|
||||
ReactServerDOMServer = require('../ReactFlightDOMServerFB');
|
||||
ReactServerDOMServer.setConfig({
|
||||
byteLength: str => Buffer.byteLength(str),
|
||||
});
|
||||
|
||||
// This reset is to load modules for the SSR/Browser scope.
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
act = require('internal-test-utils').act;
|
||||
React = require('react');
|
||||
use = React.use;
|
||||
Suspense = React.Suspense;
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
ReactServerDOMClient = require('../ReactFlightDOMClientFB');
|
||||
});
|
||||
|
||||
it('should resolve HTML with renderToDestination', async () => {
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function HTML() {
|
||||
return (
|
||||
<div>
|
||||
<Text>hello</Text>
|
||||
<Text>world</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const model = {
|
||||
html: <HTML />,
|
||||
};
|
||||
return model;
|
||||
}
|
||||
const destination = new Destination();
|
||||
ReactServerDOMServer.renderToDestination(destination, <App />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(
|
||||
destination.stream,
|
||||
{
|
||||
moduleMap,
|
||||
},
|
||||
);
|
||||
const model = await response;
|
||||
expect(model).toEqual({
|
||||
html: (
|
||||
<div>
|
||||
<span>hello</span>
|
||||
<span>world</span>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve the root', async () => {
|
||||
// Model
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function HTML() {
|
||||
return (
|
||||
<div>
|
||||
<Text>hello</Text>
|
||||
<Text>world</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function RootModel() {
|
||||
return {
|
||||
html: <HTML />,
|
||||
};
|
||||
}
|
||||
|
||||
// View
|
||||
function Message({response}) {
|
||||
return <section>{use(response).html}</section>;
|
||||
}
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Message response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const destination = new Destination();
|
||||
ReactServerDOMServer.renderToDestination(destination, <RootModel />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(
|
||||
destination.stream,
|
||||
{
|
||||
moduleMap,
|
||||
},
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe(
|
||||
'<section><div><span>hello</span><span>world</span></div></section>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not get confused by $', async () => {
|
||||
// Model
|
||||
function RootModel() {
|
||||
return {text: '$1'};
|
||||
}
|
||||
|
||||
// View
|
||||
function Message({response}) {
|
||||
return <p>{use(response).text}</p>;
|
||||
}
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Message response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
const destination = new Destination();
|
||||
ReactServerDOMServer.renderToDestination(destination, <RootModel />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(
|
||||
destination.stream,
|
||||
{
|
||||
moduleMap,
|
||||
},
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe('<p>$1</p>');
|
||||
});
|
||||
|
||||
it('should not get confused by @', async () => {
|
||||
// Model
|
||||
function RootModel() {
|
||||
return {text: '@div'};
|
||||
}
|
||||
|
||||
// View
|
||||
function Message({response}) {
|
||||
return <p>{use(response).text}</p>;
|
||||
}
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Message response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
const destination = new Destination();
|
||||
ReactServerDOMServer.renderToDestination(destination, <RootModel />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(
|
||||
destination.stream,
|
||||
{
|
||||
moduleMap,
|
||||
},
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe('<p>@div</p>');
|
||||
});
|
||||
|
||||
it('should be able to render a client component', async () => {
|
||||
const Component = function ({greeting}) {
|
||||
return greeting + ' World';
|
||||
};
|
||||
|
||||
function Print({response}) {
|
||||
return <p>{use(response)}</p>;
|
||||
}
|
||||
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Print response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const ClientComponent = clientExports(Component);
|
||||
|
||||
const destination = new Destination();
|
||||
ReactServerDOMServer.renderToDestination(
|
||||
destination,
|
||||
<ClientComponent greeting={'Hello'} />,
|
||||
moduleMap,
|
||||
);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(
|
||||
destination.stream,
|
||||
{
|
||||
moduleMap: {
|
||||
resolveClientReference(metadata) {
|
||||
return {
|
||||
getModuleId() {
|
||||
return metadata.moduleId;
|
||||
},
|
||||
load() {
|
||||
return Promise.resolve(Component);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe('<p>Hello World</p>');
|
||||
});
|
||||
|
||||
it('should render long strings', async () => {
|
||||
// Model
|
||||
const longString = 'Lorem Ipsum ❤️ '.repeat(100);
|
||||
|
||||
function RootModel() {
|
||||
return {text: longString};
|
||||
}
|
||||
|
||||
// View
|
||||
function Message({response}) {
|
||||
return <p>{use(response).text}</p>;
|
||||
}
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Message response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
const destination = new Destination();
|
||||
ReactServerDOMServer.renderToDestination(destination, <RootModel />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(
|
||||
destination.stream,
|
||||
{
|
||||
moduleMap,
|
||||
},
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe('<p>' + longString + '</p>');
|
||||
});
|
||||
|
||||
// TODO: `registerClientComponent` need to be able to support this
|
||||
it.skip('throws when accessing a member below the client exports', () => {
|
||||
const ClientModule = clientExports({
|
||||
Component: {deep: 'thing'},
|
||||
});
|
||||
function dotting() {
|
||||
return ClientModule.Component.deep;
|
||||
}
|
||||
expect(dotting).toThrowError(
|
||||
'Cannot access Component.deep on the server. ' +
|
||||
'You cannot dot into a client module from a server component. ' +
|
||||
'You can only pass the imported name through.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -18,10 +18,6 @@ export opaque type PrecomputedChunk = string;
|
|||
export opaque type Chunk = string;
|
||||
export opaque type BinaryChunk = string;
|
||||
|
||||
export function scheduleWork(callback: () => void) {
|
||||
// We don't schedule work in this model, and instead expect performWork to always be called repeatedly.
|
||||
}
|
||||
|
||||
export function flushBuffered(destination: Destination) {}
|
||||
|
||||
export const supportsRequestStorage = false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* 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} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
export * from 'react-server-dom-fb/src/ReactFlightServerConfigFBBundler';
|
||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||
|
||||
export const supportsRequestStorage = false;
|
||||
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from '../ReactServerStreamConfigFB';
|
||||
|
||||
import type {
|
||||
PrecomputedChunk,
|
||||
Chunk,
|
||||
BinaryChunk,
|
||||
} from '../ReactServerStreamConfigFB';
|
||||
|
||||
let byteLengthImpl: null | ((chunk: Chunk | PrecomputedChunk) => number) = null;
|
||||
|
||||
export function setByteLengthOfChunkImplementation(
|
||||
impl: (chunk: Chunk | PrecomputedChunk) => number,
|
||||
): void {
|
||||
byteLengthImpl = impl;
|
||||
}
|
||||
|
||||
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
|
||||
if (byteLengthImpl == null) {
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);',
|
||||
);
|
||||
}
|
||||
return byteLengthImpl(chunk);
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
beginWriting(): void;
|
||||
write(chunk: Chunk | PrecomputedChunk | BinaryChunk): void;
|
||||
completeWriting(): void;
|
||||
flushBuffered(): void;
|
||||
close(): void;
|
||||
onError(error: mixed): void;
|
||||
}
|
||||
|
||||
export function scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
}
|
||||
|
||||
export function beginWriting(destination: Destination) {
|
||||
destination.beginWriting();
|
||||
}
|
||||
|
||||
export function writeChunk(
|
||||
destination: Destination,
|
||||
chunk: Chunk | PrecomputedChunk | BinaryChunk,
|
||||
): void {
|
||||
destination.write(chunk);
|
||||
}
|
||||
|
||||
export function writeChunkAndReturn(
|
||||
destination: Destination,
|
||||
chunk: Chunk | PrecomputedChunk | BinaryChunk,
|
||||
): boolean {
|
||||
destination.write(chunk);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function completeWriting(destination: Destination) {
|
||||
destination.completeWriting();
|
||||
}
|
||||
|
||||
export function flushBuffered(destination: Destination) {
|
||||
destination.flushBuffered();
|
||||
}
|
||||
|
||||
export function close(destination: Destination) {
|
||||
destination.close();
|
||||
}
|
||||
|
||||
export function closeWithError(destination: Destination, error: mixed): void {
|
||||
destination.onError(error);
|
||||
destination.close();
|
||||
}
|
||||
|
|
@ -8,3 +8,7 @@
|
|||
*/
|
||||
|
||||
export * from '../ReactServerStreamConfigFB';
|
||||
|
||||
export function scheduleWork(callback: () => void) {
|
||||
// We don't schedule work in this model, and instead expect performWork to always be called repeatedly.
|
||||
}
|
||||
|
|
|
|||
11
packages/react/src/ReactSharedSubsetFB.js
Normal file
11
packages/react/src/ReactSharedSubsetFB.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from './ReactSharedSubset';
|
||||
export {jsx, jsxs, jsxDEV} from './jsx/ReactJSX';
|
||||
|
|
@ -75,7 +75,7 @@ export const enableFetchInstrumentation = false;
|
|||
|
||||
export const enableFormActions = false;
|
||||
|
||||
export const enableBinaryFlight = true;
|
||||
export const enableBinaryFlight = false;
|
||||
export const enableTaint = false;
|
||||
|
||||
export const enablePostpone = false;
|
||||
|
|
|
|||
|
|
@ -104,6 +104,17 @@ const bundles = [
|
|||
externals: [],
|
||||
},
|
||||
|
||||
/******* Isomorphic Shared Subset for FB *******/
|
||||
{
|
||||
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'react/src/ReactSharedSubsetFB.js',
|
||||
global: 'ReactSharedSubset',
|
||||
minifyWithProdErrorCodes: true,
|
||||
wrapWithModuleBoundaries: false,
|
||||
externals: [],
|
||||
},
|
||||
|
||||
/******* React JSX Runtime *******/
|
||||
{
|
||||
bundleTypes: [
|
||||
|
|
@ -574,6 +585,28 @@ const bundles = [
|
|||
externals: ['acorn'],
|
||||
},
|
||||
|
||||
/******* React Server DOM FB Server *******/
|
||||
{
|
||||
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
|
||||
moduleType: RENDERER,
|
||||
entry: 'react-server-dom-fb/src/ReactFlightDOMServerFB.js',
|
||||
global: 'ReactFlightDOMServer',
|
||||
minifyWithProdErrorCodes: false,
|
||||
wrapWithModuleBoundaries: false,
|
||||
externals: ['react', 'react-dom'],
|
||||
},
|
||||
|
||||
/******* React Server DOM FB Client *******/
|
||||
{
|
||||
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
|
||||
moduleType: RENDERER,
|
||||
entry: 'react-server-dom-fb/src/ReactFlightDOMClientFB.js',
|
||||
global: 'ReactFlightDOMClient',
|
||||
minifyWithProdErrorCodes: false,
|
||||
wrapWithModuleBoundaries: false,
|
||||
externals: ['react', 'react-dom'],
|
||||
},
|
||||
|
||||
/******* React Suspense Test Utils *******/
|
||||
{
|
||||
bundleTypes: [NODE_ES2015],
|
||||
|
|
|
|||
|
|
@ -63,7 +63,10 @@ const forks = Object.freeze({
|
|||
if (entry === 'react') {
|
||||
return './packages/react/src/ReactSharedInternalsClient.js';
|
||||
}
|
||||
if (entry === 'react/src/ReactSharedSubset.js') {
|
||||
if (
|
||||
entry === 'react/src/ReactSharedSubset.js' ||
|
||||
entry === 'react/src/ReactSharedSubsetFB.js'
|
||||
) {
|
||||
return './packages/react/src/ReactSharedInternalsServer.js';
|
||||
}
|
||||
if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) {
|
||||
|
|
|
|||
|
|
@ -396,13 +396,34 @@ module.exports = [
|
|||
'react-dom',
|
||||
'react-dom/src/ReactDOMSharedSubset.js',
|
||||
'react-dom-bindings',
|
||||
'react-server-dom-fb',
|
||||
'react-server-dom-fb/src/ReactDOMServerFB.js',
|
||||
'shared/ReactDOMSharedInternals',
|
||||
],
|
||||
isFlowTyped: true,
|
||||
isServerSupported: true,
|
||||
isFlightSupported: false,
|
||||
},
|
||||
{
|
||||
shortName: 'dom-fb-experimental',
|
||||
entryPoints: [
|
||||
'react-server-dom-fb/src/ReactFlightDOMClientFB.js',
|
||||
'react-server-dom-fb/src/ReactFlightDOMServerFB.js',
|
||||
],
|
||||
paths: [
|
||||
'react-dom',
|
||||
'react-dom-bindings',
|
||||
'react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js',
|
||||
'react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js',
|
||||
'react-server-dom-fb/src/ReactFlightReferencesFB.js',
|
||||
'react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js',
|
||||
'react-server-dom-fb/src/ReactFlightDOMClientFB.js',
|
||||
'react-server-dom-fb/src/ReactFlightDOMServerFB.js',
|
||||
'shared/ReactDOMSharedInternals',
|
||||
],
|
||||
isFlowTyped: true,
|
||||
isServerSupported: true,
|
||||
isFlightSupported: true,
|
||||
},
|
||||
{
|
||||
shortName: 'native',
|
||||
entryPoints: ['react-native-renderer'],
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user