Patch Promise cycles and toString on Server Functions

[Flight] Move `react-server-dom-webpack/*.unbundled` to private `react-server-dom-unbundled` (#35290)

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
This commit is contained in:
Sebastian Markbage
2025-12-10 02:25:36 -05:00
committed by Sebastian Sebbie Silbermann
parent 0eeded69c1
commit dd0519822a
58 changed files with 3389 additions and 369 deletions

View File

@@ -331,6 +331,7 @@ module.exports = {
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-server-dom-unbundled/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',

View File

@@ -45,7 +45,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -59,10 +59,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
@@ -71,7 +69,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
@@ -86,7 +84,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -102,10 +100,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
@@ -116,7 +112,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
@@ -158,10 +154,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -188,10 +182,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -220,7 +212,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -278,10 +270,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -310,7 +300,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -353,10 +343,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -444,10 +432,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -487,10 +473,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -552,10 +536,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -592,10 +574,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -744,10 +724,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -806,10 +784,8 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile

View File

@@ -3,7 +3,7 @@ import {
load as reactLoad,
getSource as getSourceImpl,
transformSource as reactTransformSource,
} from 'react-server-dom-webpack/node-loader';
} from 'react-server-dom-unbundled/node-loader';
export {resolve};

View File

@@ -36,7 +36,7 @@ const http = require('http');
const React = require('react');
const {renderToPipeableStream} = require('react-dom/server');
const {createFromNodeStream} = require('react-server-dom-webpack/client');
const {createFromNodeStream} = require('react-server-dom-unbundled/client');
const {PassThrough} = require('stream');
const app = express();

View File

@@ -5,7 +5,8 @@
const path = require('path');
const url = require('url');
const register = require('react-server-dom-webpack/node-register');
const register = require('react-server-dom-unbundled/node-register');
// TODO: This seems to have no effect anymore. Remove?
register();
const babelRegister = require('@babel/register');
@@ -76,7 +77,7 @@ function getDebugChannel(req) {
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
'react-server-dom-unbundled/server'
);
// const m = require('../src/App.js');
const m = await import('../src/App.js');
@@ -134,7 +135,7 @@ async function renderApp(res, returnValue, formState, noCache, debugChannel) {
async function prerenderApp(res, returnValue, formState, noCache) {
const {prerenderToNodeStream} = await import(
'react-server-dom-webpack/static'
'react-server-dom-unbundled/static'
);
// const m = require('../src/App.js');
const m = await import('../src/App.js');
@@ -202,7 +203,7 @@ app.get('/', async function (req, res) {
app.post('/', bodyParser.text(), async function (req, res) {
const noCache = req.headers['cache-control'] === 'no-cache';
const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} =
await import('react-server-dom-webpack/server');
await import('react-server-dom-unbundled/server');
const serverReference = req.get('rsc-action');
if (serverReference) {
// This is the client-side case

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import {renderToReadableStream} from 'react-server-dom-webpack/server';
import {renderToReadableStream} from 'react-server-dom-unbundled/server';
import {createFromReadableStream} from 'react-server-dom-webpack/client';
import {PassThrough, Readable} from 'stream';

View File

@@ -126,7 +126,7 @@
"build-for-devtools": "cross-env yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE --release-channel=experimental",
"build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
"build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
"build-for-vt-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react-dom/index,react-dom/client,react-dom/server,react-dom-server.node,react-dom-server-legacy.node,scheduler --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental",
"flow-typed-install": "yarn flow-typed install --skip --skipFlowRestart --ignore-deps=dev",
"linc": "node ./scripts/tasks/linc.js",

View File

@@ -623,6 +623,22 @@ function wakeChunkIfInitialized<T>(
rejectListeners.splice(rejectionIdx, 1);
}
}
// The status might have changed after fulfilling the reference.
switch ((chunk: SomeChunk<T>).status) {
case INITIALIZED:
const initializedChunk: InitializedChunk<T> = (chunk: any);
wakeChunk(
resolveListeners,
initializedChunk.value,
initializedChunk,
);
return;
case ERRORED:
if (rejectListeners !== null) {
rejectChunk(rejectListeners, chunk.reason);
}
return;
}
}
}
}
@@ -834,6 +850,7 @@ function resolveModuleChunk<T>(
const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
resolvedChunk.status = RESOLVED_MODULE;
resolvedChunk.value = value;
resolvedChunk.reason = null;
if (__DEV__) {
const debugInfo = getModuleDebugInfo(value);
if (debugInfo !== null) {
@@ -1055,6 +1072,8 @@ export function reportGlobalError(
// because we won't be getting any new data to resolve it.
if (chunk.status === PENDING) {
triggerErrorOnChunk(response, chunk, error);
} else if (chunk.status === INITIALIZED && chunk.reason !== null) {
chunk.reason.error(error);
}
});
if (__DEV__) {
@@ -1402,15 +1421,91 @@ function fulfillReference(
): void {
const {response, handler, parentObject, key, map, path} = reference;
for (let i = 1; i < path.length; i++) {
try {
for (let i = 1; i < path.length; i++) {
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// We never expect to see a Lazy node on this path because we encode those as
// separate models. This must mean that we have inserted an extra lazy node
// e.g. to replace a blocked element. We must instead look for it inside.
const referencedChunk: SomeChunk<any> = value._payload;
if (referencedChunk === handler.chunk) {
// This is a reference to the thing we're currently blocking. We can peak
// inside of it to get the value.
value = handler.value;
continue;
} else {
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
case BLOCKED: {
// It is possible that we're blocked on our own chunk if it's a cycle.
// Before adding the listener to the inner chunk, let's check if it would
// result in a cycle.
const cyclicHandler = resolveBlockedCycle(
referencedChunk,
reference,
);
if (cyclicHandler !== null) {
// This reference points back to this chunk. We can resolve the cycle by
// using the value from that handler.
value = cyclicHandler.value;
continue;
}
// Fallthrough
}
case PENDING: {
// If we're not yet initialized we need to skip what we've already drilled
// through and then wait for the next value to become available.
path.splice(0, i - 1);
// Add "listener" to our new chunk dependency.
if (referencedChunk.value === null) {
referencedChunk.value = [reference];
} else {
referencedChunk.value.push(reference);
}
if (referencedChunk.reason === null) {
referencedChunk.reason = [reference];
} else {
referencedChunk.reason.push(reference);
}
return;
}
case HALTED: {
// Do nothing. We couldn't fulfill.
// TODO: Mark downstreams as halted too.
return;
}
default: {
rejectReference(reference, referencedChunk.reason);
return;
}
}
}
}
value = value[path[i]];
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// We never expect to see a Lazy node on this path because we encode those as
// separate models. This must mean that we have inserted an extra lazy node
// e.g. to replace a blocked element. We must instead look for it inside.
// If what we're referencing is a Lazy it must be because we inserted one as a virtual node
// while it was blocked by other data. If it's no longer blocked, we can unwrap it.
const referencedChunk: SomeChunk<any> = value._payload;
if (referencedChunk === handler.chunk) {
// This is a reference to the thing we're currently blocking. We can peak
@@ -1431,128 +1526,57 @@ function fulfillReference(
value = referencedChunk.value;
continue;
}
case BLOCKED: {
// It is possible that we're blocked on our own chunk if it's a cycle.
// Before adding the listener to the inner chunk, let's check if it would
// result in a cycle.
const cyclicHandler = resolveBlockedCycle(
referencedChunk,
reference,
);
if (cyclicHandler !== null) {
// This reference points back to this chunk. We can resolve the cycle by
// using the value from that handler.
value = cyclicHandler.value;
continue;
}
// Fallthrough
}
case PENDING: {
// If we're not yet initialized we need to skip what we've already drilled
// through and then wait for the next value to become available.
path.splice(0, i - 1);
// Add "listener" to our new chunk dependency.
if (referencedChunk.value === null) {
referencedChunk.value = [reference];
} else {
referencedChunk.value.push(reference);
}
if (referencedChunk.reason === null) {
referencedChunk.reason = [reference];
} else {
referencedChunk.reason.push(reference);
}
return;
}
case HALTED: {
// Do nothing. We couldn't fulfill.
// TODO: Mark downstreams as halted too.
return;
}
default: {
rejectReference(reference, referencedChunk.reason);
return;
}
}
}
break;
}
value = value[path[i]];
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// If what we're referencing is a Lazy it must be because we inserted one as a virtual node
// while it was blocked by other data. If it's no longer blocked, we can unwrap it.
const referencedChunk: SomeChunk<any> = value._payload;
if (referencedChunk === handler.chunk) {
// This is a reference to the thing we're currently blocking. We can peak
// inside of it to get the value.
value = handler.value;
continue;
} else {
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && handler.value === null) {
handler.value = mappedValue;
}
// If the parent object is an unparsed React element tuple, we also need to
// update the props and owner of the parsed element object (i.e.
// handler.value).
if (
parentObject[0] === REACT_ELEMENT_TYPE &&
typeof handler.value === 'object' &&
handler.value !== null &&
handler.value.$$typeof === REACT_ELEMENT_TYPE
) {
const element: any = handler.value;
switch (key) {
case '3':
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
element.props = mappedValue;
break;
case RESOLVED_MODULE:
initializeModuleChunk(referencedChunk);
case '4':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._owner = mappedValue;
}
break;
case '5':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._debugStack = mappedValue;
}
break;
default:
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
}
} else if (__DEV__ && !reference.isDebug) {
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
}
break;
}
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && handler.value === null) {
handler.value = mappedValue;
}
// If the parent object is an unparsed React element tuple, we also need to
// update the props and owner of the parsed element object (i.e.
// handler.value).
if (
parentObject[0] === REACT_ELEMENT_TYPE &&
typeof handler.value === 'object' &&
handler.value !== null &&
handler.value.$$typeof === REACT_ELEMENT_TYPE
) {
const element: any = handler.value;
switch (key) {
case '3':
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
element.props = mappedValue;
break;
case '4':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._owner = mappedValue;
}
break;
case '5':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._debugStack = mappedValue;
}
break;
default:
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
break;
}
} else if (__DEV__ && !reference.isDebug) {
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
} catch (error) {
rejectReference(reference, error);
return;
}
handler.deps--;
@@ -1816,6 +1840,7 @@ function loadServerReference<A: Iterable<any>, T>(
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = handler.value;
initializedChunk.reason = null;
if (resolveListeners !== null) {
wakeChunk(resolveListeners, handler.value, initializedChunk);
}
@@ -2289,7 +2314,7 @@ function parseModelString(
// Symbol
return Symbol.for(value.slice(2));
}
case 'F': {
case 'h': {
// Server Reference
const ref = value.slice(2);
return getOutlinedModel(
@@ -3044,6 +3069,7 @@ function startReadableStream<T>(
streamState: StreamState,
): void {
let controller: ReadableStreamController = (null: any);
let closed = false;
const stream = new ReadableStream({
type: type,
start(c) {
@@ -3101,6 +3127,10 @@ function startReadableStream<T>(
}
},
close(json: UninitializedModel): void {
if (closed) {
return;
}
closed = true;
if (previousBlockedChunk === null) {
controller.close();
} else {
@@ -3111,6 +3141,10 @@ function startReadableStream<T>(
}
},
error(error: mixed): void {
if (closed) {
return;
}
closed = true;
if (previousBlockedChunk === null) {
// $FlowFixMe[incompatible-call]
controller.error(error);
@@ -3171,6 +3205,7 @@ function startAsyncIterable<T>(
(chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = {done: false, value: value};
initializedChunk.reason = null;
if (resolveListeners !== null) {
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
}
@@ -3195,6 +3230,9 @@ function startAsyncIterable<T>(
nextWriteIndex++;
},
close(value: UninitializedModel): void {
if (closed) {
return;
}
closed = true;
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
@@ -3222,6 +3260,9 @@ function startAsyncIterable<T>(
}
},
error(error: Error): void {
if (closed) {
return;
}
closed = true;
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] =

View File

@@ -104,7 +104,7 @@ function serializePromiseID(id: number): string {
}
function serializeServerReferenceID(id: number): string {
return '$F' + id.toString(16);
return '$h' + id.toString(16);
}
function serializeTemporaryReferenceMarker(): string {
@@ -112,7 +112,6 @@ function serializeTemporaryReferenceMarker(): string {
}
function serializeFormDataReference(id: number): string {
// Why K? F is "Function". D is "Date". What else?
return '$K' + id.toString(16);
}
@@ -474,8 +473,22 @@ export function processReply(
}
}
const existingReference = writtenObjects.get(value);
// $FlowFixMe[method-unbinding]
if (typeof value.then === 'function') {
if (existingReference !== undefined) {
if (modelRoot === value) {
// This is the ID we're currently emitting so we need to write it
// once but if we discover it again, we refer to it by id.
modelRoot = null;
} else {
// We've already emitted this as an outlined object, so we can
// just refer to that by its existing ID.
return existingReference;
}
}
// We assume that any object with a .then property is a "Thenable" type,
// or a Promise type. Either of which can be represented by a Promise.
if (formData === null) {
@@ -484,11 +497,19 @@ export function processReply(
}
pendingParts++;
const promiseId = nextPartId++;
const promiseReference = serializePromiseID(promiseId);
writtenObjects.set(value, promiseReference);
const thenable: Thenable<any> = (value: any);
thenable.then(
partValue => {
try {
const partJSON = serializeModel(partValue, promiseId);
const previousReference = writtenObjects.get(partValue);
let partJSON;
if (previousReference !== undefined) {
partJSON = JSON.stringify(previousReference);
} else {
partJSON = serializeModel(partValue, promiseId);
}
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
const data: FormData = formData;
data.append(formFieldPrefix + promiseId, partJSON);
@@ -504,10 +525,9 @@ export function processReply(
// that throws on the server instead.
reject,
);
return serializePromiseID(promiseId);
return promiseReference;
}
const existingReference = writtenObjects.get(value);
if (existingReference !== undefined) {
if (modelRoot === value) {
// This is the ID we're currently emitting so we need to write it

View File

@@ -6,13 +6,13 @@
*
* @flow
*/
export {default as rendererVersion} from 'shared/ReactVersion';
export const rendererPackageName = 'react-server-dom-webpack';
export const rendererPackageName = 'react-server-dom-unbundled';
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer';
export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode';
export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;

View File

@@ -6,13 +6,13 @@
*
* @flow
*/
export {default as rendererVersion} from 'shared/ReactVersion';
export const rendererPackageName = 'react-server-dom-webpack';
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer';
export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;

View File

@@ -88,6 +88,12 @@ function bind(this: ServerReference<any>): any {
return newFn;
}
const serverReferenceToString = {
value: () => 'function () { [omitted code] }',
configurable: true,
writable: true,
};
export function registerServerReference<T: Function>(
reference: T,
id: string,
@@ -111,12 +117,14 @@ export function registerServerReference<T: Function>(
configurable: true,
},
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}
: {
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}) as PropertyDescriptorMap,
);
}

View File

@@ -95,6 +95,12 @@ function bind(this: ServerReference<any>): any {
return newFn;
}
const serverReferenceToString = {
value: () => 'function () { [omitted code] }',
configurable: true,
writable: true,
};
export function registerServerReference<T>(
reference: ServerReference<T>,
id: string,
@@ -118,12 +124,14 @@ export function registerServerReference<T>(
configurable: true,
},
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}
: {
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}) as PropertyDescriptorMap,
);
}

View File

@@ -102,6 +102,12 @@ function bind(this: ServerReference<any>): any {
return newFn;
}
const serverReferenceToString = {
value: () => 'function () { [omitted code] }',
configurable: true,
writable: true,
};
export function registerServerReference<T: Function>(
reference: T,
id: string,
@@ -125,12 +131,14 @@ export function registerServerReference<T: Function>(
configurable: true,
},
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}
: {
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}) as PropertyDescriptorMap,
);
}

View File

@@ -0,0 +1,5 @@
# react-server-dom-unbundled
Test-only React Flight bindings for DOM using Node.js.
This only exists for internal testing.

View File

@@ -7,4 +7,4 @@
* @flow
*/
export * from './src/client/react-flight-dom-client.node.unbundled';
export * from './src/client/react-flight-dom-client.node';

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from '../src/ReactFlightUnbundledNodeLoader.js';

View File

@@ -0,0 +1,10 @@
/**
* 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
*/
throw new Error('Use react-server-dom-webpack/client instead.');

View File

@@ -0,0 +1,10 @@
/**
* 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
*/
module.exports = require('./src/ReactFlightUnbundledNodeRegister');

View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-server-dom-unbundled-client.node.production.js');
} else {
module.exports = require('./cjs/react-server-dom-unbundled-client.node.development.js');
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,12 @@
/**
* 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
*/
'use strict';
throw new Error('Use react-server-dom-unbundled/client instead.');

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = require('./cjs/react-server-dom-unbundled-node-register.js');

View File

@@ -0,0 +1,6 @@
'use strict';
throw new Error(
'The React Server Writer cannot be used outside a react-server environment. ' +
'You must configure Node.js using the `--conditions react-server` flag.'
);

View File

@@ -0,0 +1,20 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-unbundled-server.node.production.js');
} else {
s = require('./cjs/react-server-dom-unbundled-server.node.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
exports.decodeReply = s.decodeReply;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.registerServerReference = s.registerServerReference;
exports.registerClientReference = s.registerClientReference;
exports.createClientModuleProxy = s.createClientModuleProxy;
exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet;

View File

@@ -0,0 +1,6 @@
'use strict';
throw new Error(
'The React Server Writer cannot be used outside a react-server environment. ' +
'You must configure Node.js using the `--conditions react-server` flag.'
);

View File

@@ -0,0 +1,11 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-unbundled-server.node.production.js');
} else {
s = require('./cjs/react-server-dom-unbundled-server.node.development.js');
}
exports.prerender = s.prerender;
exports.prerenderToNodeStream = s.prerenderToNodeStream;

View File

@@ -0,0 +1,46 @@
{
"name": "react-server-dom-unbundled",
"description": "React Server Components bindings for DOM using Node.js. This only exists for internal testing.",
"version": "0.0.0",
"private": true,
"files": [
"LICENSE",
"README.md",
"index.js",
"client.js",
"server.js",
"server.node.js",
"static.js",
"static.node.js",
"node-register.js",
"cjs/",
"esm/"
],
"exports": {
".": "./index.js",
"./client": "./client.js",
"./server": {
"react-server": "./server.node.js",
"default": "./server.js"
},
"./server.node": "./server.node.js",
"./static": {
"react-server": "./static.node.js",
"default": "./static.js"
},
"./static.node": "./static.node.js",
"./node-loader": "./esm/react-server-dom-unbundled-node-loader.production.js",
"./node-register": "./node-register.js",
"./src/*": "./src/*.js",
"./package.json": "./package.json"
},
"main": "index.js",
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"dependencies": {
"acorn-loose": "^8.3.0",
"webpack-sources": "^3.2.0"
}
}

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
throw new Error(
'The React Server cannot be used outside a react-server environment. ' +
'You must configure Node.js using the `--conditions react-server` flag.',
);

View File

@@ -19,4 +19,4 @@ export {
registerClientReference,
createClientModuleProxy,
createTemporaryReferenceSet,
} from './src/server/react-flight-dom-server.node.unbundled';
} from './src/server/react-flight-dom-server.node';

View File

@@ -0,0 +1,804 @@
/**
* 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 * as acorn from 'acorn-loose';
import readMappings from 'webpack-sources/lib/helpers/readMappings.js';
import createMappingsSerializer from 'webpack-sources/lib/helpers/createMappingsSerializer.js';
type ResolveContext = {
conditions: Array<string>,
parentURL: string | void,
};
type ResolveFunction = (
string,
ResolveContext,
ResolveFunction,
) => {url: string} | Promise<{url: string}>;
type GetSourceContext = {
format: string,
};
type GetSourceFunction = (
string,
GetSourceContext,
GetSourceFunction,
) => Promise<{source: Source}>;
type TransformSourceContext = {
format: string,
url: string,
};
type TransformSourceFunction = (
Source,
TransformSourceContext,
TransformSourceFunction,
) => Promise<{source: Source}>;
type LoadContext = {
conditions: Array<string>,
format: string | null | void,
importAssertions: Object,
};
type LoadFunction = (
string,
LoadContext,
LoadFunction,
) => Promise<{format: string, shortCircuit?: boolean, source: Source}>;
type Source = string | ArrayBuffer | Uint8Array;
let warnedAboutConditionsFlag = false;
let stashedGetSource: null | GetSourceFunction = null;
let stashedResolve: null | ResolveFunction = null;
export async function resolve(
specifier: string,
context: ResolveContext,
defaultResolve: ResolveFunction,
): Promise<{url: string}> {
// We stash this in case we end up needing to resolve export * statements later.
stashedResolve = defaultResolve;
if (!context.conditions.includes('react-server')) {
context = {
...context,
conditions: [...context.conditions, 'react-server'],
};
if (!warnedAboutConditionsFlag) {
warnedAboutConditionsFlag = true;
// eslint-disable-next-line react-internal/no-production-logging
console.warn(
'You did not run Node.js with the `--conditions react-server` flag. ' +
'Any "react-server" override will only work with ESM imports.',
);
}
}
return await defaultResolve(specifier, context, defaultResolve);
}
export async function getSource(
url: string,
context: GetSourceContext,
defaultGetSource: GetSourceFunction,
): Promise<{source: Source}> {
// We stash this in case we end up needing to resolve export * statements later.
stashedGetSource = defaultGetSource;
return defaultGetSource(url, context, defaultGetSource);
}
type ExportedEntry = {
localName: string,
exportedName: string,
type: null | string,
loc: {
start: {line: number, column: number},
end: {line: number, column: number},
},
originalLine: number,
originalColumn: number,
originalSource: number,
nameIndex: number,
};
function addExportedEntry(
exportedEntries: Array<ExportedEntry>,
localNames: Set<string>,
localName: string,
exportedName: string,
type: null | 'function',
loc: {
start: {line: number, column: number},
end: {line: number, column: number},
},
) {
if (localNames.has(localName)) {
// If the same local name is exported more than once, we only need one of the names.
return;
}
exportedEntries.push({
localName,
exportedName,
type,
loc,
originalLine: -1,
originalColumn: -1,
originalSource: -1,
nameIndex: -1,
});
}
function addLocalExportedNames(
exportedEntries: Array<ExportedEntry>,
localNames: Set<string>,
node: any,
) {
switch (node.type) {
case 'Identifier':
addExportedEntry(
exportedEntries,
localNames,
node.name,
node.name,
null,
node.loc,
);
return;
case 'ObjectPattern':
for (let i = 0; i < node.properties.length; i++)
addLocalExportedNames(exportedEntries, localNames, node.properties[i]);
return;
case 'ArrayPattern':
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (element)
addLocalExportedNames(exportedEntries, localNames, element);
}
return;
case 'Property':
addLocalExportedNames(exportedEntries, localNames, node.value);
return;
case 'AssignmentPattern':
addLocalExportedNames(exportedEntries, localNames, node.left);
return;
case 'RestElement':
addLocalExportedNames(exportedEntries, localNames, node.argument);
return;
case 'ParenthesizedExpression':
addLocalExportedNames(exportedEntries, localNames, node.expression);
return;
}
}
function transformServerModule(
source: string,
program: any,
url: string,
sourceMap: any,
loader: LoadFunction,
): string {
const body = program.body;
// This entry list needs to be in source location order.
const exportedEntries: Array<ExportedEntry> = [];
// Dedupe set.
const localNames: Set<string> = new Set();
for (let i = 0; i < body.length; i++) {
const node = body[i];
switch (node.type) {
case 'ExportAllDeclaration':
// If export * is used, the other file needs to explicitly opt into "use server" too.
break;
case 'ExportDefaultDeclaration':
if (node.declaration.type === 'Identifier') {
addExportedEntry(
exportedEntries,
localNames,
node.declaration.name,
'default',
null,
node.declaration.loc,
);
} else if (node.declaration.type === 'FunctionDeclaration') {
if (node.declaration.id) {
addExportedEntry(
exportedEntries,
localNames,
node.declaration.id.name,
'default',
'function',
node.declaration.id.loc,
);
} else {
// TODO: This needs to be rewritten inline because it doesn't have a local name.
}
}
continue;
case 'ExportNamedDeclaration':
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
const declarations = node.declaration.declarations;
for (let j = 0; j < declarations.length; j++) {
addLocalExportedNames(
exportedEntries,
localNames,
declarations[j].id,
);
}
} else {
const name = node.declaration.id.name;
addExportedEntry(
exportedEntries,
localNames,
name,
name,
node.declaration.type === 'FunctionDeclaration'
? 'function'
: null,
node.declaration.id.loc,
);
}
}
if (node.specifiers) {
const specifiers = node.specifiers;
for (let j = 0; j < specifiers.length; j++) {
const specifier = specifiers[j];
addExportedEntry(
exportedEntries,
localNames,
specifier.local.name,
specifier.exported.name,
null,
specifier.local.loc,
);
}
}
continue;
}
}
let mappings =
sourceMap && typeof sourceMap.mappings === 'string'
? sourceMap.mappings
: '';
let newSrc = source;
if (exportedEntries.length > 0) {
let lastSourceIndex = 0;
let lastOriginalLine = 0;
let lastOriginalColumn = 0;
let lastNameIndex = 0;
let sourceLineCount = 0;
let lastMappedLine = 0;
if (sourceMap) {
// We iterate source mapping entries and our matched exports in parallel to source map
// them to their original location.
let nextEntryIdx = 0;
let nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line;
let nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column;
readMappings(
mappings,
(
generatedLine: number,
generatedColumn: number,
sourceIndex: number,
originalLine: number,
originalColumn: number,
nameIndex: number,
) => {
if (
generatedLine > nextEntryLine ||
(generatedLine === nextEntryLine &&
generatedColumn > nextEntryColumn)
) {
// We're past the entry which means that the best match we have is the previous entry.
if (lastMappedLine === nextEntryLine) {
// Match
exportedEntries[nextEntryIdx].originalLine = lastOriginalLine;
exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn;
exportedEntries[nextEntryIdx].originalSource = lastSourceIndex;
exportedEntries[nextEntryIdx].nameIndex = lastNameIndex;
} else {
// Skip if we didn't have any mappings on the exported line.
}
nextEntryIdx++;
if (nextEntryIdx < exportedEntries.length) {
nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line;
nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column;
} else {
nextEntryLine = -1;
nextEntryColumn = -1;
}
}
lastMappedLine = generatedLine;
if (sourceIndex > -1) {
lastSourceIndex = sourceIndex;
}
if (originalLine > -1) {
lastOriginalLine = originalLine;
}
if (originalColumn > -1) {
lastOriginalColumn = originalColumn;
}
if (nameIndex > -1) {
lastNameIndex = nameIndex;
}
},
);
if (nextEntryIdx < exportedEntries.length) {
if (lastMappedLine === nextEntryLine) {
// Match
exportedEntries[nextEntryIdx].originalLine = lastOriginalLine;
exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn;
exportedEntries[nextEntryIdx].originalSource = lastSourceIndex;
exportedEntries[nextEntryIdx].nameIndex = lastNameIndex;
}
}
for (
let lastIdx = mappings.length - 1;
lastIdx >= 0 && mappings[lastIdx] === ';';
lastIdx--
) {
// If the last mapped lines don't contain any segments, we don't get a callback from readMappings
// so we need to pad the number of mapped lines, with one for each empty line.
lastMappedLine++;
}
sourceLineCount = program.loc.end.line;
if (sourceLineCount < lastMappedLine) {
throw new Error(
'The source map has more mappings than there are lines.',
);
}
// If the original source string had more lines than there are mappings in the source map.
// Add some extra padding of unmapped lines so that any lines that we add line up.
for (
let extraLines = sourceLineCount - lastMappedLine;
extraLines > 0;
extraLines--
) {
mappings += ';';
}
} else {
// If a file doesn't have a source map then we generate a blank source map that just
// contains the original content and segments pointing to the original lines.
sourceLineCount = 1;
let idx = -1;
while ((idx = source.indexOf('\n', idx + 1)) !== -1) {
sourceLineCount++;
}
mappings = 'AAAA' + ';AACA'.repeat(sourceLineCount - 1);
sourceMap = {
version: 3,
sources: [url],
sourcesContent: [source],
mappings: mappings,
sourceRoot: '',
};
lastSourceIndex = 0;
lastOriginalLine = sourceLineCount;
lastOriginalColumn = 0;
lastNameIndex = -1;
lastMappedLine = sourceLineCount;
for (let i = 0; i < exportedEntries.length; i++) {
// Point each entry to original location.
const entry = exportedEntries[i];
entry.originalSource = 0;
entry.originalLine = entry.loc.start.line;
// We use column zero since we do the short-hand line-only source maps above.
entry.originalColumn = 0; // entry.loc.start.column;
}
}
newSrc += '\n\n;';
newSrc +=
'import {registerServerReference} from "react-server-dom-webpack/server";\n';
if (mappings) {
mappings += ';;';
}
const createMapping = createMappingsSerializer();
// Create an empty mapping pointing to where we last left off to reset the counters.
let generatedLine = 1;
createMapping(
generatedLine,
0,
lastSourceIndex,
lastOriginalLine,
lastOriginalColumn,
lastNameIndex,
);
for (let i = 0; i < exportedEntries.length; i++) {
const entry = exportedEntries[i];
generatedLine++;
if (entry.type !== 'function') {
// We first check if the export is a function and if so annotate it.
newSrc += 'if (typeof ' + entry.localName + ' === "function") ';
}
newSrc += 'registerServerReference(' + entry.localName + ',';
newSrc += JSON.stringify(url) + ',';
newSrc += JSON.stringify(entry.exportedName) + ');\n';
mappings += createMapping(
generatedLine,
0,
entry.originalSource,
entry.originalLine,
entry.originalColumn,
entry.nameIndex,
);
}
}
if (sourceMap) {
// Override with an new mappings and serialize an inline source map.
sourceMap.mappings = mappings;
newSrc +=
'//# sourceMappingURL=data:application/json;charset=utf-8;base64,' +
Buffer.from(JSON.stringify(sourceMap)).toString('base64');
}
return newSrc;
}
function addExportNames(names: Array<string>, node: any) {
switch (node.type) {
case 'Identifier':
names.push(node.name);
return;
case 'ObjectPattern':
for (let i = 0; i < node.properties.length; i++)
addExportNames(names, node.properties[i]);
return;
case 'ArrayPattern':
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (element) addExportNames(names, element);
}
return;
case 'Property':
addExportNames(names, node.value);
return;
case 'AssignmentPattern':
addExportNames(names, node.left);
return;
case 'RestElement':
addExportNames(names, node.argument);
return;
case 'ParenthesizedExpression':
addExportNames(names, node.expression);
return;
}
}
function resolveClientImport(
specifier: string,
parentURL: string,
): {url: string} | Promise<{url: string}> {
// Resolve an import specifier as if it was loaded by the client. This doesn't use
// the overrides that this loader does but instead reverts to the default.
// This resolution algorithm will not necessarily have the same configuration
// as the actual client loader. It should mostly work and if it doesn't you can
// always convert to explicit exported names instead.
const conditions = ['node', 'import'];
if (stashedResolve === null) {
throw new Error(
'Expected resolve to have been called before transformSource',
);
}
return stashedResolve(specifier, {conditions, parentURL}, stashedResolve);
}
async function parseExportNamesInto(
body: any,
names: Array<string>,
parentURL: string,
loader: LoadFunction,
): Promise<void> {
for (let i = 0; i < body.length; i++) {
const node = body[i];
switch (node.type) {
case 'ExportAllDeclaration':
if (node.exported) {
addExportNames(names, node.exported);
continue;
} else {
const {url} = await resolveClientImport(node.source.value, parentURL);
const {source} = await loader(
url,
{format: 'module', conditions: [], importAssertions: {}},
loader,
);
if (typeof source !== 'string') {
throw new Error('Expected the transformed source to be a string.');
}
let childBody;
try {
childBody = acorn.parse(source, {
ecmaVersion: '2024',
sourceType: 'module',
}).body;
} catch (x) {
// eslint-disable-next-line react-internal/no-production-logging
console.error('Error parsing %s %s', url, x.message);
continue;
}
await parseExportNamesInto(childBody, names, url, loader);
continue;
}
case 'ExportDefaultDeclaration':
names.push('default');
continue;
case 'ExportNamedDeclaration':
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
const declarations = node.declaration.declarations;
for (let j = 0; j < declarations.length; j++) {
addExportNames(names, declarations[j].id);
}
} else {
addExportNames(names, node.declaration.id);
}
}
if (node.specifiers) {
const specifiers = node.specifiers;
for (let j = 0; j < specifiers.length; j++) {
addExportNames(names, specifiers[j].exported);
}
}
continue;
}
}
}
async function transformClientModule(
program: any,
url: string,
sourceMap: any,
loader: LoadFunction,
): Promise<string> {
const body = program.body;
const names: Array<string> = [];
await parseExportNamesInto(body, names, url, loader);
if (names.length === 0) {
return '';
}
let newSrc =
'import {registerClientReference} from "react-server-dom-webpack/server";\n';
for (let i = 0; i < names.length; i++) {
const name = names[i];
if (name === 'default') {
newSrc += 'export default ';
newSrc += 'registerClientReference(function() {';
newSrc +=
'throw new Error(' +
JSON.stringify(
`Attempted to call the default export of ${url} from the server ` +
`but it's on the client. It's not possible to invoke a client function from ` +
`the server, it can only be rendered as a Component or passed to props of a ` +
`Client Component.`,
) +
');';
} else {
newSrc += 'export const ' + name + ' = ';
newSrc += 'registerClientReference(function() {';
newSrc +=
'throw new Error(' +
JSON.stringify(
`Attempted to call ${name}() from the server but ${name} is on the client. ` +
`It's not possible to invoke a client function from the server, it can ` +
`only be rendered as a Component or passed to props of a Client Component.`,
) +
');';
}
newSrc += '},';
newSrc += JSON.stringify(url) + ',';
newSrc += JSON.stringify(name) + ');\n';
}
// TODO: Generate source maps for Client Reference functions so they can point to their
// original locations.
return newSrc;
}
async function loadClientImport(
url: string,
defaultTransformSource: TransformSourceFunction,
): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
if (stashedGetSource === null) {
throw new Error(
'Expected getSource to have been called before transformSource',
);
}
// TODO: Validate that this is another module by calling getFormat.
const {source} = await stashedGetSource(
url,
{format: 'module'},
stashedGetSource,
);
const result = await defaultTransformSource(
source,
{format: 'module', url},
defaultTransformSource,
);
return {format: 'module', source: result.source};
}
async function transformModuleIfNeeded(
source: string,
url: string,
loader: LoadFunction,
): Promise<string> {
// Do a quick check for the exact string. If it doesn't exist, don't
// bother parsing.
if (
source.indexOf('use client') === -1 &&
source.indexOf('use server') === -1
) {
return source;
}
let sourceMappingURL = null;
let sourceMappingStart = 0;
let sourceMappingEnd = 0;
let sourceMappingLines = 0;
let program;
try {
program = acorn.parse(source, {
ecmaVersion: '2024',
sourceType: 'module',
locations: true,
onComment(
block: boolean,
text: string,
start: number,
end: number,
startLoc: {line: number, column: number},
endLoc: {line: number, column: number},
) {
if (
text.startsWith('# sourceMappingURL=') ||
text.startsWith('@ sourceMappingURL=')
) {
sourceMappingURL = text.slice(19);
sourceMappingStart = start;
sourceMappingEnd = end;
sourceMappingLines = endLoc.line - startLoc.line;
}
},
});
} catch (x) {
// eslint-disable-next-line react-internal/no-production-logging
console.error('Error parsing %s %s', url, x.message);
return source;
}
let useClient = false;
let useServer = false;
const body = program.body;
for (let i = 0; i < body.length; i++) {
const node = body[i];
if (node.type !== 'ExpressionStatement' || !node.directive) {
break;
}
if (node.directive === 'use client') {
useClient = true;
}
if (node.directive === 'use server') {
useServer = true;
}
}
if (!useClient && !useServer) {
return source;
}
if (useClient && useServer) {
throw new Error(
'Cannot have both "use client" and "use server" directives in the same file.',
);
}
let sourceMap = null;
if (sourceMappingURL) {
const sourceMapResult = await loader(
sourceMappingURL,
// $FlowFixMe
{
format: 'json',
conditions: [],
importAssertions: {type: 'json'},
importAttributes: {type: 'json'},
},
loader,
);
const sourceMapString =
typeof sourceMapResult.source === 'string'
? sourceMapResult.source
: // $FlowFixMe
sourceMapResult.source.toString('utf8');
sourceMap = JSON.parse(sourceMapString);
// Strip the source mapping comment. We'll re-add it below if needed.
source =
source.slice(0, sourceMappingStart) +
'\n'.repeat(sourceMappingLines) +
source.slice(sourceMappingEnd);
}
if (useClient) {
return transformClientModule(program, url, sourceMap, loader);
}
return transformServerModule(source, program, url, sourceMap, loader);
}
export async function transformSource(
source: Source,
context: TransformSourceContext,
defaultTransformSource: TransformSourceFunction,
): Promise<{source: Source}> {
const transformed = await defaultTransformSource(
source,
context,
defaultTransformSource,
);
if (context.format === 'module') {
const transformedSource = transformed.source;
if (typeof transformedSource !== 'string') {
throw new Error('Expected source to have been transformed to a string.');
}
const newSrc = await transformModuleIfNeeded(
transformedSource,
context.url,
(url: string, ctx: LoadContext, defaultLoad: LoadFunction) => {
return loadClientImport(url, defaultTransformSource);
},
);
return {source: newSrc};
}
return transformed;
}
export async function load(
url: string,
context: LoadContext,
defaultLoad: LoadFunction,
): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
const result = await defaultLoad(url, context, defaultLoad);
if (result.format === 'module') {
if (typeof result.source !== 'string') {
throw new Error('Expected source to have been loaded into a string.');
}
const newSrc = await transformModuleIfNeeded(
result.source,
url,
defaultLoad,
);
return {format: 'module', source: newSrc};
}
return result;
}

View File

@@ -0,0 +1,109 @@
/**
* 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
*/
const acorn = require('acorn-loose');
const url = require('url');
const Module = require('module');
module.exports = function register() {
const Server: any = require('react-server-dom-unbundled/server');
const registerServerReference = Server.registerServerReference;
const createClientModuleProxy = Server.createClientModuleProxy;
// $FlowFixMe[prop-missing] found when upgrading Flow
const originalCompile = Module.prototype._compile;
// $FlowFixMe[prop-missing] found when upgrading Flow
Module.prototype._compile = function (
this: any,
content: string,
filename: string,
): void {
// Do a quick check for the exact string. If it doesn't exist, don't
// bother parsing.
if (
content.indexOf('use client') === -1 &&
content.indexOf('use server') === -1
) {
return originalCompile.apply(this, arguments);
}
let body;
try {
body = acorn.parse(content, {
ecmaVersion: '2024',
sourceType: 'source',
}).body;
} catch (x) {
console['error']('Error parsing %s %s', url, x.message);
return originalCompile.apply(this, arguments);
}
let useClient = false;
let useServer = false;
for (let i = 0; i < body.length; i++) {
const node = body[i];
if (node.type !== 'ExpressionStatement' || !node.directive) {
break;
}
if (node.directive === 'use client') {
useClient = true;
}
if (node.directive === 'use server') {
useServer = true;
}
}
if (!useClient && !useServer) {
return originalCompile.apply(this, arguments);
}
if (useClient && useServer) {
throw new Error(
'Cannot have both "use client" and "use server" directives in the same file.',
);
}
if (useClient) {
const moduleId: string = (url.pathToFileURL(filename).href: any);
this.exports = createClientModuleProxy(moduleId);
}
if (useServer) {
originalCompile.apply(this, arguments);
const moduleId: string = (url.pathToFileURL(filename).href: any);
const exports = this.exports;
// This module is imported server to server, but opts in to exposing functions by
// reference. If there are any functions in the export.
if (typeof exports === 'function') {
// The module exports a function directly,
registerServerReference(
(exports: any),
moduleId,
// Represents the whole Module object instead of a particular import.
null,
);
} else {
const keys = Object.keys(exports);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = exports[keys[i]];
if (typeof value === 'function') {
registerServerReference((value: any), moduleId, key);
}
}
}
}
};
};

View File

@@ -0,0 +1,360 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
export type ServerReference<T: Function> = T & {
$$typeof: symbol,
$$id: string,
$$bound: null | Array<ReactClientValue>,
$$location?: Error,
};
// eslint-disable-next-line no-unused-vars
export type ClientReference<T> = {
$$typeof: symbol,
$$id: string,
$$async: boolean,
};
const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
export function isClientReference(reference: Object): boolean {
return reference.$$typeof === CLIENT_REFERENCE_TAG;
}
export function isServerReference(reference: Object): boolean {
return reference.$$typeof === SERVER_REFERENCE_TAG;
}
export function registerClientReference<T>(
proxyImplementation: any,
id: string,
exportName: string,
): ClientReference<T> {
return registerClientReferenceImpl(
proxyImplementation,
id + '#' + exportName,
false,
);
}
function registerClientReferenceImpl<T>(
proxyImplementation: any,
id: string,
async: boolean,
): ClientReference<T> {
return Object.defineProperties(proxyImplementation, {
$$typeof: {value: CLIENT_REFERENCE_TAG},
$$id: {value: id},
$$async: {value: async},
});
}
// $FlowFixMe[method-unbinding]
const FunctionBind = Function.prototype.bind;
// $FlowFixMe[method-unbinding]
const ArraySlice = Array.prototype.slice;
function bind(this: ServerReference<any>): any {
// $FlowFixMe[incompatible-call]
const newFn = FunctionBind.apply(this, arguments);
if (this.$$typeof === SERVER_REFERENCE_TAG) {
if (__DEV__) {
const thisBind = arguments[0];
if (thisBind != null) {
console.error(
'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
);
}
}
const args = ArraySlice.call(arguments, 1);
const $$typeof = {value: SERVER_REFERENCE_TAG};
const $$id = {value: this.$$id};
const $$bound = {value: this.$$bound ? this.$$bound.concat(args) : args};
return Object.defineProperties(
(newFn: any),
(__DEV__
? {
$$typeof,
$$id,
$$bound,
$$location: {
value: this.$$location,
configurable: true,
},
bind: {value: bind, configurable: true},
}
: {
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
}) as PropertyDescriptorMap,
);
}
return newFn;
}
const serverReferenceToString = {
value: () => 'function () { [omitted code] }',
configurable: true,
writable: true,
};
export function registerServerReference<T: Function>(
reference: T,
id: string,
exportName: null | string,
): ServerReference<T> {
const $$typeof = {value: SERVER_REFERENCE_TAG};
const $$id = {
value: exportName === null ? id : id + '#' + exportName,
configurable: true,
};
const $$bound = {value: null, configurable: true};
return Object.defineProperties(
(reference: any),
__DEV__
? ({
$$typeof,
$$id,
$$bound,
$$location: {
value: Error('react-stack-top-frame'),
configurable: true,
},
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
} as PropertyDescriptorMap)
: ({
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
} as PropertyDescriptorMap),
);
}
const PROMISE_PROTOTYPE = Promise.prototype;
const deepProxyHandlers: Proxy$traps<mixed> = {
get: function (
target: Function,
name: string | symbol,
receiver: Proxy<Function>,
) {
switch (name) {
// These names are read by the Flight runtime if you end up using the exports object.
case '$$typeof':
// These names are a little too common. We should probably have a way to
// have the Flight runtime extract the inner target instead.
return target.$$typeof;
case '$$id':
return target.$$id;
case '$$async':
return target.$$async;
case 'name':
return target.name;
case 'displayName':
return undefined;
// We need to special case this because createElement reads it if we pass this
// reference.
case 'defaultProps':
return undefined;
// React looks for debugInfo on thenables.
case '_debugInfo':
return undefined;
// Avoid this attempting to be serialized.
case 'toJSON':
return undefined;
case Symbol.toPrimitive:
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toPrimitive];
case Symbol.toStringTag:
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toStringTag];
case 'Provider':
throw new Error(
`Cannot render a Client Context Provider on the Server. ` +
`Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`,
);
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +
`You cannot await a client module from a server component.`,
);
}
// eslint-disable-next-line react-internal/safe-string-coercion
const expression = String(target.name) + '.' + String(name);
throw new Error(
`Cannot access ${expression} on the server. ` +
'You cannot dot into a client module from a server component. ' +
'You can only pass the imported name through.',
);
},
set: function () {
throw new Error('Cannot assign to a client module from a server module.');
},
};
function getReference(target: Function, name: string | symbol): $FlowFixMe {
switch (name) {
// These names are read by the Flight runtime if you end up using the exports object.
case '$$typeof':
return target.$$typeof;
case '$$id':
return target.$$id;
case '$$async':
return target.$$async;
case 'name':
return target.name;
// We need to special case this because createElement reads it if we pass this
// reference.
case 'defaultProps':
return undefined;
// React looks for debugInfo on thenables.
case '_debugInfo':
return undefined;
// Avoid this attempting to be serialized.
case 'toJSON':
return undefined;
case Symbol.toPrimitive:
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toPrimitive];
case Symbol.toStringTag:
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toStringTag];
case '__esModule':
// Something is conditionally checking which export to use. We'll pretend to be
// an ESM compat module but then we'll check again on the client.
const moduleId = target.$$id;
target.default = registerClientReferenceImpl(
(function () {
throw new Error(
`Attempted to call the default export of ${moduleId} from the server ` +
`but it's on the client. It's not possible to invoke a client function from ` +
`the server, it can only be rendered as a Component or passed to props of a ` +
`Client Component.`,
);
}: any),
target.$$id + '#',
target.$$async,
);
return true;
case 'then':
if (target.then) {
// Use a cached value
return target.then;
}
if (!target.$$async) {
// If this module is expected to return a Promise (such as an AsyncModule) then
// we should resolve that with a client reference that unwraps the Promise on
// the client.
const clientReference: ClientReference<any> =
registerClientReferenceImpl(({}: any), target.$$id, true);
const proxy = new Proxy(clientReference, proxyHandlers);
// Treat this as a resolved Promise for React's use()
target.status = 'fulfilled';
target.value = proxy;
const then = (target.then = registerClientReferenceImpl(
(function then(resolve, reject: any) {
// Expose to React.
return Promise.resolve(resolve(proxy));
}: any),
// If this is not used as a Promise but is treated as a reference to a `.then`
// export then we should treat it as a reference to that name.
target.$$id + '#then',
false,
));
return then;
} else {
// Since typeof .then === 'function' is a feature test we'd continue recursing
// indefinitely if we return a function. Instead, we return an object reference
// if we check further.
return undefined;
}
}
if (typeof name === 'symbol') {
throw new Error(
'Cannot read Symbol exports. Only named exports are supported on a client module ' +
'imported on the server.',
);
}
let cachedReference = target[name];
if (!cachedReference) {
const reference: ClientReference<any> = registerClientReferenceImpl(
(function () {
throw new Error(
// eslint-disable-next-line react-internal/safe-string-coercion
`Attempted to call ${String(name)}() from the server but ${String(
name,
)} is on the client. ` +
`It's not possible to invoke a client function from the server, it can ` +
`only be rendered as a Component or passed to props of a Client Component.`,
);
}: any),
target.$$id + '#' + name,
target.$$async,
);
Object.defineProperty((reference: any), 'name', {value: name});
cachedReference = target[name] = new Proxy(reference, deepProxyHandlers);
}
return cachedReference;
}
const proxyHandlers = {
get: function (
target: Function,
name: string | symbol,
receiver: Proxy<Function>,
): $FlowFixMe {
return getReference(target, name);
},
getOwnPropertyDescriptor: function (
target: Function,
name: string | symbol,
): $FlowFixMe {
let descriptor = Object.getOwnPropertyDescriptor(target, name);
if (!descriptor) {
descriptor = {
value: getReference(target, name),
writable: false,
configurable: false,
enumerable: false,
};
Object.defineProperty(target, name, descriptor);
}
return descriptor;
},
getPrototypeOf(target: Function): Object {
// Pretend to be a Promise in case anyone asks.
return PROMISE_PROTOTYPE;
},
set: function (): empty {
throw new Error('Cannot assign to a client module from a server module.');
},
};
export function createClientModuleProxy<T>(
moduleId: string,
): ClientReference<T> {
const clientReference: ClientReference<T> = registerClientReferenceImpl(
({}: any),
// Represents the whole Module object instead of a particular import.
moduleId,
false,
);
return new Proxy(clientReference, proxyHandlers);
}

View File

@@ -0,0 +1,32 @@
/**
* 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 {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig';
export type ModuleLoading = null | {
prefix: string,
crossOrigin?: 'use-credentials' | '',
};
export function prepareDestinationWithChunks(
moduleLoading: ModuleLoading,
// Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...]
chunks: Array<string>,
nonce: ?string,
) {
if (moduleLoading !== null) {
for (let i = 1; i < chunks.length; i += 2) {
preinitScriptForSSR(
moduleLoading.prefix + chunks[i],
nonce,
moduleLoading.crossOrigin,
);
}
}
}

View File

@@ -0,0 +1,249 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
import type {
DebugChannel,
FindSourceMapURLCallback,
Response as FlightResponse,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
import type {
ServerConsumerModuleMap,
ModuleLoading,
ServerManifest,
} from 'react-client/src/ReactFlightClientConfig';
type ServerConsumerManifest = {
moduleMap: ServerConsumerModuleMap,
moduleLoading: ModuleLoading,
serverModuleMap: null | ServerManifest,
};
import {
createResponse,
createStreamState,
getRoot,
reportGlobalError,
processBinaryChunk,
close,
} from 'react-client/src/ReactFlightClient';
import {
processReply,
createServerReference as createServerReferenceImpl,
} from 'react-client/src/ReactFlightReplyClient';
export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
export type {TemporaryReferenceSet};
function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
'This would create a fetch waterfall. Try to use a Server Component ' +
'to pass data to Client Components instead.',
);
}
export function createServerReference<A: Iterable<any>, T>(
id: any,
callServer: any,
): (...A) => Promise<T> {
return createServerReferenceImpl(id, noServerCall);
}
type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;
export type Options = {
serverConsumerManifest: ServerConsumerManifest,
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
// For the Edge client we only support a single-direction debug channel.
debugChannel?: {readable?: ReadableStream, ...},
};
function createResponseFromOptions(options: Options) {
const debugChannel: void | DebugChannel =
__DEV__ && options && options.debugChannel !== undefined
? {
hasReadable: options.debugChannel.readable !== undefined,
callback: null,
}
: undefined;
return createResponse(
options.serverConsumerManifest.moduleMap,
options.serverConsumerManifest.serverModuleMap,
options.serverConsumerManifest.moduleLoading,
noServerCall,
options.encodeFormAction,
typeof options.nonce === 'string' ? options.nonce : undefined,
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
__DEV__ && options && options.findSourceMapURL
? options.findSourceMapURL
: undefined,
__DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
debugChannel,
);
}
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
if (done) {
return onDone();
}
const buffer: Uint8Array = (value: any);
processBinaryChunk(response, streamState, buffer);
return reader.read().then(progress).catch(error);
}
function error(e: any) {
reportGlobalError(response, e);
}
reader.read().then(progress).catch(error);
}
function createFromReadableStream<T>(
stream: ReadableStream,
options: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
if (
__DEV__ &&
options &&
options.debugChannel &&
options.debugChannel.readable
) {
let streamDoneCount = 0;
const handleDone = () => {
if (++streamDoneCount === 2) {
close(response);
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
}
function createFromFetch<T>(
promiseForResponse: Promise<Response>,
options: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
if (
__DEV__ &&
options &&
options.debugChannel &&
options.debugChannel.readable
) {
let streamDoneCount = 0;
const handleDone = () => {
if (++streamDoneCount === 2) {
close(response);
}
};
startReadingFromStream(
response,
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
function (e) {
reportGlobalError(response, e);
},
);
return getRoot(response);
}
function encodeReply(
value: ReactServerValue,
options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal},
): Promise<
string | URLSearchParams | FormData,
> /* We don't use URLSearchParams yet but maybe */ {
return new Promise((resolve, reject) => {
const abort = processReply(
value,
'',
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
resolve,
reject,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort((signal: any).reason);
} else {
const listener = () => {
abort((signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
});
}
export {createFromFetch, createFromReadableStream, encodeReply};

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
import type {
DebugChannel,
FindSourceMapURLCallback,
Response,
} from 'react-client/src/ReactFlightClient';
import type {
ServerConsumerModuleMap,
ModuleLoading,
ServerManifest,
} from 'react-client/src/ReactFlightClientConfig';
type ServerConsumerManifest = {
moduleMap: ServerConsumerModuleMap,
moduleLoading: ModuleLoading,
serverModuleMap: null | ServerManifest,
};
import type {Readable} from 'stream';
import {
createResponse,
createStreamState,
getRoot,
reportGlobalError,
processStringChunk,
processBinaryChunk,
close,
} from 'react-client/src/ReactFlightClient';
export * from './ReactFlightDOMClientEdge';
function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
'This would create a fetch waterfall. Try to use a Server Component ' +
'to pass data to Client Components instead.',
);
}
type EncodeFormActionCallback = <A>(
id: any,
args: Promise<A>,
) => ReactCustomFormAction;
export type Options = {
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
// For the Node.js client we only support a single-direction debug channel.
debugChannel?: Readable,
};
function startReadingFromStream(
response: Response,
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState(response, stream);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, streamState, chunk);
} else {
processBinaryChunk(response, streamState, chunk);
}
});
stream.on('error', error => {
reportGlobalError(response, error);
});
stream.on('end', onEnd);
}
function createFromNodeStream<T>(
stream: Readable,
serverConsumerManifest: ServerConsumerManifest,
options?: Options,
): Thenable<T> {
const debugChannel: void | DebugChannel =
__DEV__ && options && options.debugChannel !== undefined
? {
hasReadable: options.debugChannel.readable !== undefined,
callback: null,
}
: undefined;
const response: Response = createResponse(
serverConsumerManifest.moduleMap,
serverConsumerManifest.serverModuleMap,
serverConsumerManifest.moduleLoading,
noServerCall,
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
__DEV__ && options && options.findSourceMapURL
? options.findSourceMapURL
: undefined,
__DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
debugChannel,
);
if (__DEV__ && options && options.debugChannel) {
let streamEndedCount = 0;
const handleEnd = () => {
if (++streamEndedCount === 2) {
close(response);
}
};
startReadingFromStream(response, options.debugChannel, handleEnd);
startReadingFromStream(response, stream, handleEnd);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
}
return getRoot(response);
}
export {createFromNodeStream};

