From ef9f6e77b8ef968eee659ae797da4bdc07bbbde3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 9 Feb 2023 19:45:05 -0500 Subject: [PATCH] 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. --- fixtures/flight/package.json | 1 + fixtures/flight/server/cli.js | 12 ++ fixtures/flight/server/handler.js | 64 +++--- fixtures/flight/src/App.js | 6 + fixtures/flight/src/Button.js | 15 ++ fixtures/flight/src/actions.js | 6 + fixtures/flight/src/index.js | 17 +- fixtures/flight/yarn.lock | 25 +++ .../react-client/src/ReactFlightClient.js | 79 +++++++- .../src/ReactFlightClientStream.js | 9 +- .../src/__tests__/ReactFlight-test.js | 10 +- .../ReactFlightClientHostConfig.custom.js | 2 +- .../src/ReactNoopFlightServer.js | 5 +- .../ReactFlightDOMRelayClientHostConfig.js | 8 +- .../src/ReactFlightDOMRelayProtocol.js | 4 +- .../ReactFlightDOMRelayServerHostConfig.js | 33 ++- .../ReactFlightDOMRelayClientIntegration.js | 10 +- .../ReactFlightDOMRelayServerIntegration.js | 2 +- .../ReactFlightClientWebpackBundlerConfig.js | 40 ++-- .../src/ReactFlightDOMClient.js | 9 + .../ReactFlightServerWebpackBundlerConfig.js | 39 +++- .../src/ReactFlightWebpackNodeLoader.js | 188 +++++++++++++++--- .../src/ReactFlightWebpackNodeRegister.js | 94 +++++++-- .../__tests__/ReactFlightDOMBrowser-test.js | 105 +++++++++- .../src/__tests__/utils/WebpackMock.js | 52 ++++- .../ReactFlightNativeRelayClientHostConfig.js | 8 +- .../src/ReactFlightNativeRelayProtocol.js | 4 +- .../ReactFlightNativeRelayServerHostConfig.js | 33 ++- ...ReactFlightNativeRelayClientIntegration.js | 10 +- ...ReactFlightNativeRelayServerIntegration.js | 2 +- .../react-server/src/ReactFlightServer.js | 110 +++++++--- .../ReactFlightServerBundlerConfigCustom.js | 10 +- .../src/ReactFlightServerConfigStream.js | 6 +- scripts/error-codes/codes.json | 7 +- scripts/flow/react-relay-hooks.js | 20 +- scripts/jest/setupHostConfigs.js | 3 +- scripts/rollup/build.js | 11 +- scripts/rollup/bundles.js | 4 +- 38 files changed, 844 insertions(+), 219 deletions(-) create mode 100644 fixtures/flight/src/Button.js create mode 100644 fixtures/flight/src/actions.js diff --git a/fixtures/flight/package.json b/fixtures/flight/package.json index 680c1a0055..9e0906f0b3 100644 --- a/fixtures/flight/package.json +++ b/fixtures/flight/package.json @@ -16,6 +16,7 @@ "babel-plugin-named-asset-import": "^0.3.8", "babel-preset-react-app": "^10.0.1", "bfj": "^7.0.2", + "body-parser": "^1.20.1", "browserslist": "^4.18.1", "camelcase": "^6.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", diff --git a/fixtures/flight/server/cli.js b/fixtures/flight/server/cli.js index 0705d824df..dea40db72e 100644 --- a/fixtures/flight/server/cli.js +++ b/fixtures/flight/server/cli.js @@ -25,6 +25,7 @@ babelRegister({ }); const express = require('express'); +const bodyParser = require('body-parser'); const app = express(); // Application @@ -32,6 +33,17 @@ app.get('/', function (req, res) { require('./handler.js')(req, res); }); +app.options('/', function (req, res) { + res.setHeader('Allow', 'Allow: GET,HEAD,POST'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'rsc-action'); + res.end(); +}); + +app.post('/', bodyParser.text(), function (req, res) { + require('./handler.js')(req, res); +}); + app.get('/todos', function (req, res) { res.setHeader('Access-Control-Allow-Origin', '*'); res.json([ diff --git a/fixtures/flight/server/handler.js b/fixtures/flight/server/handler.js index 5b6f6b2abe..549424afaf 100644 --- a/fixtures/flight/server/handler.js +++ b/fixtures/flight/server/handler.js @@ -1,31 +1,49 @@ 'use strict'; const {renderToPipeableStream} = require('react-server-dom-webpack/server'); -const {readFile} = require('fs'); +const {readFile} = require('fs').promises; const {resolve} = require('path'); const React = require('react'); -module.exports = function (req, res) { - // const m = require('../src/App.js'); - import('../src/App.js').then(m => { - const dist = process.env.NODE_ENV === 'development' ? 'dist' : 'build'; - readFile( - resolve(__dirname, `../${dist}/react-client-manifest.json`), - 'utf8', - (err, data) => { - if (err) { - throw err; - } - - const App = m.default.default || m.default; - res.setHeader('Access-Control-Allow-Origin', '*'); - const moduleMap = JSON.parse(data); - const {pipe} = renderToPipeableStream( - React.createElement(App), - moduleMap - ); - pipe(res); +module.exports = async function (req, res) { + switch (req.method) { + case 'POST': { + const serverReference = JSON.parse(req.get('rsc-action')); + const {filepath, name} = serverReference; + const action = (await import(filepath))[name]; + // Validate that this is actually a function we intended to expose and + // not the client trying to invoke arbitrary functions. In a real app, + // you'd have a manifest verifying this before even importing it. + if (action.$$typeof !== Symbol.for('react.server.reference')) { + throw new Error('Invalid action'); } - ); - }); + + const args = JSON.parse(req.body); + const result = action.apply(null, args); + + res.setHeader('Access-Control-Allow-Origin', '*'); + const {pipe} = renderToPipeableStream(result, {}); + pipe(res); + + return; + } + default: { + // const m = require('../src/App.js'); + const m = await import('../src/App.js'); + const dist = process.env.NODE_ENV === 'development' ? 'dist' : 'build'; + const data = await readFile( + resolve(__dirname, `../${dist}/react-client-manifest.json`), + 'utf8' + ); + const App = m.default.default || m.default; + res.setHeader('Access-Control-Allow-Origin', '*'); + const moduleMap = JSON.parse(data); + const {pipe} = renderToPipeableStream( + React.createElement(App), + moduleMap + ); + pipe(res); + return; + } + } }; diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 0f86f7edad..91583dd43e 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -6,6 +6,9 @@ import {Counter} from './Counter.js'; import {Counter as Counter2} from './Counter2.js'; import ShowMore from './ShowMore.js'; +import Button from './Button.js'; + +import {like} from './actions.js'; export default async function App() { const res = await fetch('http://localhost:3001/todos'); @@ -23,6 +26,9 @@ export default async function App() {

Lorem ipsum

