mirror of
https://github.com/facebook/react.git
synced 2026-02-26 07:25:30 +00:00
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:
committed by
Sebastian Sebbie Silbermann
parent
0eeded69c1
commit
dd0519822a
@@ -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',
|
||||
|
||||
84
.github/workflows/runtime_build_and_test.yml
vendored
84
.github/workflows/runtime_build_and_test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
279
packages/react-client/src/ReactFlightClient.js
vendored
279
packages/react-client/src/ReactFlightClient.js
vendored
@@ -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] =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
5
packages/react-server-dom-unbundled/README.md
Normal file
5
packages/react-server-dom-unbundled/README.md
Normal 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.
|
||||
@@ -7,4 +7,4 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export * from './src/client/react-flight-dom-client.node.unbundled';
|
||||
export * from './src/client/react-flight-dom-client.node';
|
||||
3
packages/react-server-dom-unbundled/esm/package.json
Normal file
3
packages/react-server-dom-unbundled/esm/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -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';
|
||||
10
packages/react-server-dom-unbundled/index.js
vendored
Normal file
10
packages/react-server-dom-unbundled/index.js
vendored
Normal 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.');
|
||||
10
packages/react-server-dom-unbundled/node-register.js
vendored
Normal file
10
packages/react-server-dom-unbundled/node-register.js
vendored
Normal 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');
|
||||
7
packages/react-server-dom-unbundled/npm/client.js
vendored
Normal file
7
packages/react-server-dom-unbundled/npm/client.js
vendored
Normal 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');
|
||||
}
|
||||
3
packages/react-server-dom-unbundled/npm/esm/package.json
Normal file
3
packages/react-server-dom-unbundled/npm/esm/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
12
packages/react-server-dom-unbundled/npm/index.js
vendored
Normal file
12
packages/react-server-dom-unbundled/npm/index.js
vendored
Normal 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.');
|
||||
3
packages/react-server-dom-unbundled/npm/node-register.js
vendored
Normal file
3
packages/react-server-dom-unbundled/npm/node-register.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./cjs/react-server-dom-unbundled-node-register.js');
|
||||
6
packages/react-server-dom-unbundled/npm/server.js
vendored
Normal file
6
packages/react-server-dom-unbundled/npm/server.js
vendored
Normal 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.'
|
||||
);
|
||||
20
packages/react-server-dom-unbundled/npm/server.node.js
Normal file
20
packages/react-server-dom-unbundled/npm/server.node.js
Normal 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;
|
||||
6
packages/react-server-dom-unbundled/npm/static.js
vendored
Normal file
6
packages/react-server-dom-unbundled/npm/static.js
vendored
Normal 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.'
|
||||
);
|
||||
11
packages/react-server-dom-unbundled/npm/static.node.js
Normal file
11
packages/react-server-dom-unbundled/npm/static.node.js
Normal 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;
|
||||
46
packages/react-server-dom-unbundled/package.json
Normal file
46
packages/react-server-dom-unbundled/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
13
packages/react-server-dom-unbundled/server.js
vendored
Normal file
13
packages/react-server-dom-unbundled/server.js
vendored
Normal 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.',
|
||||
);
|
||||
@@ -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';
|
||||
804
packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeLoader.js
vendored
Normal file
804
packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeLoader.js
vendored
Normal 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;
|
||||
}
|
||||
109
packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeRegister.js
vendored
Normal file
109
packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeRegister.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
360
packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js
vendored
Normal file
360
packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js
vendored
Normal 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);
|
||||
}
|
||||
32
packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer.js
vendored
Normal file
32
packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer.js
vendored
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
249
packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js
vendored
Normal file
249
packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js
vendored
Normal 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};
|
||||
136
packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js
vendored
Normal file
136
packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js
vendored
Normal 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};
|
||||
701
packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js
vendored
Normal file
701
packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js
vendored
Normal 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,
|
||||
};
|
||||
108
packages/react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler.js
vendored
Normal file
108
packages/react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler.js
vendored
Normal 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;
|
||||
}
|
||||
44
packages/react-server-dom-unbundled/src/shared/ReactFlightImportMetadata.js
vendored
Normal file
44
packages/react-server-dom-unbundled/src/shared/ReactFlightImportMetadata.js
vendored
Normal 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;
|
||||
}
|
||||
13
packages/react-server-dom-unbundled/static.js
vendored
Normal file
13
packages/react-server-dom-unbundled/static.js
vendored
Normal 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.',
|
||||
);
|
||||
@@ -10,4 +10,4 @@
|
||||
export {
|
||||
prerender,
|
||||
prerenderToNodeStream,
|
||||
} from './src/server/react-flight-dom-server.node.unbundled';
|
||||
} from './src/server/react-flight-dom-server.node';
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]();
|
||||
|
||||
@@ -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] }');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
198
packages/react-server/src/ReactFlightReplyServer.js
vendored
198
packages/react-server/src/ReactFlightReplyServer.js
vendored
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user