View File

@@ -0,0 +1,701 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
import type {ClientManifest} from './ReactFlightServerConfigUnbundledBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
import type {Duplex} from 'stream';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
import {
createRequest,
createPrerenderRequest,
startWork,
startFlowing,
startFlowingDebug,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
createResponse,
reportGlobalError,
close,
resolveField,
resolveFile,
resolveFileInfo,
resolveFileChunk,
resolveFileComplete,
getRoot,
} from 'react-server/src/ReactFlightReplyServer';
import {
decodeAction,
decodeFormState,
} from 'react-server/src/ReactFlightActionServer';
export {
registerServerReference,
registerClientReference,
createClientModuleProxy,
} from '../ReactFlightUnbundledReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigNode';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
function createDrainHandler(destination: Destination, request: Request) {
return () => startFlowing(request, destination);
}
function createCancelHandler(request: Request, reason: string) {
return () => {
stopFlowing(request);
abort(request, new Error(reason));
};
}
function startReadingFromDebugChannelReadable(
request: Request,
stream: Readable | WebSocket,
): void {
const stringDecoder = createStringDecoder();
let lastWasPartial = false;
let stringBuffer = '';
function onData(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
if (lastWasPartial) {
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
lastWasPartial = false;
}
stringBuffer += chunk;
} else {
const buffer: Uint8Array = (chunk: any);
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
lastWasPartial = true;
}
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
}
function onError(error: mixed) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: error,
}),
);
}
function onClose() {
closeDebugChannel(request);
}
if (
// $FlowFixMe[method-unbinding]
typeof stream.addEventListener === 'function' &&
// $FlowFixMe[method-unbinding]
typeof stream.binaryType === 'string'
) {
const ws: WebSocket = (stream: any);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', event => {
// $FlowFixMe
onData(event.data);
});
ws.addEventListener('error', event => {
// $FlowFixMe
onError(event.error);
});
ws.addEventListener('close', onClose);
} else {
const readable: Readable = (stream: any);
readable.on('data', onData);
readable.on('error', onError);
readable.on('end', onClose);
}
}
type Options = {
debugChannel?: Readable | Writable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
identifierPrefix?: string,
temporaryReferences?: TemporaryReferenceSet,
};
type PipeableStream = {
abort(reason: mixed): void,
pipe<T: Writable>(destination: T): T,
};
function renderToPipeableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options,
): PipeableStream {
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const debugChannelReadable: void | Readable | WebSocket =
__DEV__ &&
debugChannel !== undefined &&
// $FlowFixMe[method-unbinding]
(typeof debugChannel.read === 'function' ||
typeof debugChannel.readyState === 'number')
? (debugChannel: any)
: undefined;
const debugChannelWritable: void | Writable =
__DEV__ && debugChannel !== undefined
? // $FlowFixMe[method-unbinding]
typeof debugChannel.write === 'function'
? (debugChannel: any)
: // $FlowFixMe[method-unbinding]
typeof debugChannel.send === 'function'
? createFakeWritableFromWebSocket((debugChannel: any))
: undefined
: undefined;
const request = createRequest(
model,
webpackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
if (debugChannelWritable !== undefined) {
startFlowingDebug(request, debugChannelWritable);
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadable(request, debugChannelReadable);
}
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
throw new Error(
'React currently only supports piping to one writable stream.',
);
}
hasStartedFlowing = true;
startFlowing(request, destination);
destination.on('drain', createDrainHandler(destination, request));
destination.on(
'error',
createCancelHandler(
request,
'The destination stream errored while writing data.',
),
);
// We don't close until the debug channel closes.
if (!__DEV__ || debugChannelReadable === undefined) {
destination.on(
'close',
createCancelHandler(request, 'The destination stream closed early.'),
);
}
return destination;
},
abort(reason: mixed) {
abort(request, reason);
},
};
}
function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
return ({
write(chunk: string | Uint8Array) {
webSocket.send((chunk: any));
return true;
},
end() {
webSocket.close();
},
destroy(reason) {
if (typeof reason === 'object' && reason !== null) {
reason = reason.message;
}
if (typeof reason === 'string') {
webSocket.close(1011, reason);
} else {
webSocket.close(1011);
}
},
}: any);
}
function createFakeWritableFromReadableStreamController(
controller: ReadableStreamController,
): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
chunk = textEncoder.encode(chunk);
}
controller.enqueue(chunk);
// in web streams there is no backpressure so we can always write more
return true;
},
end() {
controller.close();
},
destroy(error) {
// $FlowFixMe[method-unbinding]
if (typeof controller.error === 'function') {
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
controller.error(error);
} else {
controller.close();
}
},
}: any);
}
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Omit<Options, 'debugChannel'> & {
debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const debugChannelWritable =
__DEV__ && options && options.debugChannel
? options.debugChannel.writable
: undefined;
const request = createRequest(
model,
webpackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
if (debugChannelWritable !== undefined) {
let debugWritable: Writable;
const debugStream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
debugWritable =
createFakeWritableFromReadableStreamController(controller);
},
pull: (controller): ?Promise<void> => {
startFlowingDebug(request, debugWritable);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
debugStream.pipeTo(debugChannelWritable);
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable = createFakeWritableFromReadableStreamController(controller);
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
return stream;
}
function createFakeWritableFromNodeReadable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk: string | Uint8Array) {
return readable.push(chunk);
},
end() {
readable.push(null);
},
destroy(error) {
readable.destroy(error);
},
}: any);
}
type PrerenderOptions = {
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
identifierPrefix?: string,
temporaryReferences?: TemporaryReferenceSet,
signal?: AbortSignal,
};
type StaticResult = {
prelude: Readable,
};
function prerenderToNodeStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: PrerenderOptions,
): Promise<StaticResult> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
const readable: Readable = new Readable({
read() {
startFlowing(request, writable);
},
});
const writable = createFakeWritableFromNodeReadable(readable);
resolve({prelude: readable});
}
const request = createPrerenderRequest(
model,
webpackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
const reason = (signal: any).reason;
abort(request, reason);
} else {
const listener = () => {
const reason = (signal: any).reason;
abort(request, reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
function prerender(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options & {
signal?: AbortSignal,
},
): Promise<{
prelude: ReadableStream,
}> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable =
createFakeWritableFromReadableStreamController(controller);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve({prelude: stream});
}
const request = createPrerenderRequest(
model,
webpackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
const reason = (signal: any).reason;
abort(request, reason);
} else {
const listener = () => {
const reason = (signal: any).reason;
abort(request, reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
webpackMap: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Thenable<T> {
const response = createResponse(
webpackMap,
'',
options ? options.temporaryReferences : undefined,
);
let pendingFiles = 0;
const queuedFields: Array<string> = [];
busboyStream.on('field', (name, value) => {
if (pendingFiles > 0) {
// Because the 'end' event fires two microtasks after the next 'field'
// we would resolve files and fields out of order. To handle this properly
// we queue any fields we receive until the previous file is done.
queuedFields.push(name, value);
} else {
try {
resolveField(response, name, value);
} catch (error) {
busboyStream.destroy(error);
}
}
});
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
if (encoding.toLowerCase() === 'base64') {
busboyStream.destroy(
new Error(
"React doesn't accept base64 encoded file uploads because we don't expect " +
"form data passed from a browser to ever encode data that way. If that's " +
'the wrong assumption, we can easily fix it.',
),
);
return;
}
pendingFiles++;
const file = resolveFileInfo(response, name, filename, mimeType);
value.on('data', chunk => {
resolveFileChunk(response, file, chunk);
});
value.on('end', () => {
try {
resolveFileComplete(response, name, file);
pendingFiles--;
if (pendingFiles === 0) {
// Release any queued fields
for (let i = 0; i < queuedFields.length; i += 2) {
resolveField(response, queuedFields[i], queuedFields[i + 1]);
}
queuedFields.length = 0;
}
} catch (error) {
busboyStream.destroy(error);
}
});
});
busboyStream.on('finish', () => {
close(response);
});
busboyStream.on('error', err => {
reportGlobalError(
response,
// $FlowFixMe[incompatible-call] types Error and mixed are incompatible
err,
);
});
return getRoot(response);
}
function decodeReply<T>(
body: string | FormData,
webpackMap: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
form.append('0', body);
body = form;
}
const response = createResponse(
webpackMap,
'',
options ? options.temporaryReferences : undefined,
body,
);
const root = getRoot<T>(response);
close(response);
return root;
}
function decodeReplyFromAsyncIterable<T>(
iterable: AsyncIterable<[string, string | File]>,
webpackMap: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Thenable<T> {
const iterator: AsyncIterator<[string, string | File]> =
iterable[ASYNC_ITERATOR]();
const response = createResponse(
webpackMap,
'',
options ? options.temporaryReferences : undefined,
);
function progress(
entry:
| {done: false, +value: [string, string | File], ...}
| {done: true, +value: void, ...},
) {
if (entry.done) {
close(response);
} else {
const [name, value] = entry.value;
if (typeof value === 'string') {
resolveField(response, name, value);
} else {
resolveFile(response, name, value);
}
iterator.next().then(progress, error);
}
}
function error(reason: Error) {
reportGlobalError(response, reason);
if (typeof (iterator: any).throw === 'function') {
// The iterator protocol doesn't necessarily include this but a generator do.
// $FlowFixMe should be able to pass mixed
iterator.throw(reason).then(error, error);
}
}
iterator.next().then(progress, error);
return getRoot(response);
}
export {
renderToReadableStream,
renderToPipeableStream,
prerender,
prerenderToNodeStream,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
};

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
ImportMetadata,
ImportManifestEntry,
} from '../shared/ReactFlightImportMetadata';
import type {
ClientReference,
ServerReference,
} from '../ReactFlightUnbundledReferences';
export type {ClientReference, ServerReference};
export type ClientManifest = {
[id: string]: ClientReferenceManifestEntry,
};
export type ServerReferenceId = string;
export type ClientReferenceMetadata = ImportMetadata;
export opaque type ClientReferenceManifestEntry = ImportManifestEntry;
export type ClientReferenceKey = string;
export {
isClientReference,
isServerReference,
} from '../ReactFlightUnbundledReferences';
export function getClientReferenceKey(
reference: ClientReference<any>,
): ClientReferenceKey {
return reference.$$async ? reference.$$id + '#async' : reference.$$id;
}
export function resolveClientReferenceMetadata<T>(
config: ClientManifest,
clientReference: ClientReference<T>,
): ClientReferenceMetadata {
const modulePath = clientReference.$$id;
let name = '';
let resolvedModuleData = config[modulePath];
if (resolvedModuleData) {
// The potentially aliased name.
name = resolvedModuleData.name;
} else {
// We didn't find this specific export name but we might have the * export
// which contains this name as well.
// TODO: It's unfortunate that we now have to parse this string. We should
// probably go back to encoding path and name separately on the client reference.
const idx = modulePath.lastIndexOf('#');
if (idx !== -1) {
name = modulePath.slice(idx + 1);
resolvedModuleData = config[modulePath.slice(0, idx)];
}
if (!resolvedModuleData) {
throw new Error(
'Could not find the module "' +
modulePath +
'" in the React Client Manifest. ' +
'This is probably a bug in the React Server Components bundler.',
);
}
}
if (resolvedModuleData.async === true && clientReference.$$async === true) {
throw new Error(
'The module "' +
modulePath +
'" is marked as an async ESM module but was loaded as a CJS proxy. ' +
'This is probably a bug in the React Server Components bundler.',
);
}
if (resolvedModuleData.async === true || clientReference.$$async === true) {
return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1];
} else {
return [resolvedModuleData.id, resolvedModuleData.chunks, name];
}
}
export function getServerReferenceId<T>(
config: ClientManifest,
serverReference: ServerReference<T>,
): ServerReferenceId {
return serverReference.$$id;
}
export function getServerReferenceBoundArguments<T>(
config: ClientManifest,
serverReference: ServerReference<T>,
): null | Array<ReactClientValue> {
return serverReference.$$bound;
}
export function getServerReferenceLocation<T>(
config: ClientManifest,
serverReference: ServerReference<T>,
): void | Error {
return serverReference.$$location;
}

View File

@@ -0,0 +1,44 @@
/**
* 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 ImportManifestEntry = {
id: string,
// chunks is a double indexed array of chunkId / chunkFilename pairs
chunks: Array<string>,
name: string,
async?: boolean,
};
// This is the parsed shape of the wire format which is why it is
// condensed to only the essentialy information
export type ImportMetadata =
| [
/* id */ string,
/* chunks id/filename pairs, double indexed */ Array<string>,
/* name */ string,
/* async */ 1,
]
| [
/* id */ string,
/* chunks id/filename pairs, double indexed */ Array<string>,
/* name */ string,
];
export const ID = 0;
export const CHUNKS = 1;
export const NAME = 2;
// export const ASYNC = 3;
// This logic is correct because currently only include the 4th tuple member
// when the module is async. If that changes we will need to actually assert
// the value is true. We don't index into the 4th slot because flow does not
// like the potential out of bounds access
export function isAsyncImport(metadata: ImportMetadata): boolean {
return metadata.length === 4;
}

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
throw new Error(
'The React Server cannot be used outside a react-server environment. ' +
'You must configure Node.js using the `--conditions react-server` flag.',
);