+
+ +
); } diff --git a/fixtures/flight/src/Button.js b/fixtures/flight/src/Button.js new file mode 100644 index 0000000000..811fb8da76 --- /dev/null +++ b/fixtures/flight/src/Button.js @@ -0,0 +1,15 @@ +'use client'; + +import * as React from 'react'; + +export default function Button({action, children}) { + return ( + + ); +} diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js new file mode 100644 index 0000000000..6534908e01 --- /dev/null +++ b/fixtures/flight/src/actions.js @@ -0,0 +1,6 @@ +'use server'; + +export async function like() { + console.log('Like'); + return 'Liked'; +} diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index 71c87a2ea1..8b052b327b 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -3,7 +3,22 @@ import {Suspense} from 'react'; import ReactDOM from 'react-dom/client'; import ReactServerDOMReader from 'react-server-dom-webpack/client'; -let data = ReactServerDOMReader.createFromFetch(fetch('http://localhost:3001')); +let data = ReactServerDOMReader.createFromFetch( + fetch('http://localhost:3001'), + { + callServer(id, args) { + const response = fetch('http://localhost:3001', { + method: 'POST', + cors: 'cors', + headers: { + 'rsc-action': JSON.stringify({filepath: id.id, name: id.name}), + }, + body: JSON.stringify(args), + }); + return ReactServerDOMReader.createFromFetch(response); + }, + } +); function Content() { return React.use(data); diff --git a/fixtures/flight/yarn.lock b/fixtures/flight/yarn.lock index afacff324c..687cfafec5 100644 --- a/fixtures/flight/yarn.lock +++ b/fixtures/flight/yarn.lock @@ -3221,6 +3221,24 @@ body-parser@1.20.0: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + bonjour-service@^1.0.11: version "1.0.13" resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.13.tgz#4ac003dc1626023252d58adf2946f57e5da450c1" @@ -7970,6 +7988,13 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 615ab130df..e187e5af9c 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -12,7 +12,7 @@ import type {LazyComponent} from 'react/src/ReactLazy'; import type { ClientReference, - ModuleMetaData, + ClientReferenceMetadata, UninitializedModel, Response, BundlerConfig, @@ -29,6 +29,8 @@ import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; +export type CallServerCallback = (id: any, args: A) => Promise; + export type JSONValue = | number | null @@ -148,6 +150,7 @@ Chunk.prototype.then = function ( export type ResponseBase = { _bundlerConfig: BundlerConfig, + _callServer: CallServerCallback, _chunks: Map>, ... }; @@ -468,6 +471,28 @@ function createModelReject(chunk: SomeChunk): (error: mixed) => void { return (error: mixed) => triggerErrorOnChunk(chunk, error); } +function createServerReferenceProxy, T>( + response: Response, + metaData: any, +): (...A) => Promise { + const callServer = response._callServer; + const proxy = function (): Promise { + // $FlowFixMe[method-unbinding] + const args = Array.prototype.slice.call(arguments); + const p = metaData.bound; + if (p.status === INITIALIZED) { + const bound = p.value; + return callServer(metaData, bound.concat(args)); + } + // Since this is a fake Promise whose .then doesn't chain, we have to wrap it. + // TODO: Remove the wrapper once that's fixed. + return Promise.resolve(p).then(function (bound) { + return callServer(metaData, bound.concat(args)); + }); + }; + return proxy; +} + export function parseModelString( response: Response, parentObject: Object, @@ -499,11 +524,33 @@ export function parseModelString( return chunk; } case 'S': { + // Symbol return Symbol.for(value.substring(2)); } case 'P': { + // Server Context Provider return getOrCreateServerContext(value.substring(2)).Provider; } + case 'F': { + // Server Reference + const id = parseInt(value.substring(2), 16); + const chunk = getChunk(response, id); + switch (chunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(chunk); + break; + } + // The status might have changed after initialization. + switch (chunk.status) { + case INITIALIZED: { + const metadata = chunk.value; + return createServerReferenceProxy(response, metadata); + } + // We always encode it first in the stream so it won't be pending. + default: + throw chunk.reason; + } + } default: { // We assume that anything else is a reference ID. const id = parseInt(value.substring(1), 16); @@ -551,10 +598,21 @@ export function parseModelTuple( return value; } -export function createResponse(bundlerConfig: BundlerConfig): ResponseBase { +function missingCall() { + throw new Error( + 'Trying to call a function from "use server" but the callServer option ' + + 'was not implemented in your router runtime.', + ); +} + +export function createResponse( + bundlerConfig: BundlerConfig, + callServer: void | CallServerCallback, +): ResponseBase { const chunks: Map> = new Map(); const response = { _bundlerConfig: bundlerConfig, + _callServer: callServer !== undefined ? callServer : missingCall, _chunks: chunks, }; return response; @@ -581,16 +639,19 @@ export function resolveModule( ): void { const chunks = response._chunks; const chunk = chunks.get(id); - const moduleMetaData: ModuleMetaData = parseModel(response, model); - const moduleReference = resolveClientReference<$FlowFixMe>( + const clientReferenceMetadata: ClientReferenceMetadata = parseModel( + response, + model, + ); + const clientReference = resolveClientReference<$FlowFixMe>( response._bundlerConfig, - moduleMetaData, + clientReferenceMetadata, ); // TODO: Add an option to encode modules that are lazy loaded. // For now we preload all modules as early as possible since it's likely // that we'll need them. - const promise = preloadModule(moduleReference); + const promise = preloadModule(clientReference); if (promise) { let blockedChunk: BlockedChunk; if (!chunk) { @@ -605,16 +666,16 @@ export function resolveModule( blockedChunk.status = BLOCKED; } promise.then( - () => resolveModuleChunk(blockedChunk, moduleReference), + () => resolveModuleChunk(blockedChunk, clientReference), error => triggerErrorOnChunk(blockedChunk, error), ); } else { if (!chunk) { - chunks.set(id, createResolvedModuleChunk(response, moduleReference)); + chunks.set(id, createResolvedModuleChunk(response, clientReference)); } else { // This can't actually happen because we don't have any forward // references to modules. - resolveModuleChunk(chunk, moduleReference); + resolveModuleChunk(chunk, clientReference); } } } diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index 57887e48d4..16178c5d80 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -7,8 +7,8 @@ * @flow */ +import type {CallServerCallback} from './ReactFlightClient'; import type {Response} from './ReactFlightClientHostConfigStream'; - import type {BundlerConfig} from './ReactFlightClientHostConfig'; import { @@ -120,11 +120,14 @@ function createFromJSONCallback(response: Response) { }; } -export function createResponse(bundlerConfig: BundlerConfig): Response { +export function createResponse( + bundlerConfig: BundlerConfig, + callServer: void | CallServerCallback, +): Response { // NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS. // It should be inlined to one object literal but minor changes can break it. const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null; - const response: any = createResponseBase(bundlerConfig); + const response: any = createResponseBase(bundlerConfig, callServer); response._partialRow = ''; if (supportsBinaryStreams) { response._stringDecoder = stringDecoder; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 56d4deb3fb..02d95592a1 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -447,7 +447,10 @@ describe('ReactFlight', () => { - + @@ -459,7 +462,10 @@ describe('ReactFlight', () => { - + diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js index c34bb7e25b..8262bd7f16 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js @@ -27,7 +27,7 @@ declare var $$$hostConfig: any; export type Response = any; export opaque type BundlerConfig = mixed; -export opaque type ModuleMetaData = mixed; +export opaque type ClientReferenceMetadata = mixed; export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars export const resolveClientReference = $$$hostConfig.resolveClientReference; export const preloadModule = $$$hostConfig.preloadModule; diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index d50025b87e..da0ec35a8b 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -51,10 +51,13 @@ const ReactNoopFlightServer = ReactFlightServer({ isClientReference(reference: Object): boolean { return reference.$$typeof === Symbol.for('react.client.reference'); }, + isServerReference(reference: Object): boolean { + return reference.$$typeof === Symbol.for('react.server.reference'); + }, getClientReferenceKey(reference: Object): Object { return reference; }, - resolveModuleMetaData( + resolveClientReferenceMetadata( config: void, reference: {$$typeof: symbol, value: any}, ) { diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index d17838bf3d..81b9546874 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -11,7 +11,7 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient'; import type {JSResourceReference} from 'JSResourceReference'; -import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration'; +import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration'; export type ClientReference = JSResourceReference; @@ -29,7 +29,7 @@ import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightD import isArray from 'shared/isArray'; -export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration'; +export type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration'; export type BundlerConfig = null; @@ -39,9 +39,9 @@ export type Response = ResponseBase; export function resolveClientReference( bundlerConfig: BundlerConfig, - moduleData: ModuleMetaData, + metadata: ClientReferenceMetadata, ): ClientReference { - return resolveClientReferenceImpl(moduleData); + return resolveClientReferenceImpl(metadata); } function parseModelRecursively( diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js index f770432dce..73b793f0d7 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js @@ -7,7 +7,7 @@ * @flow */ -import type {ModuleMetaData} from 'ReactFlightDOMRelayServerIntegration'; +import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration'; export type JSONValue = | string @@ -19,7 +19,7 @@ export type JSONValue = export type RowEncoding = | ['O', number, JSONValue] - | ['I', number, ModuleMetaData] + | ['I', number, ClientReferenceMetadata] | ['P', number, string] | ['S', number, string] | [ diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index bf6572110d..b0f41e156d 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -18,31 +18,37 @@ import hasOwnProperty from 'shared/hasOwnProperty'; import isArray from 'shared/isArray'; export type ClientReference = JSResourceReference; +export type ServerReference = T; +export type ServerReferenceMetadata = {}; import type { Destination, BundlerConfig, - ModuleMetaData, + ClientReferenceMetadata, } from 'ReactFlightDOMRelayServerIntegration'; import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; import { emitRow, - resolveModuleMetaData as resolveModuleMetaDataImpl, + resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl, close, } from 'ReactFlightDOMRelayServerIntegration'; export type { Destination, BundlerConfig, - ModuleMetaData, + ClientReferenceMetadata, } from 'ReactFlightDOMRelayServerIntegration'; export function isClientReference(reference: Object): boolean { return reference instanceof JSResourceReferenceImpl; } +export function isServerReference(reference: Object): boolean { + return false; +} + export type ClientReferenceKey = ClientReference; export function getClientReferenceKey( @@ -53,11 +59,18 @@ export function getClientReferenceKey( return reference; } -export function resolveModuleMetaData( +export function resolveClientReferenceMetadata( config: BundlerConfig, resource: ClientReference, -): ModuleMetaData { - return resolveModuleMetaDataImpl(config, resource); +): ClientReferenceMetadata { + return resolveClientReferenceMetadataImpl(config, resource); +} + +export function resolveServerReferenceMetadata( + config: BundlerConfig, + resource: ServerReference, +): ServerReferenceMetadata { + throw new Error('Not implemented.'); } export type Chunk = RowEncoding; @@ -162,13 +175,13 @@ export function processReferenceChunk( return ['O', id, reference]; } -export function processModuleChunk( +export function processImportChunk( request: Request, id: number, - moduleMetaData: ModuleMetaData, + clientReferenceMetadata: ClientReferenceMetadata, ): Chunk { - // The moduleMetaData is already a JSON serializable value. - return ['I', id, moduleMetaData]; + // The clientReferenceMetadata is already a JSON serializable value. + return ['I', id, clientReferenceMetadata]; } export function scheduleWork(callback: () => void) { diff --git a/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js b/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js index 50cc6f3822..1132c75cf7 100644 --- a/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js +++ b/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js @@ -10,12 +10,12 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; const ReactFlightDOMRelayClientIntegration = { - resolveClientReference(moduleData) { - return new JSResourceReferenceImpl(moduleData); + resolveClientReference(metadata) { + return new JSResourceReferenceImpl(metadata); }, - preloadModule(moduleReference) {}, - requireModule(moduleReference) { - return moduleReference._moduleId; + preloadModule(clientReference) {}, + requireModule(clientReference) { + return clientReference._moduleId; }, }; diff --git a/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js b/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js index 138b5edaf7..769e61b1ad 100644 --- a/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js +++ b/packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js @@ -12,7 +12,7 @@ const ReactFlightDOMRelayServerIntegration = { destination.push(json); }, close(destination) {}, - resolveModuleMetaData(config, resource) { + resolveClientReferenceMetadata(config, resource) { return resource._moduleId; }, }; diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js index 12cbc8b54e..739f0160ec 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -15,13 +15,13 @@ import type { export type WebpackSSRMap = { [clientId: string]: { - [clientExportName: string]: ModuleMetaData, + [clientExportName: string]: ClientReferenceMetadata, }, }; export type BundlerConfig = null | WebpackSSRMap; -export opaque type ModuleMetaData = { +export opaque type ClientReferenceMetadata = { id: string, chunks: Array, name: string, @@ -29,15 +29,15 @@ export opaque type ModuleMetaData = { }; // eslint-disable-next-line no-unused-vars -export opaque type ClientReference = ModuleMetaData; +export opaque type ClientReference = ClientReferenceMetadata; export function resolveClientReference( bundlerConfig: BundlerConfig, - moduleData: ModuleMetaData, + metadata: ClientReferenceMetadata, ): ClientReference { if (bundlerConfig) { - const resolvedModuleData = bundlerConfig[moduleData.id][moduleData.name]; - if (moduleData.async) { + const resolvedModuleData = bundlerConfig[metadata.id][metadata.name]; + if (metadata.async) { return { id: resolvedModuleData.id, chunks: resolvedModuleData.chunks, @@ -48,7 +48,7 @@ export function resolveClientReference( return resolvedModuleData; } } - return moduleData; + return metadata; } // The chunk cache contains all the chunks we've preloaded so far. @@ -64,9 +64,9 @@ function ignoreReject() { // Start preloading the modules since we might need them soon. // This function doesn't suspend. export function preloadModule( - moduleData: ClientReference, + metadata: ClientReference, ): null | Thenable { - const chunks = moduleData.chunks; + const chunks = metadata.chunks; const promises = []; for (let i = 0; i < chunks.length; i++) { const chunkId = chunks[i]; @@ -82,8 +82,8 @@ export function preloadModule( promises.push(entry); } } - if (moduleData.async) { - const existingPromise = asyncModuleCache.get(moduleData.id); + if (metadata.async) { + const existingPromise = asyncModuleCache.get(metadata.id); if (existingPromise) { if (existingPromise.status === 'fulfilled') { return null; @@ -91,7 +91,7 @@ export function preloadModule( return existingPromise; } else { const modulePromise: Thenable = Promise.all(promises).then(() => { - return __webpack_require__(moduleData.id); + return __webpack_require__(metadata.id); }); modulePromise.then( value => { @@ -107,7 +107,7 @@ export function preloadModule( rejectedThenable.reason = reason; }, ); - asyncModuleCache.set(moduleData.id, modulePromise); + asyncModuleCache.set(metadata.id, modulePromise); return modulePromise; } } else if (promises.length > 0) { @@ -119,29 +119,29 @@ export function preloadModule( // Actually require the module or suspend if it's not yet ready. // Increase priority if necessary. -export function requireModule(moduleData: ClientReference): T { +export function requireModule(metadata: ClientReference): T { let moduleExports; - if (moduleData.async) { + if (metadata.async) { // We assume that preloadModule has been called before, which // should have added something to the module cache. - const promise: any = asyncModuleCache.get(moduleData.id); + const promise: any = asyncModuleCache.get(metadata.id); if (promise.status === 'fulfilled') { moduleExports = promise.value; } else { throw promise.reason; } } else { - moduleExports = __webpack_require__(moduleData.id); + moduleExports = __webpack_require__(metadata.id); } - if (moduleData.name === '*') { + if (metadata.name === '*') { // This is a placeholder value that represents that the caller imported this // as a CommonJS module as is. return moduleExports; } - if (moduleData.name === '') { + if (metadata.name === '') { // This is a placeholder value that represents that the caller accessed the // default property of this if it was an ESM interop module. return moduleExports.__esModule ? moduleExports.default : moduleExports; } - return moduleExports[moduleData.name]; + return moduleExports[metadata.name]; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js index 1c4f53a0aa..f2b9d0d567 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js @@ -22,8 +22,14 @@ import { close, } from 'react-client/src/ReactFlightClientStream'; +type CallServerCallback = ( + {filepath: string, name: string}, + args: A, +) => Promise; + export type Options = { moduleMap?: BundlerConfig, + callServer?: CallServerCallback, }; function startReadingFromStream( @@ -59,6 +65,7 @@ function createFromReadableStream( ): Thenable { const response: FlightResponse = createResponse( options && options.moduleMap ? options.moduleMap : null, + options && options.callServer ? options.callServer : undefined, ); startReadingFromStream(response, stream); return getRoot(response); @@ -70,6 +77,7 @@ function createFromFetch( ): Thenable { const response: FlightResponse = createResponse( options && options.moduleMap ? options.moduleMap : null, + options && options.callServer ? options.callServer : undefined, ); promiseForResponse.then( function (r) { @@ -88,6 +96,7 @@ function createFromXHR( ): Thenable { const response: FlightResponse = createResponse( options && options.moduleMap ? options.moduleMap : null, + options && options.callServer ? options.callServer : undefined, ); let processedLength = 0; function progress(e: ProgressEvent): void { diff --git a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js index c662d6d51f..bfa27b4af8 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -7,14 +7,29 @@ * @flow */ +import type {ReactModel} from 'react-server/src/ReactFlightServer'; + type WebpackMap = { [filepath: string]: { - [name: string]: ModuleMetaData, + [name: string]: ClientReferenceMetadata, }, }; export type BundlerConfig = WebpackMap; +export type ServerReference = T & { + $$typeof: symbol, + $$filepath: string, + $$name: string, + $$bound: Array, +}; + +export type ServerReferenceMetadata = { + id: string, + name: string, + bound: Promise>, +}; + // eslint-disable-next-line no-unused-vars export type ClientReference = { $$typeof: symbol, @@ -23,7 +38,7 @@ export type ClientReference = { async: boolean, }; -export type ModuleMetaData = { +export type ClientReferenceMetadata = { id: string, chunks: Array, name: string, @@ -33,6 +48,7 @@ export type ModuleMetaData = { export type ClientReferenceKey = string; const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); export function getClientReferenceKey( reference: ClientReference, @@ -49,10 +65,14 @@ export function isClientReference(reference: Object): boolean { return reference.$$typeof === CLIENT_REFERENCE_TAG; } -export function resolveModuleMetaData( +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function resolveClientReferenceMetadata( config: BundlerConfig, clientReference: ClientReference, -): ModuleMetaData { +): ClientReferenceMetadata { const resolvedModuleData = config[clientReference.filepath][clientReference.name]; if (clientReference.async) { @@ -66,3 +86,14 @@ export function resolveModuleMetaData( return resolvedModuleData; } } + +export function resolveServerReferenceMetadata( + config: BundlerConfig, + serverReference: ServerReference, +): ServerReferenceMetadata { + return { + id: serverReference.$$filepath, + name: serverReference.$$name, + bound: Promise.resolve(serverReference.$$bound), + }; +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js index f6bd8a0bc7..297cb62c97 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -95,6 +95,106 @@ export async function getSource( return defaultGetSource(url, context, defaultGetSource); } +function addLocalExportedNames(names: Map, node: any) { + switch (node.type) { + case 'Identifier': + names.set(node.name, node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addLocalExportedNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addLocalExportedNames(names, element); + } + return; + case 'Property': + addLocalExportedNames(names, node.value); + return; + case 'AssignmentPattern': + addLocalExportedNames(names, node.left); + return; + case 'RestElement': + addLocalExportedNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addLocalExportedNames(names, node.expression); + return; + } +} + +function transformServerModule( + source: string, + body: any, + url: string, + loader: LoadFunction, +): string { + // If the same local name is exported more than once, we only need one of the names. + const localNames: Map = new Map(); + const localTypes: Map = new Map(); + + 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') { + localNames.set(node.declaration.name, 'default'); + } else if (node.declaration.type === 'FunctionDeclaration') { + if (node.declaration.id) { + localNames.set(node.declaration.id.name, 'default'); + localTypes.set(node.declaration.id.name, 'function'); + } 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(localNames, declarations[j].id); + } + } else { + const name = node.declaration.id.name; + localNames.set(name, name); + if (node.declaration.type === 'FunctionDeclaration') { + localTypes.set(name, 'function'); + } + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j]; + localNames.set(specifier.local.name, specifier.exported.name); + } + } + continue; + } + } + + let newSrc = source + '\n\n;'; + localNames.forEach(function (exported, local) { + if (localTypes.get(local) !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + local + ' === "function") '; + } + newSrc += 'Object.defineProperties(' + local + ',{'; + newSrc += '$$typeof: {value: Symbol.for("react.server.reference")},'; + newSrc += '$$filepath: {value: ' + JSON.stringify(url) + '},'; + newSrc += '$$name: { value: ' + JSON.stringify(exported) + '},'; + newSrc += '$$bound: { value: [] }'; + newSrc += '});\n'; + }); + return newSrc; +} + function addExportNames(names: Array, node: any) { switch (node.type) { case 'Identifier': @@ -199,39 +299,12 @@ async function parseExportNamesInto( } async function transformClientModule( - source: string, + body: any, url: string, loader: LoadFunction, ): Promise { const names: Array = []; - // Do a quick check for the exact string. If it doesn't exist, don't - // bother parsing. - if (source.indexOf('use client') === -1) { - return source; - } - - const {body} = acorn.parse(source, { - ecmaVersion: '2019', - sourceType: 'module', - }); - - let useClient = 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; - break; - } - } - - if (!useClient) { - return source; - } - await parseExportNamesInto(body, names, url, loader); let newSrc = @@ -294,6 +367,57 @@ async function loadClientImport( return {format: 'module', source: result.source}; } +async function transformModuleIfNeeded( + source: string, + url: string, + loader: LoadFunction, +): Promise { + // 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; + } + + const {body} = acorn.parse(source, { + ecmaVersion: '2019', + sourceType: 'module', + }); + + 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 source; + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + return transformClientModule(body, url, loader); + } + + return transformServerModule(source, body, url, loader); +} + export async function transformSource( source: Source, context: TransformSourceContext, @@ -309,7 +433,7 @@ export async function transformSource( if (typeof transformedSource !== 'string') { throw new Error('Expected source to have been transformed to a string.'); } - const newSrc = await transformClientModule( + const newSrc = await transformModuleIfNeeded( transformedSource, context.url, (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => { @@ -331,7 +455,11 @@ export async function load( if (typeof result.source !== 'string') { throw new Error('Expected source to have been loaded into a string.'); } - const newSrc = await transformClientModule(result.source, url, defaultLoad); + const newSrc = await transformModuleIfNeeded( + result.source, + url, + defaultLoad, + ); return {format: 'module', source: newSrc}; } return defaultLoad(url, context, defaultLoad); diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js index b4bc866388..7baab1c44c 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -15,8 +15,27 @@ const Module = require('module'); module.exports = function register() { const CLIENT_REFERENCE = Symbol.for('react.client.reference'); + const SERVER_REFERENCE = Symbol.for('react.server.reference'); const PROMISE_PROTOTYPE = Promise.prototype; + // Patch bind on the server to ensure that this creates another + // bound server reference with the additional arguments. + const originalBind = Function.prototype.bind; + /*eslint-disable no-extend-native */ + Function.prototype.bind = (function bind(this: any, self: any) { + // $FlowFixMe[unsupported-syntax] + const newFn = originalBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE) { + // $FlowFixMe[method-unbinding] + const args = Array.prototype.slice.call(arguments, 1); + newFn.$$typeof = SERVER_REFERENCE; + newFn.$$filepath = this.$$filepath; + newFn.$$name = this.$$name; + newFn.$$bound = this.$$bound.concat(args); + } + return newFn; + }: any); + const deepProxyHandlers = { get: function (target: Function, name: string, receiver: Proxy) { switch (name) { @@ -216,7 +235,10 @@ module.exports = function register() { ): void { // Do a quick check for the exact string. If it doesn't exist, don't // bother parsing. - if (content.indexOf('use client') === -1) { + if ( + content.indexOf('use client') === -1 && + content.indexOf('use server') === -1 + ) { return originalCompile.apply(this, arguments); } @@ -226,6 +248,7 @@ module.exports = function register() { }); let useClient = false; + let useServer = false; for (let i = 0; i < body.length; i++) { const node = body[i]; if (node.type !== 'ExpressionStatement' || !node.directive) { @@ -233,23 +256,68 @@ module.exports = function register() { } if (node.directive === 'use client') { useClient = true; - break; + } + if (node.directive === 'use server') { + useServer = true; } } - if (!useClient) { + if (!useClient && !useServer) { return originalCompile.apply(this, arguments); } - const moduleId: string = (url.pathToFileURL(filename).href: any); - const clientReference = Object.defineProperties(({}: any), { - // Represents the whole Module object instead of a particular import. - name: {value: '*'}, - $$typeof: {value: CLIENT_REFERENCE}, - filepath: {value: moduleId}, - async: {value: false}, - }); - // $FlowFixMe[incompatible-call] found when upgrading Flow - this.exports = new Proxy(clientReference, proxyHandlers); + 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); + const clientReference = Object.defineProperties(({}: any), { + // Represents the whole Module object instead of a particular import. + name: {value: '*'}, + $$typeof: {value: CLIENT_REFERENCE}, + filepath: {value: moduleId}, + async: {value: false}, + }); + // $FlowFixMe[incompatible-call] found when upgrading Flow + this.exports = new Proxy(clientReference, proxyHandlers); + } + + 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, + Object.defineProperties((exports: any), { + // Represents the whole Module object instead of a particular import. + $$typeof: {value: SERVER_REFERENCE}, + $$filepath: {value: moduleId}, + $$name: {value: '*'}, + $$bound: {value: []}, + }); + } 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') { + Object.defineProperties((value: any), { + $$typeof: {value: SERVER_REFERENCE}, + $$filepath: {value: moduleId}, + $$name: {value: key}, + $$bound: {value: []}, + }); + } + } + } + } }; }; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index a0056d65bd..c9cb1562b4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -16,8 +16,10 @@ global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; let clientExports; +let serverExports; let webpackMap; let webpackModules; +let webpackServerMap; let act; let React; let ReactDOMClient; @@ -33,8 +35,10 @@ describe('ReactFlightDOMBrowser', () => { act = require('jest-react').act; const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; + serverExports = WebpackMock.serverExports; webpackMap = WebpackMock.webpackMap; webpackModules = WebpackMock.webpackModules; + webpackServerMap = WebpackMock.webpackServerMap; React = require('react'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server.browser'); @@ -478,10 +482,10 @@ describe('ReactFlightDOMBrowser', () => { // Instead, we have to provide a translation from the client meta data to the SSR // meta data. - const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath]['*']; + const ssrMetadata = webpackMap[ClientComponentOnTheServer.filepath]['*']; const translationMap = { [clientId]: { - '*': ssrMetaData, + '*': ssrMetadata, }, }; @@ -811,4 +815,101 @@ describe('ReactFlightDOMBrowser', () => { }); expect(container.innerHTML).toBe('Hi'); }); + + function requireServerRef(ref) { + const metaData = webpackServerMap[ref.id][ref.name]; + const mod = __webpack_require__(metaData.id); + if (metaData.name === '*') { + return mod; + } + return mod[metaData.name]; + } + + it('can pass a function by reference from server to client', async () => { + let actionProxy; + + function Client({action}) { + actionProxy = action; + return 'Click Me'; + } + + function send(text) { + return text.toUpperCase(); + } + + const ServerModule = serverExports({ + send, + }); + const ClientRef = clientExports(Client); + + const stream = ReactServerDOMWriter.renderToReadableStream( + , + webpackMap, + ); + + const response = ReactServerDOMReader.createFromReadableStream(stream, { + async callServer(ref, args) { + const fn = requireServerRef(ref); + return fn.apply(null, args); + }, + }); + + function App() { + return use(response); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.innerHTML).toBe('Click Me'); + expect(typeof actionProxy).toBe('function'); + expect(actionProxy).not.toBe(send); + + const result = await actionProxy('hi'); + expect(result).toBe('HI'); + }); + + it('can bind arguments to a server reference', async () => { + let actionProxy; + + function Client({action}) { + actionProxy = action; + return 'Click Me'; + } + + const greet = serverExports(function greet(a, b, c) { + return a + ' ' + b + c; + }); + const ClientRef = clientExports(Client); + + const stream = ReactServerDOMWriter.renderToReadableStream( + , + webpackMap, + ); + + const response = ReactServerDOMReader.createFromReadableStream(stream, { + async callServer(ref, args) { + const fn = requireServerRef(ref); + return fn.apply(null, args); + }, + }); + + function App() { + return use(response); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.innerHTML).toBe('Click Me'); + expect(typeof actionProxy).toBe('function'); + expect(actionProxy).not.toBe(greet); + + const result = await actionProxy('!'); + expect(result).toBe('Hello World!'); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 3ddfffc577..b208cfd030 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -11,14 +11,16 @@ const url = require('url'); const Module = require('module'); let webpackModuleIdx = 0; -const webpackModules = {}; +const webpackServerModules = {}; +const webpackClientModules = {}; const webpackErroredModules = {}; -const webpackMap = {}; +const webpackServerMap = {}; +const webpackClientMap = {}; global.__webpack_require__ = function (id) { if (webpackErroredModules[id]) { throw webpackErroredModules[id]; } - return webpackModules[id]; + return webpackClientModules[id] || webpackServerModules[id]; }; const previousCompile = Module.prototype._compile; @@ -37,14 +39,15 @@ if (previousCompile === nodeCompile) { Module.prototype._compile = previousCompile; -exports.webpackMap = webpackMap; -exports.webpackModules = webpackModules; +exports.webpackMap = webpackClientMap; +exports.webpackModules = webpackClientModules; +exports.webpackServerMap = webpackServerMap; exports.clientModuleError = function clientModuleError(moduleError) { const idx = '' + webpackModuleIdx++; webpackErroredModules[idx] = moduleError; const path = url.pathToFileURL(idx).href; - webpackMap[path] = { + webpackClientMap[path] = { '': { id: idx, chunks: [], @@ -63,9 +66,9 @@ exports.clientModuleError = function clientModuleError(moduleError) { exports.clientExports = function clientExports(moduleExports) { const idx = '' + webpackModuleIdx++; - webpackModules[idx] = moduleExports; + webpackClientModules[idx] = moduleExports; const path = url.pathToFileURL(idx).href; - webpackMap[path] = { + webpackClientMap[path] = { '': { id: idx, chunks: [], @@ -81,7 +84,7 @@ exports.clientExports = function clientExports(moduleExports) { moduleExports.then( asyncModuleExports => { for (const name in asyncModuleExports) { - webpackMap[path][name] = { + webpackClientMap[path][name] = { id: idx, chunks: [], name: name, @@ -92,7 +95,7 @@ exports.clientExports = function clientExports(moduleExports) { ); } for (const name in moduleExports) { - webpackMap[path][name] = { + webpackClientMap[path][name] = { id: idx, chunks: [], name: name, @@ -102,3 +105,32 @@ exports.clientExports = function clientExports(moduleExports) { nodeCompile.call(mod, '"use client"', idx); return mod.exports; }; + +// This tests server to server references. There's another case of client to server references. +exports.serverExports = function serverExports(moduleExports) { + const idx = '' + webpackModuleIdx++; + webpackServerModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + webpackServerMap[path] = { + '': { + id: idx, + chunks: [], + name: '', + }, + '*': { + id: idx, + chunks: [], + name: '*', + }, + }; + for (const name in moduleExports) { + webpackServerMap[path][name] = { + id: idx, + chunks: [], + name: name, + }; + } + const mod = {exports: moduleExports}; + nodeCompile.call(mod, '"use server"', idx); + return mod.exports; +}; diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js index f76b9ba817..cc85a7976e 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js @@ -11,7 +11,7 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient'; import type {JSResourceReference} from 'JSResourceReference'; -import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; +import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration'; export type ClientReference = JSResourceReference; @@ -29,7 +29,7 @@ import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightN import isArray from 'shared/isArray'; -export type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; +export type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration'; export type BundlerConfig = null; @@ -39,9 +39,9 @@ export type Response = ResponseBase; export function resolveClientReference( bundlerConfig: BundlerConfig, - moduleData: ModuleMetaData, + metadata: ClientReferenceMetadata, ): ClientReference { - return resolveClientReferenceImpl(moduleData); + return resolveClientReferenceImpl(metadata); } function parseModelRecursively( diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js index 788fd14293..a242ba36cc 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js @@ -7,7 +7,7 @@ * @flow */ -import type {ModuleMetaData} from 'ReactFlightNativeRelayServerIntegration'; +import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayServerIntegration'; export type JSONValue = | string @@ -19,7 +19,7 @@ export type JSONValue = export type RowEncoding = | ['O', number, JSONValue] - | ['I', number, ModuleMetaData] + | ['I', number, ClientReferenceMetadata] | ['P', number, string] | ['S', number, string] | [ diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index a58bf22f0c..a317484597 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -15,11 +15,13 @@ import type {JSResourceReference} from 'JSResourceReference'; import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; export type ClientReference = JSResourceReference; +export type ServerReference = T; +export type ServerReferenceMetadata = {}; import type { Destination, BundlerConfig, - ModuleMetaData, + ClientReferenceMetadata, } from 'ReactFlightNativeRelayServerIntegration'; import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; @@ -27,19 +29,23 @@ import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; import { emitRow, close, - resolveModuleMetaData as resolveModuleMetaDataImpl, + resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl, } from 'ReactFlightNativeRelayServerIntegration'; export type { Destination, BundlerConfig, - ModuleMetaData, + ClientReferenceMetadata, } from 'ReactFlightNativeRelayServerIntegration'; export function isClientReference(reference: Object): boolean { return reference instanceof JSResourceReferenceImpl; } +export function isServerReference(reference: Object): boolean { + return false; +} + export type ClientReferenceKey = ClientReference; export function getClientReferenceKey( @@ -50,11 +56,18 @@ export function getClientReferenceKey( return reference; } -export function resolveModuleMetaData( +export function resolveClientReferenceMetadata( config: BundlerConfig, resource: ClientReference, -): ModuleMetaData { - return resolveModuleMetaDataImpl(config, resource); +): ClientReferenceMetadata { + return resolveClientReferenceMetadataImpl(config, resource); +} + +export function resolveServerReferenceMetadata( + config: BundlerConfig, + resource: ServerReference, +): ServerReferenceMetadata { + throw new Error('Not implemented.'); } export type Chunk = RowEncoding; @@ -157,13 +170,13 @@ export function processReferenceChunk( return ['O', id, reference]; } -export function processModuleChunk( +export function processImportChunk( request: Request, id: number, - moduleMetaData: ModuleMetaData, + clientReferenceMetadata: ClientReferenceMetadata, ): Chunk { - // The moduleMetaData is already a JSON serializable value. - return ['I', id, moduleMetaData]; + // The clientReferenceMetadata is already a JSON serializable value. + return ['I', id, clientReferenceMetadata]; } export function scheduleWork(callback: () => void) { diff --git a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js index ec0f44c840..40befb5c70 100644 --- a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js +++ b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js @@ -10,12 +10,12 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; const ReactFlightNativeRelayClientIntegration = { - resolveClientReference(moduleData) { - return new JSResourceReferenceImpl(moduleData); + resolveClientReference(metadata) { + return new JSResourceReferenceImpl(metadata); }, - preloadModule(moduleReference) {}, - requireModule(moduleReference) { - return moduleReference._moduleId; + preloadModule(clientReference) {}, + requireModule(clientReference) { + return clientReference._moduleId; }, }; diff --git a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js index 4eab1733c4..52371c59ea 100644 --- a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js +++ b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js @@ -12,7 +12,7 @@ const ReactFlightNativeRelayServerIntegration = { destination.push(json); }, close(destination) {}, - resolveModuleMetaData(config, resource) { + resolveClientReferenceMetadata(config, resource) { return resource._moduleId; }, }; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index b6d395fa7e..20800530b8 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -11,9 +11,11 @@ import type { Destination, Chunk, BundlerConfig, - ModuleMetaData, + ClientReferenceMetadata, ClientReference, ClientReferenceKey, + ServerReference, + ServerReferenceMetadata, } from './ReactFlightServerConfig'; import type {ContextSnapshot} from './ReactFlightNewContext'; import type {ThenableState} from './ReactFlightThenable'; @@ -37,13 +39,15 @@ import { close, closeWithError, processModelChunk, - processModuleChunk, + processImportChunk, processErrorChunkProd, processErrorChunkDev, processReferenceChunk, - resolveModuleMetaData, + resolveClientReferenceMetadata, + resolveServerReferenceMetadata, getClientReferenceKey, isClientReference, + isServerReference, supportsRequestStorage, requestStorage, } from './ReactFlightServerConfig'; @@ -101,7 +105,8 @@ export type ReactModel = | symbol | null | Iterable - | ReactModelObject; + | ReactModelObject + | Promise; type ReactModelObject = {+[key: string]: ReactModel}; @@ -129,11 +134,12 @@ export type Request = { pendingChunks: number, abortableTasks: Set, pingedTasks: Array, - completedModuleChunks: Array, + completedImportChunks: Array, completedJSONChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, - writtenModules: Map, + writtenClientReferences: Map, + writtenServerReferences: Map, number>, writtenProviders: Map, identifierPrefix: string, identifierCount: number, @@ -182,11 +188,12 @@ export function createRequest( pendingChunks: 0, abortableTasks: abortSet, pingedTasks: pingedTasks, - completedModuleChunks: [], - completedJSONChunks: [], - completedErrorChunks: [], + completedImportChunks: ([]: Array), + completedJSONChunks: ([]: Array), + completedErrorChunks: ([]: Array), writtenSymbols: new Map(), - writtenModules: new Map(), + writtenClientReferences: new Map(), + writtenServerReferences: new Map(), writtenProviders: new Map(), identifierPrefix: identifierPrefix || '', identifierCount: 1, @@ -509,6 +516,10 @@ function serializePromiseID(id: number): string { return '$@' + id.toString(16); } +function serializeServerReferenceID(id: number): string { + return '$F' + id.toString(16); +} + function serializeSymbolReference(name: string): string { return '$S' + name; } @@ -521,11 +532,12 @@ function serializeClientReference( request: Request, parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, key: string, - moduleReference: ClientReference, + clientReference: ClientReference, ): string { - const moduleKey: ClientReferenceKey = getClientReferenceKey(moduleReference); - const writtenModules = request.writtenModules; - const existingId = writtenModules.get(moduleKey); + const clientReferenceKey: ClientReferenceKey = + getClientReferenceKey(clientReference); + const writtenClientReferences = request.writtenClientReferences; + const existingId = writtenClientReferences.get(clientReferenceKey); if (existingId !== undefined) { if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { // If we're encoding the "type" of an element, we can refer @@ -538,23 +550,21 @@ function serializeClientReference( return serializeByValueID(existingId); } try { - const moduleMetaData: ModuleMetaData = resolveModuleMetaData( - request.bundlerConfig, - moduleReference, - ); + const clientReferenceMetadata: ClientReferenceMetadata = + resolveClientReferenceMetadata(request.bundlerConfig, clientReference); request.pendingChunks++; - const moduleId = request.nextChunkId++; - emitModuleChunk(request, moduleId, moduleMetaData); - writtenModules.set(moduleKey, moduleId); + const importId = request.nextChunkId++; + emitImportChunk(request, importId, clientReferenceMetadata); + writtenClientReferences.set(clientReferenceKey, importId); if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { // If we're encoding the "type" of an element, we can refer // to that by a lazy reference instead of directly since React // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. - return serializeLazyID(moduleId); + return serializeLazyID(importId); } - return serializeByValueID(moduleId); + return serializeByValueID(importId); } catch (x) { request.pendingChunks++; const errorId = request.nextChunkId++; @@ -569,6 +579,32 @@ function serializeClientReference( } } +function serializeServerReference( + request: Request, + parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, + key: string, + serverReference: ServerReference, +): string { + const writtenServerReferences = request.writtenServerReferences; + const existingId = writtenServerReferences.get(serverReference); + if (existingId !== undefined) { + return serializeServerReferenceID(existingId); + } + const serverReferenceMetadata: ServerReferenceMetadata = + resolveServerReferenceMetadata(request.bundlerConfig, serverReference); + request.pendingChunks++; + const metadataId = request.nextChunkId++; + // We assume that this object doesn't suspend. + const processedChunk = processModelChunk( + request, + metadataId, + serverReferenceMetadata, + ); + request.completedJSONChunks.push(processedChunk); + writtenServerReferences.set(serverReference, metadataId); + return serializeServerReferenceID(metadataId); +} + function escapeStringValue(value: string): string { if (value[0] === '$') { // We need to escape $ or @ prefixed strings since we use those to encode @@ -991,6 +1027,7 @@ export function resolveModelToJSON( if (typeof value === 'object') { if (isClientReference(value)) { return serializeClientReference(request, parent, key, (value: any)); + // $FlowFixMe[method-unbinding] } else if (typeof value.then === 'function') { // 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. @@ -1067,6 +1104,9 @@ export function resolveModelToJSON( if (isClientReference(value)) { return serializeClientReference(request, parent, key, (value: any)); } + if (isServerReference(value)) { + return serializeServerReference(request, parent, key, (value: any)); + } if (/^on[A-Z]/.test(key)) { throw new Error( 'Event handlers cannot be passed to Client Component props.' + @@ -1076,7 +1116,7 @@ export function resolveModelToJSON( } else { throw new Error( 'Functions cannot be passed directly to Client Components ' + - "because they're not serializable." + + 'unless you explicitly expose it by marking it with "use server".' + describeObjectForErrorMessage(parent, key), ); } @@ -1203,19 +1243,23 @@ function emitErrorChunkDev( request.completedErrorChunks.push(processedChunk); } -function emitModuleChunk( +function emitImportChunk( request: Request, id: number, - moduleMetaData: ModuleMetaData, + clientReferenceMetadata: ClientReferenceMetadata, ): void { - const processedChunk = processModuleChunk(request, id, moduleMetaData); - request.completedModuleChunks.push(processedChunk); + const processedChunk = processImportChunk( + request, + id, + clientReferenceMetadata, + ); + request.completedImportChunks.push(processedChunk); } function emitSymbolChunk(request: Request, id: number, name: string): void { const symbolReference = serializeSymbolReference(name); const processedChunk = processReferenceChunk(request, id, symbolReference); - request.completedModuleChunks.push(processedChunk); + request.completedImportChunks.push(processedChunk); } function emitProviderChunk( @@ -1367,11 +1411,11 @@ function flushCompletedChunks( try { // We emit module chunks first in the stream so that // they can be preloaded as early as possible. - const moduleChunks = request.completedModuleChunks; + const importsChunks = request.completedImportChunks; let i = 0; - for (; i < moduleChunks.length; i++) { + for (; i < importsChunks.length; i++) { request.pendingChunks--; - const chunk = moduleChunks[i]; + const chunk = importsChunks[i]; const keepWriting: boolean = writeChunkAndReturn(destination, chunk); if (!keepWriting) { request.destination = null; @@ -1379,7 +1423,7 @@ function flushCompletedChunks( break; } } - moduleChunks.splice(0, i); + importsChunks.splice(0, i); // Next comes model data. const jsonChunks = request.completedJSONChunks; i = 0; diff --git a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js index 0d2f84dbec..b53f0424c8 100644 --- a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js +++ b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js @@ -11,8 +11,14 @@ declare var $$$hostConfig: any; export opaque type BundlerConfig = mixed; export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars -export opaque type ModuleMetaData: any = mixed; +export opaque type ServerReference = mixed; // eslint-disable-line no-unused-vars +export opaque type ClientReferenceMetadata: any = mixed; +export opaque type ServerReferenceMetadata: any = mixed; export opaque type ClientReferenceKey: any = mixed; export const isClientReference = $$$hostConfig.isClientReference; +export const isServerReference = $$$hostConfig.isServerReference; export const getClientReferenceKey = $$$hostConfig.getClientReferenceKey; -export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData; +export const resolveClientReferenceMetadata = + $$$hostConfig.resolveClientReferenceMetadata; +export const resolveServerReferenceMetadata = + $$$hostConfig.resolveServerReferenceMetadata; diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index 7364ab86db..889a856e49 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -141,12 +141,12 @@ export function processReferenceChunk( return stringToChunk(row); } -export function processModuleChunk( +export function processImportChunk( request: Request, id: number, - moduleMetaData: ReactModel, + clientReferenceMetadata: ReactModel, ): Chunk { - const json: string = stringify(moduleMetaData); + const json: string = stringify(clientReferenceMetadata); const row = serializeRowHeader('I', id) + json + '\n'; return stringToChunk(row); } diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 52fa873e63..34db8b08e3 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -361,7 +361,7 @@ "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React.", "373": "This Hook is not supported in Server Components.", "374": "Event handlers cannot be passed to Client Component props.%s\nIf you need interactivity, consider converting part of this to a Client Component.", - "375": "Functions cannot be passed directly to Client Components because they're not serializable.%s", + "375": "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\".%s", "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", "377": "BigInt (%s) is not yet supported in Client Component props.%s", "378": "Type %s is not supported in Client Component props.%s", @@ -450,5 +450,6 @@ "462": "Unexpected SuspendedReason. This is a bug in React.", "463": "ReactDOMServer.renderToNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead.", "464": "ReactDOMServer.renderToStaticNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead.", - "465": "enableFizzExternalRuntime without enableFloat is not supported. This should never appear in production, since it means you are using a misconfigured React bundle." -} + "465": "enableFizzExternalRuntime without enableFloat is not supported. This should never appear in production, since it means you are using a misconfigured React bundle.", + "466": "Trying to call a function from \"use server\" but the callServer option was not implemented in your router runtime." +} \ No newline at end of file diff --git a/scripts/flow/react-relay-hooks.js b/scripts/flow/react-relay-hooks.js index 4b52e1d857..62d2d13909 100644 --- a/scripts/flow/react-relay-hooks.js +++ b/scripts/flow/react-relay-hooks.js @@ -46,19 +46,19 @@ declare module 'ReactFlightDOMRelayServerIntegration' { ): void; declare export function close(destination: Destination): void; - declare export type ModuleMetaData = JSONValue; - declare export function resolveModuleMetaData( + declare export type ClientReferenceMetadata = JSONValue; + declare export function resolveClientReferenceMetadata( config: BundlerConfig, resourceReference: JSResourceReference, - ): ModuleMetaData; + ): ClientReferenceMetadata; } declare module 'ReactFlightDOMRelayClientIntegration' { import type {JSResourceReference} from 'JSResourceReference'; - declare export opaque type ModuleMetaData; + declare export opaque type ClientReferenceMetadata; declare export function resolveClientReference( - moduleData: ModuleMetaData, + moduleData: ClientReferenceMetadata, ): JSResourceReference; declare export function preloadModule( moduleReference: JSResourceReference, @@ -79,19 +79,19 @@ declare module 'ReactFlightNativeRelayServerIntegration' { ): void; declare export function close(destination: Destination): void; - declare export type ModuleMetaData = JSONValue; - declare export function resolveModuleMetaData( + declare export type ClientReferenceMetadata = JSONValue; + declare export function resolveClientReferenceMetadata( config: BundlerConfig, resourceReference: JSResourceReference, - ): ModuleMetaData; + ): ClientReferenceMetadata; } declare module 'ReactFlightNativeRelayClientIntegration' { import type {JSResourceReference} from 'JSResourceReference'; - declare export opaque type ModuleMetaData; + declare export opaque type ClientReferenceMetadata; declare export function resolveClientReference( - moduleData: ModuleMetaData, + moduleData: ClientReferenceMetadata, ): JSResourceReference; declare export function preloadModule( moduleReference: JSResourceReference, diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index 495b1126fc..14c62ca4b4 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -83,8 +83,9 @@ jest.mock('react-server/flight', () => { jest.mock(shimServerFormatConfigPath, () => config); jest.mock('react-server/src/ReactFlightServerBundlerConfigCustom', () => ({ isClientReference: config.isClientReference, + isServerReference: config.isServerReference, getClientReferenceKey: config.getClientReferenceKey, - resolveModuleMetaData: config.resolveModuleMetaData, + resolveClientReferenceMetadata: config.resolveClientReferenceMetadata, })); jest.mock(shimFlightServerConfigPath, () => jest.requireActual( diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 73e2e30531..f4e4198b06 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -225,6 +225,7 @@ function isProductionBundleType(bundleType) { switch (bundleType) { case NODE_ES2015: case NODE_ESM: + return true; case UMD_DEV: case NODE_DEV: case BUN_DEV: @@ -377,12 +378,18 @@ function getPlugins( // Please don't enable this for anything else! isUMDBundle && entry === 'react-art' && commonjs(), // Apply dead code elimination and/or minification. + // closure doesn't yet support leaving ESM imports intact isProduction && + bundleType !== NODE_ESM && closure({ compilation_level: 'SIMPLE', - language_in: 'ECMASCRIPT_2015', + language_in: 'ECMASCRIPT_2018', language_out: - bundleType === BROWSER_SCRIPT ? 'ECMASCRIPT5' : 'ECMASCRIPT5_STRICT', + bundleType === NODE_ES2015 + ? 'ECMASCRIPT_2018' + : bundleType === BROWSER_SCRIPT + ? 'ECMASCRIPT5' + : 'ECMASCRIPT5_STRICT', env: 'CUSTOM', warning_level: 'QUIET', apply_input_source_maps: false, diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 3b97f2ef2c..e048f68183 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -417,7 +417,7 @@ const bundles = [ bundleTypes: [FB_WWW_DEV, FB_WWW_PROD], moduleType: RENDERER, entry: 'react-server-dom-relay/server', - global: 'ReactFlightDOMRelayServer', // TODO: Rename to Writer + global: 'ReactFlightDOMRelayServer', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: [ @@ -465,7 +465,7 @@ const bundles = [ bundleTypes: [RN_FB_DEV, RN_FB_PROD], moduleType: RENDERER, entry: 'react-server-native-relay', - global: 'ReactFlightNativeRelayClient', // TODO: Rename to Reader + global: 'ReactFlightNativeRelayClient', minifyWithProdErrorCodes: true, wrapWithModuleBoundaries: false, externals: [