From 5d1b15a4f06fa93e76cd89f37ea5bfd62cc66183 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 16 Jan 2024 19:58:11 -0500 Subject: [PATCH 01/90] Rename "shared subset" to "server" (#27939) The internal file ReactSharedSubset is what the `react` module resolves to when imported from a Server Component environment. We gave it this name because, originally, the idea was that Server Components can access a subset of the APIs available on the client. However, since then, we've also added APIs that can _only_ by accessed on the server and not the client. In other words, it's no longer a subset, it's a slightly different overlapping set. So this commit renames ReactSharedSubet to ReactServer and updates all the references. This does not affect the public API, only our internal implementation. --- .../src/__tests__/ReactFlight-test.js | 4 ++-- .../react-dom/npm/react-dom.react-server.js | 7 +++++++ .../react-dom/npm/react-dom.shared-subset.js | 7 ------- packages/react-dom/package.json | 4 ++-- ...actDOMSharedSubset.js => ReactDOMServer.js} | 0 .../__tests__/ReactDOMInReactServer-test.js | 2 +- .../ReactFlightDOMServerFB-test.internal.js | 2 +- .../__tests__/ReactFlightTurbopackDOM-test.js | 2 +- .../ReactFlightTurbopackDOMBrowser-test.js | 2 +- .../ReactFlightTurbopackDOMEdge-test.js | 2 +- .../ReactFlightTurbopackDOMForm-test.js | 2 +- .../ReactFlightTurbopackDOMNode-test.js | 2 +- .../ReactFlightTurbopackDOMReply-test.js | 2 +- .../ReactFlightTurbopackDOMReplyEdge-test.js | 2 +- .../src/__tests__/ReactFlightDOM-test.js | 2 +- .../__tests__/ReactFlightDOMBrowser-test.js | 2 +- .../src/__tests__/ReactFlightDOMEdge-test.js | 2 +- .../src/__tests__/ReactFlightDOMForm-test.js | 2 +- .../src/__tests__/ReactFlightDOMNode-test.js | 2 +- .../src/__tests__/ReactFlightDOMReply-test.js | 2 +- .../__tests__/ReactFlightDOMReplyEdge-test.js | 2 +- packages/react/npm/react.react-server.js | 7 +++++++ packages/react/npm/react.shared-subset.js | 7 ------- packages/react/package.json | 4 ++-- ....shared-subset.js => react.react-server.js} | 2 +- ...rimental.js => ReactServer.experimental.js} | 0 .../{ReactSharedSubset.js => ReactServer.js} | 0 ...ReactSharedSubsetFB.js => ReactServerFB.js} | 2 +- .../react/src/__tests__/ReactFetch-test.js | 2 +- .../react/src/__tests__/ReactFetchEdge-test.js | 2 +- scripts/jest/setupHostConfigs.js | 4 ++-- scripts/rollup/bundles.js | 12 ++++++------ scripts/rollup/forks.js | 6 +++--- scripts/shared/inlinedHostConfigs.js | 18 +++++++++--------- 34 files changed, 60 insertions(+), 60 deletions(-) create mode 100644 packages/react-dom/npm/react-dom.react-server.js delete mode 100644 packages/react-dom/npm/react-dom.shared-subset.js rename packages/react-dom/src/{ReactDOMSharedSubset.js => ReactDOMServer.js} (100%) create mode 100644 packages/react/npm/react.react-server.js delete mode 100644 packages/react/npm/react.shared-subset.js rename packages/react/{react.shared-subset.js => react.react-server.js} (83%) rename packages/react/src/{ReactSharedSubset.experimental.js => ReactServer.experimental.js} (100%) rename packages/react/src/{ReactSharedSubset.js => ReactServer.js} (100%) rename packages/react/src/{ReactSharedSubsetFB.js => ReactServerFB.js} (87%) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 45e6d6df5ad4f..fc351132a3135 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -43,7 +43,7 @@ let assertLog; describe('ReactFlight', () => { beforeEach(() => { jest.resetModules(); - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); ReactServer = require('react'); ReactNoopFlightServer = require('react-noop-renderer/flight-server'); // This stores the state so we need to preserve it @@ -1465,7 +1465,7 @@ describe('ReactFlight', () => { // Reset all modules, except flight-modules which keeps the registry of Client Components const flightModules = require('react-noop-renderer/flight-modules'); jest.resetModules(); - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-noop-renderer/flight-modules', () => flightModules); ReactServer = require('react'); diff --git a/packages/react-dom/npm/react-dom.react-server.js b/packages/react-dom/npm/react-dom.react-server.js new file mode 100644 index 0000000000000..7ba04f2fec7c0 --- /dev/null +++ b/packages/react-dom/npm/react-dom.react-server.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-dom.react-server.production.min.js'); +} else { + module.exports = require('./cjs/react-dom.react-server.development.js'); +} diff --git a/packages/react-dom/npm/react-dom.shared-subset.js b/packages/react-dom/npm/react-dom.shared-subset.js deleted file mode 100644 index b74e646f9ca17..0000000000000 --- a/packages/react-dom/npm/react-dom.shared-subset.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react-dom.shared-subset.production.min.js'); -} else { - module.exports = require('./cjs/react-dom.shared-subset.development.js'); -} diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index 739e045447ddf..3d71298844dc4 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -42,13 +42,13 @@ "test-utils.js", "unstable_testing.js", "unstable_server-external-runtime.js", - "react-dom.shared-subset.js", + "react-dom.react-server.js", "cjs/", "umd/" ], "exports": { ".": { - "react-server": "./react-dom.shared-subset.js", + "react-server": "./react-dom.react-server.js", "default": "./index.js" }, "./client": "./client.js", diff --git a/packages/react-dom/src/ReactDOMSharedSubset.js b/packages/react-dom/src/ReactDOMServer.js similarity index 100% rename from packages/react-dom/src/ReactDOMSharedSubset.js rename to packages/react-dom/src/ReactDOMServer.js diff --git a/packages/react-dom/src/__tests__/ReactDOMInReactServer-test.js b/packages/react-dom/src/__tests__/ReactDOMInReactServer-test.js index a86dbc3815953..f3bc6ea8aded2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInReactServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInReactServer-test.js @@ -12,7 +12,7 @@ describe('ReactDOMInReactServer', () => { beforeEach(() => { jest.resetModules(); - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); }); it('can require react-dom', () => { diff --git a/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js b/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js index 69cc1ff5e52e2..57730807c5c1c 100644 --- a/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js +++ b/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js @@ -73,7 +73,7 @@ describe('ReactFlightDOM for FB', () => { // condition jest.resetModules(); - jest.mock('react', () => require('react/src/ReactSharedSubsetFB')); + jest.mock('react', () => require('react/src/ReactServerFB')); jest.mock('shared/ReactFeatureFlags', () => { jest.mock( diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js index 81cd49b6cf19a..f74143b220634 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js @@ -41,7 +41,7 @@ describe('ReactFlightDOM', () => { jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.node.unbundled'), ); - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); const TurbopackMock = require('./utils/TurbopackMock'); clientExports = TurbopackMock.clientExports; diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js index 3aa7f712a0025..d797946a3fd3d 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js @@ -24,7 +24,7 @@ describe('ReactFlightDOMBrowser', () => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.browser'), ); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js index d0d14915bcda8..67d25c967f472 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js @@ -33,7 +33,7 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.edge'), ); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js index ad7e0d0feed76..3f63222d0ae2d 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js @@ -33,7 +33,7 @@ describe('ReactFlightDOMForm', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.edge'), ); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js index a3068003db864..e06ee0a32f950 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js @@ -29,7 +29,7 @@ describe('ReactFlightDOMNode', () => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.node'), ); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js index f089789598850..e47352cfe981d 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js @@ -24,7 +24,7 @@ describe('ReactFlightDOMReply', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.browser'), ); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js index 4ff2b436f3aaa..0cd8605c7e8d6 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js @@ -24,7 +24,7 @@ describe('ReactFlightDOMReply', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.edge'), ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index c12c6904b872f..91ba06dd329b9 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -50,7 +50,7 @@ describe('ReactFlightDOM', () => { jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.node.unbundled'), ); - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; 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 779e117627072..efbaba033c3f9 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -36,7 +36,7 @@ describe('ReactFlightDOMBrowser', () => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.browser'), ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 2f643fbbfbfa0..1cbac757430f1 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -34,7 +34,7 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 89b436907f201..b95a6c824f092 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -38,7 +38,7 @@ describe('ReactFlightDOMForm', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 6fd51a5d1fb49..87fc83360018e 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -29,7 +29,7 @@ describe('ReactFlightDOMNode', () => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.node'), ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index ed2f8a8f566bb..1162d1d0fe738 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -24,7 +24,7 @@ describe('ReactFlightDOMReply', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.browser'), ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index e657022f75912..8e45472956294 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -24,7 +24,7 @@ describe('ReactFlightDOMReplyEdge', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); diff --git a/packages/react/npm/react.react-server.js b/packages/react/npm/react.react-server.js new file mode 100644 index 0000000000000..42772dc4ba3c7 --- /dev/null +++ b/packages/react/npm/react.react-server.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react.react-server.production.min.js'); +} else { + module.exports = require('./cjs/react.react-server.development.js'); +} diff --git a/packages/react/npm/react.shared-subset.js b/packages/react/npm/react.shared-subset.js deleted file mode 100644 index 694e966729ff9..0000000000000 --- a/packages/react/npm/react.shared-subset.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react.shared-subset.production.min.js'); -} else { - module.exports = require('./cjs/react.shared-subset.development.js'); -} diff --git a/packages/react/package.json b/packages/react/package.json index ab362a97a4298..39b545825875c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -16,12 +16,12 @@ "umd/", "jsx-runtime.js", "jsx-dev-runtime.js", - "react.shared-subset.js" + "react.react-server.js" ], "main": "index.js", "exports": { ".": { - "react-server": "./react.shared-subset.js", + "react-server": "./react.react-server.js", "default": "./index.js" }, "./package.json": "./package.json", diff --git a/packages/react/react.shared-subset.js b/packages/react/react.react-server.js similarity index 83% rename from packages/react/react.shared-subset.js rename to packages/react/react.react-server.js index 13d2fae7fa84c..54ab1834fd919 100644 --- a/packages/react/react.shared-subset.js +++ b/packages/react/react.react-server.js @@ -7,4 +7,4 @@ * @flow */ -export * from './src/ReactSharedSubset'; +export * from './src/ReactServer'; diff --git a/packages/react/src/ReactSharedSubset.experimental.js b/packages/react/src/ReactServer.experimental.js similarity index 100% rename from packages/react/src/ReactSharedSubset.experimental.js rename to packages/react/src/ReactServer.experimental.js diff --git a/packages/react/src/ReactSharedSubset.js b/packages/react/src/ReactServer.js similarity index 100% rename from packages/react/src/ReactSharedSubset.js rename to packages/react/src/ReactServer.js diff --git a/packages/react/src/ReactSharedSubsetFB.js b/packages/react/src/ReactServerFB.js similarity index 87% rename from packages/react/src/ReactSharedSubsetFB.js rename to packages/react/src/ReactServerFB.js index b904cfc0e9cd4..1060040c145a6 100644 --- a/packages/react/src/ReactSharedSubsetFB.js +++ b/packages/react/src/ReactServerFB.js @@ -7,5 +7,5 @@ * @flow */ -export * from './ReactSharedSubset'; +export * from './ReactServer'; export {jsx, jsxs, jsxDEV} from './jsx/ReactJSX'; diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index e91c6e09941d8..2b42bfdd25a3e 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -44,7 +44,7 @@ describe('ReactFetch', () => { fetchCount = 0; global.fetch = fetchMock; - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.browser'), ); diff --git a/packages/react/src/__tests__/ReactFetchEdge-test.js b/packages/react/src/__tests__/ReactFetchEdge-test.js index b4e313faa728f..741306a9a2e4a 100644 --- a/packages/react/src/__tests__/ReactFetchEdge-test.js +++ b/packages/react/src/__tests__/ReactFetchEdge-test.js @@ -48,7 +48,7 @@ describe('ReactFetch', () => { fetchCount = 0; global.fetch = fetchMock; - jest.mock('react', () => require('react/react.shared-subset')); + jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index b296f65224eb7..5fde144c255e3 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -58,9 +58,9 @@ global.__unmockReact = mockReact; mockReact(); -jest.mock('react/react.shared-subset', () => { +jest.mock('react/react.react-server', () => { const resolvedEntryPoint = resolveEntryFork( - require.resolve('react/src/ReactSharedSubset'), + require.resolve('react/src/ReactServer'), global.__WWW__ ); return jest.requireActual(resolvedEntryPoint); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 0ce6d052f8ca6..a4f3f08bbd522 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -96,8 +96,8 @@ const bundles = [ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: ISOMORPHIC, - entry: 'react/src/ReactSharedSubset.js', - name: 'react.shared-subset', + entry: 'react/src/ReactServer.js', + name: 'react.react-server', global: 'React', minifyWithProdErrorCodes: true, wrapWithModuleBoundaries: false, @@ -108,8 +108,8 @@ const bundles = [ { bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [], moduleType: ISOMORPHIC, - entry: 'react/src/ReactSharedSubsetFB.js', - global: 'ReactSharedSubset', + entry: 'react/src/ReactServerFB.js', + global: 'ReactServer', minifyWithProdErrorCodes: true, wrapWithModuleBoundaries: false, externals: [], @@ -180,8 +180,8 @@ const bundles = [ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RENDERER, - entry: 'react-dom/src/ReactDOMSharedSubset.js', - name: 'react-dom.shared-subset', + entry: 'react-dom/src/ReactDOMServer.js', + name: 'react-dom.react-server', global: 'ReactDOM', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 916e857d57631..173ff58379251 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -64,8 +64,8 @@ const forks = Object.freeze({ return './packages/react/src/ReactSharedInternalsClient.js'; } if ( - entry === 'react/src/ReactSharedSubset.js' || - entry === 'react/src/ReactSharedSubsetFB.js' + entry === 'react/src/ReactServer.js' || + entry === 'react/src/ReactServerFB.js' ) { return './packages/react/src/ReactSharedInternalsServer.js'; } @@ -93,7 +93,7 @@ const forks = Object.freeze({ if ( entry === 'react-dom' || entry === 'react-dom/server-rendering-stub' || - entry === 'react-dom/src/ReactDOMSharedSubset.js' + entry === 'react-dom/src/ReactDOMServer.js' ) { return './packages/react-dom/src/ReactDOMSharedInternals.js'; } diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 00353682abf23..fa6916cee621b 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -11,7 +11,7 @@ module.exports = [ shortName: 'dom-node', entryPoints: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom/unstable_testing', 'react-dom/src/server/react-dom-server.node.js', 'react-dom/static.node', @@ -22,7 +22,7 @@ module.exports = [ ], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-dom/client', 'react-dom/server', @@ -190,7 +190,7 @@ module.exports = [ ], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-dom/client', 'react-dom/server.browser', @@ -221,7 +221,7 @@ module.exports = [ entryPoints: ['react-server-dom-esm/client.browser'], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom/client', 'react-dom/server', 'react-dom/server.node', @@ -280,7 +280,7 @@ module.exports = [ ], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-dom/client', 'react-dom/server.edge', @@ -314,7 +314,7 @@ module.exports = [ ], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-dom/client', 'react-dom/server.edge', @@ -348,7 +348,7 @@ module.exports = [ ], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-dom/client', 'react-dom/server', @@ -383,7 +383,7 @@ module.exports = [ ], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-server-dom-webpack', 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Browser and *Node files @@ -401,7 +401,7 @@ module.exports = [ entryPoints: ['react-server-dom-fb/src/ReactDOMServerFB.js'], paths: [ 'react-dom', - 'react-dom/src/ReactDOMSharedSubset.js', + 'react-dom/src/ReactDOMServer.js', 'react-dom-bindings', 'react-server-dom-fb/src/ReactDOMServerFB.js', 'shared/ReactDOMSharedInternals', From f16344ea6db5bcc108de80dbc39a41ec28e8210d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 16 Jan 2024 20:19:01 -0500 Subject: [PATCH 02/90] Refactor React Server entrypoint to not depend on the client one (#27940) This refactors the Server Components entrypoint for the `react` package (ReactServer.js) so that it doesn't depend on the client entrypoint (React.js). I also renamed React.js to ReactClient.js to make the separation clearer. This structure will make it easier to add client-only and server-only features. --- packages/react/index.classic.fb.js | 2 +- packages/react/index.experimental.js | 4 +- packages/react/index.js | 2 +- packages/react/index.modern.fb.js | 2 +- packages/react/index.stable.js | 2 +- .../react/src/{React.js => ReactClient.js} | 23 +- packages/react/src/ReactElement.js | 593 +----------------- packages/react/src/ReactElementProd.js | 573 +++++++++++++++++ packages/react/src/ReactElementValidator.js | 2 +- .../react/src/ReactServer.experimental.js | 57 +- packages/react/src/ReactServer.js | 43 +- 11 files changed, 695 insertions(+), 608 deletions(-) rename packages/react/src/{React.js => ReactClient.js} (84%) create mode 100644 packages/react/src/ReactElementProd.js diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 035e5c0aec2d9..a417f3e755cab 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -58,5 +58,5 @@ export { useSyncExternalStore, useTransition, version, -} from './src/React'; +} from './src/ReactClient'; export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index bdb26a4c42076..dd86090f093e1 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -57,9 +57,9 @@ export { useSyncExternalStore, useTransition, version, -} from './src/React'; +} from './src/ReactClient'; -import {useOptimistic} from './src/React'; +import {useOptimistic} from './src/ReactClient'; export function experimental_useOptimistic( passthrough: S, diff --git a/packages/react/index.js b/packages/react/index.js index b60e8957c5b0a..abce6537b5675 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -80,4 +80,4 @@ export { useState, useTransition, version, -} from './src/React'; +} from './src/ReactClient'; diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 1ef63b605af3a..10ae150f64eef 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -56,5 +56,5 @@ export { useSyncExternalStore, useTransition, version, -} from './src/React'; +} from './src/ReactClient'; export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index b88a83d78b9b2..9f8e46063782a 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -47,4 +47,4 @@ export { useSyncExternalStore, useTransition, version, -} from './src/React'; +} from './src/ReactClient'; diff --git a/packages/react/src/React.js b/packages/react/src/ReactClient.js similarity index 84% rename from packages/react/src/React.js rename to packages/react/src/ReactClient.js index 5728a2ba50d78..5aaae5aaf8e37 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/ReactClient.js @@ -26,9 +26,9 @@ import {Component, PureComponent} from './ReactBaseClasses'; import {createRef} from './ReactCreateRef'; import {forEach, map, count, toArray, only} from './ReactChildren'; import { - createElement as createElementProd, - createFactory as createFactoryProd, - cloneElement as cloneElementProd, + createElement, + createFactory, + cloneElement, isValidElement, } from './ReactElement'; import {createContext} from './ReactContext'; @@ -61,27 +61,12 @@ import { useMemoCache, useOptimistic, } from './ReactHooks'; -import { - createElementWithValidation, - createFactoryWithValidation, - cloneElementWithValidation, -} from './ReactElementValidator'; + import {createServerContext} from './ReactServerContext'; import ReactSharedInternals from './ReactSharedInternalsClient'; import {startTransition} from './ReactStartTransition'; import {act} from './ReactAct'; -// TODO: Move this branching into the other module instead and just re-export. -const createElement: any = __DEV__ - ? createElementWithValidation - : createElementProd; -const cloneElement: any = __DEV__ - ? cloneElementWithValidation - : cloneElementProd; -const createFactory: any = __DEV__ - ? createFactoryWithValidation - : createFactoryProd; - const Children = { map, forEach, diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index e9d721b92693a..ab3df9600ab86 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -3,571 +3,30 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */ - -import getComponentNameFromType from 'shared/getComponentNameFromType'; -import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; -import assign from 'shared/assign'; -import hasOwnProperty from 'shared/hasOwnProperty'; -import {checkKeyStringCoercion} from 'shared/CheckStringCoercion'; - -import ReactCurrentOwner from './ReactCurrentOwner'; - -const RESERVED_PROPS = { - key: true, - ref: true, - __self: true, - __source: true, -}; - -let specialPropKeyWarningShown, - specialPropRefWarningShown, - didWarnAboutStringRefs; - -if (__DEV__) { - didWarnAboutStringRefs = {}; -} - -function hasValidRef(config) { - if (__DEV__) { - if (hasOwnProperty.call(config, 'ref')) { - const getter = Object.getOwnPropertyDescriptor(config, 'ref').get; - if (getter && getter.isReactWarning) { - return false; - } - } - } - return config.ref !== undefined; -} - -function hasValidKey(config) { - if (__DEV__) { - if (hasOwnProperty.call(config, 'key')) { - const getter = Object.getOwnPropertyDescriptor(config, 'key').get; - if (getter && getter.isReactWarning) { - return false; - } - } - } - return config.key !== undefined; -} - -function defineKeyPropWarningGetter(props, displayName) { - const warnAboutAccessingKey = function () { - if (__DEV__) { - if (!specialPropKeyWarningShown) { - specialPropKeyWarningShown = true; - console.error( - '%s: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://reactjs.org/link/special-props)', - displayName, - ); - } - } - }; - warnAboutAccessingKey.isReactWarning = true; - Object.defineProperty(props, 'key', { - get: warnAboutAccessingKey, - configurable: true, - }); -} - -function defineRefPropWarningGetter(props, displayName) { - const warnAboutAccessingRef = function () { - if (__DEV__) { - if (!specialPropRefWarningShown) { - specialPropRefWarningShown = true; - console.error( - '%s: `ref` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://reactjs.org/link/special-props)', - displayName, - ); - } - } - }; - warnAboutAccessingRef.isReactWarning = true; - Object.defineProperty(props, 'ref', { - get: warnAboutAccessingRef, - configurable: true, - }); -} - -function warnIfStringRefCannotBeAutoConverted(config) { - if (__DEV__) { - if ( - typeof config.ref === 'string' && - ReactCurrentOwner.current && - config.__self && - ReactCurrentOwner.current.stateNode !== config.__self - ) { - const componentName = getComponentNameFromType( - ReactCurrentOwner.current.type, - ); - - if (!didWarnAboutStringRefs[componentName]) { - console.error( - 'Component "%s" contains the string ref "%s". ' + - 'Support for string refs will be removed in a future major release. ' + - 'This case cannot be automatically converted to an arrow function. ' + - 'We ask you to manually fix this case by using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: ' + - 'https://reactjs.org/link/strict-mode-string-ref', - componentName, - config.ref, - ); - didWarnAboutStringRefs[componentName] = true; - } - } - } -} - -/** - * Factory method to create a new React element. This no longer adheres to - * the class pattern, so do not use new to call it. Also, instanceof check - * will not work. Instead test $$typeof field against Symbol.for('react.element') to check - * if something is a React Element. * - * @param {*} type - * @param {*} props - * @param {*} key - * @param {string|object} ref - * @param {*} owner - * @param {*} self A *temporary* helper to detect places where `this` is - * different from the `owner` when React.createElement is called, so that we - * can warn. We want to get rid of owner and replace string `ref`s with arrow - * functions, and as long as `this` and owner are the same, there will be no - * change in behavior. - * @param {*} source An annotation object (added by a transpiler or otherwise) - * indicating filename, line number, and/or other information. - * @internal - */ -function ReactElement(type, key, ref, self, source, owner, props) { - const element = { - // This tag allows us to uniquely identify this as a React Element - $$typeof: REACT_ELEMENT_TYPE, - - // Built-in properties that belong on the element - type: type, - key: key, - ref: ref, - props: props, - - // Record the component responsible for creating this element. - _owner: owner, - }; - - if (__DEV__) { - // The validation flag is currently mutative. We put it on - // an external backing store so that we can freeze the whole object. - // This can be replaced with a WeakMap once they are implemented in - // commonly used development environments. - element._store = {}; - - // To make comparing ReactElements easier for testing purposes, we make - // the validation flag non-enumerable (where possible, which should - // include every environment we run tests in), so the test framework - // ignores it. - Object.defineProperty(element._store, 'validated', { - configurable: false, - enumerable: false, - writable: true, - value: false, - }); - // self and source are DEV only properties. - Object.defineProperty(element, '_self', { - configurable: false, - enumerable: false, - writable: false, - value: self, - }); - // Two elements created in two different places should be considered - // equal for testing purposes and therefore we hide it from enumeration. - Object.defineProperty(element, '_source', { - configurable: false, - enumerable: false, - writable: false, - value: source, - }); - if (Object.freeze) { - Object.freeze(element.props); - Object.freeze(element); - } - } - - return element; -} - -/** - * https://github.com/reactjs/rfcs/pull/107 - * @param {*} type - * @param {object} props - * @param {string} key - */ -export function jsx(type, config, maybeKey) { - let propName; - - // Reserved names are extracted - const props = {}; - - let key = null; - let ref = null; - - // Currently, key can be spread in as a prop. This causes a potential - // issue if key is also explicitly declared (ie.
- // or
). We want to deprecate key spread, - // but as an intermediary step, we will use jsxDEV for everything except - //
, because we aren't currently able to tell if - // key is explicitly declared to be undefined or not. - if (maybeKey !== undefined) { - if (__DEV__) { - checkKeyStringCoercion(maybeKey); - } - key = '' + maybeKey; - } - - if (hasValidKey(config)) { - if (__DEV__) { - checkKeyStringCoercion(config.key); - } - key = '' + config.key; - } - - if (hasValidRef(config)) { - ref = config.ref; - } - - // Remaining properties are added to a new props object - for (propName in config) { - if ( - hasOwnProperty.call(config, propName) && - !RESERVED_PROPS.hasOwnProperty(propName) - ) { - props[propName] = config[propName]; - } - } - - // Resolve default props - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - } - - return ReactElement( - type, - key, - ref, - undefined, - undefined, - ReactCurrentOwner.current, - props, - ); -} - -/** - * https://github.com/reactjs/rfcs/pull/107 - * @param {*} type - * @param {object} props - * @param {string} key - */ -export function jsxDEV(type, config, maybeKey, source, self) { - let propName; - - // Reserved names are extracted - const props = {}; - - let key = null; - let ref = null; - - // Currently, key can be spread in as a prop. This causes a potential - // issue if key is also explicitly declared (ie.
- // or
). We want to deprecate key spread, - // but as an intermediary step, we will use jsxDEV for everything except - //
, because we aren't currently able to tell if - // key is explicitly declared to be undefined or not. - if (maybeKey !== undefined) { - if (__DEV__) { - checkKeyStringCoercion(maybeKey); - } - key = '' + maybeKey; - } - - if (hasValidKey(config)) { - if (__DEV__) { - checkKeyStringCoercion(config.key); - } - key = '' + config.key; - } - - if (hasValidRef(config)) { - ref = config.ref; - warnIfStringRefCannotBeAutoConverted(config); - } - - // Remaining properties are added to a new props object - for (propName in config) { - if ( - hasOwnProperty.call(config, propName) && - !RESERVED_PROPS.hasOwnProperty(propName) - ) { - props[propName] = config[propName]; - } - } - - // Resolve default props - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - } - - if (key || ref) { - const displayName = - typeof type === 'function' - ? type.displayName || type.name || 'Unknown' - : type; - if (key) { - defineKeyPropWarningGetter(props, displayName); - } - if (ref) { - defineRefPropWarningGetter(props, displayName); - } - } - - return ReactElement( - type, - key, - ref, - self, - source, - ReactCurrentOwner.current, - props, - ); -} - -/** - * Create and return a new ReactElement of the given type. - * See https://reactjs.org/docs/react-api.html#createelement - */ -export function createElement(type, config, children) { - let propName; - - // Reserved names are extracted - const props = {}; - - let key = null; - let ref = null; - let self = null; - let source = null; - - if (config != null) { - if (hasValidRef(config)) { - ref = config.ref; - - if (__DEV__) { - warnIfStringRefCannotBeAutoConverted(config); - } - } - if (hasValidKey(config)) { - if (__DEV__) { - checkKeyStringCoercion(config.key); - } - key = '' + config.key; - } - - self = config.__self === undefined ? null : config.__self; - source = config.__source === undefined ? null : config.__source; - // Remaining properties are added to a new props object - for (propName in config) { - if ( - hasOwnProperty.call(config, propName) && - !RESERVED_PROPS.hasOwnProperty(propName) - ) { - props[propName] = config[propName]; - } - } - } - - // Children can be more than one argument, and those are transferred onto - // the newly allocated props object. - const childrenLength = arguments.length - 2; - if (childrenLength === 1) { - props.children = children; - } else if (childrenLength > 1) { - const childArray = Array(childrenLength); - for (let i = 0; i < childrenLength; i++) { - childArray[i] = arguments[i + 2]; - } - if (__DEV__) { - if (Object.freeze) { - Object.freeze(childArray); - } - } - props.children = childArray; - } - - // Resolve default props - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - } - if (__DEV__) { - if (key || ref) { - const displayName = - typeof type === 'function' - ? type.displayName || type.name || 'Unknown' - : type; - if (key) { - defineKeyPropWarningGetter(props, displayName); - } - if (ref) { - defineRefPropWarningGetter(props, displayName); - } - } - } - return ReactElement( - type, - key, - ref, - self, - source, - ReactCurrentOwner.current, - props, - ); -} - -/** - * Return a function that produces ReactElements of a given type. - * See https://reactjs.org/docs/react-api.html#createfactory - */ -export function createFactory(type) { - const factory = createElement.bind(null, type); - // Expose the type on the factory and the prototype so that it can be - // easily accessed on elements. E.g. `.type === Foo`. - // This should not be named `constructor` since this may not be the function - // that created the element, and it may not even be a constructor. - // Legacy hook: remove it - factory.type = type; - return factory; -} - -export function cloneAndReplaceKey(oldElement, newKey) { - const newElement = ReactElement( - oldElement.type, - newKey, - oldElement.ref, - oldElement._self, - oldElement._source, - oldElement._owner, - oldElement.props, - ); - - return newElement; -} - -/** - * Clone and return a new ReactElement using element as the starting point. - * See https://reactjs.org/docs/react-api.html#cloneelement - */ -export function cloneElement(element, config, children) { - if (element === null || element === undefined) { - throw new Error( - `React.cloneElement(...): The argument must be a React element, but you passed ${element}.`, - ); - } - - let propName; - - // Original props are copied - const props = assign({}, element.props); - - // Reserved names are extracted - let key = element.key; - let ref = element.ref; - // Self is preserved since the owner is preserved. - const self = element._self; - // Source is preserved since cloneElement is unlikely to be targeted by a - // transpiler, and the original source is probably a better indicator of the - // true owner. - const source = element._source; - - // Owner will be preserved, unless ref is overridden - let owner = element._owner; - - if (config != null) { - if (hasValidRef(config)) { - // Silently steal the ref from the parent. - ref = config.ref; - owner = ReactCurrentOwner.current; - } - if (hasValidKey(config)) { - if (__DEV__) { - checkKeyStringCoercion(config.key); - } - key = '' + config.key; - } - - // Remaining properties override existing props - let defaultProps; - if (element.type && element.type.defaultProps) { - defaultProps = element.type.defaultProps; - } - for (propName in config) { - if ( - hasOwnProperty.call(config, propName) && - !RESERVED_PROPS.hasOwnProperty(propName) - ) { - if (config[propName] === undefined && defaultProps !== undefined) { - // Resolve default props - props[propName] = defaultProps[propName]; - } else { - props[propName] = config[propName]; - } - } - } - } - - // Children can be more than one argument, and those are transferred onto - // the newly allocated props object. - const childrenLength = arguments.length - 2; - if (childrenLength === 1) { - props.children = children; - } else if (childrenLength > 1) { - const childArray = Array(childrenLength); - for (let i = 0; i < childrenLength; i++) { - childArray[i] = arguments[i + 2]; - } - props.children = childArray; - } - - return ReactElement(element.type, key, ref, self, source, owner, props); -} - -/** - * Verifies the object is a ReactElement. - * See https://reactjs.org/docs/react-api.html#isvalidelement - * @param {?object} object - * @return {boolean} True if `object` is a ReactElement. - * @final - */ -export function isValidElement(object) { - return ( - typeof object === 'object' && - object !== null && - object.$$typeof === REACT_ELEMENT_TYPE - ); -} + * @flow + */ + +import { + createElement as createElementProd, + createFactory as createFactoryProd, + cloneElement as cloneElementProd, +} from './ReactElementProd'; + +import { + createElementWithValidation, + createFactoryWithValidation, + cloneElementWithValidation, +} from './ReactElementValidator'; + +export {isValidElement, cloneAndReplaceKey} from './ReactElementProd'; + +export const createElement: any = __DEV__ + ? createElementWithValidation + : createElementProd; +export const cloneElement: any = __DEV__ + ? cloneElementWithValidation + : cloneElementProd; +export const createFactory: any = __DEV__ + ? createFactoryWithValidation + : createFactoryProd; diff --git a/packages/react/src/ReactElementProd.js b/packages/react/src/ReactElementProd.js new file mode 100644 index 0000000000000..e9d721b92693a --- /dev/null +++ b/packages/react/src/ReactElementProd.js @@ -0,0 +1,573 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import getComponentNameFromType from 'shared/getComponentNameFromType'; +import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; +import assign from 'shared/assign'; +import hasOwnProperty from 'shared/hasOwnProperty'; +import {checkKeyStringCoercion} from 'shared/CheckStringCoercion'; + +import ReactCurrentOwner from './ReactCurrentOwner'; + +const RESERVED_PROPS = { + key: true, + ref: true, + __self: true, + __source: true, +}; + +let specialPropKeyWarningShown, + specialPropRefWarningShown, + didWarnAboutStringRefs; + +if (__DEV__) { + didWarnAboutStringRefs = {}; +} + +function hasValidRef(config) { + if (__DEV__) { + if (hasOwnProperty.call(config, 'ref')) { + const getter = Object.getOwnPropertyDescriptor(config, 'ref').get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.ref !== undefined; +} + +function hasValidKey(config) { + if (__DEV__) { + if (hasOwnProperty.call(config, 'key')) { + const getter = Object.getOwnPropertyDescriptor(config, 'key').get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.key !== undefined; +} + +function defineKeyPropWarningGetter(props, displayName) { + const warnAboutAccessingKey = function () { + if (__DEV__) { + if (!specialPropKeyWarningShown) { + specialPropKeyWarningShown = true; + console.error( + '%s: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://reactjs.org/link/special-props)', + displayName, + ); + } + } + }; + warnAboutAccessingKey.isReactWarning = true; + Object.defineProperty(props, 'key', { + get: warnAboutAccessingKey, + configurable: true, + }); +} + +function defineRefPropWarningGetter(props, displayName) { + const warnAboutAccessingRef = function () { + if (__DEV__) { + if (!specialPropRefWarningShown) { + specialPropRefWarningShown = true; + console.error( + '%s: `ref` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://reactjs.org/link/special-props)', + displayName, + ); + } + } + }; + warnAboutAccessingRef.isReactWarning = true; + Object.defineProperty(props, 'ref', { + get: warnAboutAccessingRef, + configurable: true, + }); +} + +function warnIfStringRefCannotBeAutoConverted(config) { + if (__DEV__) { + if ( + typeof config.ref === 'string' && + ReactCurrentOwner.current && + config.__self && + ReactCurrentOwner.current.stateNode !== config.__self + ) { + const componentName = getComponentNameFromType( + ReactCurrentOwner.current.type, + ); + + if (!didWarnAboutStringRefs[componentName]) { + console.error( + 'Component "%s" contains the string ref "%s". ' + + 'Support for string refs will be removed in a future major release. ' + + 'This case cannot be automatically converted to an arrow function. ' + + 'We ask you to manually fix this case by using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: ' + + 'https://reactjs.org/link/strict-mode-string-ref', + componentName, + config.ref, + ); + didWarnAboutStringRefs[componentName] = true; + } + } + } +} + +/** + * Factory method to create a new React element. This no longer adheres to + * the class pattern, so do not use new to call it. Also, instanceof check + * will not work. Instead test $$typeof field against Symbol.for('react.element') to check + * if something is a React Element. + * + * @param {*} type + * @param {*} props + * @param {*} key + * @param {string|object} ref + * @param {*} owner + * @param {*} self A *temporary* helper to detect places where `this` is + * different from the `owner` when React.createElement is called, so that we + * can warn. We want to get rid of owner and replace string `ref`s with arrow + * functions, and as long as `this` and owner are the same, there will be no + * change in behavior. + * @param {*} source An annotation object (added by a transpiler or otherwise) + * indicating filename, line number, and/or other information. + * @internal + */ +function ReactElement(type, key, ref, self, source, owner, props) { + const element = { + // This tag allows us to uniquely identify this as a React Element + $$typeof: REACT_ELEMENT_TYPE, + + // Built-in properties that belong on the element + type: type, + key: key, + ref: ref, + props: props, + + // Record the component responsible for creating this element. + _owner: owner, + }; + + if (__DEV__) { + // The validation flag is currently mutative. We put it on + // an external backing store so that we can freeze the whole object. + // This can be replaced with a WeakMap once they are implemented in + // commonly used development environments. + element._store = {}; + + // To make comparing ReactElements easier for testing purposes, we make + // the validation flag non-enumerable (where possible, which should + // include every environment we run tests in), so the test framework + // ignores it. + Object.defineProperty(element._store, 'validated', { + configurable: false, + enumerable: false, + writable: true, + value: false, + }); + // self and source are DEV only properties. + Object.defineProperty(element, '_self', { + configurable: false, + enumerable: false, + writable: false, + value: self, + }); + // Two elements created in two different places should be considered + // equal for testing purposes and therefore we hide it from enumeration. + Object.defineProperty(element, '_source', { + configurable: false, + enumerable: false, + writable: false, + value: source, + }); + if (Object.freeze) { + Object.freeze(element.props); + Object.freeze(element); + } + } + + return element; +} + +/** + * https://github.com/reactjs/rfcs/pull/107 + * @param {*} type + * @param {object} props + * @param {string} key + */ +export function jsx(type, config, maybeKey) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + + // Currently, key can be spread in as a prop. This causes a potential + // issue if key is also explicitly declared (ie.
+ // or
). We want to deprecate key spread, + // but as an intermediary step, we will use jsxDEV for everything except + //
, because we aren't currently able to tell if + // key is explicitly declared to be undefined or not. + if (maybeKey !== undefined) { + if (__DEV__) { + checkKeyStringCoercion(maybeKey); + } + key = '' + maybeKey; + } + + if (hasValidKey(config)) { + if (__DEV__) { + checkKeyStringCoercion(config.key); + } + key = '' + config.key; + } + + if (hasValidRef(config)) { + ref = config.ref; + } + + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + + return ReactElement( + type, + key, + ref, + undefined, + undefined, + ReactCurrentOwner.current, + props, + ); +} + +/** + * https://github.com/reactjs/rfcs/pull/107 + * @param {*} type + * @param {object} props + * @param {string} key + */ +export function jsxDEV(type, config, maybeKey, source, self) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + + // Currently, key can be spread in as a prop. This causes a potential + // issue if key is also explicitly declared (ie.
+ // or
). We want to deprecate key spread, + // but as an intermediary step, we will use jsxDEV for everything except + //
, because we aren't currently able to tell if + // key is explicitly declared to be undefined or not. + if (maybeKey !== undefined) { + if (__DEV__) { + checkKeyStringCoercion(maybeKey); + } + key = '' + maybeKey; + } + + if (hasValidKey(config)) { + if (__DEV__) { + checkKeyStringCoercion(config.key); + } + key = '' + config.key; + } + + if (hasValidRef(config)) { + ref = config.ref; + warnIfStringRefCannotBeAutoConverted(config); + } + + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + + if (key || ref) { + const displayName = + typeof type === 'function' + ? type.displayName || type.name || 'Unknown' + : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } + } + + return ReactElement( + type, + key, + ref, + self, + source, + ReactCurrentOwner.current, + props, + ); +} + +/** + * Create and return a new ReactElement of the given type. + * See https://reactjs.org/docs/react-api.html#createelement + */ +export function createElement(type, config, children) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + let self = null; + let source = null; + + if (config != null) { + if (hasValidRef(config)) { + ref = config.ref; + + if (__DEV__) { + warnIfStringRefCannotBeAutoConverted(config); + } + } + if (hasValidKey(config)) { + if (__DEV__) { + checkKeyStringCoercion(config.key); + } + key = '' + config.key; + } + + self = config.__self === undefined ? null : config.__self; + source = config.__source === undefined ? null : config.__source; + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + } + + // Children can be more than one argument, and those are transferred onto + // the newly allocated props object. + const childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + const childArray = Array(childrenLength); + for (let i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + if (__DEV__) { + if (Object.freeze) { + Object.freeze(childArray); + } + } + props.children = childArray; + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + if (__DEV__) { + if (key || ref) { + const displayName = + typeof type === 'function' + ? type.displayName || type.name || 'Unknown' + : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } + } + } + return ReactElement( + type, + key, + ref, + self, + source, + ReactCurrentOwner.current, + props, + ); +} + +/** + * Return a function that produces ReactElements of a given type. + * See https://reactjs.org/docs/react-api.html#createfactory + */ +export function createFactory(type) { + const factory = createElement.bind(null, type); + // Expose the type on the factory and the prototype so that it can be + // easily accessed on elements. E.g. `.type === Foo`. + // This should not be named `constructor` since this may not be the function + // that created the element, and it may not even be a constructor. + // Legacy hook: remove it + factory.type = type; + return factory; +} + +export function cloneAndReplaceKey(oldElement, newKey) { + const newElement = ReactElement( + oldElement.type, + newKey, + oldElement.ref, + oldElement._self, + oldElement._source, + oldElement._owner, + oldElement.props, + ); + + return newElement; +} + +/** + * Clone and return a new ReactElement using element as the starting point. + * See https://reactjs.org/docs/react-api.html#cloneelement + */ +export function cloneElement(element, config, children) { + if (element === null || element === undefined) { + throw new Error( + `React.cloneElement(...): The argument must be a React element, but you passed ${element}.`, + ); + } + + let propName; + + // Original props are copied + const props = assign({}, element.props); + + // Reserved names are extracted + let key = element.key; + let ref = element.ref; + // Self is preserved since the owner is preserved. + const self = element._self; + // Source is preserved since cloneElement is unlikely to be targeted by a + // transpiler, and the original source is probably a better indicator of the + // true owner. + const source = element._source; + + // Owner will be preserved, unless ref is overridden + let owner = element._owner; + + if (config != null) { + if (hasValidRef(config)) { + // Silently steal the ref from the parent. + ref = config.ref; + owner = ReactCurrentOwner.current; + } + if (hasValidKey(config)) { + if (__DEV__) { + checkKeyStringCoercion(config.key); + } + key = '' + config.key; + } + + // Remaining properties override existing props + let defaultProps; + if (element.type && element.type.defaultProps) { + defaultProps = element.type.defaultProps; + } + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + if (config[propName] === undefined && defaultProps !== undefined) { + // Resolve default props + props[propName] = defaultProps[propName]; + } else { + props[propName] = config[propName]; + } + } + } + } + + // Children can be more than one argument, and those are transferred onto + // the newly allocated props object. + const childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + const childArray = Array(childrenLength); + for (let i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + props.children = childArray; + } + + return ReactElement(element.type, key, ref, self, source, owner, props); +} + +/** + * Verifies the object is a ReactElement. + * See https://reactjs.org/docs/react-api.html#isvalidelement + * @param {?object} object + * @return {boolean} True if `object` is a ReactElement. + * @final + */ +export function isValidElement(object) { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_ELEMENT_TYPE + ); +} diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 44e9033fd3578..78f8e498d816f 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -30,7 +30,7 @@ import { createElement, cloneElement, jsxDEV, -} from './ReactElement'; +} from './ReactElementProd'; import {setExtraStackFrame} from './ReactDebugCurrentFrame'; import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame'; import hasOwnProperty from 'shared/hasOwnProperty'; diff --git a/packages/react/src/ReactServer.experimental.js b/packages/react/src/ReactServer.experimental.js index 45baaadc89f7b..330707b7b5992 100644 --- a/packages/react/src/ReactServer.experimental.js +++ b/packages/react/src/ReactServer.experimental.js @@ -14,6 +14,43 @@ export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './R export {default as __SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactServerSharedInternals'; +import {forEach, map, count, toArray, only} from './ReactChildren'; +import { + REACT_FRAGMENT_TYPE, + REACT_PROFILER_TYPE, + REACT_STRICT_MODE_TYPE, + REACT_SUSPENSE_TYPE, + REACT_DEBUG_TRACING_MODE_TYPE, +} from 'shared/ReactSymbols'; +import {cloneElement, createElement, isValidElement} from './ReactElement'; +import {createRef} from './ReactCreateRef'; +import {createServerContext} from './ReactServerContext'; +import { + use, + useId, + useCallback, + useContext, + useDebugValue, + useMemo, + getCacheSignal, + getCacheForType, +} from './ReactHooks'; +import {forwardRef} from './ReactForwardRef'; +import {lazy} from './ReactLazy'; +import {memo} from './ReactMemo'; +import {cache} from './ReactCache'; +import {startTransition} from './ReactStartTransition'; +import {postpone} from './ReactPostpone'; +import version from 'shared/ReactVersion'; + +const Children = { + map, + forEach, + count, + toArray, + only, +}; + // These are server-only export { taintUniqueValue as experimental_taintUniqueValue, @@ -22,10 +59,10 @@ export { export { Children, - Fragment, - Profiler, - StrictMode, - Suspense, + REACT_FRAGMENT_TYPE as Fragment, + REACT_PROFILER_TYPE as Profiler, + REACT_STRICT_MODE_TYPE as StrictMode, + REACT_SUSPENSE_TYPE as Suspense, cloneElement, createElement, createRef, @@ -37,15 +74,15 @@ export { memo, cache, startTransition, - unstable_DebugTracingMode, - unstable_SuspenseList, - unstable_getCacheSignal, - unstable_getCacheForType, - unstable_postpone, + REACT_DEBUG_TRACING_MODE_TYPE as unstable_DebugTracingMode, + REACT_SUSPENSE_TYPE as unstable_SuspenseList, + getCacheSignal as unstable_getCacheSignal, + getCacheForType as unstable_getCacheForType, + postpone as unstable_postpone, useId, useCallback, useContext, useDebugValue, useMemo, version, -} from './React'; +}; diff --git a/packages/react/src/ReactServer.js b/packages/react/src/ReactServer.js index 1bc2b3036f455..5631319132fda 100644 --- a/packages/react/src/ReactServer.js +++ b/packages/react/src/ReactServer.js @@ -14,12 +14,45 @@ export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './R export {default as __SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactServerSharedInternals'; +import {forEach, map, count, toArray, only} from './ReactChildren'; +import { + REACT_FRAGMENT_TYPE, + REACT_PROFILER_TYPE, + REACT_STRICT_MODE_TYPE, + REACT_SUSPENSE_TYPE, +} from 'shared/ReactSymbols'; +import {cloneElement, createElement, isValidElement} from './ReactElement'; +import {createRef} from './ReactCreateRef'; +import {createServerContext} from './ReactServerContext'; +import { + use, + useId, + useCallback, + useContext, + useDebugValue, + useMemo, +} from './ReactHooks'; +import {forwardRef} from './ReactForwardRef'; +import {lazy} from './ReactLazy'; +import {memo} from './ReactMemo'; +import {cache} from './ReactCache'; +import {startTransition} from './ReactStartTransition'; +import version from 'shared/ReactVersion'; + +const Children = { + map, + forEach, + count, + toArray, + only, +}; + export { Children, - Fragment, - Profiler, - StrictMode, - Suspense, + REACT_FRAGMENT_TYPE as Fragment, + REACT_PROFILER_TYPE as Profiler, + REACT_STRICT_MODE_TYPE as StrictMode, + REACT_SUSPENSE_TYPE as Suspense, cloneElement, createElement, createRef, @@ -37,4 +70,4 @@ export { useDebugValue, useMemo, version, -} from './React'; +}; From 5c607369ceebe56d85175df84b7b6ad58dd25e1f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 16 Jan 2024 20:27:15 -0500 Subject: [PATCH 03/90] Remove client caching from cache() API (#27977) We haven't yet decided how we want `cache` to work on the client. The lifetime of the cache is more complex than on the server, where it only has to live as long as a single request. Since it's more important to ship this on the server, we're removing the existing behavior from the client for now. On the client (i.e. not a Server Components environment) `cache` will have not have any caching behavior. `cache(fn)` will return the function as-is. We intend to implement client caching in a future major release. In the meantime, it's only exposed as an API so that Shared Components can use per-request caching on the server without breaking on the client. --- .../src/__tests__/ReactCache-test.js | 1841 ++--------------- .../src/__tests__/ReactCacheElement-test.js | 1597 ++++++++++++++ .../src/__tests__/ReactUse-test.js | 51 +- packages/react/src/ReactCacheClient.js | 27 + .../{ReactCache.js => ReactCacheServer.js} | 0 packages/react/src/ReactClient.js | 2 +- .../react/src/ReactServer.experimental.js | 2 +- packages/react/src/ReactServer.js | 2 +- 8 files changed, 1810 insertions(+), 1712 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactCacheElement-test.js create mode 100644 packages/react/src/ReactCacheClient.js rename packages/react/src/{ReactCache.js => ReactCacheServer.js} (100%) diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js index dd4a7c5d5fd1c..b32479b70bbf5 100644 --- a/packages/react-reconciler/src/__tests__/ReactCache-test.js +++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js @@ -1,1616 +1,34 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + let React; -let ReactNoop; -let Cache; -let getCacheSignal; -let Scheduler; -let assertLog; -let act; -let Suspense; -let Activity; -let useCacheRefresh; -let startTransition; -let useState; +let ReactNoopFlightServer; +let ReactNoopFlightClient; let cache; -let getTextCache; -let textCaches; -let seededCache; - describe('ReactCache', () => { beforeEach(() => { jest.resetModules(); - + jest.mock('react', () => require('react/react.react-server')); React = require('react'); - ReactNoop = require('react-noop-renderer'); - Cache = React.unstable_Cache; - Scheduler = require('scheduler'); - act = require('internal-test-utils').act; - Suspense = React.Suspense; - cache = React.cache; - Activity = React.unstable_Activity; - getCacheSignal = React.unstable_getCacheSignal; - useCacheRefresh = React.unstable_useCacheRefresh; - startTransition = React.startTransition; - useState = React.useState; - - const InternalTestUtils = require('internal-test-utils'); - assertLog = InternalTestUtils.assertLog; - - textCaches = []; - seededCache = null; - - if (gate(flags => flags.enableCache)) { - getTextCache = cache(() => { - if (seededCache !== null) { - // Trick to seed a cache before it exists. - // TODO: Need a built-in API to seed data before the initial render (i.e. - // not a refresh because nothing has mounted yet). - const textCache = seededCache; - seededCache = null; - return textCache; - } - - const data = new Map(); - const version = textCaches.length + 1; - const textCache = { - version, - data, - resolve(text) { - const record = data.get(text); - if (record === undefined) { - const newRecord = { - status: 'resolved', - value: text, - cleanupScheduled: false, - }; - data.set(text, newRecord); - } else if (record.status === 'pending') { - record.value.resolve(); - } - }, - reject(text, error) { - const record = data.get(text); - if (record === undefined) { - const newRecord = { - status: 'rejected', - value: error, - cleanupScheduled: false, - }; - data.set(text, newRecord); - } else if (record.status === 'pending') { - record.value.reject(); - } - }, - }; - textCaches.push(textCache); - return textCache; - }); - } - }); - - function readText(text) { - const signal = getCacheSignal ? getCacheSignal() : null; - const textCache = getTextCache(); - const record = textCache.data.get(text); - if (record !== undefined) { - if (!record.cleanupScheduled) { - // This record was seeded prior to the abort signal being available: - // schedule a cleanup function for it. - // TODO: Add ability to cleanup entries seeded w useCacheRefresh() - record.cleanupScheduled = true; - if (getCacheSignal) { - signal.addEventListener('abort', () => { - Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`); - }); - } - } - switch (record.status) { - case 'pending': - throw record.value; - case 'rejected': - throw record.value; - case 'resolved': - return textCache.version; - } - } else { - Scheduler.log(`Cache miss! [${text}]`); - - let resolve; - let reject; - const thenable = new Promise((res, rej) => { - resolve = res; - reject = rej; - }).then( - value => { - if (newRecord.status === 'pending') { - newRecord.status = 'resolved'; - newRecord.value = value; - } - }, - error => { - if (newRecord.status === 'pending') { - newRecord.status = 'rejected'; - newRecord.value = error; - } - }, - ); - thenable.resolve = resolve; - thenable.reject = reject; - - const newRecord = { - status: 'pending', - value: thenable, - cleanupScheduled: true, - }; - textCache.data.set(text, newRecord); - - if (getCacheSignal) { - signal.addEventListener('abort', () => { - Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`); - }); - } - throw thenable; - } - } - - function Text({text}) { - Scheduler.log(text); - return text; - } - - function AsyncText({text, showVersion}) { - const version = readText(text); - const fullText = showVersion ? `${text} [v${version}]` : text; - Scheduler.log(fullText); - return fullText; - } - - function seedNextTextCache(text) { - if (seededCache === null) { - seededCache = getTextCache(); - } - seededCache.resolve(text); - } - - function resolveMostRecentTextCache(text) { - if (textCaches.length === 0) { - throw Error('Cache does not exist.'); - } else { - // Resolve the most recently created cache. An older cache can by - // resolved with `textCaches[index].resolve(text)`. - textCaches[textCaches.length - 1].resolve(text); - } - } - - // @gate enableCacheElement && enableCache - test('render Cache component', async () => { - const root = ReactNoop.createRoot(); - await act(() => { - root.render(Hi); - }); - expect(root).toMatchRenderedOutput('Hi'); - }); - - // @gate enableCacheElement && enableCache - test('mount new data', async () => { - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - - }> - - - , - ); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A']); - expect(root).toMatchRenderedOutput('A'); - - await act(() => { - root.render('Bye'); - }); - // no cleanup: cache is still retained at the root - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCache - test('root acts as implicit cache boundary', async () => { - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - }> - - , - ); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A']); - expect(root).toMatchRenderedOutput('A'); - - await act(() => { - root.render('Bye'); - }); - // no cleanup: cache is still retained at the root - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('multiple new Cache boundaries in the same mount share the same, fresh root cache', async () => { - function App() { - return ( - <> - - }> - - - - - }> - - - - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - - // Even though there are two new trees, they should share the same - // data cache. So there should be only a single cache miss for A. - assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A', 'A']); - expect(root).toMatchRenderedOutput('AA'); - - await act(() => { - root.render('Bye'); - }); - // no cleanup: cache is still retained at the root - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => { - function App({showMore}) { - return showMore ? ( - <> - - }> - - - - - }> - - - - - ) : ( - '(empty)' - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog([]); - expect(root).toMatchRenderedOutput('(empty)'); - - await act(() => { - root.render(); - }); - // Even though there are two new trees, they should share the same - // data cache. So there should be only a single cache miss for A. - assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A', 'A']); - expect(root).toMatchRenderedOutput('AA'); - - await act(() => { - root.render('Bye'); - }); - // cleanup occurs for the cache shared by the inner cache boundaries (which - // are not shared w the root because they were added in an update) - // note that no cache is created for the root since the cache is never accessed - assertLog(['Cache cleanup: A [v1]']); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test( - 'nested cache boundaries share the same cache as the root during ' + - 'the initial render', - async () => { - function App() { - return ( - }> - - - - - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - // Even though there is a nested boundary, it should share the same - // data cache as the root. So there should be only a single cache miss for A. - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A', 'A']); - expect(root).toMatchRenderedOutput('AA'); - - await act(() => { - root.render('Bye'); - }); - // no cleanup: cache is still retained at the root - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }, - ); - - // @gate enableCacheElement && enableCache - test('new content inside an existing Cache boundary should re-use already cached data', async () => { - function App({showMore}) { - return ( - - }> - - - {showMore ? ( - }> - - - ) : null} - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - seedNextTextCache('A'); - root.render(); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Add a new cache boundary - await act(() => { - root.render(); - }); - assertLog([ - 'A [v1]', - // New tree should use already cached data - 'A [v1]', - ]); - expect(root).toMatchRenderedOutput('A [v1]A [v1]'); - - await act(() => { - root.render('Bye'); - }); - // no cleanup: cache is still retained at the root - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('a new Cache boundary uses fresh cache', async () => { - // The only difference from the previous test is that the "Show More" - // content is wrapped in a nested boundary - function App({showMore}) { - return ( - - }> - - - {showMore ? ( - - }> - - - - ) : null} - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - seedNextTextCache('A'); - root.render(); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Add a new cache boundary - await act(() => { - root.render(); - }); - assertLog([ - 'A [v1]', - // New tree should load fresh data. - 'Cache miss! [A]', - 'Loading...', - ]); - expect(root).toMatchRenderedOutput('A [v1]Loading...'); - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v2]']); - expect(root).toMatchRenderedOutput('A [v1]A [v2]'); - - // Replace all the children: this should retain the root Cache instance, - // but cleanup the separate cache instance created for the fresh cache - // boundary - await act(() => { - root.render('Bye!'); - }); - // Cleanup occurs for the *second* cache instance: the first is still - // referenced by the root - assertLog(['Cache cleanup: A [v2]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test('inner/outer cache boundaries uses the same cache instance on initial render', async () => { - const root = ReactNoop.createRoot(); - - function App() { - return ( - - }> - {/* The shell reads A */} - - {/* The inner content reads both A and B */} - }> - - - - - - - - ); - } - - function Shell({children}) { - readText('A'); - return ( - <> -
- -
-
{children}
- - ); - } - - function Content() { - readText('A'); - readText('B'); - return ; - } - - await act(() => { - root.render(); - }); - assertLog(['Cache miss! [A]', 'Loading shell...']); - expect(root).toMatchRenderedOutput('Loading shell...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog([ - 'Shell', - // There's a cache miss for B, because it hasn't been read yet. But not - // A, because it was cached when we rendered the shell. - 'Cache miss! [B]', - 'Loading content...', - ]); - expect(root).toMatchRenderedOutput( - <> -
Shell
-
Loading content...
- , - ); - - await act(() => { - resolveMostRecentTextCache('B'); - }); - assertLog(['Content']); - expect(root).toMatchRenderedOutput( - <> -
Shell
-
Content
- , - ); - - await act(() => { - root.render('Bye'); - }); - // no cleanup: cache is still retained at the root - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => { - const root = ReactNoop.createRoot(); - - function App({showMore}) { - return showMore ? ( - - }> - {/* The shell reads A */} - - {/* The inner content reads both A and B */} - }> - - - - - - - - ) : ( - '(empty)' - ); - } - - function Shell({children}) { - readText('A'); - return ( - <> -
- -
-
{children}
- - ); - } - - function Content() { - readText('A'); - readText('B'); - return ; - } - - await act(() => { - root.render(); - }); - assertLog([]); - expect(root).toMatchRenderedOutput('(empty)'); - - await act(() => { - root.render(); - }); - assertLog(['Cache miss! [A]', 'Loading shell...']); - expect(root).toMatchRenderedOutput('Loading shell...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog([ - 'Shell', - // There's a cache miss for B, because it hasn't been read yet. But not - // A, because it was cached when we rendered the shell. - 'Cache miss! [B]', - 'Loading content...', - ]); - expect(root).toMatchRenderedOutput( - <> -
Shell
-
Loading content...
- , - ); - - await act(() => { - resolveMostRecentTextCache('B'); - }); - assertLog(['Content']); - expect(root).toMatchRenderedOutput( - <> -
Shell
-
Content
- , - ); - - await act(() => { - root.render('Bye'); - }); - assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: B [v1]']); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCache - test('refresh a cache boundary', async () => { - let refresh; - function App() { - refresh = useCacheRefresh(); - return ; - } - - // Mount initial data - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - }> - - , - ); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Refresh for new data. - await act(() => { - startTransition(() => refresh()); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('A [v1]'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - // Note that the version has updated - if (getCacheSignal) { - assertLog(['A [v2]', 'Cache cleanup: A [v1]']); - } else { - assertLog(['A [v2]']); - } - expect(root).toMatchRenderedOutput('A [v2]'); - - await act(() => { - root.render('Bye'); - }); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('refresh the root cache', async () => { - let refresh; - function App() { - refresh = useCacheRefresh(); - return ; - } - - // Mount initial data - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - }> - - , - ); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Refresh for new data. - await act(() => { - startTransition(() => refresh()); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('A [v1]'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - // Note that the version has updated, and the previous cache is cleared - assertLog(['A [v2]', 'Cache cleanup: A [v1]']); - expect(root).toMatchRenderedOutput('A [v2]'); - - await act(() => { - root.render('Bye'); - }); - // the original root cache already cleaned up when the refresh completed - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('refresh the root cache without a transition', async () => { - let refresh; - function App() { - refresh = useCacheRefresh(); - return ; - } - - // Mount initial data - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - }> - - , - ); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Refresh for new data. - await act(() => { - refresh(); - }); - assertLog([ - 'Cache miss! [A]', - 'Loading...', - // The v1 cache can be cleaned up since everything that references it has - // been replaced by a fallback. When the boundary switches back to visible - // it will use the v2 cache. - 'Cache cleanup: A [v1]', - ]); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - // Note that the version has updated, and the previous cache is cleared - assertLog(['A [v2]']); - expect(root).toMatchRenderedOutput('A [v2]'); - - await act(() => { - root.render('Bye'); - }); - // the original root cache already cleaned up when the refresh completed - assertLog([]); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('refresh a cache with seed data', async () => { - let refreshWithSeed; - function App() { - const refresh = useCacheRefresh(); - const [seed, setSeed] = useState({fn: null}); - if (seed.fn) { - seed.fn(); - seed.fn = null; - } - refreshWithSeed = fn => { - setSeed({fn}); - refresh(); - }; - return ; - } - - // Mount initial data - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - - }> - - - , - ); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Refresh for new data. - await act(() => { - // Refresh the cache with seeded data, like you would receive from a - // server mutation. - // TODO: Seeding multiple typed textCaches. Should work by calling `refresh` - // multiple times with different key/value pairs - startTransition(() => - refreshWithSeed(() => { - const textCache = getTextCache(); - textCache.resolve('A'); - }), - ); - }); - // The root should re-render without a cache miss. - // The cache is not cleared up yet, since it's still reference by the root - assertLog(['A [v2]']); - expect(root).toMatchRenderedOutput('A [v2]'); - - await act(() => { - root.render('Bye'); - }); - // the refreshed cache boundary is unmounted and cleans up - assertLog(['Cache cleanup: A [v2]']); - expect(root).toMatchRenderedOutput('Bye'); - }); - - // @gate enableCacheElement && enableCache - test('refreshing a parent cache also refreshes its children', async () => { - let refreshShell; - function RefreshShell() { - refreshShell = useCacheRefresh(); - return null; - } - - function App({showMore}) { - return ( - - - }> - - - {showMore ? ( - - }> - - - - ) : null} - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - seedNextTextCache('A'); - root.render(); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Add a new cache boundary - await act(() => { - seedNextTextCache('A'); - root.render(); - }); - assertLog([ - 'A [v1]', - // New tree should load fresh data. - 'A [v2]', - ]); - expect(root).toMatchRenderedOutput('A [v1]A [v2]'); - - // Now refresh the shell. This should also cause the "Show More" contents to - // refresh, since its cache is nested inside the outer one. - await act(() => { - startTransition(() => refreshShell()); - }); - assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']); - expect(root).toMatchRenderedOutput('A [v1]A [v2]'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog([ - 'A [v3]', - 'A [v3]', - // once the refresh completes the inner showMore boundary frees its previous - // cache instance, since it is now using the refreshed parent instance. - 'Cache cleanup: A [v2]', - ]); - expect(root).toMatchRenderedOutput('A [v3]A [v3]'); - - await act(() => { - root.render('Bye!'); - }); - // Unmounting children releases the refreshed cache instance only; the root - // still retains the original cache instance used for the first render - assertLog(['Cache cleanup: A [v3]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test( - 'refreshing a cache boundary does not refresh the other boundaries ' + - 'that mounted at the same time (i.e. the ones that share the same cache)', - async () => { - let refreshFirstBoundary; - function RefreshFirstBoundary() { - refreshFirstBoundary = useCacheRefresh(); - return null; - } - - function App({showMore}) { - return showMore ? ( - <> - - }> - - - - - - }> - - - - - ) : null; - } - - // First mount the initial shell without the nested boundaries. This is - // necessary for this test because we want the two inner boundaries to be - // treated like sibling providers that happen to share an underlying - // cache, as opposed to consumers of the root-level cache. - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - - // Now reveal the boundaries. In a real app this would be a navigation. - await act(() => { - root.render(); - }); - - // Even though there are two new trees, they should share the same - // data cache. So there should be only a single cache miss for A. - assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]', 'A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]A [v1]'); - - // Refresh the first boundary. It should not refresh the second boundary, - // even though they previously shared the same underlying cache. - await act(async () => { - await refreshFirstBoundary(); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v2]']); - expect(root).toMatchRenderedOutput('A [v2]A [v1]'); - - // Unmount children: this should clear *both* cache instances: - // the root doesn't have a cache instance (since it wasn't accessed - // during the initial render, and all subsequent cache accesses were within - // a fresh boundary). Therefore this causes cleanup for both the fresh cache - // instance in the refreshed first boundary and cleanup for the non-refreshed - // sibling boundary. - await act(() => { - root.render('Bye!'); - }); - assertLog(['Cache cleanup: A [v2]', 'Cache cleanup: A [v1]']); - expect(root).toMatchRenderedOutput('Bye!'); - }, - ); - - // @gate enableCacheElement && enableCache - test( - 'mount a new Cache boundary in a sibling while simultaneously ' + - 'resolving a Suspense boundary', - async () => { - function App({showMore}) { - return ( - <> - {showMore ? ( - }> - - - - - ) : null} - }> - - {' '} - {' '} - - - - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - // This will resolve the content in the first cache - resolveMostRecentTextCache('A'); - resolveMostRecentTextCache('B'); - // And mount the second tree, which includes new content - root.render(); - }); - assertLog([ - // The new tree should use a fresh cache - 'Cache miss! [A]', - 'Loading...', - // The other tree uses the cached responses. This demonstrates that the - // requests are not dropped. - 'A [v1]', - 'B [v1]', - ]); - expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]'); - - // Now resolve the second tree - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v2]']); - expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]'); - - await act(() => { - root.render('Bye!'); - }); - // Unmounting children releases both cache boundaries, but the original - // cache instance (used by second boundary) is still referenced by the root. - // only the second cache instance is freed. - assertLog(['Cache cleanup: A [v2]']); - expect(root).toMatchRenderedOutput('Bye!'); - }, - ); - - // @gate enableCacheElement && enableCache - test('cache pool is cleared once transitions that depend on it commit their shell', async () => { - function Child({text}) { - return ( - - - - ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - }>(empty), - ); - }); - assertLog([]); - expect(root).toMatchRenderedOutput('(empty)'); - - await act(() => { - startTransition(() => { - root.render( - }> - - , - ); - }); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('(empty)'); - - await act(() => { - startTransition(() => { - root.render( - }> - - - , - ); - }); - }); - assertLog([ - // No cache miss, because it uses the pooled cache - 'Loading...', - ]); - expect(root).toMatchRenderedOutput('(empty)'); - - // Resolve the request - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]', 'A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]A [v1]'); - - // Now do another transition - await act(() => { - startTransition(() => { - root.render( - }> - - - - , - ); - }); - }); - assertLog([ - // First two children use the old cache because they already finished - 'A [v1]', - 'A [v1]', - // The new child uses a fresh cache - 'Cache miss! [A]', - 'Loading...', - ]); - expect(root).toMatchRenderedOutput('A [v1]A [v1]'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]', 'A [v1]', 'A [v2]']); - expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]'); - - // Unmount children: the first text cache instance is created only after the root - // commits, so both fresh cache instances are released by their cache boundaries, - // cleaning up v1 (used for the first two children which render together) and - // v2 (used for the third boundary added later). - await act(() => { - root.render('Bye!'); - }); - assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: A [v2]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test('cache pool is not cleared by arbitrary commits', async () => { - function App() { - return ( - <> - - - - ); - } - - let showMore; - function ShowMore() { - const [shouldShow, _showMore] = useState(false); - showMore = () => _showMore(true); - return ( - <> - }> - {shouldShow ? ( - - - - ) : null} - - - ); - } - - let updateUnrelated; - function Unrelated() { - const [count, _updateUnrelated] = useState(0); - updateUnrelated = _updateUnrelated; - return ; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog(['0']); - expect(root).toMatchRenderedOutput('0'); - - await act(() => { - startTransition(() => { - showMore(); - }); - }); - assertLog(['Cache miss! [A]', 'Loading...']); - expect(root).toMatchRenderedOutput('0'); - - await act(() => { - updateUnrelated(1); - }); - assertLog([ - '1', - - // Happens to re-render the fallback. Doesn't need to, but not relevant - // to this test. - 'Loading...', - ]); - expect(root).toMatchRenderedOutput('1'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]1'); - - // Unmount children: the first text cache instance is created only after initial - // render after calling showMore(). This instance is cleaned up when that boundary - // is unmounted. Bc root cache instance is never accessed, the inner cache - // boundary ends up at v1. - await act(() => { - root.render('Bye!'); - }); - assertLog(['Cache cleanup: A [v1]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test('cache boundary uses a fresh cache when its key changes', async () => { - const root = ReactNoop.createRoot(); - seedNextTextCache('A'); - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - seedNextTextCache('B'); - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['B [v2]']); - expect(root).toMatchRenderedOutput('B [v2]'); - - // Unmount children: the fresh cache instance for B cleans up since the cache boundary - // is the only owner, while the original cache instance (for A) is still retained by - // the root. - await act(() => { - root.render('Bye!'); - }); - assertLog(['Cache cleanup: B [v2]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test('overlapping transitions after an initial mount use the same fresh cache', async () => { - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['Cache miss! [A]']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // After a mount, subsequent transitions use a fresh cache - await act(() => { - startTransition(() => { - root.render( - - - - - , - ); - }); - }); - assertLog(['Cache miss! [B]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Update to a different text and with a different key for the cache - // boundary: this should still use the fresh cache instance created - // for the earlier transition - await act(() => { - startTransition(() => { - root.render( - - - - - , - ); - }); - }); - assertLog(['Cache miss! [C]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - await act(() => { - resolveMostRecentTextCache('C'); - }); - assertLog(['C [v2]']); - expect(root).toMatchRenderedOutput('C [v2]'); - - // Unmount children: the fresh cache used for the updates is freed, while the - // original cache (with A) is still retained at the root. - await act(() => { - root.render('Bye!'); - }); - assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test('overlapping updates after an initial mount use the same fresh cache', async () => { - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['Cache miss! [A]']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('A'); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // After a mount, subsequent updates use a fresh cache - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['Cache miss! [B]']); - expect(root).toMatchRenderedOutput('Loading...'); - - // A second update uses the same fresh cache: even though this is a new - // Cache boundary, the render uses the fresh cache from the pending update. - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['Cache miss! [C]']); - expect(root).toMatchRenderedOutput('Loading...'); - - await act(() => { - resolveMostRecentTextCache('C'); - }); - assertLog(['C [v2]']); - expect(root).toMatchRenderedOutput('C [v2]'); - - // Unmount children: the fresh cache used for the updates is freed, while the - // original cache (with A) is still retained at the root. - await act(() => { - root.render('Bye!'); - }); - assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test('cleans up cache only used in an aborted transition', async () => { - const root = ReactNoop.createRoot(); - seedNextTextCache('A'); - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Start a transition from A -> B..., which should create a fresh cache - // for the new cache boundary (bc of the different key) - await act(() => { - startTransition(() => { - root.render( - - - - - , - ); - }); - }); - assertLog(['Cache miss! [B]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // ...but cancel by transitioning "back" to A (which we never really left) - await act(() => { - startTransition(() => { - root.render( - - - - - , - ); - }); - }); - assertLog(['A [v1]', 'Cache cleanup: B [v2]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Unmount children: ... - await act(() => { - root.render('Bye!'); - }); - assertLog([]); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableCacheElement && enableCache - test.skip('if a root cache refresh never commits its fresh cache is released', async () => { - const root = ReactNoop.createRoot(); - let refresh; - function Example({text}) { - refresh = useCacheRefresh(); - return ; - } - seedNextTextCache('A'); - await act(() => { - root.render( - - - , - ); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - await act(() => { - startTransition(() => { - refresh(); - }); - }); - assertLog(['Cache miss! [A]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - await act(() => { - root.render('Bye!'); - }); - assertLog([ - // TODO: the v1 cache should *not* be cleaned up, it is still retained by the root - // The following line is presently yielded but should not be: - // 'Cache cleanup: A [v1]', - // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh - // The following line is presently not yielded but should be: - 'Cache cleanup: A [v2]', - ]); - expect(root).toMatchRenderedOutput('Bye!'); - }); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + ReactNoopFlightClient = require('react-noop-renderer/flight-client'); - // @gate enableCacheElement && enableCache - test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => { - const root = ReactNoop.createRoot(); - let refresh; - function Example({text}) { - refresh = useCacheRefresh(); - return ; - } - seedNextTextCache('A'); - await act(() => { - root.render( - - - - - , - ); - }); - assertLog(['A [v1]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - await act(() => { - startTransition(() => { - refresh(); - }); - }); - assertLog(['Cache miss! [A]']); - expect(root).toMatchRenderedOutput('A [v1]'); - - // Unmount the boundary before the refresh can complete - await act(() => { - root.render('Bye!'); - }); - assertLog([ - // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh - // The following line is presently not yielded but should be: - 'Cache cleanup: A [v2]', - ]); - expect(root).toMatchRenderedOutput('Bye!'); - }); - - // @gate enableActivity - // @gate enableCache - test('prerender a new cache boundary inside an Activity tree', async () => { - function App({prerenderMore}) { - return ( - -
- {prerenderMore ? ( - - - - ) : null} -
-
- ); - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog([]); - expect(root).toMatchRenderedOutput(