View File

@@ -10,4 +10,4 @@
export {
prerender,
prerenderToNodeStream,
} from './src/server/react-flight-dom-server.node.unbundled';
} from './src/server/react-flight-dom-server.node';

View File

@@ -17,17 +17,14 @@
"client.browser.js",
"client.edge.js",
"client.node.js",
"client.node.unbundled.js",
"server.js",
"server.browser.js",
"server.edge.js",
"server.node.js",
"server.node.unbundled.js",
"static.js",
"static.browser.js",
"static.edge.js",
"static.node.js",
"static.node.unbundled.js",
"node-register.js",
"cjs/",
"esm/"
@@ -39,10 +36,7 @@
"workerd": "./client.edge.js",
"deno": "./client.edge.js",
"worker": "./client.edge.js",
"node": {
"webpack": "./client.node.js",
"default": "./client.node.unbundled.js"
},
"node": "./client.node.js",
"edge-light": "./client.edge.js",
"browser": "./client.browser.js",
"default": "./client.browser.js"
@@ -50,15 +44,11 @@
"./client.browser": "./client.browser.js",
"./client.edge": "./client.edge.js",
"./client.node": "./client.node.js",
"./client.node.unbundled": "./client.node.unbundled.js",
"./server": {
"react-server": {
"workerd": "./server.edge.js",
"deno": "./server.browser.js",
"node": {
"webpack": "./server.node.js",
"default": "./server.node.unbundled.js"
},
"node": "./server.node.js",
"edge-light": "./server.edge.js",
"browser": "./server.browser.js"
},
@@ -67,15 +57,11 @@
"./server.browser": "./server.browser.js",
"./server.edge": "./server.edge.js",
"./server.node": "./server.node.js",
"./server.node.unbundled": "./server.node.unbundled.js",
"./static": {
"react-server": {
"workerd": "./static.edge.js",
"deno": "./static.browser.js",
"node": {
"webpack": "./static.node.js",
"default": "./static.node.unbundled.js"
},
"node": "./static.node.js",
"edge-light": "./static.edge.js",
"browser": "./static.browser.js"
},
@@ -84,7 +70,6 @@
"./static.browser": "./static.browser.js",
"./static.edge": "./static.edge.js",
"./static.node": "./static.node.js",
"./static.node.unbundled": "./static.node.unbundled.js",
"./node-loader": "./esm/react-server-dom-webpack-node-loader.production.js",
"./node-register": "./node-register.js",
"./src/*": "./src/*.js",

View File

@@ -102,6 +102,12 @@ function bind(this: ServerReference<any>): any {
return newFn;
}
const serverReferenceToString = {
value: () => 'function () { [omitted code] }',
configurable: true,
writable: true,
};
export function registerServerReference<T: Function>(
reference: T,
id: string,
@@ -125,12 +131,14 @@ export function registerServerReference<T: Function>(
configurable: true,
},
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
} as PropertyDescriptorMap)
: ({
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
} as PropertyDescriptorMap),
);
}

