From c7ba8c098889b6dc47fa9c807bbba3975a658584 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?=
Date: Fri, 29 Sep 2023 18:24:05 -0400
Subject: [PATCH] Enforce that the "react-server" build of "react" is used
(#27436)
I do this by simply renaming the secret export name in the "subset"
bundle and this renamed version is what the FlightServer uses.
This requires us to be more diligent about always using the correct
instance of "react" in our tests so there's a bunch of clean up for
that.
---
.eslintrc.js | 1 +
.../src/__tests__/ReactFlight-test.js | 94 +-
packages/react-dom/package.json | 1 +
.../src/__tests__/ReactFlightDOM-test.js | 2 +-
.../__tests__/ReactFlightDOMBrowser-test.js | 1179 +----------------
.../src/__tests__/ReactFlightDOMEdge-test.js | 8 +-
.../src/__tests__/ReactFlightDOMForm-test.js | 5 +-
.../src/__tests__/ReactFlightDOMNode-test.js | 2 +-
.../src/__tests__/ReactFlightDOMReply-test.js | 2 +
.../src/__tests__/ReactFlightDOM-test.js | 2 +-
.../__tests__/ReactFlightDOMBrowser-test.js | 49 +-
.../src/__tests__/ReactFlightDOMEdge-test.js | 5 +-
.../src/__tests__/ReactFlightDOMForm-test.js | 5 +-
.../src/__tests__/ReactFlightDOMNode-test.js | 2 +-
.../src/__tests__/ReactFlightDOMReply-test.js | 2 +
.../react-server/src/ReactFlightServer.js | 7 +-
.../src/ReactServerSharedInternals.js | 24 +
packages/react/src/React.js | 2 +-
packages/react/src/ReactServerContext.js | 4 +-
.../react/src/ReactServerSharedInternals.js | 16 +
...rnals.js => ReactSharedInternalsClient.js} | 0
.../react/src/ReactSharedInternalsServer.js | 25 +
.../src/ReactSharedSubset.experimental.js | 5 +-
packages/react/src/ReactSharedSubset.js | 5 +-
.../react/src/__tests__/ReactFetch-test.js | 15 +-
.../src/__tests__/ReactFetchEdge-test.js | 6 +-
scripts/error-codes/codes.json | 3 +-
scripts/jest/setupHostConfigs.js | 23 +-
scripts/jest/setupTests.build.js | 2 +
scripts/rollup/forks.js | 7 +-
30 files changed, 245 insertions(+), 1258 deletions(-)
create mode 100644 packages/react-server/src/ReactServerSharedInternals.js
create mode 100644 packages/react/src/ReactServerSharedInternals.js
rename packages/react/src/{ReactSharedInternals.js => ReactSharedInternalsClient.js} (100%)
create mode 100644 packages/react/src/ReactSharedInternalsServer.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 4d53738e281ae..a00174fea7122 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -517,6 +517,7 @@ module.exports = {
__TEST__: 'readonly',
__UMD__: 'readonly',
__VARIANT__: 'readonly',
+ __unmockReact: 'readonly',
gate: 'readonly',
trustedTypes: 'readonly',
IS_REACT_ACT_ENVIRONMENT: 'readonly',
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 2ebbb9f2d52f5..30cfda19af6ec 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -14,6 +14,7 @@ let act;
let use;
let startTransition;
let React;
+let ReactServer;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
@@ -25,12 +26,18 @@ let assertLog;
describe('ReactFlight', () => {
beforeEach(() => {
jest.resetModules();
-
+ jest.mock('react', () => require('react/react.shared-subset'));
+ ReactServer = require('react');
+ ReactNoopFlightServer = require('react-noop-renderer/flight-server');
+ // This stores the state so we need to preserve it
+ const flightModules = require('react-noop-renderer/flight-modules');
+ __unmockReact();
+ jest.resetModules();
+ jest.mock('react-noop-renderer/flight-modules', () => flightModules);
React = require('react');
startTransition = React.startTransition;
use = React.use;
ReactNoop = require('react-noop-renderer');
- ReactNoopFlightServer = require('react-noop-renderer/flight-server');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = require('internal-test-utils').act;
Scheduler = require('scheduler');
@@ -111,6 +118,19 @@ describe('ReactFlight', () => {
return ctx;
}
+ function createServerServerContext(globalName, defaultValue, withStack) {
+ let ctx;
+ expect(() => {
+ ctx = ReactServer.createServerContext(globalName, defaultValue);
+ }).toErrorDev(
+ 'Server Context is deprecated and will soon be removed. ' +
+ 'It was never documented and we have found it not to be useful ' +
+ 'enough to warrant the downside it imposes on all apps.',
+ {withoutStack: !withStack},
+ );
+ return ctx;
+ }
+
function clientReference(value) {
return Object.defineProperties(
function () {
@@ -970,7 +990,7 @@ describe('ReactFlight', () => {
const Context = React.createContext();
const ClientContext = clientReference(Context);
function ServerComponent() {
- return React.useContext(ClientContext);
+ return ReactServer.useContext(ClientContext);
}
expect(() => {
const transport = ReactNoopFlightServer.render();
@@ -982,7 +1002,7 @@ describe('ReactFlight', () => {
describe('Hooks', () => {
function DivWithId({children}) {
- const id = React.useId();
+ const id = ReactServer.useId();
return
{children}
;
}
@@ -1039,7 +1059,7 @@ describe('ReactFlight', () => {
// so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now
// we just accept that it is a nuance of useId in Flight
function App() {
- const id = React.useId();
+ const id = ReactServer.useId();
const div =
- );
- } else {
- errorBoundaryFn = e => {
- expect(e.message).toBe(
- 'An error occurred in the Server Components render. The specific message is omitted in production' +
- ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
- ' may provide additional details about the nature of the error.',
- );
- return
');
-
- // This isn't enough to show anything.
- await act(() => {
- resolveFriends();
- });
- expect(container.innerHTML).toBe('
(loading)
');
-
- // We can now show the details. Sidebar and posts are still loading.
- await act(() => {
- resolveName();
- });
- // Advance time enough to trigger a nested fallback.
- jest.advanceTimersByTime(500);
- expect(container.innerHTML).toBe(
- '
- );
- } else {
- errorBoundaryFn = e => {
- expect(e.message).toBe(
- 'An error occurred in the Server Components render. The specific message is omitted in production' +
- ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
- ' may provide additional details about the nature of the error.',
- );
- return
';
- expect(container.innerHTML).toBe(expectedValue);
-
- expect(reportedErrors).toEqual(['for reasons']);
- });
-
- it('basic use(promise)', async () => {
- function Server() {
- return (
- use(Promise.resolve('A')) +
- use(Promise.resolve('B')) +
- use(Promise.resolve('C'))
- );
- }
-
- const stream = ReactServerDOMServer.renderToReadableStream();
- const response = ReactServerDOMClient.createFromReadableStream(stream);
-
- function Client() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render(
-
-
- ,
- );
- });
- expect(container.innerHTML).toBe('ABC');
- });
-
- it('basic use(context)', async () => {
- let ContextA;
- let ContextB;
- expect(() => {
- ContextA = React.createServerContext('ContextA', '');
- ContextB = React.createServerContext('ContextB', 'B');
- }).toErrorDev(
- [
- 'Server Context is deprecated and will soon be removed. ' +
- 'It was never documented and we have found it not to be useful ' +
- 'enough to warrant the downside it imposes on all apps.',
- 'Server Context is deprecated and will soon be removed. ' +
- 'It was never documented and we have found it not to be useful ' +
- 'enough to warrant the downside it imposes on all apps.',
- ],
- {withoutStack: true},
- );
-
- function ServerComponent() {
- return use(ContextA) + use(ContextB);
- }
- function Server() {
- return (
-
-
-
- );
- }
- const stream = ReactServerDOMServer.renderToReadableStream();
- const response = ReactServerDOMClient.createFromReadableStream(stream);
-
- function Client() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- // Client uses a different renderer.
- // We reset _currentRenderer here to not trigger a warning about multiple
- // renderers concurrently using this context
- ContextA._currentRenderer = null;
- root.render();
- });
- expect(container.innerHTML).toBe('AB');
- });
-
- it('use(promise) in multiple components', async () => {
- function Child({prefix}) {
- return prefix + use(Promise.resolve('C')) + use(Promise.resolve('D'));
- }
-
- function Parent() {
- return (
-
- );
- }
-
- const stream = ReactServerDOMServer.renderToReadableStream();
- const response = ReactServerDOMClient.createFromReadableStream(stream);
-
- function Client() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render(
-
-
- ,
- );
- });
- expect(container.innerHTML).toBe('ABCD');
- });
-
- it('using a rejected promise will throw', async () => {
- const promiseA = Promise.resolve('A');
- const promiseB = Promise.reject(new Error('Oops!'));
- const promiseC = Promise.resolve('C');
-
- // Jest/Node will raise an unhandled rejected error unless we await this. It
- // works fine in the browser, though.
- await expect(promiseB).rejects.toThrow('Oops!');
-
- function Server() {
- return use(promiseA) + use(promiseB) + use(promiseC);
- }
-
- const reportedErrors = [];
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- {
- onError(x) {
- reportedErrors.push(x);
- return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
- },
- },
- );
- const response = ReactServerDOMClient.createFromReadableStream(stream);
-
- class ErrorBoundary extends React.Component {
- state = {error: null};
- static getDerivedStateFromError(error) {
- return {error};
- }
- render() {
- if (this.state.error) {
- return __DEV__
- ? this.state.error.message + ' + ' + this.state.error.digest
- : this.state.error.digest;
- }
- return this.props.children;
- }
- }
-
- function Client() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render(
-
-
- ,
- );
- });
- expect(container.innerHTML).toBe(
- __DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")',
- );
- expect(reportedErrors.length).toBe(1);
- expect(reportedErrors[0].message).toBe('Oops!');
- });
-
- it("use a promise that's already been instrumented and resolved", async () => {
- const thenable = {
- status: 'fulfilled',
- value: 'Hi',
- then() {},
- };
-
- // This will never suspend because the thenable already resolved
- function Server() {
- return use(thenable);
- }
-
- const stream = ReactServerDOMServer.renderToReadableStream();
- const response = ReactServerDOMClient.createFromReadableStream(stream);
-
- function Client() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
- expect(container.innerHTML).toBe('Hi');
- });
-
- it('unwraps thenable that fulfills synchronously without suspending', async () => {
- function Server() {
- const thenable = {
- then(resolve) {
- // This thenable immediately resolves, synchronously, without waiting
- // a microtask.
- resolve('Hi');
- },
- };
- try {
- return use(thenable);
- } catch {
- throw new Error(
- '`use` should not suspend because the thenable resolved synchronously.',
- );
- }
- }
-
- // Because the thenable resolves synchronously, we should be able to finish
- // rendering synchronously, with no fallback.
- const stream = ReactServerDOMServer.renderToReadableStream();
- const response = ReactServerDOMClient.createFromReadableStream(stream);
-
- function Client() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
- expect(container.innerHTML).toBe('Hi');
- });
-
- it('can pass a higher order function by reference from server to client', async () => {
- let actionProxy;
-
- function Client({action}) {
- actionProxy = action;
- return 'Click Me';
- }
-
- function greet(transform, text) {
- return 'Hello ' + transform(text);
- }
-
- function upper(text) {
- return text.toUpperCase();
- }
-
- const ServerModuleA = serverExports({
- greet,
- });
- const ServerModuleB = serverExports({
- upper,
- });
- const ClientRef = clientExports(Client);
-
- const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);
-
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- const response = ReactServerDOMClient.createFromReadableStream(stream, {
- async callServer(ref, args) {
- const body = await ReactServerDOMClient.encodeReply(args);
- return callServer(ref, body);
- },
- });
-
- function App() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
- expect(container.innerHTML).toBe('Click Me');
- expect(typeof actionProxy).toBe('function');
- expect(actionProxy).not.toBe(boundFn);
-
- const result = await actionProxy('hi');
- expect(result).toBe('Hello HI');
- });
-
- it('can call a module split server function', async () => {
- let actionProxy;
-
- function Client({action}) {
- actionProxy = action;
- return 'Click Me';
- }
-
- function greet(text) {
- return 'Hello ' + text;
- }
-
- const ServerModule = serverExports({
- // This gets split into another module
- split: greet,
- });
- const ClientRef = clientExports(Client);
-
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- const response = ReactServerDOMClient.createFromReadableStream(stream, {
- async callServer(ref, args) {
- const body = await ReactServerDOMClient.encodeReply(args);
- return callServer(ref, body);
- },
- });
-
- function App() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
- expect(container.innerHTML).toBe('Click Me');
- expect(typeof actionProxy).toBe('function');
-
- const result = await actionProxy('Split');
- expect(result).toBe('Hello Split');
- });
-
- it('can pass a server function by importing from client back to server', async () => {
- function greet(transform, text) {
- return 'Hello ' + transform(text);
- }
-
- function upper(text) {
- return text.toUpperCase();
- }
-
- const ServerModuleA = serverExports({
- greet,
- });
- const ServerModuleB = serverExports({
- upper,
- });
-
- let actionProxy;
-
- // This is a Proxy representing ServerModuleB in the Client bundle.
- const ServerModuleBImportedOnClient = {
- upper: ReactServerDOMClient.createServerReference(
- ServerModuleB.upper.$$id,
- async function (ref, args) {
- const body = await ReactServerDOMClient.encodeReply(args);
- return callServer(ref, body);
- },
- ),
- };
-
- function Client({action}) {
- // Client side pass a Server Reference into an action.
- actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
- return 'Click Me';
- }
-
- const ClientRef = clientExports(Client);
-
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- const response = ReactServerDOMClient.createFromReadableStream(stream, {
- async callServer(ref, args) {
- const body = await ReactServerDOMClient.encodeReply(args);
- return callServer(ref, body);
- },
- });
-
- function App() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
- expect(container.innerHTML).toBe('Click Me');
-
- const result = await actionProxy('hi');
- expect(result).toBe('Hello 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 = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- const response = ReactServerDOMClient.createFromReadableStream(stream, {
- async callServer(actionId, args) {
- const body = await ReactServerDOMClient.encodeReply(args);
- return callServer(actionId, body);
- },
- });
-
- function App() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- 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!');
- });
-
- it('propagates server reference errors to the client', async () => {
- let actionProxy;
-
- function Client({action}) {
- actionProxy = action;
- return 'Click Me';
- }
-
- async function send(text) {
- return Promise.reject(new Error(`Error for ${text}`));
- }
-
- const ServerModule = serverExports({send});
- const ClientRef = clientExports(Client);
-
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- const response = ReactServerDOMClient.createFromReadableStream(stream, {
- async callServer(actionId, args) {
- const body = await ReactServerDOMClient.encodeReply(args);
- return ReactServerDOMClient.createFromReadableStream(
- ReactServerDOMServer.renderToReadableStream(
- callServer(actionId, body),
- null,
- {onError: error => 'test-error-digest'},
- ),
- );
- },
- });
-
- function App() {
- return use(response);
- }
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
-
- if (__DEV__) {
- await expect(actionProxy('test')).rejects.toThrow('Error for test');
- } else {
- let thrownError;
-
- try {
- await actionProxy('test');
- } catch (error) {
- thrownError = error;
- }
-
- expect(thrownError).toEqual(
- new Error(
- 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
- ),
- );
-
- expect(thrownError.digest).toBe('test-error-digest');
- }
- });
-
- it('supports Float hints before the first await in server components in Fiber', async () => {
- function Component() {
- return
hello world
;
- }
-
- const ClientComponent = clientExports(Component);
-
- async function ServerComponent() {
- ReactDOM.preload('before', {as: 'style'});
- await 1;
- ReactDOM.preload('after', {as: 'style'});
- return ;
- }
-
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- let response = null;
- function getResponse() {
- if (response === null) {
- response = ReactServerDOMClient.createFromReadableStream(stream);
- }
- return response;
- }
-
- function App() {
- return getResponse();
- }
-
- // pausing to let Flight runtime tick. This is a test only artifact of the fact that
- // we aren't operating separate module graphs for flight and fiber. In a real app
- // each would have their own dispatcher and there would be no cross dispatching.
- await 1;
-
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
- await act(() => {
- root.render();
- });
- expect(document.head.innerHTML).toBe(
- '',
- );
- expect(container.innerHTML).toBe('
hello world
');
- });
-
- it('Does not support Float hints in server components anywhere in Fizz', async () => {
- // In environments that do not support AsyncLocalStorage the Flight client has no ability
- // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
- // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
- // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
- function Component() {
- return
hello world
;
- }
-
- const ClientComponent = clientExports(Component);
-
- async function ServerComponent() {
- ReactDOM.preload('before', {as: 'style'});
- await 1;
- ReactDOM.preload('after', {as: 'style'});
- return ;
- }
-
- const stream = ReactServerDOMServer.renderToReadableStream(
- ,
- turbopackMap,
- );
-
- let response = null;
- function getResponse() {
- if (response === null) {
- response = ReactServerDOMClient.createFromReadableStream(stream);
- }
- return response;
- }
-
- function App() {
- return (
-
- {getResponse()}
-
- );
- }
-
- // pausing to let Flight runtime tick. This is a test only artifact of the fact that
- // we aren't operating separate module graphs for flight and fiber. In a real app
- // each would have their own dispatcher and there would be no cross dispatching.
- await 1;
-
- let fizzStream;
- await act(async () => {
- fizzStream = await ReactDOMFizzServer.renderToReadableStream();
- });
-
- const decoder = new TextDecoder();
- const reader = fizzStream.getReader();
- let content = '';
- while (true) {
- const {done, value} = await reader.read();
- if (done) {
- content += decoder.decode();
- break;
- }
- content += decoder.decode(value, {stream: true});
- }
-
- expect(content).toEqual(
- '' +
- '