[Float][Flight] Flight support for Float (#26502)
Stacked on #26557 Supporting Float methods such as ReactDOM.preload() are challenging for flight because it does not have an easy means to convey direct executions in other environments. Because the flight wire format is a JSON-like serialization that is expected to be rendered it currently only describes renderable elements. We need a way to convey a function invocation that gets run in the context of the client environment whether that is Fizz or Fiber. Fiber is somewhat straightforward because the HostDispatcher is always active and we can just have the FlightClient dispatch the serialized directive. Fizz is much more challenging becaue the dispatcher is always scoped but the specific request the dispatch belongs to is not readily available. Environments that support AsyncLocalStorage (or in the future AsyncContext) we will use this to be able to resolve directives in Fizz to the appropriate Request. For other environments directives will be elided. Right now this is pragmatic and non-breaking because all directives are opportunistic and non-critical. If this changes in the future we will need to reconsider how widespread support for async context tracking is. For Flight, if AsyncLocalStorage is available Float methods can be called before and after await points and be expected to work. If AsyncLocalStorage is not available float methods called in the sync phase of a component render will be captured but anything after an await point will be a noop. If a float call is dropped in this manner a DEV warning should help you realize your code may need to be modified. This PR also introduces a way for resources (Fizz) and hints (Flight) to flush even if there is not active task being worked on. This will help when Float methods are called in between async points within a function execution but the task is blocked on the entire function finishing. This PR also introduces deduping of Hints in Flight using the same resource keys used in Fizz. This will help shrink payload sizes when the same hint is attempted to emit over and over again
This commit is contained in:
parent
8f42196892
commit
36e4cbe2e9
|
@ -18,11 +18,14 @@ import type {
|
||||||
SSRManifest,
|
SSRManifest,
|
||||||
} from './ReactFlightClientConfig';
|
} from './ReactFlightClientConfig';
|
||||||
|
|
||||||
|
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resolveClientReference,
|
resolveClientReference,
|
||||||
preloadModule,
|
preloadModule,
|
||||||
requireModule,
|
requireModule,
|
||||||
parseModel,
|
parseModel,
|
||||||
|
dispatchHint,
|
||||||
} from './ReactFlightClientConfig';
|
} from './ReactFlightClientConfig';
|
||||||
|
|
||||||
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
|
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
|
||||||
|
@ -778,6 +781,15 @@ export function resolveErrorDev(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveHint(
|
||||||
|
response: Response,
|
||||||
|
code: string,
|
||||||
|
model: UninitializedModel,
|
||||||
|
): void {
|
||||||
|
const hintModel = parseModel<HintModel>(response, model);
|
||||||
|
dispatchHint(code, hintModel);
|
||||||
|
}
|
||||||
|
|
||||||
export function close(response: Response): void {
|
export function close(response: Response): void {
|
||||||
// In case there are any remaining unresolved chunks, they won't
|
// In case there are any remaining unresolved chunks, they won't
|
||||||
// be resolved now. So we need to issue an error to those.
|
// be resolved now. So we need to issue an error to those.
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
resolveModel,
|
resolveModel,
|
||||||
resolveErrorProd,
|
resolveErrorProd,
|
||||||
resolveErrorDev,
|
resolveErrorDev,
|
||||||
|
resolveHint,
|
||||||
createResponse as createResponseBase,
|
createResponse as createResponseBase,
|
||||||
parseModelString,
|
parseModelString,
|
||||||
parseModelTuple,
|
parseModelTuple,
|
||||||
|
@ -46,6 +47,11 @@ function processFullRow(response: Response, row: string): void {
|
||||||
resolveModule(response, id, row.slice(colon + 2));
|
resolveModule(response, id, row.slice(colon + 2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'H': {
|
||||||
|
const code = row[colon + 2];
|
||||||
|
resolveHint(response, code, row.slice(colon + 3));
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'E': {
|
case 'E': {
|
||||||
const errorInfo = JSON.parse(row.slice(colon + 2));
|
const errorInfo = JSON.parse(row.slice(colon + 2));
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference;
|
||||||
export const resolveServerReference = $$$config.resolveServerReference;
|
export const resolveServerReference = $$$config.resolveServerReference;
|
||||||
export const preloadModule = $$$config.preloadModule;
|
export const preloadModule = $$$config.preloadModule;
|
||||||
export const requireModule = $$$config.requireModule;
|
export const requireModule = $$$config.requireModule;
|
||||||
|
export const dispatchHint = $$$config.dispatchHint;
|
||||||
|
|
||||||
export opaque type Source = mixed;
|
export opaque type Source = mixed;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher';
|
||||||
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
|
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
|
||||||
import type {DOMEventName} from '../events/DOMEventNames';
|
import type {DOMEventName} from '../events/DOMEventNames';
|
||||||
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
|
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
|
||||||
|
@ -1917,10 +1918,6 @@ export function clearSingleton(instance: Instance): void {
|
||||||
|
|
||||||
export const supportsResources = true;
|
export const supportsResources = true;
|
||||||
|
|
||||||
// The resource types we support. currently they match the form for the as argument.
|
|
||||||
// In the future this may need to change, especially when modules / scripts are supported
|
|
||||||
type ResourceType = 'style' | 'font' | 'script';
|
|
||||||
|
|
||||||
type HoistableTagType = 'link' | 'meta' | 'title';
|
type HoistableTagType = 'link' | 'meta' | 'title';
|
||||||
type TResource<
|
type TResource<
|
||||||
T: 'stylesheet' | 'style' | 'script' | 'void',
|
T: 'stylesheet' | 'style' | 'script' | 'void',
|
||||||
|
@ -2011,7 +2008,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
|
||||||
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
|
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
|
||||||
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
|
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
|
||||||
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
|
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
|
||||||
export const ReactDOMClientDispatcher = {
|
export const ReactDOMClientDispatcher: HostDispatcher = {
|
||||||
prefetchDNS,
|
prefetchDNS,
|
||||||
preconnect,
|
preconnect,
|
||||||
preload,
|
preload,
|
||||||
|
@ -2085,7 +2082,10 @@ function prefetchDNS(href: string, options?: mixed) {
|
||||||
preconnectAs('dns-prefetch', null, href);
|
preconnectAs('dns-prefetch', null, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preconnect(href: string, options?: {crossOrigin?: string}) {
|
function preconnect(href: string, options: ?{crossOrigin?: string}) {
|
||||||
|
if (!enableFloat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (typeof href !== 'string' || !href) {
|
if (typeof href !== 'string' || !href) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -2113,9 +2113,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
|
||||||
preconnectAs('preconnect', crossOrigin, href);
|
preconnectAs('preconnect', crossOrigin, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreloadAs = ResourceType;
|
|
||||||
type PreloadOptions = {
|
type PreloadOptions = {
|
||||||
as: PreloadAs,
|
as: string,
|
||||||
crossOrigin?: string,
|
crossOrigin?: string,
|
||||||
integrity?: string,
|
integrity?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
|
@ -2164,7 +2163,7 @@ function preload(href: string, options: PreloadOptions) {
|
||||||
|
|
||||||
function preloadPropsFromPreloadOptions(
|
function preloadPropsFromPreloadOptions(
|
||||||
href: string,
|
href: string,
|
||||||
as: ResourceType,
|
as: string,
|
||||||
options: PreloadOptions,
|
options: PreloadOptions,
|
||||||
): PreloadProps {
|
): PreloadProps {
|
||||||
return {
|
return {
|
||||||
|
@ -2177,9 +2176,8 @@ function preloadPropsFromPreloadOptions(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreinitAs = 'style' | 'script';
|
|
||||||
type PreinitOptions = {
|
type PreinitOptions = {
|
||||||
as: PreinitAs,
|
as: string,
|
||||||
precedence?: string,
|
precedence?: string,
|
||||||
crossOrigin?: string,
|
crossOrigin?: string,
|
||||||
integrity?: string,
|
integrity?: string,
|
||||||
|
|
118
packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js
vendored
Normal file
118
packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js
vendored
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
HostDispatcher,
|
||||||
|
PrefetchDNSOptions,
|
||||||
|
PreconnectOptions,
|
||||||
|
PreloadOptions,
|
||||||
|
PreinitOptions,
|
||||||
|
} from 'react-dom/src/ReactDOMDispatcher';
|
||||||
|
|
||||||
|
import {enableFloat} from 'shared/ReactFeatureFlags';
|
||||||
|
|
||||||
|
import {
|
||||||
|
emitHint,
|
||||||
|
getHints,
|
||||||
|
resolveRequest,
|
||||||
|
} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
|
export const ReactDOMFlightServerDispatcher: HostDispatcher = {
|
||||||
|
prefetchDNS,
|
||||||
|
preconnect,
|
||||||
|
preload,
|
||||||
|
preinit,
|
||||||
|
};
|
||||||
|
|
||||||
|
function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
|
||||||
|
if (enableFloat) {
|
||||||
|
if (typeof href === 'string') {
|
||||||
|
const request = resolveRequest();
|
||||||
|
if (request) {
|
||||||
|
const hints = getHints(request);
|
||||||
|
const key = 'D' + href;
|
||||||
|
if (hints.has(key)) {
|
||||||
|
// duplicate hint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hints.add(key);
|
||||||
|
if (options) {
|
||||||
|
emitHint(request, 'D', [href, options]);
|
||||||
|
} else {
|
||||||
|
emitHint(request, 'D', href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preconnect(href: string, options: ?PreconnectOptions) {
|
||||||
|
if (enableFloat) {
|
||||||
|
if (typeof href === 'string') {
|
||||||
|
const request = resolveRequest();
|
||||||
|
if (request) {
|
||||||
|
const hints = getHints(request);
|
||||||
|
const crossOrigin =
|
||||||
|
options == null || typeof options.crossOrigin !== 'string'
|
||||||
|
? null
|
||||||
|
: options.crossOrigin === 'use-credentials'
|
||||||
|
? 'use-credentials'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`;
|
||||||
|
if (hints.has(key)) {
|
||||||
|
// duplicate hint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hints.add(key);
|
||||||
|
if (options) {
|
||||||
|
emitHint(request, 'C', [href, options]);
|
||||||
|
} else {
|
||||||
|
emitHint(request, 'C', href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preload(href: string, options: PreloadOptions) {
|
||||||
|
if (enableFloat) {
|
||||||
|
if (typeof href === 'string') {
|
||||||
|
const request = resolveRequest();
|
||||||
|
if (request) {
|
||||||
|
const hints = getHints(request);
|
||||||
|
const key = 'L' + href;
|
||||||
|
if (hints.has(key)) {
|
||||||
|
// duplicate hint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hints.add(key);
|
||||||
|
emitHint(request, 'L', [href, options]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preinit(href: string, options: PreinitOptions) {
|
||||||
|
if (enableFloat) {
|
||||||
|
if (typeof href === 'string') {
|
||||||
|
const request = resolveRequest();
|
||||||
|
if (request) {
|
||||||
|
const hints = getHints(request);
|
||||||
|
const key = 'I' + href;
|
||||||
|
if (hints.has(key)) {
|
||||||
|
// duplicate hint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hints.add(key);
|
||||||
|
emitHint(request, 'I', [href, options]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) {
|
||||||
|
|
||||||
export function flushBuffered(destination: Destination) {}
|
export function flushBuffered(destination: Destination) {}
|
||||||
|
|
||||||
export const supportsRequestStorage = false;
|
|
||||||
export const requestStorage: AsyncLocalStorage<any> = (null: any);
|
|
||||||
|
|
||||||
export function beginWriting(destination: Destination) {}
|
export function beginWriting(destination: Destination) {}
|
||||||
|
|
||||||
export function writeChunk(
|
export function writeChunk(
|
||||||
|
|
|
@ -38,6 +38,11 @@ import {
|
||||||
stringToPrecomputedChunk,
|
stringToPrecomputedChunk,
|
||||||
clonePrecomputedChunk,
|
clonePrecomputedChunk,
|
||||||
} from 'react-server/src/ReactServerStreamConfig';
|
} from 'react-server/src/ReactServerStreamConfig';
|
||||||
|
import {
|
||||||
|
resolveRequest,
|
||||||
|
getResources,
|
||||||
|
flushResources,
|
||||||
|
} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
|
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
|
||||||
import isUnitlessNumber from '../shared/isUnitlessNumber';
|
import isUnitlessNumber from '../shared/isUnitlessNumber';
|
||||||
|
@ -79,30 +84,15 @@ import {
|
||||||
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
||||||
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
|
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
|
||||||
|
|
||||||
const ReactDOMServerDispatcher = enableFloat
|
const ReactDOMServerDispatcher = {
|
||||||
? {
|
prefetchDNS,
|
||||||
prefetchDNS,
|
preconnect,
|
||||||
preconnect,
|
preload,
|
||||||
preload,
|
preinit,
|
||||||
preinit,
|
};
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
let currentResources: null | Resources = null;
|
export function prepareHostDispatcher() {
|
||||||
const currentResourcesStack = [];
|
|
||||||
|
|
||||||
export function prepareToRender(resources: Resources): mixed {
|
|
||||||
currentResourcesStack.push(currentResources);
|
|
||||||
currentResources = resources;
|
|
||||||
|
|
||||||
const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
|
|
||||||
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
|
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
|
||||||
return previousHostDispatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanupAfterRender(previousDispatcher: mixed) {
|
|
||||||
currentResources = currentResourcesStack.pop();
|
|
||||||
ReactDOMCurrentDispatcher.current = previousDispatcher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to distinguish these contexts from ones used in other renderers.
|
// Used to distinguish these contexts from ones used in other renderers.
|
||||||
|
@ -4030,7 +4020,7 @@ export function writePreamble(
|
||||||
// (User code could choose to send this even earlier by calling
|
// (User code could choose to send this even earlier by calling
|
||||||
// preinit(...), if they know they will suspend).
|
// preinit(...), if they know they will suspend).
|
||||||
const {src, integrity} = responseState.externalRuntimeConfig;
|
const {src, integrity} = responseState.externalRuntimeConfig;
|
||||||
preinitImpl(resources, src, {as: 'script', integrity});
|
internalPreinitScript(resources, src, integrity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const htmlChunks = responseState.htmlChunks;
|
const htmlChunks = responseState.htmlChunks;
|
||||||
|
@ -4804,16 +4794,19 @@ function getResourceKey(as: string, href: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prefetchDNS(href: string, options?: mixed) {
|
export function prefetchDNS(href: string, options?: mixed) {
|
||||||
if (!currentResources) {
|
if (!enableFloat) {
|
||||||
// While we expect that preconnect calls are primarily going to be observed
|
|
||||||
// during render because effects and events don't run on the server it is
|
|
||||||
// still possible that these get called in module scope. This is valid on
|
|
||||||
// the client since there is still a document to interact with but on the
|
|
||||||
// server we need a request to associate the call to. Because of this we
|
|
||||||
// simply return and do not warn.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resources = currentResources;
|
const request = resolveRequest();
|
||||||
|
if (!request) {
|
||||||
|
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
|
||||||
|
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
|
||||||
|
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
|
||||||
|
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
|
||||||
|
// fetching) and we don't want to warn in those cases.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resources = getResources(request);
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (typeof href !== 'string' || !href) {
|
if (typeof href !== 'string' || !href) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -4855,20 +4848,24 @@ export function prefetchDNS(href: string, options?: mixed) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
resources.preconnects.add(resource);
|
resources.preconnects.add(resource);
|
||||||
|
flushResources(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preconnect(href: string, options?: {crossOrigin?: string}) {
|
export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
|
||||||
if (!currentResources) {
|
if (!enableFloat) {
|
||||||
// While we expect that preconnect calls are primarily going to be observed
|
|
||||||
// during render because effects and events don't run on the server it is
|
|
||||||
// still possible that these get called in module scope. This is valid on
|
|
||||||
// the client since there is still a document to interact with but on the
|
|
||||||
// server we need a request to associate the call to. Because of this we
|
|
||||||
// simply return and do not warn.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resources = currentResources;
|
const request = resolveRequest();
|
||||||
|
if (!request) {
|
||||||
|
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
|
||||||
|
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
|
||||||
|
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
|
||||||
|
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
|
||||||
|
// fetching) and we don't want to warn in those cases.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resources = getResources(request);
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (typeof href !== 'string' || !href) {
|
if (typeof href !== 'string' || !href) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -4914,27 +4911,30 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
resources.preconnects.add(resource);
|
resources.preconnects.add(resource);
|
||||||
|
flushResources(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreloadAs = 'style' | 'font' | 'script';
|
|
||||||
type PreloadOptions = {
|
type PreloadOptions = {
|
||||||
as: PreloadAs,
|
as: string,
|
||||||
crossOrigin?: string,
|
crossOrigin?: string,
|
||||||
integrity?: string,
|
integrity?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
};
|
};
|
||||||
export function preload(href: string, options: PreloadOptions) {
|
export function preload(href: string, options: PreloadOptions) {
|
||||||
if (!currentResources) {
|
if (!enableFloat) {
|
||||||
// While we expect that preload calls are primarily going to be observed
|
|
||||||
// during render because effects and events don't run on the server it is
|
|
||||||
// still possible that these get called in module scope. This is valid on
|
|
||||||
// the client since there is still a document to interact with but on the
|
|
||||||
// server we need a request to associate the call to. Because of this we
|
|
||||||
// simply return and do not warn.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resources = currentResources;
|
const request = resolveRequest();
|
||||||
|
if (!request) {
|
||||||
|
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
|
||||||
|
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
|
||||||
|
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
|
||||||
|
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
|
||||||
|
// fetching) and we don't want to warn in those cases.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resources = getResources(request);
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (typeof href !== 'string' || !href) {
|
if (typeof href !== 'string' || !href) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -5055,37 +5055,30 @@ export function preload(href: string, options: PreloadOptions) {
|
||||||
resources.explicitOtherPreloads.add(resource);
|
resources.explicitOtherPreloads.add(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushResources(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreinitAs = 'style' | 'script';
|
|
||||||
type PreinitOptions = {
|
type PreinitOptions = {
|
||||||
as: PreinitAs,
|
as: string,
|
||||||
precedence?: string,
|
precedence?: string,
|
||||||
crossOrigin?: string,
|
crossOrigin?: string,
|
||||||
integrity?: string,
|
integrity?: string,
|
||||||
};
|
};
|
||||||
export function preinit(href: string, options: PreinitOptions): void {
|
function preinit(href: string, options: PreinitOptions): void {
|
||||||
if (!currentResources) {
|
if (!enableFloat) {
|
||||||
// While we expect that preinit calls are primarily going to be observed
|
|
||||||
// during render because effects and events don't run on the server it is
|
|
||||||
// still possible that these get called in module scope. This is valid on
|
|
||||||
// the client since there is still a document to interact with but on the
|
|
||||||
// server we need a request to associate the call to. Because of this we
|
|
||||||
// simply return and do not warn.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
preinitImpl(currentResources, href, options);
|
const request = resolveRequest();
|
||||||
}
|
if (!request) {
|
||||||
|
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
|
||||||
// On the server, preinit may be called outside of render when sending an
|
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
|
||||||
// external SSR runtime as part of the initial resources payload. Since this
|
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
|
||||||
// is an internal React call, we do not need to use the resources stack.
|
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
|
||||||
function preinitImpl(
|
// fetching) and we don't want to warn in those cases.
|
||||||
resources: Resources,
|
return;
|
||||||
href: string,
|
}
|
||||||
options: PreinitOptions,
|
const resources = getResources(request);
|
||||||
): void {
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (typeof href !== 'string' || !href) {
|
if (typeof href !== 'string' || !href) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -5214,6 +5207,7 @@ function preinitImpl(
|
||||||
resources.stylePrecedences.set(precedence, emptyStyleResource);
|
resources.stylePrecedences.set(precedence, emptyStyleResource);
|
||||||
}
|
}
|
||||||
precedenceSet.add(resource);
|
precedenceSet.add(resource);
|
||||||
|
flushResources(request);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5288,6 +5282,7 @@ function preinitImpl(
|
||||||
}
|
}
|
||||||
resources.scripts.add(resource);
|
resources.scripts.add(resource);
|
||||||
pushScriptImpl(resource.chunks, resourceProps);
|
pushScriptImpl(resource.chunks, resourceProps);
|
||||||
|
flushResources(request);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5295,9 +5290,37 @@ function preinitImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method is trusted. It must only be called from within this codebase and it assumes the arguments
|
||||||
|
// conform to the types because no user input is being passed in. It also assumes that it is being called as
|
||||||
|
// part of a work or flush loop and therefore does not need to request Fizz to flush Resources.
|
||||||
|
function internalPreinitScript(
|
||||||
|
resources: Resources,
|
||||||
|
src: string,
|
||||||
|
integrity: ?string,
|
||||||
|
): void {
|
||||||
|
const key = getResourceKey('script', src);
|
||||||
|
let resource = resources.scriptsMap.get(key);
|
||||||
|
if (!resource) {
|
||||||
|
resource = {
|
||||||
|
type: 'script',
|
||||||
|
chunks: [],
|
||||||
|
state: NoState,
|
||||||
|
props: null,
|
||||||
|
};
|
||||||
|
resources.scriptsMap.set(key, resource);
|
||||||
|
resources.scripts.add(resource);
|
||||||
|
pushScriptImpl(resource.chunks, {
|
||||||
|
async: true,
|
||||||
|
src,
|
||||||
|
integrity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function preloadPropsFromPreloadOptions(
|
function preloadPropsFromPreloadOptions(
|
||||||
href: string,
|
href: string,
|
||||||
as: PreloadAs,
|
as: string,
|
||||||
options: PreloadOptions,
|
options: PreloadOptions,
|
||||||
): PreloadProps {
|
): PreloadProps {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -137,8 +137,7 @@ export {
|
||||||
writePostamble,
|
writePostamble,
|
||||||
hoistResources,
|
hoistResources,
|
||||||
setCurrentlyRenderingBoundaryResourcesTarget,
|
setCurrentlyRenderingBoundaryResourcesTarget,
|
||||||
prepareToRender,
|
prepareHostDispatcher,
|
||||||
cleanupAfterRender,
|
|
||||||
} from './ReactFizzConfigDOM';
|
} from './ReactFizzConfigDOM';
|
||||||
|
|
||||||
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
|
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
|
||||||
|
|
|
@ -7,6 +7,35 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
PrefetchDNSOptions,
|
||||||
|
PreconnectOptions,
|
||||||
|
PreloadOptions,
|
||||||
|
PreinitOptions,
|
||||||
|
} from 'react-dom/src/ReactDOMDispatcher';
|
||||||
|
|
||||||
|
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
||||||
|
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
|
||||||
|
|
||||||
|
import {ReactDOMFlightServerDispatcher} from './ReactDOMFlightServerHostDispatcher';
|
||||||
|
|
||||||
|
export function prepareHostDispatcher(): void {
|
||||||
|
ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
// Used to distinguish these contexts from ones used in other renderers.
|
// Used to distinguish these contexts from ones used in other renderers.
|
||||||
// E.g. this can be used to distinguish legacy renderers from this modern one.
|
// E.g. this can be used to distinguish legacy renderers from this modern one.
|
||||||
export const isPrimaryRenderer = true;
|
export const isPrimaryRenderer = true;
|
||||||
|
|
||||||
|
export type HintModel =
|
||||||
|
| string
|
||||||
|
| [
|
||||||
|
string,
|
||||||
|
PrefetchDNSOptions | PreconnectOptions | PreloadOptions | PreinitOptions,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type Hints = Set<string>;
|
||||||
|
|
||||||
|
export function createHints(): Hints {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
|
@ -10,4 +10,44 @@
|
||||||
// This client file is in the shared folder because it applies to both SSR and browser contexts.
|
// 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 configuraiton of the FlightClient behavior which can run in either environment.
|
||||||
|
|
||||||
// In a future update this is where we will implement `dispatchDirective` such as for Float methods
|
import type {HintModel} from '../server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
||||||
|
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
|
||||||
|
|
||||||
|
export function dispatchHint(code: string, model: HintModel): void {
|
||||||
|
const dispatcher = ReactDOMCurrentDispatcher.current;
|
||||||
|
if (dispatcher) {
|
||||||
|
let href, options;
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
href = model;
|
||||||
|
} else {
|
||||||
|
href = model[0];
|
||||||
|
options = model[1];
|
||||||
|
}
|
||||||
|
switch (code) {
|
||||||
|
case 'D': {
|
||||||
|
// $FlowFixMe[prop-missing] options are not refined to their types by code
|
||||||
|
dispatcher.prefetchDNS(href, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'C': {
|
||||||
|
// $FlowFixMe[prop-missing] options are not refined to their types by code
|
||||||
|
dispatcher.preconnect(href, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'L': {
|
||||||
|
// $FlowFixMe[prop-missing] options are not refined to their types by code
|
||||||
|
// $FlowFixMe[incompatible-call] options are not refined to their types by code
|
||||||
|
dispatcher.preload(href, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'I': {
|
||||||
|
// $FlowFixMe[prop-missing] options are not refined to their types by code
|
||||||
|
// $FlowFixMe[incompatible-call] options are not refined to their types by code
|
||||||
|
dispatcher.preinit(href, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* 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 type PrefetchDNSOptions = {};
|
||||||
|
export type PreconnectOptions = {crossOrigin?: string};
|
||||||
|
export type PreloadOptions = {
|
||||||
|
as: string,
|
||||||
|
crossOrigin?: string,
|
||||||
|
integrity?: string,
|
||||||
|
type?: string,
|
||||||
|
};
|
||||||
|
export type PreinitOptions = {
|
||||||
|
as: string,
|
||||||
|
precedence?: string,
|
||||||
|
crossOrigin?: string,
|
||||||
|
integrity?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HostDispatcher = {
|
||||||
|
prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void,
|
||||||
|
preconnect: (href: string, options: ?PreconnectOptions) => void,
|
||||||
|
preload: (href: string, options: PreloadOptions) => void,
|
||||||
|
preinit: (href: string, options: PreinitOptions) => void,
|
||||||
|
};
|
|
@ -1,39 +1,72 @@
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
PreconnectOptions,
|
||||||
|
PreloadOptions,
|
||||||
|
PreinitOptions,
|
||||||
|
} from './ReactDOMDispatcher';
|
||||||
|
|
||||||
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
||||||
|
const Dispatcher = ReactDOMSharedInternals.Dispatcher;
|
||||||
|
|
||||||
export function prefetchDNS() {
|
export function prefetchDNS(href: string) {
|
||||||
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
|
let passedOptionArg: any;
|
||||||
|
if (__DEV__) {
|
||||||
|
if (arguments[1] !== undefined) {
|
||||||
|
passedOptionArg = arguments[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dispatcher = Dispatcher.current;
|
||||||
if (dispatcher) {
|
if (dispatcher) {
|
||||||
dispatcher.prefetchDNS.apply(this, arguments);
|
if (__DEV__) {
|
||||||
|
if (passedOptionArg !== undefined) {
|
||||||
|
// prefetchDNS will warn if you pass reserved options arg. We pass it along in Dev only to
|
||||||
|
// elicit the warning. In prod we do not forward since it is not a part of the interface.
|
||||||
|
// @TODO move all arg validation into this file. It needs to be universal anyway so may as well lock down the interace here and
|
||||||
|
// let the rest of the codebase trust the types
|
||||||
|
dispatcher.prefetchDNS(href, passedOptionArg);
|
||||||
|
} else {
|
||||||
|
dispatcher.prefetchDNS(href);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatcher.prefetchDNS(href);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
|
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
|
||||||
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
||||||
// so we favor silent bailout over warning or erroring.
|
// so we favor silent bailout over warning or erroring.
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preconnect() {
|
export function preconnect(href: string, options?: ?PreconnectOptions) {
|
||||||
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
|
const dispatcher = Dispatcher.current;
|
||||||
if (dispatcher) {
|
if (dispatcher) {
|
||||||
dispatcher.preconnect.apply(this, arguments);
|
dispatcher.preconnect(href, options);
|
||||||
}
|
}
|
||||||
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
|
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
|
||||||
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
||||||
// so we favor silent bailout over warning or erroring.
|
// so we favor silent bailout over warning or erroring.
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preload() {
|
export function preload(href: string, options: PreloadOptions) {
|
||||||
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
|
const dispatcher = Dispatcher.current;
|
||||||
if (dispatcher) {
|
if (dispatcher) {
|
||||||
dispatcher.preload.apply(this, arguments);
|
dispatcher.preload(href, options);
|
||||||
}
|
}
|
||||||
// We don't error because preload needs to be resilient to being called in a variety of scopes
|
// We don't error because preload needs to be resilient to being called in a variety of scopes
|
||||||
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
||||||
// so we favor silent bailout over warning or erroring.
|
// so we favor silent bailout over warning or erroring.
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preinit() {
|
export function preinit(href: string, options: PreinitOptions) {
|
||||||
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
|
const dispatcher = Dispatcher.current;
|
||||||
if (dispatcher) {
|
if (dispatcher) {
|
||||||
dispatcher.preinit.apply(this, arguments);
|
dispatcher.preinit(href, options);
|
||||||
}
|
}
|
||||||
// We don't error because preinit needs to be resilient to being called in a variety of scopes
|
// We don't error because preinit needs to be resilient to being called in a variety of scopes
|
||||||
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
// and the runtime may not be capable of responding. The function is optimistic and not critical
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {HostDispatcher} from './ReactDOMDispatcher';
|
||||||
|
|
||||||
type InternalsType = {
|
type InternalsType = {
|
||||||
usingClientEntryPoint: boolean,
|
usingClientEntryPoint: boolean,
|
||||||
Events: [any, any, any, any, any, any],
|
Events: [any, any, any, any, any, any],
|
||||||
Dispatcher: {
|
Dispatcher: {
|
||||||
current: mixed,
|
current: null | HostDispatcher,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3701,7 +3701,7 @@ describe('ReactDOMFizzServer', () => {
|
||||||
Array.from(document.head.getElementsByTagName('script')).map(
|
Array.from(document.head.getElementsByTagName('script')).map(
|
||||||
n => n.outerHTML,
|
n => n.outerHTML,
|
||||||
),
|
),
|
||||||
).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
|
).toEqual(['<script async="" src="src-of-external-runtime"></script>']);
|
||||||
|
|
||||||
expect(getVisibleChildren(document)).toEqual(
|
expect(getVisibleChildren(document)).toEqual(
|
||||||
<html>
|
<html>
|
||||||
|
|
|
@ -3981,7 +3981,7 @@ body {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableFloat
|
// @gate enableFloat
|
||||||
it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
|
it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
|
||||||
function App() {
|
function App() {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
ReactDOM.preinit('foo', {as: 'script'});
|
ReactDOM.preinit('foo', {as: 'script'});
|
||||||
|
|
|
@ -13,8 +13,6 @@ import type {
|
||||||
TransitionTracingCallbacks,
|
TransitionTracingCallbacks,
|
||||||
} from 'react-reconciler/src/ReactInternalTypes';
|
} from 'react-reconciler/src/ReactInternalTypes';
|
||||||
|
|
||||||
import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
|
|
||||||
const {Dispatcher} = ReactDOMSharedInternals;
|
|
||||||
import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
|
import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
|
||||||
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
|
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
|
||||||
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||||
|
@ -25,13 +23,19 @@ import {
|
||||||
disableCommentsAsDOMContainers,
|
disableCommentsAsDOMContainers,
|
||||||
} from 'shared/ReactFeatureFlags';
|
} from 'shared/ReactFeatureFlags';
|
||||||
|
|
||||||
|
import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
|
||||||
|
const {Dispatcher} = ReactDOMSharedInternals;
|
||||||
|
if (enableFloat && typeof document !== 'undefined') {
|
||||||
|
// Set the default dispatcher to the client dispatcher
|
||||||
|
Dispatcher.current = ReactDOMClientDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
export type RootType = {
|
export type RootType = {
|
||||||
render(children: ReactNodeList): void,
|
render(children: ReactNodeList): void,
|
||||||
unmount(): void,
|
unmount(): void,
|
||||||
_internalRoot: FiberRoot | null,
|
_internalRoot: FiberRoot | null,
|
||||||
...
|
...
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateRootOptions = {
|
export type CreateRootOptions = {
|
||||||
unstable_strictMode?: boolean,
|
unstable_strictMode?: boolean,
|
||||||
unstable_concurrentUpdatesByDefault?: boolean,
|
unstable_concurrentUpdatesByDefault?: boolean,
|
||||||
|
|
|
@ -339,8 +339,7 @@ export function hoistResources(
|
||||||
boundaryResources: BoundaryResources,
|
boundaryResources: BoundaryResources,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export function prepareToRender(resources: Resources) {}
|
export function prepareHostDispatcher() {}
|
||||||
export function cleanupAfterRender(previousDispatcher: mixed) {}
|
|
||||||
export function createResources() {}
|
export function createResources() {}
|
||||||
export function createBoundaryResources() {}
|
export function createBoundaryResources() {}
|
||||||
export function setCurrentlyRenderingBoundaryResourcesTarget(
|
export function setCurrentlyRenderingBoundaryResourcesTarget(
|
||||||
|
|
|
@ -8,3 +8,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const isPrimaryRenderer = true;
|
export const isPrimaryRenderer = true;
|
||||||
|
|
||||||
|
export type Hints = null;
|
||||||
|
export type HintModel = '';
|
||||||
|
|
||||||
|
export function createHints(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareHostDispatcher() {}
|
||||||
|
|
|
@ -63,6 +63,7 @@ const ReactNoopFlightServer = ReactFlightServer({
|
||||||
) {
|
) {
|
||||||
return saveModule(reference.value);
|
return saveModule(reference.value);
|
||||||
},
|
},
|
||||||
|
prepareHostDispatcher() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
|
|
@ -279,8 +279,7 @@ const ReactNoopServer = ReactFizzServer({
|
||||||
|
|
||||||
setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
|
setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
|
||||||
|
|
||||||
prepareToRender() {},
|
prepareHostDispatcher() {},
|
||||||
cleanupAfterRender() {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
resolveModule,
|
resolveModule,
|
||||||
resolveErrorDev,
|
resolveErrorDev,
|
||||||
resolveErrorProd,
|
resolveErrorProd,
|
||||||
|
resolveHint,
|
||||||
close,
|
close,
|
||||||
getRoot,
|
getRoot,
|
||||||
} from 'react-client/src/ReactFlightClient';
|
} from 'react-client/src/ReactFlightClient';
|
||||||
|
@ -30,10 +31,14 @@ export function resolveRow(response: Response, chunk: RowEncoding): void {
|
||||||
} else if (chunk[0] === 'I') {
|
} else if (chunk[0] === 'I') {
|
||||||
// $FlowFixMe[incompatible-call] unable to refine on array indices
|
// $FlowFixMe[incompatible-call] unable to refine on array indices
|
||||||
resolveModule(response, chunk[1], chunk[2]);
|
resolveModule(response, chunk[1], chunk[2]);
|
||||||
|
} else if (chunk[0] === 'H') {
|
||||||
|
// $FlowFixMe[incompatible-call] unable to refine on array indices
|
||||||
|
resolveHint(response, chunk[1], chunk[2]);
|
||||||
} else {
|
} else {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
resolveErrorDev(
|
resolveErrorDev(
|
||||||
response,
|
response,
|
||||||
|
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||||
chunk[1],
|
chunk[1],
|
||||||
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||||
// $FlowFixMe[prop-missing]
|
// $FlowFixMe[prop-missing]
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
||||||
import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration';
|
import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration';
|
||||||
|
|
||||||
export type JSONValue =
|
export type JSONValue =
|
||||||
|
@ -20,6 +21,7 @@ export type JSONValue =
|
||||||
export type RowEncoding =
|
export type RowEncoding =
|
||||||
| ['O', number, JSONValue]
|
| ['O', number, JSONValue]
|
||||||
| ['I', number, ClientReferenceMetadata]
|
| ['I', number, ClientReferenceMetadata]
|
||||||
|
| ['H', string, HintModel]
|
||||||
| ['P', number, string]
|
| ['P', number, string]
|
||||||
| ['S', number, string]
|
| ['S', number, string]
|
||||||
| [
|
| [
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
||||||
import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol';
|
import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -191,6 +192,16 @@ export function processImportChunk(
|
||||||
return ['I', id, clientReferenceMetadata];
|
return ['I', id, clientReferenceMetadata];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function processHintChunk(
|
||||||
|
request: Request,
|
||||||
|
id: number,
|
||||||
|
code: string,
|
||||||
|
model: HintModel,
|
||||||
|
): Chunk {
|
||||||
|
// The hint is already a JSON serializable value.
|
||||||
|
return ['H', code, model];
|
||||||
|
}
|
||||||
|
|
||||||
export function scheduleWork(callback: () => void) {
|
export function scheduleWork(callback: () => void) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
@ -198,8 +209,7 @@ export function scheduleWork(callback: () => void) {
|
||||||
export function flushBuffered(destination: Destination) {}
|
export function flushBuffered(destination: Destination) {}
|
||||||
|
|
||||||
export const supportsRequestStorage = false;
|
export const supportsRequestStorage = false;
|
||||||
export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
(null: any);
|
|
||||||
|
|
||||||
export function beginWriting(destination: Destination) {}
|
export function beginWriting(destination: Destination) {}
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,7 @@ export function scheduleWork(callback: () => void) {
|
||||||
export function flushBuffered(destination: Destination) {}
|
export function flushBuffered(destination: Destination) {}
|
||||||
|
|
||||||
export const supportsRequestStorage = false;
|
export const supportsRequestStorage = false;
|
||||||
export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
(null: any);
|
|
||||||
|
|
||||||
export function beginWriting(destination: Destination) {}
|
export function beginWriting(destination: Destination) {}
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,11 @@ let clientModuleError;
|
||||||
let webpackMap;
|
let webpackMap;
|
||||||
let Stream;
|
let Stream;
|
||||||
let React;
|
let React;
|
||||||
|
let ReactDOM;
|
||||||
let ReactDOMClient;
|
let ReactDOMClient;
|
||||||
let ReactServerDOMServer;
|
let ReactServerDOMServer;
|
||||||
let ReactServerDOMClient;
|
let ReactServerDOMClient;
|
||||||
|
let ReactDOMFizzServer;
|
||||||
let Suspense;
|
let Suspense;
|
||||||
let ErrorBoundary;
|
let ErrorBoundary;
|
||||||
|
|
||||||
|
@ -42,6 +44,8 @@ describe('ReactFlightDOM', () => {
|
||||||
|
|
||||||
Stream = require('stream');
|
Stream = require('stream');
|
||||||
React = require('react');
|
React = require('react');
|
||||||
|
ReactDOM = require('react-dom');
|
||||||
|
ReactDOMFizzServer = require('react-dom/server.node');
|
||||||
use = React.use;
|
use = React.use;
|
||||||
Suspense = React.Suspense;
|
Suspense = React.Suspense;
|
||||||
ReactDOMClient = require('react-dom/client');
|
ReactDOMClient = require('react-dom/client');
|
||||||
|
@ -1153,4 +1157,292 @@ describe('ReactFlightDOM', () => {
|
||||||
);
|
);
|
||||||
expect(reportedErrors).toEqual([theError]);
|
expect(reportedErrors).toEqual([theError]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate enableUseHook
|
||||||
|
it('should support ReactDOM.preload when rendering in Fiber', async () => {
|
||||||
|
function Component() {
|
||||||
|
return <p>hello world</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientComponent = clientExports(Component);
|
||||||
|
|
||||||
|
async function ServerComponent() {
|
||||||
|
ReactDOM.preload('before', {as: 'style'});
|
||||||
|
await 1;
|
||||||
|
ReactDOM.preload('after', {as: 'style'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {writable, readable} = getTestStream();
|
||||||
|
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
<ServerComponent />,
|
||||||
|
webpackMap,
|
||||||
|
);
|
||||||
|
pipe(writable);
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
function getResponse() {
|
||||||
|
if (response === null) {
|
||||||
|
response = ReactServerDOMClient.createFromReadableStream(readable);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We pause to allow the float call after the await point to process before the
|
||||||
|
// HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
|
||||||
|
// because the module graphs are not different and the HostDispatcher is shared.
|
||||||
|
// In a real environment the Fiber and Flight code would each have their own independent
|
||||||
|
// dispatcher.
|
||||||
|
// @TODO consider what happens when Server-Components-On-The-Client exist. we probably
|
||||||
|
// want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
|
||||||
|
// but we need to make sure that actually makes sense
|
||||||
|
await 1;
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const root = ReactDOMClient.createRoot(container);
|
||||||
|
await act(() => {
|
||||||
|
root.render(<App />);
|
||||||
|
});
|
||||||
|
expect(document.head.innerHTML).toBe(
|
||||||
|
'<link href="before" rel="preload" as="style">' +
|
||||||
|
'<link href="after" rel="preload" as="style">',
|
||||||
|
);
|
||||||
|
expect(container.innerHTML).toBe('<p>hello world</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// @gate enableUseHook
|
||||||
|
it('should support ReactDOM.preload when rendering in Fizz', async () => {
|
||||||
|
function Component() {
|
||||||
|
return <p>hello world</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientComponent = clientExports(Component);
|
||||||
|
|
||||||
|
async function ServerComponent() {
|
||||||
|
ReactDOM.preload('before', {as: 'style'});
|
||||||
|
await 1;
|
||||||
|
ReactDOM.preload('after', {as: 'style'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {writable: flightWritable, readable: flightReadable} =
|
||||||
|
getTestStream();
|
||||||
|
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
|
||||||
|
|
||||||
|
// In a real environment you would want to call the render during the Fizz render.
|
||||||
|
// The reason we cannot do this in our test is because we don't actually have two separate
|
||||||
|
// module graphs and we are contriving the sequencing to work in a way where
|
||||||
|
// the right HostDispatcher is in scope during the Flight Server Float calls and the
|
||||||
|
// Flight Client hint dispatches
|
||||||
|
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
<ServerComponent />,
|
||||||
|
webpackMap,
|
||||||
|
);
|
||||||
|
pipe(flightWritable);
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
function getResponse() {
|
||||||
|
if (response === null) {
|
||||||
|
response =
|
||||||
|
ReactServerDOMClient.createFromReadableStream(flightReadable);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>{getResponse()}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
|
||||||
|
});
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const reader = fizzReadable.getReader();
|
||||||
|
let content = '';
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
content += decoder.decode();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
content += decoder.decode(value, {stream: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(content).toEqual(
|
||||||
|
'<!DOCTYPE html><html><head><link rel="preload" as="style" href="before"/>' +
|
||||||
|
'<link rel="preload" as="style" href="after"/></head><body><p>hello world</p></body></html>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
|
||||||
|
function Component() {
|
||||||
|
return <p>hello world</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientComponent = clientExports(Component);
|
||||||
|
|
||||||
|
async function ServerComponent1() {
|
||||||
|
ReactDOM.preload('before1', {as: 'style'});
|
||||||
|
await 1;
|
||||||
|
ReactDOM.preload('after1', {as: 'style'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ServerComponent2() {
|
||||||
|
ReactDOM.preload('before2', {as: 'style'});
|
||||||
|
await 1;
|
||||||
|
ReactDOM.preload('after2', {as: 'style'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {writable: flightWritable1, readable: flightReadable1} =
|
||||||
|
getTestStream();
|
||||||
|
const {writable: flightWritable2, readable: flightReadable2} =
|
||||||
|
getTestStream();
|
||||||
|
|
||||||
|
ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
<ServerComponent1 />,
|
||||||
|
webpackMap,
|
||||||
|
).pipe(flightWritable1);
|
||||||
|
|
||||||
|
ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
<ServerComponent2 />,
|
||||||
|
webpackMap,
|
||||||
|
).pipe(flightWritable2);
|
||||||
|
|
||||||
|
const responses = new Map();
|
||||||
|
function getResponse(stream) {
|
||||||
|
let response = responses.get(stream);
|
||||||
|
if (!response) {
|
||||||
|
response = ReactServerDOMClient.createFromReadableStream(stream);
|
||||||
|
responses.set(stream, response);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({stream}) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>{getResponse(stream)}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pausing to let Flight runtime tick. This is a test only artifact of the fact that
|
||||||
|
// we aren't operating separate module graphs for flight and fiber. In a real app
|
||||||
|
// each would have their own dispatcher and there would be no cross dispatching.
|
||||||
|
await 1;
|
||||||
|
|
||||||
|
const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
|
||||||
|
const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
|
||||||
|
await act(async () => {
|
||||||
|
ReactDOMFizzServer.renderToPipeableStream(
|
||||||
|
<App stream={flightReadable1} />,
|
||||||
|
).pipe(fizzWritable1);
|
||||||
|
ReactDOMFizzServer.renderToPipeableStream(
|
||||||
|
<App stream={flightReadable2} />,
|
||||||
|
).pipe(fizzWritable2);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function read(stream) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const reader = stream.getReader();
|
||||||
|
let buffer = '';
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
buffer += decoder.decode();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buffer += decoder.decode(value, {stream: true});
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [content1, content2] = await Promise.all([
|
||||||
|
read(fizzReadable1),
|
||||||
|
read(fizzReadable2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(content1).toEqual(
|
||||||
|
'<!DOCTYPE html><html><head><link rel="preload" as="style" href="before1"/>' +
|
||||||
|
'<link rel="preload" as="style" href="after1"/></head><body><p>hello world</p></body></html>',
|
||||||
|
);
|
||||||
|
expect(content2).toEqual(
|
||||||
|
'<!DOCTYPE html><html><head><link rel="preload" as="style" href="before2"/>' +
|
||||||
|
'<link rel="preload" as="style" href="after2"/></head><body><p>hello world</p></body></html>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports deduping hints by Float key', async () => {
|
||||||
|
function Component() {
|
||||||
|
return <p>hello world</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientComponent = clientExports(Component);
|
||||||
|
|
||||||
|
async function ServerComponent() {
|
||||||
|
ReactDOM.prefetchDNS('dns');
|
||||||
|
ReactDOM.preconnect('preconnect');
|
||||||
|
ReactDOM.preload('load', {as: 'style'});
|
||||||
|
ReactDOM.preinit('init', {as: 'script'});
|
||||||
|
// again but vary preconnect to demonstrate crossOrigin participates in the key
|
||||||
|
ReactDOM.prefetchDNS('dns');
|
||||||
|
ReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
|
||||||
|
ReactDOM.preload('load', {as: 'style'});
|
||||||
|
ReactDOM.preinit('init', {as: 'script'});
|
||||||
|
await 1;
|
||||||
|
// after an async point
|
||||||
|
ReactDOM.prefetchDNS('dns');
|
||||||
|
ReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
|
||||||
|
ReactDOM.preload('load', {as: 'style'});
|
||||||
|
ReactDOM.preinit('init', {as: 'script'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {writable, readable} = getTestStream();
|
||||||
|
|
||||||
|
ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
<ServerComponent />,
|
||||||
|
webpackMap,
|
||||||
|
).pipe(writable);
|
||||||
|
|
||||||
|
const hintRows = [];
|
||||||
|
async function collectHints(stream) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const reader = stream.getReader();
|
||||||
|
let buffer = '';
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
buffer += decoder.decode();
|
||||||
|
if (buffer.includes(':H')) {
|
||||||
|
hintRows.push(buffer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buffer += decoder.decode(value, {stream: true});
|
||||||
|
let line;
|
||||||
|
while ((line = buffer.indexOf('\n')) > -1) {
|
||||||
|
const row = buffer.slice(0, line);
|
||||||
|
buffer = buffer.slice(line + 1);
|
||||||
|
if (row.includes(':H')) {
|
||||||
|
hintRows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await collectHints(readable);
|
||||||
|
expect(hintRows.length).toEqual(6);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,9 @@ let webpackMap;
|
||||||
let webpackServerMap;
|
let webpackServerMap;
|
||||||
let act;
|
let act;
|
||||||
let React;
|
let React;
|
||||||
|
let ReactDOM;
|
||||||
let ReactDOMClient;
|
let ReactDOMClient;
|
||||||
|
let ReactDOMFizzServer;
|
||||||
let ReactServerDOMServer;
|
let ReactServerDOMServer;
|
||||||
let ReactServerDOMClient;
|
let ReactServerDOMClient;
|
||||||
let Suspense;
|
let Suspense;
|
||||||
|
@ -37,7 +39,9 @@ describe('ReactFlightDOMBrowser', () => {
|
||||||
webpackMap = WebpackMock.webpackMap;
|
webpackMap = WebpackMock.webpackMap;
|
||||||
webpackServerMap = WebpackMock.webpackServerMap;
|
webpackServerMap = WebpackMock.webpackServerMap;
|
||||||
React = require('react');
|
React = require('react');
|
||||||
|
ReactDOM = require('react-dom');
|
||||||
ReactDOMClient = require('react-dom/client');
|
ReactDOMClient = require('react-dom/client');
|
||||||
|
ReactDOMFizzServer = require('react-dom/server.browser');
|
||||||
ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
|
ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
|
||||||
ReactServerDOMClient = require('react-server-dom-webpack/client');
|
ReactServerDOMClient = require('react-server-dom-webpack/client');
|
||||||
Suspense = React.Suspense;
|
Suspense = React.Suspense;
|
||||||
|
@ -1062,4 +1066,118 @@ describe('ReactFlightDOMBrowser', () => {
|
||||||
expect(thrownError.digest).toBe('test-error-digest');
|
expect(thrownError.digest).toBe('test-error-digest');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports Float hints before the first await in server components in Fiber', async () => {
|
||||||
|
function Component() {
|
||||||
|
return <p>hello world</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientComponent = clientExports(Component);
|
||||||
|
|
||||||
|
async function ServerComponent() {
|
||||||
|
ReactDOM.preload('before', {as: 'style'});
|
||||||
|
await 1;
|
||||||
|
ReactDOM.preload('after', {as: 'style'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = ReactServerDOMServer.renderToReadableStream(
|
||||||
|
<ServerComponent />,
|
||||||
|
webpackMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
function getResponse() {
|
||||||
|
if (response === null) {
|
||||||
|
response = ReactServerDOMClient.createFromReadableStream(stream);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// pausing to let Flight runtime tick. This is a test only artifact of the fact that
|
||||||
|
// we aren't operating separate module graphs for flight and fiber. In a real app
|
||||||
|
// each would have their own dispatcher and there would be no cross dispatching.
|
||||||
|
await 1;
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const root = ReactDOMClient.createRoot(container);
|
||||||
|
await act(() => {
|
||||||
|
root.render(<App />);
|
||||||
|
});
|
||||||
|
expect(document.head.innerHTML).toBe(
|
||||||
|
'<link href="before" rel="preload" as="style">',
|
||||||
|
);
|
||||||
|
expect(container.innerHTML).toBe('<p>hello world</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not support Float hints in server components anywhere in Fizz', async () => {
|
||||||
|
// In environments that do not support AsyncLocalStorage the Flight client has no ability
|
||||||
|
// to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
|
||||||
|
// the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
|
||||||
|
// a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
|
||||||
|
function Component() {
|
||||||
|
return <p>hello world</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientComponent = clientExports(Component);
|
||||||
|
|
||||||
|
async function ServerComponent() {
|
||||||
|
ReactDOM.preload('before', {as: 'style'});
|
||||||
|
await 1;
|
||||||
|
ReactDOM.preload('after', {as: 'style'});
|
||||||
|
return <ClientComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = ReactServerDOMServer.renderToReadableStream(
|
||||||
|
<ServerComponent />,
|
||||||
|
webpackMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
function getResponse() {
|
||||||
|
if (response === null) {
|
||||||
|
response = ReactServerDOMClient.createFromReadableStream(stream);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>{getResponse()}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pausing to let Flight runtime tick. This is a test only artifact of the fact that
|
||||||
|
// we aren't operating separate module graphs for flight and fiber. In a real app
|
||||||
|
// each would have their own dispatcher and there would be no cross dispatching.
|
||||||
|
await 1;
|
||||||
|
|
||||||
|
let fizzStream;
|
||||||
|
await act(async () => {
|
||||||
|
fizzStream = await ReactDOMFizzServer.renderToReadableStream(<App />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const reader = fizzStream.getReader();
|
||||||
|
let content = '';
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
content += decoder.decode();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
content += decoder.decode(value, {stream: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(content).toEqual(
|
||||||
|
'<!DOCTYPE html><html><head>' +
|
||||||
|
'</head><body><p>hello world</p></body></html>',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,3 +95,5 @@ const dummy = {};
|
||||||
export function parseModel<T>(response: Response, json: UninitializedModel): T {
|
export function parseModel<T>(response: Response, json: UninitializedModel): T {
|
||||||
return (parseModelRecursively(response, dummy, '', json): any);
|
return (parseModelRecursively(response, dummy, '', json): any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dispatchHint(code: string, model: mixed) {}
|
||||||
|
|
|
@ -187,6 +187,17 @@ export function processImportChunk(
|
||||||
return ['I', id, clientReferenceMetadata];
|
return ['I', id, clientReferenceMetadata];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function processHintChunk(
|
||||||
|
request: Request,
|
||||||
|
id: number,
|
||||||
|
code: string,
|
||||||
|
model: JSONValue,
|
||||||
|
): Chunk {
|
||||||
|
throw new Error(
|
||||||
|
'React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function scheduleWork(callback: () => void) {
|
export function scheduleWork(callback: () => void) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
@ -194,8 +205,7 @@ export function scheduleWork(callback: () => void) {
|
||||||
export function flushBuffered(destination: Destination) {}
|
export function flushBuffered(destination: Destination) {}
|
||||||
|
|
||||||
export const supportsRequestStorage = false;
|
export const supportsRequestStorage = false;
|
||||||
export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
(null: any);
|
|
||||||
|
|
||||||
export function beginWriting(destination: Destination) {}
|
export function beginWriting(destination: Destination) {}
|
||||||
|
|
||||||
|
|
|
@ -71,11 +71,12 @@ import {
|
||||||
writeHoistables,
|
writeHoistables,
|
||||||
writePostamble,
|
writePostamble,
|
||||||
hoistResources,
|
hoistResources,
|
||||||
prepareToRender,
|
|
||||||
cleanupAfterRender,
|
|
||||||
setCurrentlyRenderingBoundaryResourcesTarget,
|
setCurrentlyRenderingBoundaryResourcesTarget,
|
||||||
createResources,
|
createResources,
|
||||||
createBoundaryResources,
|
createBoundaryResources,
|
||||||
|
prepareHostDispatcher,
|
||||||
|
supportsRequestStorage,
|
||||||
|
requestStorage,
|
||||||
} from './ReactFizzConfig';
|
} from './ReactFizzConfig';
|
||||||
import {
|
import {
|
||||||
constructClassInstance,
|
constructClassInstance,
|
||||||
|
@ -210,6 +211,7 @@ const CLOSED = 2;
|
||||||
|
|
||||||
export opaque type Request = {
|
export opaque type Request = {
|
||||||
destination: null | Destination,
|
destination: null | Destination,
|
||||||
|
flushScheduled: boolean,
|
||||||
+responseState: ResponseState,
|
+responseState: ResponseState,
|
||||||
+progressiveChunkSize: number,
|
+progressiveChunkSize: number,
|
||||||
status: 0 | 1 | 2,
|
status: 0 | 1 | 2,
|
||||||
|
@ -277,11 +279,13 @@ export function createRequest(
|
||||||
onShellError: void | ((error: mixed) => void),
|
onShellError: void | ((error: mixed) => void),
|
||||||
onFatalError: void | ((error: mixed) => void),
|
onFatalError: void | ((error: mixed) => void),
|
||||||
): Request {
|
): Request {
|
||||||
|
prepareHostDispatcher();
|
||||||
const pingedTasks: Array<Task> = [];
|
const pingedTasks: Array<Task> = [];
|
||||||
const abortSet: Set<Task> = new Set();
|
const abortSet: Set<Task> = new Set();
|
||||||
const resources: Resources = createResources();
|
const resources: Resources = createResources();
|
||||||
const request: Request = {
|
const request: Request = {
|
||||||
destination: null,
|
destination: null,
|
||||||
|
flushScheduled: false,
|
||||||
responseState,
|
responseState,
|
||||||
progressiveChunkSize:
|
progressiveChunkSize:
|
||||||
progressiveChunkSize === undefined
|
progressiveChunkSize === undefined
|
||||||
|
@ -332,10 +336,22 @@ export function createRequest(
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentRequest: null | Request = null;
|
||||||
|
|
||||||
|
export function resolveRequest(): null | Request {
|
||||||
|
if (currentRequest) return currentRequest;
|
||||||
|
if (supportsRequestStorage) {
|
||||||
|
const store = requestStorage.getStore();
|
||||||
|
if (store) return store;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function pingTask(request: Request, task: Task): void {
|
function pingTask(request: Request, task: Task): void {
|
||||||
const pingedTasks = request.pingedTasks;
|
const pingedTasks = request.pingedTasks;
|
||||||
pingedTasks.push(task);
|
pingedTasks.push(task);
|
||||||
if (pingedTasks.length === 1) {
|
if (request.pingedTasks.length === 1) {
|
||||||
|
request.flushScheduled = request.destination !== null;
|
||||||
scheduleWork(() => performWork(request));
|
scheduleWork(() => performWork(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1947,7 +1963,9 @@ export function performWork(request: Request): void {
|
||||||
ReactCurrentCache.current = DefaultCacheDispatcher;
|
ReactCurrentCache.current = DefaultCacheDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousHostDispatcher = prepareToRender(request.resources);
|
const prevRequest = currentRequest;
|
||||||
|
currentRequest = request;
|
||||||
|
|
||||||
let prevGetCurrentStackImpl;
|
let prevGetCurrentStackImpl;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
|
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
|
||||||
|
@ -1975,7 +1993,6 @@ export function performWork(request: Request): void {
|
||||||
if (enableCache) {
|
if (enableCache) {
|
||||||
ReactCurrentCache.current = prevCacheDispatcher;
|
ReactCurrentCache.current = prevCacheDispatcher;
|
||||||
}
|
}
|
||||||
cleanupAfterRender(previousHostDispatcher);
|
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
|
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
|
||||||
|
@ -1990,6 +2007,7 @@ export function performWork(request: Request): void {
|
||||||
// we'll to restore the context to what it was before returning.
|
// we'll to restore the context to what it was before returning.
|
||||||
switchContext(prevContext);
|
switchContext(prevContext);
|
||||||
}
|
}
|
||||||
|
currentRequest = prevRequest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2389,6 +2407,7 @@ function flushCompletedQueues(
|
||||||
// We don't need to check any partially completed segments because
|
// We don't need to check any partially completed segments because
|
||||||
// either they have pending task or they're complete.
|
// either they have pending task or they're complete.
|
||||||
) {
|
) {
|
||||||
|
request.flushScheduled = false;
|
||||||
if (enableFloat) {
|
if (enableFloat) {
|
||||||
writePostamble(destination, request.responseState);
|
writePostamble(destination, request.responseState);
|
||||||
}
|
}
|
||||||
|
@ -2411,7 +2430,27 @@ function flushCompletedQueues(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startWork(request: Request): void {
|
export function startWork(request: Request): void {
|
||||||
scheduleWork(() => performWork(request));
|
request.flushScheduled = request.destination !== null;
|
||||||
|
if (supportsRequestStorage) {
|
||||||
|
scheduleWork(() => requestStorage.run(request, performWork, request));
|
||||||
|
} else {
|
||||||
|
scheduleWork(() => performWork(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueFlush(request: Request): void {
|
||||||
|
if (
|
||||||
|
request.flushScheduled === false &&
|
||||||
|
// If there are pinged tasks we are going to flush anyway after work completes
|
||||||
|
request.pingedTasks.length === 0 &&
|
||||||
|
// If there is no destination there is nothing we can flush to. A flush will
|
||||||
|
// happen when we start flowing again
|
||||||
|
request.destination !== null
|
||||||
|
) {
|
||||||
|
const destination = request.destination;
|
||||||
|
request.flushScheduled = true;
|
||||||
|
scheduleWork(() => flushCompletedQueues(request, destination));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startFlowing(request: Request, destination: Destination): void {
|
export function startFlowing(request: Request, destination: Destination): void {
|
||||||
|
@ -2456,3 +2495,11 @@ export function abort(request: Request, reason: mixed): void {
|
||||||
fatalError(request, error);
|
fatalError(request, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flushResources(request: Request): void {
|
||||||
|
enqueueFlush(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResources(request: Request): Resources {
|
||||||
|
return request.resources;
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ import type {
|
||||||
ClientReferenceKey,
|
ClientReferenceKey,
|
||||||
ServerReference,
|
ServerReference,
|
||||||
ServerReferenceId,
|
ServerReferenceId,
|
||||||
|
Hints,
|
||||||
|
HintModel,
|
||||||
} from './ReactFlightServerConfig';
|
} from './ReactFlightServerConfig';
|
||||||
import type {ContextSnapshot} from './ReactFlightNewContext';
|
import type {ContextSnapshot} from './ReactFlightNewContext';
|
||||||
import type {ThenableState} from './ReactFlightThenable';
|
import type {ThenableState} from './ReactFlightThenable';
|
||||||
|
@ -44,6 +46,7 @@ import {
|
||||||
processErrorChunkProd,
|
processErrorChunkProd,
|
||||||
processErrorChunkDev,
|
processErrorChunkDev,
|
||||||
processReferenceChunk,
|
processReferenceChunk,
|
||||||
|
processHintChunk,
|
||||||
resolveClientReferenceMetadata,
|
resolveClientReferenceMetadata,
|
||||||
getServerReferenceId,
|
getServerReferenceId,
|
||||||
getServerReferenceBoundArguments,
|
getServerReferenceBoundArguments,
|
||||||
|
@ -52,6 +55,8 @@ import {
|
||||||
isServerReference,
|
isServerReference,
|
||||||
supportsRequestStorage,
|
supportsRequestStorage,
|
||||||
requestStorage,
|
requestStorage,
|
||||||
|
prepareHostDispatcher,
|
||||||
|
createHints,
|
||||||
} from './ReactFlightServerConfig';
|
} from './ReactFlightServerConfig';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -61,11 +66,7 @@ import {
|
||||||
getThenableStateAfterSuspending,
|
getThenableStateAfterSuspending,
|
||||||
resetHooksForRequest,
|
resetHooksForRequest,
|
||||||
} from './ReactFlightHooks';
|
} from './ReactFlightHooks';
|
||||||
import {
|
import {DefaultCacheDispatcher} from './flight/ReactFlightServerCache';
|
||||||
DefaultCacheDispatcher,
|
|
||||||
getCurrentCache,
|
|
||||||
setCurrentCache,
|
|
||||||
} from './ReactFlightCache';
|
|
||||||
import {
|
import {
|
||||||
pushProvider,
|
pushProvider,
|
||||||
popProvider,
|
popProvider,
|
||||||
|
@ -148,15 +149,18 @@ type Task = {
|
||||||
|
|
||||||
export type Request = {
|
export type Request = {
|
||||||
status: 0 | 1 | 2,
|
status: 0 | 1 | 2,
|
||||||
|
flushScheduled: boolean,
|
||||||
fatalError: mixed,
|
fatalError: mixed,
|
||||||
destination: null | Destination,
|
destination: null | Destination,
|
||||||
bundlerConfig: ClientManifest,
|
bundlerConfig: ClientManifest,
|
||||||
cache: Map<Function, mixed>,
|
cache: Map<Function, mixed>,
|
||||||
nextChunkId: number,
|
nextChunkId: number,
|
||||||
pendingChunks: number,
|
pendingChunks: number,
|
||||||
|
hints: Hints,
|
||||||
abortableTasks: Set<Task>,
|
abortableTasks: Set<Task>,
|
||||||
pingedTasks: Array<Task>,
|
pingedTasks: Array<Task>,
|
||||||
completedImportChunks: Array<Chunk>,
|
completedImportChunks: Array<Chunk>,
|
||||||
|
completedHintChunks: Array<Chunk>,
|
||||||
completedJSONChunks: Array<Chunk>,
|
completedJSONChunks: Array<Chunk>,
|
||||||
completedErrorChunks: Array<Chunk>,
|
completedErrorChunks: Array<Chunk>,
|
||||||
writtenSymbols: Map<symbol, number>,
|
writtenSymbols: Map<symbol, number>,
|
||||||
|
@ -196,21 +200,26 @@ export function createRequest(
|
||||||
'Currently React only supports one RSC renderer at a time.',
|
'Currently React only supports one RSC renderer at a time.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
prepareHostDispatcher();
|
||||||
ReactCurrentCache.current = DefaultCacheDispatcher;
|
ReactCurrentCache.current = DefaultCacheDispatcher;
|
||||||
|
|
||||||
const abortSet: Set<Task> = new Set();
|
const abortSet: Set<Task> = new Set();
|
||||||
const pingedTasks: Array<Task> = [];
|
const pingedTasks: Array<Task> = [];
|
||||||
|
const hints = createHints();
|
||||||
const request: Request = {
|
const request: Request = {
|
||||||
status: OPEN,
|
status: OPEN,
|
||||||
|
flushScheduled: false,
|
||||||
fatalError: null,
|
fatalError: null,
|
||||||
destination: null,
|
destination: null,
|
||||||
bundlerConfig,
|
bundlerConfig,
|
||||||
cache: new Map(),
|
cache: new Map(),
|
||||||
nextChunkId: 0,
|
nextChunkId: 0,
|
||||||
pendingChunks: 0,
|
pendingChunks: 0,
|
||||||
|
hints,
|
||||||
abortableTasks: abortSet,
|
abortableTasks: abortSet,
|
||||||
pingedTasks: pingedTasks,
|
pingedTasks: pingedTasks,
|
||||||
completedImportChunks: ([]: Array<Chunk>),
|
completedImportChunks: ([]: Array<Chunk>),
|
||||||
|
completedHintChunks: ([]: Array<Chunk>),
|
||||||
completedJSONChunks: ([]: Array<Chunk>),
|
completedJSONChunks: ([]: Array<Chunk>),
|
||||||
completedErrorChunks: ([]: Array<Chunk>),
|
completedErrorChunks: ([]: Array<Chunk>),
|
||||||
writtenSymbols: new Map(),
|
writtenSymbols: new Map(),
|
||||||
|
@ -232,6 +241,17 @@ export function createRequest(
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentRequest: null | Request = null;
|
||||||
|
|
||||||
|
export function resolveRequest(): null | Request {
|
||||||
|
if (currentRequest) return currentRequest;
|
||||||
|
if (supportsRequestStorage) {
|
||||||
|
const store = requestStorage.getStore();
|
||||||
|
if (store) return store;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function createRootContext(
|
function createRootContext(
|
||||||
reqContext?: Array<[string, ServerContextJSONValue]>,
|
reqContext?: Array<[string, ServerContextJSONValue]>,
|
||||||
) {
|
) {
|
||||||
|
@ -320,6 +340,23 @@ function serializeThenable(request: Request, thenable: Thenable<any>): number {
|
||||||
return newTask.id;
|
return newTask.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emitHint(
|
||||||
|
request: Request,
|
||||||
|
code: string,
|
||||||
|
model: HintModel,
|
||||||
|
): void {
|
||||||
|
emitHintChunk(request, code, model);
|
||||||
|
enqueueFlush(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHints(request: Request): Hints {
|
||||||
|
return request.hints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCache(request: Request): Map<Function, mixed> {
|
||||||
|
return request.cache;
|
||||||
|
}
|
||||||
|
|
||||||
function readThenable<T>(thenable: Thenable<T>): T {
|
function readThenable<T>(thenable: Thenable<T>): T {
|
||||||
if (thenable.status === 'fulfilled') {
|
if (thenable.status === 'fulfilled') {
|
||||||
return thenable.value;
|
return thenable.value;
|
||||||
|
@ -502,6 +539,7 @@ function pingTask(request: Request, task: Task): void {
|
||||||
const pingedTasks = request.pingedTasks;
|
const pingedTasks = request.pingedTasks;
|
||||||
pingedTasks.push(task);
|
pingedTasks.push(task);
|
||||||
if (pingedTasks.length === 1) {
|
if (pingedTasks.length === 1) {
|
||||||
|
request.flushScheduled = request.destination !== null;
|
||||||
scheduleWork(() => performWork(request));
|
scheduleWork(() => performWork(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1082,6 +1120,16 @@ function emitImportChunk(
|
||||||
request.completedImportChunks.push(processedChunk);
|
request.completedImportChunks.push(processedChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitHintChunk(request: Request, code: string, model: HintModel): void {
|
||||||
|
const processedChunk = processHintChunk(
|
||||||
|
request,
|
||||||
|
request.nextChunkId++,
|
||||||
|
code,
|
||||||
|
model,
|
||||||
|
);
|
||||||
|
request.completedHintChunks.push(processedChunk);
|
||||||
|
}
|
||||||
|
|
||||||
function emitSymbolChunk(request: Request, id: number, name: string): void {
|
function emitSymbolChunk(request: Request, id: number, name: string): void {
|
||||||
const symbolReference = serializeSymbolReference(name);
|
const symbolReference = serializeSymbolReference(name);
|
||||||
const processedChunk = processReferenceChunk(request, id, symbolReference);
|
const processedChunk = processReferenceChunk(request, id, symbolReference);
|
||||||
|
@ -1195,9 +1243,9 @@ function retryTask(request: Request, task: Task): void {
|
||||||
|
|
||||||
function performWork(request: Request): void {
|
function performWork(request: Request): void {
|
||||||
const prevDispatcher = ReactCurrentDispatcher.current;
|
const prevDispatcher = ReactCurrentDispatcher.current;
|
||||||
const prevCache = getCurrentCache();
|
|
||||||
ReactCurrentDispatcher.current = HooksDispatcher;
|
ReactCurrentDispatcher.current = HooksDispatcher;
|
||||||
setCurrentCache(request.cache);
|
const prevRequest = currentRequest;
|
||||||
|
currentRequest = request;
|
||||||
prepareToUseHooksForRequest(request);
|
prepareToUseHooksForRequest(request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1215,8 +1263,8 @@ function performWork(request: Request): void {
|
||||||
fatalError(request, error);
|
fatalError(request, error);
|
||||||
} finally {
|
} finally {
|
||||||
ReactCurrentDispatcher.current = prevDispatcher;
|
ReactCurrentDispatcher.current = prevDispatcher;
|
||||||
setCurrentCache(prevCache);
|
|
||||||
resetHooksForRequest();
|
resetHooksForRequest();
|
||||||
|
currentRequest = prevRequest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1250,6 +1298,21 @@ function flushCompletedChunks(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
importsChunks.splice(0, i);
|
importsChunks.splice(0, i);
|
||||||
|
|
||||||
|
// Next comes hints.
|
||||||
|
const hintChunks = request.completedHintChunks;
|
||||||
|
i = 0;
|
||||||
|
for (; i < hintChunks.length; i++) {
|
||||||
|
const chunk = hintChunks[i];
|
||||||
|
const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
|
||||||
|
if (!keepWriting) {
|
||||||
|
request.destination = null;
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hintChunks.splice(0, i);
|
||||||
|
|
||||||
// Next comes model data.
|
// Next comes model data.
|
||||||
const jsonChunks = request.completedJSONChunks;
|
const jsonChunks = request.completedJSONChunks;
|
||||||
i = 0;
|
i = 0;
|
||||||
|
@ -1264,6 +1327,7 @@ function flushCompletedChunks(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsonChunks.splice(0, i);
|
jsonChunks.splice(0, i);
|
||||||
|
|
||||||
// Finally, errors are sent. The idea is that it's ok to delay
|
// Finally, errors are sent. The idea is that it's ok to delay
|
||||||
// any error messages and prioritize display of other parts of
|
// any error messages and prioritize display of other parts of
|
||||||
// the page.
|
// the page.
|
||||||
|
@ -1281,6 +1345,7 @@ function flushCompletedChunks(
|
||||||
}
|
}
|
||||||
errorChunks.splice(0, i);
|
errorChunks.splice(0, i);
|
||||||
} finally {
|
} finally {
|
||||||
|
request.flushScheduled = false;
|
||||||
completeWriting(destination);
|
completeWriting(destination);
|
||||||
}
|
}
|
||||||
flushBuffered(destination);
|
flushBuffered(destination);
|
||||||
|
@ -1291,13 +1356,29 @@ function flushCompletedChunks(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startWork(request: Request): void {
|
export function startWork(request: Request): void {
|
||||||
|
request.flushScheduled = request.destination !== null;
|
||||||
if (supportsRequestStorage) {
|
if (supportsRequestStorage) {
|
||||||
scheduleWork(() => requestStorage.run(request.cache, performWork, request));
|
scheduleWork(() => requestStorage.run(request, performWork, request));
|
||||||
} else {
|
} else {
|
||||||
scheduleWork(() => performWork(request));
|
scheduleWork(() => performWork(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enqueueFlush(request: Request): void {
|
||||||
|
if (
|
||||||
|
request.flushScheduled === false &&
|
||||||
|
// If there are pinged tasks we are going to flush anyway after work completes
|
||||||
|
request.pingedTasks.length === 0 &&
|
||||||
|
// If there is no destination there is nothing we can flush to. A flush will
|
||||||
|
// happen when we start flowing again
|
||||||
|
request.destination !== null
|
||||||
|
) {
|
||||||
|
const destination = request.destination;
|
||||||
|
request.flushScheduled = true;
|
||||||
|
scheduleWork(() => flushCompletedChunks(request, destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function startFlowing(request: Request, destination: Destination): void {
|
export function startFlowing(request: Request, destination: Destination): void {
|
||||||
if (request.status === CLOSING) {
|
if (request.status === CLOSING) {
|
||||||
request.status = CLOSED;
|
request.status = CLOSED;
|
||||||
|
|
|
@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata =
|
||||||
export const getServerReferenceId = $$$config.getServerReferenceId;
|
export const getServerReferenceId = $$$config.getServerReferenceId;
|
||||||
export const getServerReferenceBoundArguments =
|
export const getServerReferenceBoundArguments =
|
||||||
$$$config.getServerReferenceBoundArguments;
|
$$$config.getServerReferenceBoundArguments;
|
||||||
|
export const prepareHostDispatcher = $$$config.prepareHostDispatcher;
|
||||||
|
|
|
@ -75,11 +75,6 @@ import type {Chunk} from './ReactServerStreamConfig';
|
||||||
|
|
||||||
export type {Destination, Chunk} from './ReactServerStreamConfig';
|
export type {Destination, Chunk} from './ReactServerStreamConfig';
|
||||||
|
|
||||||
export {
|
|
||||||
supportsRequestStorage,
|
|
||||||
requestStorage,
|
|
||||||
} from './ReactServerStreamConfig';
|
|
||||||
|
|
||||||
const stringify = JSON.stringify;
|
const stringify = JSON.stringify;
|
||||||
|
|
||||||
function serializeRowHeader(tag: string, id: number) {
|
function serializeRowHeader(tag: string, id: number) {
|
||||||
|
@ -156,6 +151,17 @@ export function processImportChunk(
|
||||||
return stringToChunk(row);
|
return stringToChunk(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function processHintChunk(
|
||||||
|
request: Request,
|
||||||
|
id: number,
|
||||||
|
code: string,
|
||||||
|
model: JSONValue,
|
||||||
|
): Chunk {
|
||||||
|
const json: string = stringify(model);
|
||||||
|
const row = serializeRowHeader('H' + code, id) + json + '\n';
|
||||||
|
return stringToChunk(row);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
scheduleWork,
|
scheduleWork,
|
||||||
flushBuffered,
|
flushBuffered,
|
||||||
|
|
|
@ -21,10 +21,6 @@ export function flushBuffered(destination: Destination) {
|
||||||
// transform streams. https://github.com/whatwg/streams/issues/960
|
// transform streams. https://github.com/whatwg/streams/issues/960
|
||||||
}
|
}
|
||||||
|
|
||||||
export const supportsRequestStorage = false;
|
|
||||||
export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
|
|
||||||
(null: any);
|
|
||||||
|
|
||||||
const VIEW_SIZE = 512;
|
const VIEW_SIZE = 512;
|
||||||
let currentView = null;
|
let currentView = null;
|
||||||
let writtenBytes = 0;
|
let writtenBytes = 0;
|
||||||
|
|
|
@ -26,10 +26,6 @@ export function flushBuffered(destination: Destination) {
|
||||||
// transform streams. https://github.com/whatwg/streams/issues/960
|
// transform streams. https://github.com/whatwg/streams/issues/960
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncLocalStorage is not available in bun
|
|
||||||
export const supportsRequestStorage = false;
|
|
||||||
export const requestStorage = (null: any);
|
|
||||||
|
|
||||||
export function beginWriting(destination: Destination) {}
|
export function beginWriting(destination: Destination) {}
|
||||||
|
|
||||||
export function writeChunk(
|
export function writeChunk(
|
||||||
|
|
|
@ -21,11 +21,6 @@ export function flushBuffered(destination: Destination) {
|
||||||
// transform streams. https://github.com/whatwg/streams/issues/960
|
// transform streams. https://github.com/whatwg/streams/issues/960
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, we get this from the global scope, but this will likely move to a module.
|
|
||||||
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
|
|
||||||
export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
|
|
||||||
supportsRequestStorage ? new AsyncLocalStorage() : (null: any);
|
|
||||||
|
|
||||||
const VIEW_SIZE = 512;
|
const VIEW_SIZE = 512;
|
||||||
let currentView = null;
|
let currentView = null;
|
||||||
let writtenBytes = 0;
|
let writtenBytes = 0;
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Writable} from 'stream';
|
import type {Writable} from 'stream';
|
||||||
|
|
||||||
import {TextEncoder} from 'util';
|
import {TextEncoder} from 'util';
|
||||||
import {AsyncLocalStorage} from 'async_hooks';
|
|
||||||
|
|
||||||
interface MightBeFlushable {
|
interface MightBeFlushable {
|
||||||
flush?: () => void;
|
flush?: () => void;
|
||||||
|
@ -34,10 +34,6 @@ export function flushBuffered(destination: Destination) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const supportsRequestStorage = true;
|
|
||||||
export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
|
|
||||||
new AsyncLocalStorage();
|
|
||||||
|
|
||||||
const VIEW_SIZE = 2048;
|
const VIEW_SIZE = 2048;
|
||||||
let currentView = null;
|
let currentView = null;
|
||||||
let writtenBytes = 0;
|
let writtenBytes = 0;
|
||||||
|
|
|
@ -9,24 +9,17 @@
|
||||||
|
|
||||||
import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes';
|
import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes';
|
||||||
|
|
||||||
import {
|
import {resolveRequest, getCache} from '../ReactFlightServer';
|
||||||
supportsRequestStorage,
|
|
||||||
requestStorage,
|
|
||||||
} from './ReactFlightServerConfig';
|
|
||||||
|
|
||||||
function createSignal(): AbortSignal {
|
function createSignal(): AbortSignal {
|
||||||
return new AbortController().signal;
|
return new AbortController().signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCache(): Map<Function, mixed> {
|
function resolveCache(): Map<Function, mixed> {
|
||||||
if (currentCache) return currentCache;
|
const request = resolveRequest();
|
||||||
if (supportsRequestStorage) {
|
if (request) {
|
||||||
const cache = requestStorage.getStore();
|
return getCache(request);
|
||||||
if (cache) return cache;
|
|
||||||
}
|
}
|
||||||
// Since we override the dispatcher all the time, we're effectively always
|
|
||||||
// active and so to support cache() and fetch() outside of render, we yield
|
|
||||||
// an empty Map.
|
|
||||||
return new Map();
|
return new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,16 +44,3 @@ export const DefaultCacheDispatcher: CacheDispatcher = {
|
||||||
return entry;
|
return entry;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentCache: Map<Function, mixed> | null = null;
|
|
||||||
|
|
||||||
export function setCurrentCache(
|
|
||||||
cache: Map<Function, mixed> | null,
|
|
||||||
): Map<Function, mixed> | null {
|
|
||||||
currentCache = cache;
|
|
||||||
return currentCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentCache(): Map<Function, mixed> | null {
|
|
||||||
return currentCache;
|
|
||||||
}
|
|
|
@ -23,6 +23,8 @@
|
||||||
// So `$$$config` looks like a global variable, but it's
|
// So `$$$config` looks like a global variable, but it's
|
||||||
// really an argument to a top-level wrapping function.
|
// really an argument to a top-level wrapping function.
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
declare var $$$config: any;
|
declare var $$$config: any;
|
||||||
export opaque type Destination = mixed; // eslint-disable-line no-undef
|
export opaque type Destination = mixed; // eslint-disable-line no-undef
|
||||||
export opaque type ResponseState = mixed;
|
export opaque type ResponseState = mixed;
|
||||||
|
@ -33,6 +35,9 @@ export opaque type SuspenseBoundaryID = mixed;
|
||||||
|
|
||||||
export const isPrimaryRenderer = false;
|
export const isPrimaryRenderer = false;
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
||||||
export const getChildFormatContext = $$$config.getChildFormatContext;
|
export const getChildFormatContext = $$$config.getChildFormatContext;
|
||||||
export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID =
|
export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID =
|
||||||
$$$config.UNINITIALIZED_SUSPENSE_BOUNDARY_ID;
|
$$$config.UNINITIALIZED_SUSPENSE_BOUNDARY_ID;
|
||||||
|
@ -68,8 +73,7 @@ export const writeCompletedBoundaryInstruction =
|
||||||
$$$config.writeCompletedBoundaryInstruction;
|
$$$config.writeCompletedBoundaryInstruction;
|
||||||
export const writeClientRenderBoundaryInstruction =
|
export const writeClientRenderBoundaryInstruction =
|
||||||
$$$config.writeClientRenderBoundaryInstruction;
|
$$$config.writeClientRenderBoundaryInstruction;
|
||||||
export const prepareToRender = $$$config.prepareToRender;
|
export const prepareHostDispatcher = $$$config.prepareHostDispatcher;
|
||||||
export const cleanupAfterRender = $$$config.cleanupAfterRender;
|
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Resources
|
// Resources
|
||||||
|
|
|
@ -6,5 +6,9 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,5 +6,9 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,5 +6,12 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||||
|
|
||||||
|
// For now, we get this from the global scope, but this will likely move to a module.
|
||||||
|
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
|
||||||
|
? new AsyncLocalStorage()
|
||||||
|
: (null: any);
|
||||||
|
|
|
@ -6,5 +6,9 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,5 +6,12 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import {AsyncLocalStorage} from 'async_hooks';
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = true;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> =
|
||||||
|
new AsyncLocalStorage();
|
||||||
|
|
|
@ -7,4 +7,12 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AsyncLocalStorage} from 'async_hooks';
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = true;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> =
|
||||||
|
new AsyncLocalStorage();
|
||||||
|
|
|
@ -6,5 +6,9 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,5 +6,9 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFizzServer';
|
||||||
|
|
||||||
export * from 'react-native-renderer/src/server/ReactFizzConfigNative';
|
export * from 'react-native-renderer/src/server/ReactFizzConfigNative';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,8 +6,21 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from '../ReactFlightServerConfigBundlerCustom';
|
export * from '../ReactFlightServerConfigBundlerCustom';
|
||||||
|
|
||||||
|
export type Hints = null;
|
||||||
|
export type HintModel = '';
|
||||||
|
|
||||||
export const isPrimaryRenderer = false;
|
export const isPrimaryRenderer = false;
|
||||||
|
|
||||||
|
export const prepareHostDispatcher = () => {};
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
||||||
|
export function createHints(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
||||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from '../ReactFlightServerConfigBundlerCustom';
|
export * from '../ReactFlightServerConfigBundlerCustom';
|
||||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,7 +6,14 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
||||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
// For now, we get this from the global scope, but this will likely move to a module.
|
||||||
|
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
|
||||||
|
? new AsyncLocalStorage()
|
||||||
|
: (null: any);
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
||||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = false;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||||
|
|
|
@ -6,7 +6,14 @@
|
||||||
*
|
*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import {AsyncLocalStorage} from 'async_hooks';
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
||||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = true;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> =
|
||||||
|
new AsyncLocalStorage();
|
||||||
|
|
|
@ -7,6 +7,14 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AsyncLocalStorage} from 'async_hooks';
|
||||||
|
|
||||||
|
import type {Request} from 'react-server/src/ReactFlightServer';
|
||||||
|
|
||||||
export * from '../ReactFlightServerConfigStream';
|
export * from '../ReactFlightServerConfigStream';
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
|
||||||
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
|
||||||
|
|
||||||
|
export const supportsRequestStorage = true;
|
||||||
|
export const requestStorage: AsyncLocalStorage<Request> =
|
||||||
|
new AsyncLocalStorage();
|
||||||
|
|
|
@ -35,8 +35,6 @@ export const writeChunk = $$$config.writeChunk;
|
||||||
export const writeChunkAndReturn = $$$config.writeChunkAndReturn;
|
export const writeChunkAndReturn = $$$config.writeChunkAndReturn;
|
||||||
export const completeWriting = $$$config.completeWriting;
|
export const completeWriting = $$$config.completeWriting;
|
||||||
export const flushBuffered = $$$config.flushBuffered;
|
export const flushBuffered = $$$config.flushBuffered;
|
||||||
export const supportsRequestStorage = $$$config.supportsRequestStorage;
|
|
||||||
export const requestStorage = $$$config.requestStorage;
|
|
||||||
export const close = $$$config.close;
|
export const close = $$$config.close;
|
||||||
export const closeWithError = $$$config.closeWithError;
|
export const closeWithError = $$$config.closeWithError;
|
||||||
export const stringToChunk = $$$config.stringToChunk;
|
export const stringToChunk = $$$config.stringToChunk;
|
||||||
|
|
|
@ -461,5 +461,6 @@
|
||||||
"473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it.",
|
"473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it.",
|
||||||
"474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.",
|
"474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.",
|
||||||
"475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.",
|
"475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.",
|
||||||
"476": "Expected the form instance to be a HostComponent. This is a bug in React."
|
"476": "Expected the form instance to be a HostComponent. This is a bug in React.",
|
||||||
|
"477": "React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React."
|
||||||
}
|
}
|
||||||
|
|
|
@ -386,7 +386,7 @@ const bundles = [
|
||||||
global: 'ReactServerDOMClient',
|
global: 'ReactServerDOMClient',
|
||||||
minifyWithProdErrorCodes: false,
|
minifyWithProdErrorCodes: false,
|
||||||
wrapWithModuleBoundaries: false,
|
wrapWithModuleBoundaries: false,
|
||||||
externals: ['react'],
|
externals: ['react', 'react-dom'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bundleTypes: [NODE_DEV, NODE_PROD],
|
bundleTypes: [NODE_DEV, NODE_PROD],
|
||||||
|
@ -395,7 +395,7 @@ const bundles = [
|
||||||
global: 'ReactServerDOMClient',
|
global: 'ReactServerDOMClient',
|
||||||
minifyWithProdErrorCodes: false,
|
minifyWithProdErrorCodes: false,
|
||||||
wrapWithModuleBoundaries: false,
|
wrapWithModuleBoundaries: false,
|
||||||
externals: ['react', 'util'],
|
externals: ['react', 'react-dom', 'util'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bundleTypes: [NODE_DEV, NODE_PROD],
|
bundleTypes: [NODE_DEV, NODE_PROD],
|
||||||
|
@ -404,7 +404,7 @@ const bundles = [
|
||||||
global: 'ReactServerDOMClient',
|
global: 'ReactServerDOMClient',
|
||||||
minifyWithProdErrorCodes: false,
|
minifyWithProdErrorCodes: false,
|
||||||
wrapWithModuleBoundaries: false,
|
wrapWithModuleBoundaries: false,
|
||||||
externals: ['react', 'util'],
|
externals: ['react', 'react-dom', 'util'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bundleTypes: [NODE_DEV, NODE_PROD],
|
bundleTypes: [NODE_DEV, NODE_PROD],
|
||||||
|
@ -413,7 +413,7 @@ const bundles = [
|
||||||
global: 'ReactServerDOMClient',
|
global: 'ReactServerDOMClient',
|
||||||
minifyWithProdErrorCodes: false,
|
minifyWithProdErrorCodes: false,
|
||||||
wrapWithModuleBoundaries: false,
|
wrapWithModuleBoundaries: false,
|
||||||
externals: ['react'],
|
externals: ['react', 'react-dom'],
|
||||||
},
|
},
|
||||||
|
|
||||||
/******* React Server DOM Webpack Plugin *******/
|
/******* React Server DOM Webpack Plugin *******/
|
||||||
|
|
Loading…
Reference in New Issue