View File

@@ -61,10 +61,10 @@ describe('ReactFlightDOM', () => {
FlightReactDOM = require('react-dom');
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.node.unbundled'),
require('react-server-dom-unbundled/server.node'),
);
jest.mock('react-server-dom-webpack/static', () =>
require('react-server-dom-webpack/static.node.unbundled'),
require('react-server-dom-unbundled/static.node'),
);
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
@@ -158,6 +158,23 @@ describe('ReactFlightDOM', () => {
};
}
function createUnclosingStream(
stream: ReadableStream<Uint8Array>,
): ReadableStream<Uint8Array> {
const reader = stream.getReader();
const s = new ReadableStream({
async pull(controller) {
const {done, value} = await reader.read();
if (!done) {
controller.enqueue(value);
}
},
});
return s;
}
const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
@@ -3055,7 +3072,7 @@ describe('ReactFlightDOM', () => {
const {prelude} = await pendingResult;
const result = await ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude),
createUnclosingStream(Readable.toWeb(prelude)),
);
const iterator = result.multiShotIterable[Symbol.asyncIterator]();

View File

@@ -232,7 +232,7 @@ describe('ReactFlightDOMEdge', () => {
async function createBufferedUnclosingStream(
stream: ReadableStream<Uint8Array>,
): ReadableStream<Uint8Array> {
): Promise<ReadableStream<Uint8Array>> {
const chunks: Array<Uint8Array> = [];
const reader = stream.getReader();
while (true) {
@@ -2201,4 +2201,59 @@ describe('ReactFlightDOMEdge', () => {
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
);
});
it('should properly resolve with deduped objects', async () => {
const obj = {foo: 'hi'};
function Test(props) {
return props.obj.foo;
}
const root = {
obj: obj,
node: <Test obj={obj} />,
};
const stream = ReactServerDOMServer.renderToReadableStream(root);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const result = await response;
expect(result).toEqual({obj: obj, node: 'hi'});
});
it('does not leak the server reference code', async () => {
function foo() {
return 'foo';
}
const bar = () => {
return 'bar';
};
const anonymous = (
() => () =>
'anonymous'
)();
expect(
ReactServerDOMServer.registerServerReference(foo, 'foo-id').toString(),
).toBe('function () { [omitted code] }');
expect(
ReactServerDOMServer.registerServerReference(bar, 'bar-id').toString(),
).toBe('function () { [omitted code] }');
expect(
ReactServerDOMServer.registerServerReference(
anonymous,
'anonymous-id',
).toString(),
).toBe('function () { [omitted code] }');
});
});

View File

@@ -45,12 +45,12 @@ describe('ReactFlightDOMNode', () => {
// Simulate the condition resolution
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.node'),
jest.requireActual('react-server-dom-webpack/server.node'),
);
ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');
jest.mock('react-server-dom-webpack/static', () =>
require('react-server-dom-webpack/static.node'),
jest.requireActual('react-server-dom-webpack/static.node'),
);
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
@@ -64,7 +64,7 @@ describe('ReactFlightDOMNode', () => {
__unmockReact();
jest.unmock('react-server-dom-webpack/server');
jest.mock('react-server-dom-webpack/client', () =>
require('react-server-dom-webpack/client.node'),
jest.requireActual('react-server-dom-webpack/client.node'),
);
React = require('react');
@@ -127,7 +127,7 @@ describe('ReactFlightDOMNode', () => {
async function createBufferedUnclosingStream(
stream: ReadableStream<Uint8Array>,
): ReadableStream<Uint8Array> {
): Promise<ReadableStream<Uint8Array>> {
const chunks: Array<Uint8Array> = [];
const reader = stream.getReader();
while (true) {
@@ -394,7 +394,7 @@ describe('ReactFlightDOMNode', () => {
);
});
it('should cancels the underlying ReadableStream when we are cancelled', async () => {
it('should cancel the underlying and transported ReadableStreams when we are cancelled', async () => {
let controller;
let cancelReason;
const s = new ReadableStream({
@@ -418,16 +418,30 @@ describe('ReactFlightDOMNode', () => {
),
);
const writable = new Stream.PassThrough(streamOptions);
rscStream.pipe(writable);
const readable = new Stream.PassThrough(streamOptions);
rscStream.pipe(readable);
const result = await ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: webpackModuleLoading,
});
const reader = result.getReader();
controller.enqueue('hi');
await serverAct(async () => {
// We should be able to read the part we already emitted before the abort
expect(await reader.read()).toEqual({
value: 'hi',
done: false,
});
});
const reason = new Error('aborted');
writable.destroy(reason);
readable.destroy(reason);
await new Promise(resolve => {
writable.on('error', () => {
readable.on('error', () => {
resolve();
});
});
@@ -435,9 +449,17 @@ describe('ReactFlightDOMNode', () => {
expect(cancelReason.message).toBe(
'The destination stream errored while writing data.',
);
let error = null;
try {
await reader.read();
} catch (x) {
error = x;
}
expect(error).toBe(reason);
});
it('should cancels the underlying ReadableStream when we abort', async () => {
it('should cancel the underlying and transported ReadableStreams when we abort', async () => {
const errors = [];
let controller;
let cancelReason;

View File

@@ -147,7 +147,7 @@ describe('ReactFlightDOMReplyEdge', () => {
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
});
it('should supports ReadableStreams with typed arrays', async () => {
it('should support ReadableStreams with typed arrays', async () => {
const buffer = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]).buffer;
@@ -246,6 +246,53 @@ describe('ReactFlightDOMReplyEdge', () => {
);
});
it('should cancel the transported ReadableStream when we are cancelled', async () => {
const s = new ReadableStream({
start(controller) {
controller.enqueue('hi');
controller.close();
},
});
const body = await ReactServerDOMClient.encodeReply(s);
const iterable = {
async *[Symbol.asyncIterator]() {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const entry of body) {
if (entry[1] === 'C') {
// Return before finishing the stream.
return;
}
yield entry;
}
},
};
const result = await ReactServerDOMServer.decodeReplyFromAsyncIterable(
iterable,
webpackServerMap,
);
const reader = result.getReader();
// We should be able to read the part we already emitted before the abort
expect(await reader.read()).toEqual({
value: 'hi',
done: false,
});
let error = null;
try {
await reader.read();
} catch (x) {
error = x;
}
expect(error).not.toBe(null);
expect(error.message).toBe('Connection closed.');
});
it('should abort when parsing an incomplete payload', async () => {
const infinitePromise = new Promise(() => {});
const controller = new AbortController();

View File

@@ -128,6 +128,24 @@ ReactPromise.prototype.then = function <T>(
switch (chunk.status) {
case INITIALIZED:
if (typeof resolve === 'function') {
let inspectedValue = chunk.value;
// Recursively check if the value is itself a ReactPromise and if so if it points
// back to itself. This helps catch recursive thenables early error.
while (inspectedValue instanceof ReactPromise) {
if (inspectedValue === chunk) {
if (typeof reject === 'function') {
reject(new Error('Cannot have cyclic thenables.'));
}
return;
}
if (inspectedValue.status === INITIALIZED) {
inspectedValue = inspectedValue.value;
} else {
// If this is lazily resolved, pending or blocked, it'll eventually become
// initialized and break the loop. Rejected also breaks it.
break;
}
}
resolve(chunk.value);
}
break;
@@ -336,7 +354,10 @@ function createResolvedModelChunk<T>(
id: number,
): ResolvedModelChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODEL, value, {id, [RESPONSE_SYMBOL]: response});
return new ReactPromise(RESOLVED_MODEL, value, {
id,
[RESPONSE_SYMBOL]: response,
});
}
function createErroredChunk<T>(
@@ -434,6 +455,11 @@ function loadServerReference<A: Iterable<any>, T>(
if (typeof id !== 'string') {
return (null: any);
}
if (key === 'then') {
// This should never happen because we always serialize objects with then-functions
// as "thenable" which reduces to ReactPromise with no other fields.
return (null: any);
}
const serverReference: ServerReference<T> =
resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
// We expect most servers to not really need this because you'd just have all
@@ -498,6 +524,7 @@ function loadServerReference<A: Iterable<any>, T>(
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = handler.value;
initializedChunk.reason = null;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, handler.value);
}
@@ -666,6 +693,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = value;
initializedChunk.reason = null;
} catch (error) {
const erroredChunk: ErroredChunk<T> = (chunk: any);
erroredChunk.status = ERRORED;
@@ -686,6 +714,8 @@ export function reportGlobalError(response: Response, error: Error): void {
// because we won't be getting any new data to resolve it.
if (chunk.status === PENDING) {
triggerErrorOnChunk(response, chunk, error);
} else if (chunk.status === INITIALIZED && chunk.reason !== null) {
chunk.reason.error(error);
}
});
}
@@ -720,57 +750,32 @@ function fulfillReference(
): void {
const {handler, parentObject, key, map, path} = reference;
for (let i = 1; i < path.length; i++) {
// The server doesn't have any lazy references but we unwrap Chunks here in the same way as the client.
while (value instanceof ReactPromise) {
const referencedChunk: SomeChunk<any> = value;
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
case BLOCKED:
case PENDING: {
// If we're not yet initialized we need to skip what we've already drilled
// through and then wait for the next value to become available.
path.splice(0, i - 1);
// Add "listener" to our new chunk dependency.
if (referencedChunk.value === null) {
referencedChunk.value = [reference];
} else {
referencedChunk.value.push(reference);
}
if (referencedChunk.reason === null) {
referencedChunk.reason = [reference];
} else {
referencedChunk.reason.push(reference);
}
return;
}
default: {
rejectReference(response, reference.handler, referencedChunk.reason);
return;
}
try {
for (let i = 1; i < path.length; i++) {
// The server doesn't have any lazy references so we don't expect to go through a Promise.
const name = path[i];
if (
typeof value === 'object' &&
hasOwnProperty.call(value, name) &&
!(value instanceof Promise)
) {
value = value[name];
} else {
throw new Error('Invalid reference.');
}
}
const name = path[i];
if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
value = value[name];
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && handler.value === null) {
handler.value = mappedValue;
}
}
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && handler.value === null) {
handler.value = mappedValue;
} catch (error) {
rejectReference(response, reference.handler, error);
return;
}
// There are no Elements or Debug Info to transfer here.
@@ -881,53 +886,15 @@ function getOutlinedModel<T>(
case INITIALIZED:
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
// The server doesn't have any lazy references but we unwrap Chunks here in the same way as the client.
while (value instanceof ReactPromise) {
const referencedChunk: SomeChunk<any> = value;
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
break;
}
case BLOCKED:
case PENDING: {
return waitForReference(
referencedChunk,
parentObject,
key,
response,
map,
path.slice(i - 1),
);
}
default: {
// This is an error. Instead of erroring directly, we're going to encode this on
// an initialization handler so that we can catch it at the nearest Element.
if (initializingHandler) {
initializingHandler.errored = true;
initializingHandler.value = null;
initializingHandler.reason = referencedChunk.reason;
} else {
initializingHandler = {
chunk: null,
value: null,
reason: referencedChunk.reason,
deps: 0,
errored: true,
};
}
return (null: any);
}
}
}
const name = path[i];
if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
if (
typeof value === 'object' &&
hasOwnProperty.call(value, name) &&
!(value instanceof Promise)
) {
value = value[name];
} else {
throw new Error('Invalid reference.');
}
}
const chunkValue = map(response, value, parentObject, key);
@@ -973,7 +940,17 @@ function extractIterator(response: Response, model: Array<any>): Iterator<any> {
return model[Symbol.iterator]();
}
function createModel(response: Response, model: any): any {
function createModel(
response: Response,
model: any,
parentObject: Object,
key: string,
): any {
if (key === 'then' && typeof model === 'function') {
// This should never happen because we always serialize objects with then-functions
// as "thenable" which reduces to ReactPromise with no other fields.
return null;
}
return model;
}
@@ -988,6 +965,11 @@ function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
const id = parseInt(reference.slice(2), 16);
const prefix = response._prefix;
const key = prefix + id;
const chunks = response._chunks;
if (chunks.has(id)) {
throw new Error('Already initialized typed array.');
}
// We should have this backingEntry in the store already because we emitted
// it before referencing it. It should be a Blob.
// TODO: Use getOutlinedModel to allow us to emit the Blob later. We should be able to do that now.
@@ -1037,6 +1019,7 @@ function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = handler.value;
initializedChunk.reason = null;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, handler.value);
}
@@ -1098,8 +1081,13 @@ function parseReadableStream<T>(
parentKey: string,
): ReadableStream {
const id = parseInt(reference.slice(2), 16);
const chunks = response._chunks;
if (chunks.has(id)) {
throw new Error('Already initialized stream.');
}
let controller: ReadableStreamController = (null: any);
let closed = false;
const stream = new ReadableStream({
type: type,
start(c) {
@@ -1148,6 +1136,10 @@ function parseReadableStream<T>(
}
},
close(json: string): void {
if (closed) {
return;
}
closed = true;
if (previousBlockedChunk === null) {
controller.close();
} else {
@@ -1158,6 +1150,10 @@ function parseReadableStream<T>(
}
},
error(error: mixed): void {
if (closed) {
return;
}
closed = true;
if (previousBlockedChunk === null) {
// $FlowFixMe[incompatible-call]
controller.error(error);
@@ -1200,6 +1196,10 @@ function parseAsyncIterable<T>(
parentKey: string,
): $AsyncIterable<T, T, void> | $AsyncIterator<T, T, void> {
const id = parseInt(reference.slice(2), 16);
const chunks = response._chunks;
if (chunks.has(id)) {
throw new Error('Already initialized stream.');
}
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
let closed = false;
@@ -1223,6 +1223,9 @@ function parseAsyncIterable<T>(
nextWriteIndex++;
},
close(value: string): void {
if (closed) {
return;
}
closed = true;
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
@@ -1250,6 +1253,9 @@ function parseAsyncIterable<T>(
}
},
error(error: Error): void {
if (closed) {
return;
}
closed = true;
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] =
@@ -1311,7 +1317,7 @@ function parseModelString(
const chunk = getChunk(response, id);
return chunk;
}
case 'F': {
case 'h': {
// Server Reference
const ref = value.slice(2);
return getOutlinedModel(response, ref, obj, key, loadServerReference);

View File

@@ -2672,7 +2672,7 @@ function serializePromiseID(id: number): string {
}
function serializeServerReferenceID(id: number): string {
return '$F' + id.toString(16);
return '$h' + id.toString(16);
}
function serializeSymbolReference(name: string): string {

View File

@@ -38,7 +38,7 @@ describe('ReactFlightAsyncDebugInfo', () => {
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.node'),
jest.requireActual('react-server-dom-webpack/server.node'),
);
ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');
@@ -51,7 +51,7 @@ describe('ReactFlightAsyncDebugInfo', () => {
__unmockReact();
jest.unmock('react-server-dom-webpack/server');
jest.mock('react-server-dom-webpack/client', () =>
require('react-server-dom-webpack/client.node'),
jest.requireActual('react-server-dom-webpack/client.node'),
);
React = require('react');

View File

@@ -0,0 +1,29 @@
/**
* 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 {AsyncLocalStorage} from 'async_hooks';
import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';
export * from 'react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage<Request | void> =
new AsyncLocalStorage();
export const supportsComponentStorage = __DEV__;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
export * from '../ReactFlightServerConfigDebugNode';
export * from '../ReactFlightStackConfigV8';
export * from '../ReactServerConsoleConfigServer';

View File

@@ -551,5 +551,9 @@
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
"564": "Unknown command. The debugChannel was not wired up properly.",
"565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
"566": "FragmentInstance.experimental_scrollIntoView() does not support scrollIntoViewOptions. Use the alignToTop boolean instead."
"566": "FragmentInstance.experimental_scrollIntoView() does not support scrollIntoViewOptions. Use the alignToTop boolean instead.",
"567": "Already initialized stream.",
"568": "Already initialized typed array.",
"569": "Cannot have cyclic thenables.",
"570": "Invalid reference."
}

View File

@@ -476,25 +476,6 @@ const bundles = [
'util',
],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry:
'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled',
name: 'react-server-dom-webpack-server.node.unbundled',
condition: 'react-server',
global: 'ReactServerDOMServer',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: [
'react',
'react-dom',
'async_hooks',
'crypto',
'stream',
'util',
],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
@@ -529,17 +510,6 @@ const bundles = [
wrapWithModuleBoundaries: false,
externals: ['react', 'react-dom', 'util', 'crypto'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry:
'react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled',
name: 'react-server-dom-webpack-client.node.unbundled',
global: 'ReactServerDOMClient',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'react-dom', 'util', 'crypto'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
@@ -786,6 +756,63 @@ const bundles = [
externals: ['acorn'],
},
/******* React Server DOM Unbundled Server *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry: 'react-server-dom-unbundled/src/server/react-flight-dom-server.node',
name: 'react-server-dom-unbundled-server.node',
condition: 'react-server',
global: 'ReactServerDOMServer',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: [
'react',
'react-dom',
'async_hooks',
'crypto',
'stream',
'util',
],
},
/******* React Server DOM Unbundled Client *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry: 'react-server-dom-unbundled/src/client/react-flight-dom-client.node',
name: 'react-server-dom-unbundled-client.node',
global: 'ReactServerDOMClient',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'react-dom', 'util', 'crypto'],
},
/******* React Server DOM Unbundled Node.js Loader *******/
{
bundleTypes: [ESM_PROD],
moduleType: RENDERER_UTILS,
entry: 'react-server-dom-unbundled/node-loader',
condition: 'react-server',
global: 'ReactServerUnbundledNodeLoader',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['acorn'],
},
/******* React Server DOM Unbundled Node.js CommonJS Loader *******/
{
bundleTypes: [NODE_ES2015],
moduleType: RENDERER_UTILS,
entry: 'react-server-dom-unbundled/src/ReactFlightUnbundledNodeRegister',
name: 'react-server-dom-unbundled-node-register',
condition: 'react-server',
global: 'ReactFlightUnbundledNodeRegister',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['url', 'module', 'react-server-dom-unbundled/server'],
},
/******* React Suspense Test Utils *******/
{
bundleTypes: [NODE_ES2015],

View File

@@ -63,8 +63,8 @@ module.exports = [
'react-dom/src/server/react-dom-server.node.js',
'react-dom/test-utils',
'react-dom/unstable_server-external-runtime',
'react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled',
'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled',
'react-server-dom-webpack/src/client/react-flight-dom-client.node',
'react-server-dom-webpack/src/server/react-flight-dom-server.node',
],
paths: [
'react-dom',
@@ -84,20 +84,13 @@ module.exports = [
'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-webpack',
'react-server-dom-webpack/client.node.unbundled',
'react-server-dom-webpack/server',
'react-server-dom-webpack/server.node.unbundled',
'react-server-dom-webpack/static',
'react-server-dom-webpack/static.node.unbundled',
'react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.node
'react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js',
'react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled',
'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled',
'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js',
'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer.js',
'react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js', // react-server-dom-webpack/src/server/react-flight-dom-server.node
'react-devtools',
'react-devtools-core',
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
@@ -240,6 +233,49 @@ module.exports = [
isFlowTyped: true,
isServerSupported: true,
},
{
shortName: 'dom-node-unbundled',
entryPoints: [
'react-server-dom-unbundled/src/client/react-flight-dom-client.node',
'react-server-dom-unbundled/src/server/react-flight-dom-server.node',
],
paths: [
'react-dom',
'react-dom-bindings',
'react-dom/client',
'react-dom/profiling',
'react-dom/server',
'react-dom/server.node',
'react-dom/static',
'react-dom/static.node',
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-unbundled',
'react-server-dom-unbundled/client',
'react-server-dom-unbundled/server',
'react-server-dom-unbundled/server.node',
'react-server-dom-unbundled/static',
'react-server-dom-unbundled/static.node',
'react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-unbundled/client.node
'react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js', // react-server-dom-unbundled/client.node
'react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode.js',
'react-server-dom-unbundled/src/client/react-flight-dom-client.node',
'react-server-dom-unbundled/src/server/react-flight-dom-server.node',
'react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js', // react-server-dom-unbundled/src/server/react-flight-dom-server.node
'react-devtools',
'react-devtools-core',
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
},
{
shortName: 'dom-bun',
entryPoints: ['react-dom/src/server/react-dom-server.bun.js'],