@samitouri / QOS-React / commits / ef9f6e77b8

Enable passing Server References from Server to Client (#26124)

This is the first of a series of PRs, that let you pass functions, by reference, to the client and back. E.g. through Server Context. It's like client references but they're opaque on the client and resolved on the server. To do this, for security, you must opt-in to exposing these functions to the client using the `"use server"` directive. The `"use client"` directive lets you enter the client from the server. The `"use server"` directive lets you enter the server from the client. This works by tagging those functions as Server References. We could potentially expand this to other non-serializable or stateful objects too like classes. This only implements server->server CJS imports and server->server ESM imports. We really should add a loader to the webpack plug-in for client->server imports too. I'll leave closures as an exercise for integrators. You can't "call" a client reference on the server, however, you can "call" a server reference on the client. This invokes a callback on the Flight client options called `callServer`. This lets a router implement calling back to the server. Effectively creating an RPC. This is using JSON for serializing those arguments but more utils coming from client->server serialization.

Sebastian Markbåge committed Feb 9, 2023 at 19:45 UTC ef9f6e77b8ef968eee659ae797da4bdc07bbbde3
38 files changed +844 -219
fixtures/flight/package.json
+1
@@ -16,6 +16,7 @@
16 "babel-plugin-named-asset-import": "^0.3.8",
17 "babel-preset-react-app": "^10.0.1",
18 "bfj": "^7.0.2",
19 + "body-parser": "^1.20.1",
20 "browserslist": "^4.18.1",
21 "camelcase": "^6.2.1",
22 "case-sensitive-paths-webpack-plugin": "^2.4.0",
fixtures/flight/server/cli.js
+12
@@ -25,6 +25,7 @@ babelRegister({
25 });
26
27 const express = require('express');
28 +const bodyParser = require('body-parser');
29 const app = express();
30
31 // Application
@@ -32,6 +33,17 @@ app.get('/', function (req, res) {
33 require('./handler.js')(req, res);
34 });
35
36 +app.options('/', function (req, res) {
37 + res.setHeader('Allow', 'Allow: GET,HEAD,POST');
38 + res.setHeader('Access-Control-Allow-Origin', '*');
39 + res.setHeader('Access-Control-Allow-Headers', 'rsc-action');
40 + res.end();
41 +});
42 +
43 +app.post('/', bodyParser.text(), function (req, res) {
44 + require('./handler.js')(req, res);
45 +});
46 +
47 app.get('/todos', function (req, res) {
48 res.setHeader('Access-Control-Allow-Origin', '*');
49 res.json([
fixtures/flight/server/handler.js
+41 -23
@@ -1,31 +1,49 @@
1 'use strict';
2
3 const {renderToPipeableStream} = require('react-server-dom-webpack/server');
4 -const {readFile} = require('fs');
4 +const {readFile} = require('fs').promises;
5 const {resolve} = require('path');
6 const React = require('react');
7
8 -module.exports = function (req, res) {
9 - // const m = require('../src/App.js');
10 - import('../src/App.js').then(m => {
11 - const dist = process.env.NODE_ENV === 'development' ? 'dist' : 'build';
12 - readFile(
13 - resolve(__dirname, `../${dist}/react-client-manifest.json`),
14 - 'utf8',
15 - (err, data) => {
16 - if (err) {
17 - throw err;
18 - }
19 -
20 - const App = m.default.default || m.default;
21 - res.setHeader('Access-Control-Allow-Origin', '*');
22 - const moduleMap = JSON.parse(data);
23 - const {pipe} = renderToPipeableStream(
24 - React.createElement(App),
25 - moduleMap
26 - );
27 - pipe(res);
8 +module.exports = async function (req, res) {
9 + switch (req.method) {
10 + case 'POST': {
11 + const serverReference = JSON.parse(req.get('rsc-action'));
12 + const {filepath, name} = serverReference;
13 + const action = (await import(filepath))[name];
14 + // Validate that this is actually a function we intended to expose and
15 + // not the client trying to invoke arbitrary functions. In a real app,
16 + // you'd have a manifest verifying this before even importing it.
17 + if (action.$$typeof !== Symbol.for('react.server.reference')) {
18 + throw new Error('Invalid action');
19 }
29 - );
30 - });
20 +
21 + const args = JSON.parse(req.body);
22 + const result = action.apply(null, args);
23 +
24 + res.setHeader('Access-Control-Allow-Origin', '*');
25 + const {pipe} = renderToPipeableStream(result, {});
26 + pipe(res);
27 +
28 + return;
29 + }
30 + default: {
31 + // const m = require('../src/App.js');
32 + const m = await import('../src/App.js');
33 + const dist = process.env.NODE_ENV === 'development' ? 'dist' : 'build';
34 + const data = await readFile(
35 + resolve(__dirname, `../${dist}/react-client-manifest.json`),
36 + 'utf8'
37 + );
38 + const App = m.default.default || m.default;
39 + res.setHeader('Access-Control-Allow-Origin', '*');
40 + const moduleMap = JSON.parse(data);
41 + const {pipe} = renderToPipeableStream(
42 + React.createElement(App),
43 + moduleMap
44 + );
45 + pipe(res);
46 + return;
47 + }
48 + }
49 };
fixtures/flight/src/App.js
+6
@@ -6,6 +6,9 @@ import {Counter} from './Counter.js';
6 import {Counter as Counter2} from './Counter2.js';
7
8 import ShowMore from './ShowMore.js';
9 +import Button from './Button.js';
10 +
11 +import {like} from './actions.js';
12
13 export default async function App() {
14 const res = await fetch('http://localhost:3001/todos');
@@ -23,6 +26,9 @@ export default async function App() {
26 <ShowMore>
27 <p>Lorem ipsum</p>
28 </ShowMore>
29 + <div>
30 + <Button action={like}>Like</Button>
31 + </div>
32 </Container>
33 );
34 }
fixtures/flight/src/Button.js new
+15
@@ -0,0 +1,15 @@
1 +'use client';
2 +
3 +import * as React from 'react';
4 +
5 +export default function Button({action, children}) {
6 + return (
7 + <button
8 + onClick={async () => {
9 + const result = await action();
10 + console.log(result);
11 + }}>
12 + {children}
13 + </button>
14 + );
15 +}
fixtures/flight/src/actions.js new
+6
@@ -0,0 +1,6 @@
1 +'use server';
2 +
3 +export async function like() {
4 + console.log('Like');
5 + return 'Liked';
6 +}
fixtures/flight/src/index.js
+16 -1
@@ -3,7 +3,22 @@ import {Suspense} from 'react';
3 import ReactDOM from 'react-dom/client';
4 import ReactServerDOMReader from 'react-server-dom-webpack/client';
5
6 -let data = ReactServerDOMReader.createFromFetch(fetch('http://localhost:3001'));
6 +let data = ReactServerDOMReader.createFromFetch(
7 + fetch('http://localhost:3001'),
8 + {
9 + callServer(id, args) {
10 + const response = fetch('http://localhost:3001', {
11 + method: 'POST',
12 + cors: 'cors',
13 + headers: {
14 + 'rsc-action': JSON.stringify({filepath: id.id, name: id.name}),
15 + },
16 + body: JSON.stringify(args),
17 + });
18 + return ReactServerDOMReader.createFromFetch(response);
19 + },
20 + }
21 +);
22
23 function Content() {
24 return React.use(data);
fixtures/flight/yarn.lock
+25
@@ -3221,6 +3221,24 @@ body-parser@1.20.0:
3221 type-is "~1.6.18"
3222 unpipe "1.0.0"
3223
3224 +body-parser@^1.20.1:
3225 + version "1.20.1"
3226 + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
3227 + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
3228 + dependencies:
3229 + bytes "3.1.2"
3230 + content-type "~1.0.4"
3231 + debug "2.6.9"
3232 + depd "2.0.0"
3233 + destroy "1.2.0"
3234 + http-errors "2.0.0"
3235 + iconv-lite "0.4.24"
3236 + on-finished "2.4.1"
3237 + qs "6.11.0"
3238 + raw-body "2.5.1"
3239 + type-is "~1.6.18"
3240 + unpipe "1.0.0"
3241 +
3242 bonjour-service@^1.0.11:
3243 version "1.0.13"
3244 resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.13.tgz#4ac003dc1626023252d58adf2946f57e5da450c1"
@@ -7970,6 +7988,13 @@ qs@6.10.3:
7988 dependencies:
7989 side-channel "^1.0.4"
7990
7991 +qs@6.11.0:
7992 + version "6.11.0"
7993 + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
7994 + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
7995 + dependencies:
7996 + side-channel "^1.0.4"
7997 +
7998 quick-lru@^5.1.1:
7999 version "5.1.1"
8000 resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
packages/react-client/src/ReactFlightClient.js
+70 -9
@@ -12,7 +12,7 @@ import type {LazyComponent} from 'react/src/ReactLazy';
12
13 import type {
14 ClientReference,
15 - ModuleMetaData,
15 + ClientReferenceMetadata,
16 UninitializedModel,
17 Response,
18 BundlerConfig,
@@ -29,6 +29,8 @@ import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
29
30 import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
31
32 +export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
33 +
34 export type JSONValue =
35 | number
36 | null
@@ -148,6 +150,7 @@ Chunk.prototype.then = function <T>(
150
151 export type ResponseBase = {
152 _bundlerConfig: BundlerConfig,
153 + _callServer: CallServerCallback,
154 _chunks: Map<number, SomeChunk<any>>,
155 ...
156 };
@@ -468,6 +471,28 @@ function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
471 return (error: mixed) => triggerErrorOnChunk(chunk, error);
472 }
473
474 +function createServerReferenceProxy<A: Iterable<any>, T>(
475 + response: Response,
476 + metaData: any,
477 +): (...A) => Promise<T> {
478 + const callServer = response._callServer;
479 + const proxy = function (): Promise<T> {
480 + // $FlowFixMe[method-unbinding]
481 + const args = Array.prototype.slice.call(arguments);
482 + const p = metaData.bound;
483 + if (p.status === INITIALIZED) {
484 + const bound = p.value;
485 + return callServer(metaData, bound.concat(args));
486 + }
487 + // Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
488 + // TODO: Remove the wrapper once that's fixed.
489 + return Promise.resolve(p).then(function (bound) {
490 + return callServer(metaData, bound.concat(args));
491 + });
492 + };
493 + return proxy;
494 +}
495 +
496 export function parseModelString(
497 response: Response,
498 parentObject: Object,
@@ -499,11 +524,33 @@ export function parseModelString(
524 return chunk;
525 }
526 case 'S': {
527 + // Symbol
528 return Symbol.for(value.substring(2));
529 }
530 case 'P': {
531 + // Server Context Provider
532 return getOrCreateServerContext(value.substring(2)).Provider;
533 }
534 + case 'F': {
535 + // Server Reference
536 + const id = parseInt(value.substring(2), 16);
537 + const chunk = getChunk(response, id);
538 + switch (chunk.status) {
539 + case RESOLVED_MODEL:
540 + initializeModelChunk(chunk);
541 + break;
542 + }
543 + // The status might have changed after initialization.
544 + switch (chunk.status) {
545 + case INITIALIZED: {
546 + const metadata = chunk.value;
547 + return createServerReferenceProxy(response, metadata);
548 + }
549 + // We always encode it first in the stream so it won't be pending.
550 + default:
551 + throw chunk.reason;
552 + }
553 + }
554 default: {
555 // We assume that anything else is a reference ID.
556 const id = parseInt(value.substring(1), 16);
@@ -551,10 +598,21 @@ export function parseModelTuple(
598 return value;
599 }
600
554 -export function createResponse(bundlerConfig: BundlerConfig): ResponseBase {
601 +function missingCall() {
602 + throw new Error(
603 + 'Trying to call a function from "use server" but the callServer option ' +
604 + 'was not implemented in your router runtime.',
605 + );
606 +}
607 +
608 +export function createResponse(
609 + bundlerConfig: BundlerConfig,
610 + callServer: void | CallServerCallback,
611 +): ResponseBase {
612 const chunks: Map<number, SomeChunk<any>> = new Map();
613 const response = {
614 _bundlerConfig: bundlerConfig,
615 + _callServer: callServer !== undefined ? callServer : missingCall,
616 _chunks: chunks,
617 };
618 return response;
@@ -581,16 +639,19 @@ export function resolveModule(
639 ): void {
640 const chunks = response._chunks;
641 const chunk = chunks.get(id);
584 - const moduleMetaData: ModuleMetaData = parseModel(response, model);
585 - const moduleReference = resolveClientReference<$FlowFixMe>(
642 + const clientReferenceMetadata: ClientReferenceMetadata = parseModel(
643 + response,
644 + model,
645 + );
646 + const clientReference = resolveClientReference<$FlowFixMe>(
647 response._bundlerConfig,
587 - moduleMetaData,
648 + clientReferenceMetadata,
649 );
650
651 // TODO: Add an option to encode modules that are lazy loaded.
652 // For now we preload all modules as early as possible since it's likely
653 // that we'll need them.
593 - const promise = preloadModule(moduleReference);
654 + const promise = preloadModule(clientReference);
655 if (promise) {
656 let blockedChunk: BlockedChunk<any>;
657 if (!chunk) {
@@ -605,16 +666,16 @@ export function resolveModule(
666 blockedChunk.status = BLOCKED;
667 }
668 promise.then(
608 - () => resolveModuleChunk(blockedChunk, moduleReference),
669 + () => resolveModuleChunk(blockedChunk, clientReference),
670 error => triggerErrorOnChunk(blockedChunk, error),
671 );
672 } else {
673 if (!chunk) {
613 - chunks.set(id, createResolvedModuleChunk(response, moduleReference));
674 + chunks.set(id, createResolvedModuleChunk(response, clientReference));
675 } else {
676 // This can't actually happen because we don't have any forward
677 // references to modules.
617 - resolveModuleChunk(chunk, moduleReference);
678 + resolveModuleChunk(chunk, clientReference);
679 }
680 }
681 }
packages/react-client/src/ReactFlightClientStream.js
+6 -3
@@ -7,8 +7,8 @@
7 * @flow
8 */
9
10 +import type {CallServerCallback} from './ReactFlightClient';
11 import type {Response} from './ReactFlightClientHostConfigStream';
11 -
12 import type {BundlerConfig} from './ReactFlightClientHostConfig';
13
14 import {
@@ -120,11 +120,14 @@ function createFromJSONCallback(response: Response) {
120 };
121 }
122
123 -export function createResponse(bundlerConfig: BundlerConfig): Response {
123 +export function createResponse(
124 + bundlerConfig: BundlerConfig,
125 + callServer: void | CallServerCallback,
126 +): Response {
127 // NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
128 // It should be inlined to one object literal but minor changes can break it.
129 const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
127 - const response: any = createResponseBase(bundlerConfig);
130 + const response: any = createResponseBase(bundlerConfig, callServer);
131 response._partialRow = '';
132 if (supportsBinaryStreams) {
133 response._stringDecoder = stringDecoder;
packages/react-client/src/__tests__/ReactFlight-test.js
+8 -2
@@ -447,7 +447,10 @@ describe('ReactFlight', () => {
447 <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
448 <Render promise={ReactNoopFlightClient.read(event)} />
449 </ErrorBoundary>
450 - <ErrorBoundary expectedMessage="Functions cannot be passed directly to Client Components because they're not serializable.">
450 + <ErrorBoundary
451 + expectedMessage={
452 + 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
453 + }>
454 <Render promise={ReactNoopFlightClient.read(fn)} />
455 </ErrorBoundary>
456 <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
@@ -459,7 +462,10 @@ describe('ReactFlight', () => {
462 <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
463 <Render promise={ReactNoopFlightClient.read(eventClient)} />
464 </ErrorBoundary>
462 - <ErrorBoundary expectedMessage="Functions cannot be passed directly to Client Components because they're not serializable.">
465 + <ErrorBoundary
466 + expectedMessage={
467 + 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
468 + }>
469 <Render promise={ReactNoopFlightClient.read(fnClient)} />
470 </ErrorBoundary>
471 <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js
+1 -1
@@ -27,7 +27,7 @@ declare var $$$hostConfig: any;
27
28 export type Response = any;
29 export opaque type BundlerConfig = mixed;
30 -export opaque type ModuleMetaData = mixed;
30 +export opaque type ClientReferenceMetadata = mixed;
31 export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
32 export const resolveClientReference = $$$hostConfig.resolveClientReference;
33 export const preloadModule = $$$hostConfig.preloadModule;
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+4 -1
@@ -51,10 +51,13 @@ const ReactNoopFlightServer = ReactFlightServer({
51 isClientReference(reference: Object): boolean {
52 return reference.$$typeof === Symbol.for('react.client.reference');
53 },
54 + isServerReference(reference: Object): boolean {
55 + return reference.$$typeof === Symbol.for('react.server.reference');
56 + },
57 getClientReferenceKey(reference: Object): Object {
58 return reference;
59 },
57 - resolveModuleMetaData(
60 + resolveClientReferenceMetadata(
61 config: void,
62 reference: {$$typeof: symbol, value: any},
63 ) {
packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+4 -4
@@ -11,7 +11,7 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
11
12 import type {JSResourceReference} from 'JSResourceReference';
13
14 -import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
14 +import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
15
16 export type ClientReference<T> = JSResourceReference<T>;
17
@@ -29,7 +29,7 @@ import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightD
29
30 import isArray from 'shared/isArray';
31
32 -export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
32 +export type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
33
34 export type BundlerConfig = null;
35
@@ -39,9 +39,9 @@ export type Response = ResponseBase;
39
40 export function resolveClientReference<T>(
41 bundlerConfig: BundlerConfig,
42 - moduleData: ModuleMetaData,
42 + metadata: ClientReferenceMetadata,
43 ): ClientReference<T> {
44 - return resolveClientReferenceImpl(moduleData);
44 + return resolveClientReferenceImpl(metadata);
45 }
46
47 function parseModelRecursively(
packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js
+2 -2
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {ModuleMetaData} from 'ReactFlightDOMRelayServerIntegration';
10 +import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration';
11
12 export type JSONValue =
13 | string
@@ -19,7 +19,7 @@ export type JSONValue =
19
20 export type RowEncoding =
21 | ['O', number, JSONValue]
22 - | ['I', number, ModuleMetaData]
22 + | ['I', number, ClientReferenceMetadata]
23 | ['P', number, string]
24 | ['S', number, string]
25 | [
packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+23 -10
@@ -18,31 +18,37 @@ import hasOwnProperty from 'shared/hasOwnProperty';
18 import isArray from 'shared/isArray';
19
20 export type ClientReference<T> = JSResourceReference<T>;
21 +export type ServerReference<T> = T;
22 +export type ServerReferenceMetadata = {};
23
24 import type {
25 Destination,
26 BundlerConfig,
25 - ModuleMetaData,
27 + ClientReferenceMetadata,
28 } from 'ReactFlightDOMRelayServerIntegration';
29
30 import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
31
32 import {
33 emitRow,
32 - resolveModuleMetaData as resolveModuleMetaDataImpl,
34 + resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl,
35 close,
36 } from 'ReactFlightDOMRelayServerIntegration';
37
38 export type {
39 Destination,
40 BundlerConfig,
39 - ModuleMetaData,
41 + ClientReferenceMetadata,
42 } from 'ReactFlightDOMRelayServerIntegration';
43
44 export function isClientReference(reference: Object): boolean {
45 return reference instanceof JSResourceReferenceImpl;
46 }
47
48 +export function isServerReference(reference: Object): boolean {
49 + return false;
50 +}
51 +
52 export type ClientReferenceKey = ClientReference<any>;
53
54 export function getClientReferenceKey(
@@ -53,11 +59,18 @@ export function getClientReferenceKey(
59 return reference;
60 }
61
56 -export function resolveModuleMetaData<T>(
62 +export function resolveClientReferenceMetadata<T>(
63 config: BundlerConfig,
64 resource: ClientReference<T>,
59 -): ModuleMetaData {
60 - return resolveModuleMetaDataImpl(config, resource);
65 +): ClientReferenceMetadata {
66 + return resolveClientReferenceMetadataImpl(config, resource);
67 +}
68 +
69 +export function resolveServerReferenceMetadata<T>(
70 + config: BundlerConfig,
71 + resource: ServerReference<T>,
72 +): ServerReferenceMetadata {
73 + throw new Error('Not implemented.');
74 }
75
76 export type Chunk = RowEncoding;
@@ -162,13 +175,13 @@ export function processReferenceChunk(
175 return ['O', id, reference];
176 }
177
165 -export function processModuleChunk(
178 +export function processImportChunk(
179 request: Request,
180 id: number,
168 - moduleMetaData: ModuleMetaData,
181 + clientReferenceMetadata: ClientReferenceMetadata,
182 ): Chunk {
170 - // The moduleMetaData is already a JSON serializable value.
171 - return ['I', id, moduleMetaData];
183 + // The clientReferenceMetadata is already a JSON serializable value.
184 + return ['I', id, clientReferenceMetadata];
185 }
186
187 export function scheduleWork(callback: () => void) {
packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js
+5 -5
@@ -10,12 +10,12 @@
10 import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
11
12 const ReactFlightDOMRelayClientIntegration = {
13 - resolveClientReference(moduleData) {
14 - return new JSResourceReferenceImpl(moduleData);
13 + resolveClientReference(metadata) {
14 + return new JSResourceReferenceImpl(metadata);
15 },
16 - preloadModule(moduleReference) {},
17 - requireModule(moduleReference) {
18 - return moduleReference._moduleId;
16 + preloadModule(clientReference) {},
17 + requireModule(clientReference) {
18 + return clientReference._moduleId;
19 },
20 };
21
packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js
+1 -1
@@ -12,7 +12,7 @@ const ReactFlightDOMRelayServerIntegration = {
12 destination.push(json);
13 },
14 close(destination) {},
15 - resolveModuleMetaData(config, resource) {
15 + resolveClientReferenceMetadata(config, resource) {
16 return resource._moduleId;
17 },
18 };
packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js
+20 -20
@@ -15,13 +15,13 @@ import type {
15
16 export type WebpackSSRMap = {
17 [clientId: string]: {
18 - [clientExportName: string]: ModuleMetaData,
18 + [clientExportName: string]: ClientReferenceMetadata,
19 },
20 };
21
22 export type BundlerConfig = null | WebpackSSRMap;
23
24 -export opaque type ModuleMetaData = {
24 +export opaque type ClientReferenceMetadata = {
25 id: string,
26 chunks: Array<string>,
27 name: string,
@@ -29,15 +29,15 @@ export opaque type ModuleMetaData = {
29 };
30
31 // eslint-disable-next-line no-unused-vars
32 -export opaque type ClientReference<T> = ModuleMetaData;
32 +export opaque type ClientReference<T> = ClientReferenceMetadata;
33
34 export function resolveClientReference<T>(
35 bundlerConfig: BundlerConfig,
36 - moduleData: ModuleMetaData,
36 + metadata: ClientReferenceMetadata,
37 ): ClientReference<T> {
38 if (bundlerConfig) {
39 - const resolvedModuleData = bundlerConfig[moduleData.id][moduleData.name];
40 - if (moduleData.async) {
39 + const resolvedModuleData = bundlerConfig[metadata.id][metadata.name];
40 + if (metadata.async) {
41 return {
42 id: resolvedModuleData.id,
43 chunks: resolvedModuleData.chunks,
@@ -48,7 +48,7 @@ export function resolveClientReference<T>(
48 return resolvedModuleData;
49 }
50 }
51 - return moduleData;
51 + return metadata;
52 }
53
54 // The chunk cache contains all the chunks we've preloaded so far.
@@ -64,9 +64,9 @@ function ignoreReject() {
64 // Start preloading the modules since we might need them soon.
65 // This function doesn't suspend.
66 export function preloadModule<T>(
67 - moduleData: ClientReference<T>,
67 + metadata: ClientReference<T>,
68 ): null | Thenable<any> {
69 - const chunks = moduleData.chunks;
69 + const chunks = metadata.chunks;
70 const promises = [];
71 for (let i = 0; i < chunks.length; i++) {
72 const chunkId = chunks[i];
@@ -82,8 +82,8 @@ export function preloadModule<T>(
82 promises.push(entry);
83 }
84 }
85 - if (moduleData.async) {
86 - const existingPromise = asyncModuleCache.get(moduleData.id);
85 + if (metadata.async) {
86 + const existingPromise = asyncModuleCache.get(metadata.id);
87 if (existingPromise) {
88 if (existingPromise.status === 'fulfilled') {
89 return null;
@@ -91,7 +91,7 @@ export function preloadModule<T>(
91 return existingPromise;
92 } else {
93 const modulePromise: Thenable<T> = Promise.all(promises).then(() => {
94 - return __webpack_require__(moduleData.id);
94 + return __webpack_require__(metadata.id);
95 });
96 modulePromise.then(
97 value => {
@@ -107,7 +107,7 @@ export function preloadModule<T>(
107 rejectedThenable.reason = reason;
108 },
109 );
110 - asyncModuleCache.set(moduleData.id, modulePromise);
110 + asyncModuleCache.set(metadata.id, modulePromise);
111 return modulePromise;
112 }
113 } else if (promises.length > 0) {
@@ -119,29 +119,29 @@ export function preloadModule<T>(
119
120 // Actually require the module or suspend if it's not yet ready.
121 // Increase priority if necessary.
122 -export function requireModule<T>(moduleData: ClientReference<T>): T {
122 +export function requireModule<T>(metadata: ClientReference<T>): T {
123 let moduleExports;
124 - if (moduleData.async) {
124 + if (metadata.async) {
125 // We assume that preloadModule has been called before, which
126 // should have added something to the module cache.
127 - const promise: any = asyncModuleCache.get(moduleData.id);
127 + const promise: any = asyncModuleCache.get(metadata.id);
128 if (promise.status === 'fulfilled') {
129 moduleExports = promise.value;
130 } else {
131 throw promise.reason;
132 }
133 } else {
134 - moduleExports = __webpack_require__(moduleData.id);
134 + moduleExports = __webpack_require__(metadata.id);
135 }
136 - if (moduleData.name === '*') {
136 + if (metadata.name === '*') {
137 // This is a placeholder value that represents that the caller imported this
138 // as a CommonJS module as is.
139 return moduleExports;
140 }
141 - if (moduleData.name === '') {
141 + if (metadata.name === '') {
142 // This is a placeholder value that represents that the caller accessed the
143 // default property of this if it was an ESM interop module.
144 return moduleExports.__esModule ? moduleExports.default : moduleExports;
145 }
146 - return moduleExports[moduleData.name];
146 + return moduleExports[metadata.name];
147 }
packages/react-server-dom-webpack/src/ReactFlightDOMClient.js
+9
@@ -22,8 +22,14 @@ import {
22 close,
23 } from 'react-client/src/ReactFlightClientStream';
24
25 +type CallServerCallback = <A, T>(
26 + {filepath: string, name: string},
27 + args: A,
28 +) => Promise<T>;
29 +
30 export type Options = {
31 moduleMap?: BundlerConfig,
32 + callServer?: CallServerCallback,
33 };
34
35 function startReadingFromStream(
@@ -59,6 +65,7 @@ function createFromReadableStream<T>(
65 ): Thenable<T> {
66 const response: FlightResponse = createResponse(
67 options && options.moduleMap ? options.moduleMap : null,
68 + options && options.callServer ? options.callServer : undefined,
69 );
70 startReadingFromStream(response, stream);
71 return getRoot(response);
@@ -70,6 +77,7 @@ function createFromFetch<T>(
77 ): Thenable<T> {
78 const response: FlightResponse = createResponse(
79 options && options.moduleMap ? options.moduleMap : null,
80 + options && options.callServer ? options.callServer : undefined,
81 );
82 promiseForResponse.then(
83 function (r) {
@@ -88,6 +96,7 @@ function createFromXHR<T>(
96 ): Thenable<T> {
97 const response: FlightResponse = createResponse(
98 options && options.moduleMap ? options.moduleMap : null,
99 + options && options.callServer ? options.callServer : undefined,
100 );
101 let processedLength = 0;
102 function progress(e: ProgressEvent): void {
packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js
+35 -4
@@ -7,14 +7,29 @@
7 * @flow
8 */
9
10 +import type {ReactModel} from 'react-server/src/ReactFlightServer';
11 +
12 type WebpackMap = {
13 [filepath: string]: {
12 - [name: string]: ModuleMetaData,
14 + [name: string]: ClientReferenceMetadata,
15 },
16 };
17
18 export type BundlerConfig = WebpackMap;
19
20 +export type ServerReference<T: Function> = T & {
21 + $$typeof: symbol,
22 + $$filepath: string,
23 + $$name: string,
24 + $$bound: Array<ReactModel>,
25 +};
26 +
27 +export type ServerReferenceMetadata = {
28 + id: string,
29 + name: string,
30 + bound: Promise<Array<ReactModel>>,
31 +};
32 +
33 // eslint-disable-next-line no-unused-vars
34 export type ClientReference<T> = {
35 $$typeof: symbol,
@@ -23,7 +38,7 @@ export type ClientReference<T> = {
38 async: boolean,
39 };
40
26 -export type ModuleMetaData = {
41 +export type ClientReferenceMetadata = {
42 id: string,
43 chunks: Array<string>,
44 name: string,
@@ -33,6 +48,7 @@ export type ModuleMetaData = {
48 export type ClientReferenceKey = string;
49
50 const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
51 +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
52
53 export function getClientReferenceKey(
54 reference: ClientReference<any>,
@@ -49,10 +65,14 @@ export function isClientReference(reference: Object): boolean {
65 return reference.$$typeof === CLIENT_REFERENCE_TAG;
66 }
67
52 -export function resolveModuleMetaData<T>(
68 +export function isServerReference(reference: Object): boolean {
69 + return reference.$$typeof === SERVER_REFERENCE_TAG;
70 +}
71 +
72 +export function resolveClientReferenceMetadata<T>(
73 config: BundlerConfig,
74 clientReference: ClientReference<T>,
55 -): ModuleMetaData {
75 +): ClientReferenceMetadata {
76 const resolvedModuleData =
77 config[clientReference.filepath][clientReference.name];
78 if (clientReference.async) {
@@ -66,3 +86,14 @@ export function resolveModuleMetaData<T>(
86 return resolvedModuleData;
87 }
88 }
89 +
90 +export function resolveServerReferenceMetadata<T>(
91 + config: BundlerConfig,
92 + serverReference: ServerReference<T>,
93 +): ServerReferenceMetadata {
94 + return {
95 + id: serverReference.$$filepath,
96 + name: serverReference.$$name,
97 + bound: Promise.resolve(serverReference.$$bound),
98 + };
99 +}
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js
+158 -30
@@ -95,6 +95,106 @@ export async function getSource(
95 return defaultGetSource(url, context, defaultGetSource);
96 }
97
98 +function addLocalExportedNames(names: Map<string, string>, node: any) {
99 + switch (node.type) {
100 + case 'Identifier':
101 + names.set(node.name, node.name);
102 + return;
103 + case 'ObjectPattern':
104 + for (let i = 0; i < node.properties.length; i++)
105 + addLocalExportedNames(names, node.properties[i]);
106 + return;
107 + case 'ArrayPattern':
108 + for (let i = 0; i < node.elements.length; i++) {
109 + const element = node.elements[i];
110 + if (element) addLocalExportedNames(names, element);
111 + }
112 + return;
113 + case 'Property':
114 + addLocalExportedNames(names, node.value);
115 + return;
116 + case 'AssignmentPattern':
117 + addLocalExportedNames(names, node.left);
118 + return;
119 + case 'RestElement':
120 + addLocalExportedNames(names, node.argument);
121 + return;
122 + case 'ParenthesizedExpression':
123 + addLocalExportedNames(names, node.expression);
124 + return;
125 + }
126 +}
127 +
128 +function transformServerModule(
129 + source: string,
130 + body: any,
131 + url: string,
132 + loader: LoadFunction,
133 +): string {
134 + // If the same local name is exported more than once, we only need one of the names.
135 + const localNames: Map<string, string> = new Map();
136 + const localTypes: Map<string, string> = new Map();
137 +
138 + for (let i = 0; i < body.length; i++) {
139 + const node = body[i];
140 + switch (node.type) {
141 + case 'ExportAllDeclaration':
142 + // If export * is used, the other file needs to explicitly opt into "use server" too.
143 + break;
144 + case 'ExportDefaultDeclaration':
145 + if (node.declaration.type === 'Identifier') {
146 + localNames.set(node.declaration.name, 'default');
147 + } else if (node.declaration.type === 'FunctionDeclaration') {
148 + if (node.declaration.id) {
149 + localNames.set(node.declaration.id.name, 'default');
150 + localTypes.set(node.declaration.id.name, 'function');
151 + } else {
152 + // TODO: This needs to be rewritten inline because it doesn't have a local name.
153 + }
154 + }
155 + continue;
156 + case 'ExportNamedDeclaration':
157 + if (node.declaration) {
158 + if (node.declaration.type === 'VariableDeclaration') {
159 + const declarations = node.declaration.declarations;
160 + for (let j = 0; j < declarations.length; j++) {
161 + addLocalExportedNames(localNames, declarations[j].id);
162 + }
163 + } else {
164 + const name = node.declaration.id.name;
165 + localNames.set(name, name);
166 + if (node.declaration.type === 'FunctionDeclaration') {
167 + localTypes.set(name, 'function');
168 + }
169 + }
170 + }
171 + if (node.specifiers) {
172 + const specifiers = node.specifiers;
173 + for (let j = 0; j < specifiers.length; j++) {
174 + const specifier = specifiers[j];
175 + localNames.set(specifier.local.name, specifier.exported.name);
176 + }
177 + }
178 + continue;
179 + }
180 + }
181 +
182 + let newSrc = source + '\n\n;';
183 + localNames.forEach(function (exported, local) {
184 + if (localTypes.get(local) !== 'function') {
185 + // We first check if the export is a function and if so annotate it.
186 + newSrc += 'if (typeof ' + local + ' === "function") ';
187 + }
188 + newSrc += 'Object.defineProperties(' + local + ',{';
189 + newSrc += '$$typeof: {value: Symbol.for("react.server.reference")},';
190 + newSrc += '$$filepath: {value: ' + JSON.stringify(url) + '},';
191 + newSrc += '$$name: { value: ' + JSON.stringify(exported) + '},';
192 + newSrc += '$$bound: { value: [] }';
193 + newSrc += '});\n';
194 + });
195 + return newSrc;
196 +}
197 +
198 function addExportNames(names: Array<string>, node: any) {
199 switch (node.type) {
200 case 'Identifier':
@@ -199,39 +299,12 @@ async function parseExportNamesInto(
299 }
300
301 async function transformClientModule(
202 - source: string,
302 + body: any,
303 url: string,
304 loader: LoadFunction,
305 ): Promise<string> {
306 const names: Array<string> = [];
307
208 - // Do a quick check for the exact string. If it doesn't exist, don't
209 - // bother parsing.
210 - if (source.indexOf('use client') === -1) {
211 - return source;
212 - }
213 -
214 - const {body} = acorn.parse(source, {
215 - ecmaVersion: '2019',
216 - sourceType: 'module',
217 - });
218 -
219 - let useClient = false;
220 - for (let i = 0; i < body.length; i++) {
221 - const node = body[i];
222 - if (node.type !== 'ExpressionStatement' || !node.directive) {
223 - break;
224 - }
225 - if (node.directive === 'use client') {
226 - useClient = true;
227 - break;
228 - }
229 - }
230 -
231 - if (!useClient) {
232 - return source;
233 - }
234 -
308 await parseExportNamesInto(body, names, url, loader);
309
310 let newSrc =
@@ -294,6 +367,57 @@ async function loadClientImport(
367 return {format: 'module', source: result.source};
368 }
369
370 +async function transformModuleIfNeeded(
371 + source: string,
372 + url: string,
373 + loader: LoadFunction,
374 +): Promise<string> {
375 + // Do a quick check for the exact string. If it doesn't exist, don't
376 + // bother parsing.
377 + if (
378 + source.indexOf('use client') === -1 &&
379 + source.indexOf('use server') === -1
380 + ) {
381 + return source;
382 + }
383 +
384 + const {body} = acorn.parse(source, {
385 + ecmaVersion: '2019',
386 + sourceType: 'module',
387 + });
388 +
389 + let useClient = false;
390 + let useServer = false;
391 + for (let i = 0; i < body.length; i++) {
392 + const node = body[i];
393 + if (node.type !== 'ExpressionStatement' || !node.directive) {
394 + break;
395 + }
396 + if (node.directive === 'use client') {
397 + useClient = true;
398 + }
399 + if (node.directive === 'use server') {
400 + useServer = true;
401 + }
402 + }
403 +
404 + if (!useClient && !useServer) {
405 + return source;
406 + }
407 +
408 + if (useClient && useServer) {
409 + throw new Error(
410 + 'Cannot have both "use client" and "use server" directives in the same file.',
411 + );
412 + }
413 +
414 + if (useClient) {
415 + return transformClientModule(body, url, loader);
416 + }
417 +
418 + return transformServerModule(source, body, url, loader);
419 +}
420 +
421 export async function transformSource(
422 source: Source,
423 context: TransformSourceContext,
@@ -309,7 +433,7 @@ export async function transformSource(
433 if (typeof transformedSource !== 'string') {
434 throw new Error('Expected source to have been transformed to a string.');
435 }
312 - const newSrc = await transformClientModule(
436 + const newSrc = await transformModuleIfNeeded(
437 transformedSource,
438 context.url,
439 (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => {
@@ -331,7 +455,11 @@ export async function load(
455 if (typeof result.source !== 'string') {
456 throw new Error('Expected source to have been loaded into a string.');
457 }
334 - const newSrc = await transformClientModule(result.source, url, defaultLoad);
458 + const newSrc = await transformModuleIfNeeded(
459 + result.source,
460 + url,
461 + defaultLoad,
462 + );
463 return {format: 'module', source: newSrc};
464 }
465 return defaultLoad(url, context, defaultLoad);
packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js
+81 -13
@@ -15,8 +15,27 @@ const Module = require('module');
15
16 module.exports = function register() {
17 const CLIENT_REFERENCE = Symbol.for('react.client.reference');
18 + const SERVER_REFERENCE = Symbol.for('react.server.reference');
19 const PROMISE_PROTOTYPE = Promise.prototype;
20
21 + // Patch bind on the server to ensure that this creates another
22 + // bound server reference with the additional arguments.
23 + const originalBind = Function.prototype.bind;
24 + /*eslint-disable no-extend-native */
25 + Function.prototype.bind = (function bind(this: any, self: any) {
26 + // $FlowFixMe[unsupported-syntax]
27 + const newFn = originalBind.apply(this, arguments);
28 + if (this.$$typeof === SERVER_REFERENCE) {
29 + // $FlowFixMe[method-unbinding]
30 + const args = Array.prototype.slice.call(arguments, 1);
31 + newFn.$$typeof = SERVER_REFERENCE;
32 + newFn.$$filepath = this.$$filepath;
33 + newFn.$$name = this.$$name;
34 + newFn.$$bound = this.$$bound.concat(args);
35 + }
36 + return newFn;
37 + }: any);
38 +
39 const deepProxyHandlers = {
40 get: function (target: Function, name: string, receiver: Proxy<Function>) {
41 switch (name) {
@@ -216,7 +235,10 @@ module.exports = function register() {
235 ): void {
236 // Do a quick check for the exact string. If it doesn't exist, don't
237 // bother parsing.
219 - if (content.indexOf('use client') === -1) {
238 + if (
239 + content.indexOf('use client') === -1 &&
240 + content.indexOf('use server') === -1
241 + ) {
242 return originalCompile.apply(this, arguments);
243 }
244
@@ -226,6 +248,7 @@ module.exports = function register() {
248 });
249
250 let useClient = false;
251 + let useServer = false;
252 for (let i = 0; i < body.length; i++) {
253 const node = body[i];
254 if (node.type !== 'ExpressionStatement' || !node.directive) {
@@ -233,23 +256,68 @@ module.exports = function register() {
256 }
257 if (node.directive === 'use client') {
258 useClient = true;
236 - break;
259 + }
260 + if (node.directive === 'use server') {
261 + useServer = true;
262 }
263 }
264
240 - if (!useClient) {
265 + if (!useClient && !useServer) {
266 return originalCompile.apply(this, arguments);
267 }
268
244 - const moduleId: string = (url.pathToFileURL(filename).href: any);
245 - const clientReference = Object.defineProperties(({}: any), {
246 - // Represents the whole Module object instead of a particular import.
247 - name: {value: '*'},
248 - $$typeof: {value: CLIENT_REFERENCE},
249 - filepath: {value: moduleId},
250 - async: {value: false},
251 - });
252 - // $FlowFixMe[incompatible-call] found when upgrading Flow
253 - this.exports = new Proxy(clientReference, proxyHandlers);
269 + if (useClient && useServer) {
270 + throw new Error(
271 + 'Cannot have both "use client" and "use server" directives in the same file.',
272 + );
273 + }
274 +
275 + if (useClient) {
276 + const moduleId: string = (url.pathToFileURL(filename).href: any);
277 + const clientReference = Object.defineProperties(({}: any), {
278 + // Represents the whole Module object instead of a particular import.
279 + name: {value: '*'},
280 + $$typeof: {value: CLIENT_REFERENCE},
281 + filepath: {value: moduleId},
282 + async: {value: false},
283 + });
284 + // $FlowFixMe[incompatible-call] found when upgrading Flow
285 + this.exports = new Proxy(clientReference, proxyHandlers);
286 + }
287 +
288 + if (useServer) {
289 + originalCompile.apply(this, arguments);
290 +
291 + const moduleId: string = (url.pathToFileURL(filename).href: any);
292 +
293 + const exports = this.exports;
294 +
295 + // This module is imported server to server, but opts in to exposing functions by
296 + // reference. If there are any functions in the export.
297 + if (typeof exports === 'function') {
298 + // The module exports a function directly,
299 + Object.defineProperties((exports: any), {
300 + // Represents the whole Module object instead of a particular import.
301 + $$typeof: {value: SERVER_REFERENCE},
302 + $$filepath: {value: moduleId},
303 + $$name: {value: '*'},
304 + $$bound: {value: []},
305 + });
306 + } else {
307 + const keys = Object.keys(exports);
308 + for (let i = 0; i < keys.length; i++) {
309 + const key = keys[i];
310 + const value = exports[keys[i]];
311 + if (typeof value === 'function') {
312 + Object.defineProperties((value: any), {
313 + $$typeof: {value: SERVER_REFERENCE},
314 + $$filepath: {value: moduleId},
315 + $$name: {value: key},
316 + $$bound: {value: []},
317 + });
318 + }
319 + }
320 + }
321 + }
322 };
323 };
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+103 -2
@@ -16,8 +16,10 @@ global.TextEncoder = require('util').TextEncoder;
16 global.TextDecoder = require('util').TextDecoder;
17
18 let clientExports;
19 +let serverExports;
20 let webpackMap;
21 let webpackModules;
22 +let webpackServerMap;
23 let act;
24 let React;
25 let ReactDOMClient;
@@ -33,8 +35,10 @@ describe('ReactFlightDOMBrowser', () => {
35 act = require('jest-react').act;
36 const WebpackMock = require('./utils/WebpackMock');
37 clientExports = WebpackMock.clientExports;
38 + serverExports = WebpackMock.serverExports;
39 webpackMap = WebpackMock.webpackMap;
40 webpackModules = WebpackMock.webpackModules;
41 + webpackServerMap = WebpackMock.webpackServerMap;
42 React = require('react');
43 ReactDOMClient = require('react-dom/client');
44 ReactDOMServer = require('react-dom/server.browser');
@@ -478,10 +482,10 @@ describe('ReactFlightDOMBrowser', () => {
482
483 // Instead, we have to provide a translation from the client meta data to the SSR
484 // meta data.
481 - const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath]['*'];
485 + const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*'];
486 const translationMap = {
487 [clientId]: {
484 - '*': ssrMetaData,
488 + '*': ssrMetadata,
489 },
490 };
491
@@ -811,4 +815,101 @@ describe('ReactFlightDOMBrowser', () => {
815 });
816 expect(container.innerHTML).toBe('Hi');
817 });
818 +
819 + function requireServerRef(ref) {
820 + const metaData = webpackServerMap[ref.id][ref.name];
821 + const mod = __webpack_require__(metaData.id);
822 + if (metaData.name === '*') {
823 + return mod;
824 + }
825 + return mod[metaData.name];
826 + }
827 +
828 + it('can pass a function by reference from server to client', async () => {
829 + let actionProxy;
830 +
831 + function Client({action}) {
832 + actionProxy = action;
833 + return 'Click Me';
834 + }
835 +
836 + function send(text) {
837 + return text.toUpperCase();
838 + }
839 +
840 + const ServerModule = serverExports({
841 + send,
842 + });
843 + const ClientRef = clientExports(Client);
844 +
845 + const stream = ReactServerDOMWriter.renderToReadableStream(
846 + <ClientRef action={ServerModule.send} />,
847 + webpackMap,
848 + );
849 +
850 + const response = ReactServerDOMReader.createFromReadableStream(stream, {
851 + async callServer(ref, args) {
852 + const fn = requireServerRef(ref);
853 + return fn.apply(null, args);
854 + },
855 + });
856 +
857 + function App() {
858 + return use(response);
859 + }
860 +
861 + const container = document.createElement('div');
862 + const root = ReactDOMClient.createRoot(container);
863 + await act(async () => {
864 + root.render(<App />);
865 + });
866 + expect(container.innerHTML).toBe('Click Me');
867 + expect(typeof actionProxy).toBe('function');
868 + expect(actionProxy).not.toBe(send);
869 +
870 + const result = await actionProxy('hi');
871 + expect(result).toBe('HI');
872 + });
873 +
874 + it('can bind arguments to a server reference', async () => {
875 + let actionProxy;
876 +
877 + function Client({action}) {
878 + actionProxy = action;
879 + return 'Click Me';
880 + }
881 +
882 + const greet = serverExports(function greet(a, b, c) {
883 + return a + ' ' + b + c;
884 + });
885 + const ClientRef = clientExports(Client);
886 +
887 + const stream = ReactServerDOMWriter.renderToReadableStream(
888 + <ClientRef action={greet.bind(null, 'Hello', 'World')} />,
889 + webpackMap,
890 + );
891 +
892 + const response = ReactServerDOMReader.createFromReadableStream(stream, {
893 + async callServer(ref, args) {
894 + const fn = requireServerRef(ref);
895 + return fn.apply(null, args);
896 + },
897 + });
898 +
899 + function App() {
900 + return use(response);
901 + }
902 +
903 + const container = document.createElement('div');
904 + const root = ReactDOMClient.createRoot(container);
905 + await act(async () => {
906 + root.render(<App />);
907 + });
908 + expect(container.innerHTML).toBe('Click Me');
909 + expect(typeof actionProxy).toBe('function');
910 + expect(actionProxy).not.toBe(greet);
911 +
912 + const result = await actionProxy('!');
913 + expect(result).toBe('Hello World!');
914 + });
915 });
packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js
+42 -10
@@ -11,14 +11,16 @@ const url = require('url');
11 const Module = require('module');
12
13 let webpackModuleIdx = 0;
14 -const webpackModules = {};
14 +const webpackServerModules = {};
15 +const webpackClientModules = {};
16 const webpackErroredModules = {};
16 -const webpackMap = {};
17 +const webpackServerMap = {};
18 +const webpackClientMap = {};
19 global.__webpack_require__ = function (id) {
20 if (webpackErroredModules[id]) {
21 throw webpackErroredModules[id];
22 }
21 - return webpackModules[id];
23 + return webpackClientModules[id] || webpackServerModules[id];
24 };
25
26 const previousCompile = Module.prototype._compile;
@@ -37,14 +39,15 @@ if (previousCompile === nodeCompile) {
39
40 Module.prototype._compile = previousCompile;
41
40 -exports.webpackMap = webpackMap;
41 -exports.webpackModules = webpackModules;
42 +exports.webpackMap = webpackClientMap;
43 +exports.webpackModules = webpackClientModules;
44 +exports.webpackServerMap = webpackServerMap;
45
46 exports.clientModuleError = function clientModuleError(moduleError) {
47 const idx = '' + webpackModuleIdx++;
48 webpackErroredModules[idx] = moduleError;
49 const path = url.pathToFileURL(idx).href;
47 - webpackMap[path] = {
50 + webpackClientMap[path] = {
51 '': {
52 id: idx,
53 chunks: [],
@@ -63,9 +66,9 @@ exports.clientModuleError = function clientModuleError(moduleError) {
66
67 exports.clientExports = function clientExports(moduleExports) {
68 const idx = '' + webpackModuleIdx++;
66 - webpackModules[idx] = moduleExports;
69 + webpackClientModules[idx] = moduleExports;
70 const path = url.pathToFileURL(idx).href;
68 - webpackMap[path] = {
71 + webpackClientMap[path] = {
72 '': {
73 id: idx,
74 chunks: [],
@@ -81,7 +84,7 @@ exports.clientExports = function clientExports(moduleExports) {
84 moduleExports.then(
85 asyncModuleExports => {
86 for (const name in asyncModuleExports) {
84 - webpackMap[path][name] = {
87 + webpackClientMap[path][name] = {
88 id: idx,
89 chunks: [],
90 name: name,
@@ -92,7 +95,7 @@ exports.clientExports = function clientExports(moduleExports) {
95 );
96 }
97 for (const name in moduleExports) {
95 - webpackMap[path][name] = {
98 + webpackClientMap[path][name] = {
99 id: idx,
100 chunks: [],
101 name: name,
@@ -102,3 +105,32 @@ exports.clientExports = function clientExports(moduleExports) {
105 nodeCompile.call(mod, '"use client"', idx);
106 return mod.exports;
107 };
108 +
109 +// This tests server to server references. There's another case of client to server references.
110 +exports.serverExports = function serverExports(moduleExports) {
111 + const idx = '' + webpackModuleIdx++;
112 + webpackServerModules[idx] = moduleExports;
113 + const path = url.pathToFileURL(idx).href;
114 + webpackServerMap[path] = {
115 + '': {
116 + id: idx,
117 + chunks: [],
118 + name: '',
119 + },
120 + '*': {
121 + id: idx,
122 + chunks: [],
123 + name: '*',
124 + },
125 + };
126 + for (const name in moduleExports) {
127 + webpackServerMap[path][name] = {
128 + id: idx,
129 + chunks: [],
130 + name: name,
131 + };
132 + }
133 + const mod = {exports: moduleExports};
134 + nodeCompile.call(mod, '"use server"', idx);
135 + return mod.exports;
136 +};
packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js
+4 -4
@@ -11,7 +11,7 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
11
12 import type {JSResourceReference} from 'JSResourceReference';
13
14 -import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
14 +import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration';
15
16 export type ClientReference<T> = JSResourceReference<T>;
17
@@ -29,7 +29,7 @@ import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightN
29
30 import isArray from 'shared/isArray';
31
32 -export type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
32 +export type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration';
33
34 export type BundlerConfig = null;
35
@@ -39,9 +39,9 @@ export type Response = ResponseBase;
39
40 export function resolveClientReference<T>(
41 bundlerConfig: BundlerConfig,
42 - moduleData: ModuleMetaData,
42 + metadata: ClientReferenceMetadata,
43 ): ClientReference<T> {
44 - return resolveClientReferenceImpl(moduleData);
44 + return resolveClientReferenceImpl(metadata);
45 }
46
47 function parseModelRecursively(
packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js
+2 -2
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {ModuleMetaData} from 'ReactFlightNativeRelayServerIntegration';
10 +import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayServerIntegration';
11
12 export type JSONValue =
13 | string
@@ -19,7 +19,7 @@ export type JSONValue =
19
20 export type RowEncoding =
21 | ['O', number, JSONValue]
22 - | ['I', number, ModuleMetaData]
22 + | ['I', number, ClientReferenceMetadata]
23 | ['P', number, string]
24 | ['S', number, string]
25 | [
packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js
+23 -10
@@ -15,11 +15,13 @@ import type {JSResourceReference} from 'JSResourceReference';
15 import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
16
17 export type ClientReference<T> = JSResourceReference<T>;
18 +export type ServerReference<T> = T;
19 +export type ServerReferenceMetadata = {};
20
21 import type {
22 Destination,
23 BundlerConfig,
22 - ModuleMetaData,
24 + ClientReferenceMetadata,
25 } from 'ReactFlightNativeRelayServerIntegration';
26
27 import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
@@ -27,19 +29,23 @@ import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
29 import {
30 emitRow,
31 close,
30 - resolveModuleMetaData as resolveModuleMetaDataImpl,
32 + resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl,
33 } from 'ReactFlightNativeRelayServerIntegration';
34
35 export type {
36 Destination,
37 BundlerConfig,
36 - ModuleMetaData,
38 + ClientReferenceMetadata,
39 } from 'ReactFlightNativeRelayServerIntegration';
40
41 export function isClientReference(reference: Object): boolean {
42 return reference instanceof JSResourceReferenceImpl;
43 }
44
45 +export function isServerReference(reference: Object): boolean {
46 + return false;
47 +}
48 +
49 export type ClientReferenceKey = ClientReference<any>;
50
51 export function getClientReferenceKey(
@@ -50,11 +56,18 @@ export function getClientReferenceKey(
56 return reference;
57 }
58
53 -export function resolveModuleMetaData<T>(
59 +export function resolveClientReferenceMetadata<T>(
60 config: BundlerConfig,
61 resource: ClientReference<T>,
56 -): ModuleMetaData {
57 - return resolveModuleMetaDataImpl(config, resource);
62 +): ClientReferenceMetadata {
63 + return resolveClientReferenceMetadataImpl(config, resource);
64 +}
65 +
66 +export function resolveServerReferenceMetadata<T>(
67 + config: BundlerConfig,
68 + resource: ServerReference<T>,
69 +): ServerReferenceMetadata {
70 + throw new Error('Not implemented.');
71 }
72
73 export type Chunk = RowEncoding;
@@ -157,13 +170,13 @@ export function processReferenceChunk(
170 return ['O', id, reference];
171 }
172
160 -export function processModuleChunk(
173 +export function processImportChunk(
174 request: Request,
175 id: number,
163 - moduleMetaData: ModuleMetaData,
176 + clientReferenceMetadata: ClientReferenceMetadata,
177 ): Chunk {
165 - // The moduleMetaData is already a JSON serializable value.
166 - return ['I', id, moduleMetaData];
178 + // The clientReferenceMetadata is already a JSON serializable value.
179 + return ['I', id, clientReferenceMetadata];
180 }
181
182 export function scheduleWork(callback: () => void) {
packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js
+5 -5
@@ -10,12 +10,12 @@
10 import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
11
12 const ReactFlightNativeRelayClientIntegration = {
13 - resolveClientReference(moduleData) {
14 - return new JSResourceReferenceImpl(moduleData);
13 + resolveClientReference(metadata) {
14 + return new JSResourceReferenceImpl(metadata);
15 },
16 - preloadModule(moduleReference) {},
17 - requireModule(moduleReference) {
18 - return moduleReference._moduleId;
16 + preloadModule(clientReference) {},
17 + requireModule(clientReference) {
18 + return clientReference._moduleId;
19 },
20 };
21
packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js
+1 -1
@@ -12,7 +12,7 @@ const ReactFlightNativeRelayServerIntegration = {
12 destination.push(json);
13 },
14 close(destination) {},
15 - resolveModuleMetaData(config, resource) {
15 + resolveClientReferenceMetadata(config, resource) {
16 return resource._moduleId;
17 },
18 };
packages/react-server/src/ReactFlightServer.js
+77 -33
@@ -11,9 +11,11 @@ import type {
11 Destination,
12 Chunk,
13 BundlerConfig,
14 - ModuleMetaData,
14 + ClientReferenceMetadata,
15 ClientReference,
16 ClientReferenceKey,
17 + ServerReference,
18 + ServerReferenceMetadata,
19 } from './ReactFlightServerConfig';
20 import type {ContextSnapshot} from './ReactFlightNewContext';
21 import type {ThenableState} from './ReactFlightThenable';
@@ -37,13 +39,15 @@ import {
39 close,
40 closeWithError,
41 processModelChunk,
40 - processModuleChunk,
42 + processImportChunk,
43 processErrorChunkProd,
44 processErrorChunkDev,
45 processReferenceChunk,
44 - resolveModuleMetaData,
46 + resolveClientReferenceMetadata,
47 + resolveServerReferenceMetadata,
48 getClientReferenceKey,
49 isClientReference,
50 + isServerReference,
51 supportsRequestStorage,
52 requestStorage,
53 } from './ReactFlightServerConfig';
@@ -101,7 +105,8 @@ export type ReactModel =
105 | symbol
106 | null
107 | Iterable<ReactModel>
104 - | ReactModelObject;
108 + | ReactModelObject
109 + | Promise<ReactModel>;
110
111 type ReactModelObject = {+[key: string]: ReactModel};
112
@@ -129,11 +134,12 @@ export type Request = {
134 pendingChunks: number,
135 abortableTasks: Set<Task>,
136 pingedTasks: Array<Task>,
132 - completedModuleChunks: Array<Chunk>,
137 + completedImportChunks: Array<Chunk>,
138 completedJSONChunks: Array<Chunk>,
139 completedErrorChunks: Array<Chunk>,
140 writtenSymbols: Map<symbol, number>,
136 - writtenModules: Map<ClientReferenceKey, number>,
141 + writtenClientReferences: Map<ClientReferenceKey, number>,
142 + writtenServerReferences: Map<ServerReference<any>, number>,
143 writtenProviders: Map<string, number>,
144 identifierPrefix: string,
145 identifierCount: number,
@@ -182,11 +188,12 @@ export function createRequest(
188 pendingChunks: 0,
189 abortableTasks: abortSet,
190 pingedTasks: pingedTasks,
185 - completedModuleChunks: [],
186 - completedJSONChunks: [],
187 - completedErrorChunks: [],
191 + completedImportChunks: ([]: Array<Chunk>),
192 + completedJSONChunks: ([]: Array<Chunk>),
193 + completedErrorChunks: ([]: Array<Chunk>),
194 writtenSymbols: new Map(),
189 - writtenModules: new Map(),
195 + writtenClientReferences: new Map(),
196 + writtenServerReferences: new Map(),
197 writtenProviders: new Map(),
198 identifierPrefix: identifierPrefix || '',
199 identifierCount: 1,
@@ -509,6 +516,10 @@ function serializePromiseID(id: number): string {
516 return '$@' + id.toString(16);
517 }
518
519 +function serializeServerReferenceID(id: number): string {
520 + return '$F' + id.toString(16);
521 +}
522 +
523 function serializeSymbolReference(name: string): string {
524 return '$S' + name;
525 }
@@ -521,11 +532,12 @@ function serializeClientReference(
532 request: Request,
533 parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray<ReactModel>,
534 key: string,
524 - moduleReference: ClientReference<any>,
535 + clientReference: ClientReference<any>,
536 ): string {
526 - const moduleKey: ClientReferenceKey = getClientReferenceKey(moduleReference);
527 - const writtenModules = request.writtenModules;
528 - const existingId = writtenModules.get(moduleKey);
537 + const clientReferenceKey: ClientReferenceKey =
538 + getClientReferenceKey(clientReference);
539 + const writtenClientReferences = request.writtenClientReferences;
540 + const existingId = writtenClientReferences.get(clientReferenceKey);
541 if (existingId !== undefined) {
542 if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
543 // If we're encoding the "type" of an element, we can refer
@@ -538,23 +550,21 @@ function serializeClientReference(
550 return serializeByValueID(existingId);
551 }
552 try {
541 - const moduleMetaData: ModuleMetaData = resolveModuleMetaData(
542 - request.bundlerConfig,
543 - moduleReference,
544 - );
553 + const clientReferenceMetadata: ClientReferenceMetadata =
554 + resolveClientReferenceMetadata(request.bundlerConfig, clientReference);
555 request.pendingChunks++;
546 - const moduleId = request.nextChunkId++;
547 - emitModuleChunk(request, moduleId, moduleMetaData);
548 - writtenModules.set(moduleKey, moduleId);
556 + const importId = request.nextChunkId++;
557 + emitImportChunk(request, importId, clientReferenceMetadata);
558 + writtenClientReferences.set(clientReferenceKey, importId);
559 if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
560 // If we're encoding the "type" of an element, we can refer
561 // to that by a lazy reference instead of directly since React
562 // knows how to deal with lazy values. This lets us suspend
563 // on this component rather than its parent until the code has
564 // loaded.
555 - return serializeLazyID(moduleId);
565 + return serializeLazyID(importId);
566 }
557 - return serializeByValueID(moduleId);
567 + return serializeByValueID(importId);
568 } catch (x) {
569 request.pendingChunks++;
570 const errorId = request.nextChunkId++;
@@ -569,6 +579,32 @@ function serializeClientReference(
579 }
580 }
581
582 +function serializeServerReference(
583 + request: Request,
584 + parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray<ReactModel>,
585 + key: string,
586 + serverReference: ServerReference<any>,
587 +): string {
588 + const writtenServerReferences = request.writtenServerReferences;
589 + const existingId = writtenServerReferences.get(serverReference);
590 + if (existingId !== undefined) {
591 + return serializeServerReferenceID(existingId);
592 + }
593 + const serverReferenceMetadata: ServerReferenceMetadata =
594 + resolveServerReferenceMetadata(request.bundlerConfig, serverReference);
595 + request.pendingChunks++;
596 + const metadataId = request.nextChunkId++;
597 + // We assume that this object doesn't suspend.
598 + const processedChunk = processModelChunk(
599 + request,
600 + metadataId,
601 + serverReferenceMetadata,
602 + );
603 + request.completedJSONChunks.push(processedChunk);
604 + writtenServerReferences.set(serverReference, metadataId);
605 + return serializeServerReferenceID(metadataId);
606 +}
607 +
608 function escapeStringValue(value: string): string {
609 if (value[0] === '$') {
610 // We need to escape $ or @ prefixed strings since we use those to encode
@@ -991,6 +1027,7 @@ export function resolveModelToJSON(
1027 if (typeof value === 'object') {
1028 if (isClientReference(value)) {
1029 return serializeClientReference(request, parent, key, (value: any));
1030 + // $FlowFixMe[method-unbinding]
1031 } else if (typeof value.then === 'function') {
1032 // We assume that any object with a .then property is a "Thenable" type,
1033 // or a Promise type. Either of which can be represented by a Promise.
@@ -1067,6 +1104,9 @@ export function resolveModelToJSON(
1104 if (isClientReference(value)) {
1105 return serializeClientReference(request, parent, key, (value: any));
1106 }
1107 + if (isServerReference(value)) {
1108 + return serializeServerReference(request, parent, key, (value: any));
1109 + }
1110 if (/^on[A-Z]/.test(key)) {
1111 throw new Error(
1112 'Event handlers cannot be passed to Client Component props.' +
@@ -1076,7 +1116,7 @@ export function resolveModelToJSON(
1116 } else {
1117 throw new Error(
1118 'Functions cannot be passed directly to Client Components ' +
1079 - "because they're not serializable." +
1119 + 'unless you explicitly expose it by marking it with "use server".' +
1120 describeObjectForErrorMessage(parent, key),
1121 );
1122 }
@@ -1203,19 +1243,23 @@ function emitErrorChunkDev(
1243 request.completedErrorChunks.push(processedChunk);
1244 }
1245
1206 -function emitModuleChunk(
1246 +function emitImportChunk(
1247 request: Request,
1248 id: number,
1209 - moduleMetaData: ModuleMetaData,
1249 + clientReferenceMetadata: ClientReferenceMetadata,
1250 ): void {
1211 - const processedChunk = processModuleChunk(request, id, moduleMetaData);
1212 - request.completedModuleChunks.push(processedChunk);
1251 + const processedChunk = processImportChunk(
1252 + request,
1253 + id,
1254 + clientReferenceMetadata,
1255 + );
1256 + request.completedImportChunks.push(processedChunk);
1257 }
1258
1259 function emitSymbolChunk(request: Request, id: number, name: string): void {
1260 const symbolReference = serializeSymbolReference(name);
1261 const processedChunk = processReferenceChunk(request, id, symbolReference);
1218 - request.completedModuleChunks.push(processedChunk);
1262 + request.completedImportChunks.push(processedChunk);
1263 }
1264
1265 function emitProviderChunk(
@@ -1367,11 +1411,11 @@ function flushCompletedChunks(
1411 try {
1412 // We emit module chunks first in the stream so that
1413 // they can be preloaded as early as possible.
1370 - const moduleChunks = request.completedModuleChunks;
1414 + const importsChunks = request.completedImportChunks;
1415 let i = 0;
1372 - for (; i < moduleChunks.length; i++) {
1416 + for (; i < importsChunks.length; i++) {
1417 request.pendingChunks--;
1374 - const chunk = moduleChunks[i];
1418 + const chunk = importsChunks[i];
1419 const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
1420 if (!keepWriting) {
1421 request.destination = null;
@@ -1379,7 +1423,7 @@ function flushCompletedChunks(
1423 break;
1424 }
1425 }
1382 - moduleChunks.splice(0, i);
1426 + importsChunks.splice(0, i);
1427 // Next comes model data.
1428 const jsonChunks = request.completedJSONChunks;
1429 i = 0;
packages/react-server/src/ReactFlightServerBundlerConfigCustom.js
+8 -2
@@ -11,8 +11,14 @@ declare var $$$hostConfig: any;
11
12 export opaque type BundlerConfig = mixed;
13 export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
14 -export opaque type ModuleMetaData: any = mixed;
14 +export opaque type ServerReference<T> = mixed; // eslint-disable-line no-unused-vars
15 +export opaque type ClientReferenceMetadata: any = mixed;
16 +export opaque type ServerReferenceMetadata: any = mixed;
17 export opaque type ClientReferenceKey: any = mixed;
18 export const isClientReference = $$$hostConfig.isClientReference;
19 +export const isServerReference = $$$hostConfig.isServerReference;
20 export const getClientReferenceKey = $$$hostConfig.getClientReferenceKey;
18 -export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData;
21 +export const resolveClientReferenceMetadata =
22 + $$$hostConfig.resolveClientReferenceMetadata;
23 +export const resolveServerReferenceMetadata =
24 + $$$hostConfig.resolveServerReferenceMetadata;
packages/react-server/src/ReactFlightServerConfigStream.js
+3 -3
@@ -141,12 +141,12 @@ export function processReferenceChunk(
141 return stringToChunk(row);
142 }
143
144 -export function processModuleChunk(
144 +export function processImportChunk(
145 request: Request,
146 id: number,
147 - moduleMetaData: ReactModel,
147 + clientReferenceMetadata: ReactModel,
148 ): Chunk {
149 - const json: string = stringify(moduleMetaData);
149 + const json: string = stringify(clientReferenceMetadata);
150 const row = serializeRowHeader('I', id) + json + '\n';
151 return stringToChunk(row);
152 }
scripts/error-codes/codes.json
+4 -3
@@ -361,7 +361,7 @@
361 "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React.",
362 "373": "This Hook is not supported in Server Components.",
363 "374": "Event handlers cannot be passed to Client Component props.%s\nIf you need interactivity, consider converting part of this to a Client Component.",
364 - "375": "Functions cannot be passed directly to Client Components because they're not serializable.%s",
364 + "375": "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\".%s",
365 "376": "Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(%s) cannot be found among global symbols.%s",
366 "377": "BigInt (%s) is not yet supported in Client Component props.%s",
367 "378": "Type %s is not supported in Client Component props.%s",
@@ -450,5 +450,6 @@
450 "462": "Unexpected SuspendedReason. This is a bug in React.",
451 "463": "ReactDOMServer.renderToNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead.",
452 "464": "ReactDOMServer.renderToStaticNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead.",
453 - "465": "enableFizzExternalRuntime without enableFloat is not supported. This should never appear in production, since it means you are using a misconfigured React bundle."
454 -}
453 + "465": "enableFizzExternalRuntime without enableFloat is not supported. This should never appear in production, since it means you are using a misconfigured React bundle.",
454 + "466": "Trying to call a function from \"use server\" but the callServer option was not implemented in your router runtime."
455 +}
\ No newline at end of file
scripts/flow/react-relay-hooks.js
+10 -10
@@ -46,19 +46,19 @@ declare module 'ReactFlightDOMRelayServerIntegration' {
46 ): void;
47 declare export function close(destination: Destination): void;
48
49 - declare export type ModuleMetaData = JSONValue;
50 - declare export function resolveModuleMetaData<T>(
49 + declare export type ClientReferenceMetadata = JSONValue;
50 + declare export function resolveClientReferenceMetadata<T>(
51 config: BundlerConfig,
52 resourceReference: JSResourceReference<T>,
53 - ): ModuleMetaData;
53 + ): ClientReferenceMetadata;
54 }
55
56 declare module 'ReactFlightDOMRelayClientIntegration' {
57 import type {JSResourceReference} from 'JSResourceReference';
58
59 - declare export opaque type ModuleMetaData;
59 + declare export opaque type ClientReferenceMetadata;
60 declare export function resolveClientReference<T>(
61 - moduleData: ModuleMetaData,
61 + moduleData: ClientReferenceMetadata,
62 ): JSResourceReference<T>;
63 declare export function preloadModule<T>(
64 moduleReference: JSResourceReference<T>,
@@ -79,19 +79,19 @@ declare module 'ReactFlightNativeRelayServerIntegration' {
79 ): void;
80 declare export function close(destination: Destination): void;
81
82 - declare export type ModuleMetaData = JSONValue;
83 - declare export function resolveModuleMetaData<T>(
82 + declare export type ClientReferenceMetadata = JSONValue;
83 + declare export function resolveClientReferenceMetadata<T>(
84 config: BundlerConfig,
85 resourceReference: JSResourceReference<T>,
86 - ): ModuleMetaData;
86 + ): ClientReferenceMetadata;
87 }
88
89 declare module 'ReactFlightNativeRelayClientIntegration' {
90 import type {JSResourceReference} from 'JSResourceReference';
91
92 - declare export opaque type ModuleMetaData;
92 + declare export opaque type ClientReferenceMetadata;
93 declare export function resolveClientReference<T>(
94 - moduleData: ModuleMetaData,
94 + moduleData: ClientReferenceMetadata,
95 ): JSResourceReference<T>;
96 declare export function preloadModule<T>(
97 moduleReference: JSResourceReference<T>,
scripts/jest/setupHostConfigs.js
+2 -1
@@ -83,8 +83,9 @@ jest.mock('react-server/flight', () => {
83 jest.mock(shimServerFormatConfigPath, () => config);
84 jest.mock('react-server/src/ReactFlightServerBundlerConfigCustom', () => ({
85 isClientReference: config.isClientReference,
86 + isServerReference: config.isServerReference,
87 getClientReferenceKey: config.getClientReferenceKey,
87 - resolveModuleMetaData: config.resolveModuleMetaData,
88 + resolveClientReferenceMetadata: config.resolveClientReferenceMetadata,
89 }));
90 jest.mock(shimFlightServerConfigPath, () =>
91 jest.requireActual(
scripts/rollup/build.js
+9 -2
@@ -225,6 +225,7 @@ function isProductionBundleType(bundleType) {
225 switch (bundleType) {
226 case NODE_ES2015:
227 case NODE_ESM:
228 + return true;
229 case UMD_DEV:
230 case NODE_DEV:
231 case BUN_DEV:
@@ -377,12 +378,18 @@ function getPlugins(
378 // Please don't enable this for anything else!
379 isUMDBundle && entry === 'react-art' && commonjs(),
380 // Apply dead code elimination and/or minification.
381 + // closure doesn't yet support leaving ESM imports intact
382 isProduction &&
383 + bundleType !== NODE_ESM &&
384 closure({
385 compilation_level: 'SIMPLE',
383 - language_in: 'ECMASCRIPT_2015',
386 + language_in: 'ECMASCRIPT_2018',
387 language_out:
385 - bundleType === BROWSER_SCRIPT ? 'ECMASCRIPT5' : 'ECMASCRIPT5_STRICT',
388 + bundleType === NODE_ES2015
389 + ? 'ECMASCRIPT_2018'
390 + : bundleType === BROWSER_SCRIPT
391 + ? 'ECMASCRIPT5'
392 + : 'ECMASCRIPT5_STRICT',
393 env: 'CUSTOM',
394 warning_level: 'QUIET',
395 apply_input_source_maps: false,
scripts/rollup/bundles.js
+2 -2
@@ -417,7 +417,7 @@ const bundles = [
417 bundleTypes: [FB_WWW_DEV, FB_WWW_PROD],
418 moduleType: RENDERER,
419 entry: 'react-server-dom-relay/server',
420 - global: 'ReactFlightDOMRelayServer', // TODO: Rename to Writer
420 + global: 'ReactFlightDOMRelayServer',
421 minifyWithProdErrorCodes: false,
422 wrapWithModuleBoundaries: false,
423 externals: [
@@ -465,7 +465,7 @@ const bundles = [
465 bundleTypes: [RN_FB_DEV, RN_FB_PROD],
466 moduleType: RENDERER,
467 entry: 'react-server-native-relay',
468 - global: 'ReactFlightNativeRelayClient', // TODO: Rename to Reader
468 + global: 'ReactFlightNativeRelayClient',
469 minifyWithProdErrorCodes: true,
470 wrapWithModuleBoundaries: false,
471 externals: [