diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b222f2b64153..67600e3386c8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,32 +7,6 @@ aliases: - &environment TZ: /usr/share/zoneinfo/America/Los_Angeles - - &restore_yarn_cache - restore_cache: - name: Restore yarn cache - keys: - - v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} - - v1-yarn_cache-{{ arch }}- - - v1-yarn_cache- - - - &yarn_install - run: - name: Install dependencies - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - - &yarn_install_retry - run: - name: Install dependencies (retry) - when: on_fail - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - - &save_yarn_cache - save_cache: - name: Save yarn cache - key: v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn - - &restore_yarn_cache_fixtures_dom restore_cache: name: Restore yarn cache for fixtures/dom @@ -62,47 +36,34 @@ aliases: paths: - ~/.cache/yarn - - &save_node_modules - save_cache: - name: Save node_modules cache - # Cache only for the current revision to prevent cache injections from - # malicious PRs. - key: v1-node_modules-{{ arch }}-{{ .Revision }} - paths: - - node_modules - - packages/eslint-plugin-react-hooks/node_modules - - packages/react-art/node_modules - - packages/react-client/node_modules - - packages/react-devtools-core/node_modules - - packages/react-devtools-extensions/node_modules - - packages/react-devtools-inline/node_modules - - packages/react-devtools-shared/node_modules - - packages/react-devtools-shell/node_modules - - packages/react-devtools-timeline/node_modules - - packages/react-devtools/node_modules - - packages/react-dom/node_modules - - packages/react-interactions/node_modules - - packages/react-native-renderer/node_modules - - packages/react-reconciler/node_modules - - packages/react-server-dom-relay/node_modules - - packages/react-server-dom-webpack/node_modules - - packages/react-server-native-relay/node_modules - - packages/react-server/node_modules - - packages/react-test-renderer/node_modules - - packages/react/node_modules - - packages/scheduler/node_modules - - - &restore_node_modules - restore_cache: - name: Restore node_modules cache - keys: - - v1-node_modules-{{ arch }}-{{ .Revision }} - - &TEST_PARALLELISM 20 - &attach_workspace at: build +commands: + setup_node_modules: + description: "Restore node_modules" + steps: + - restore_cache: + name: Restore yarn cache + keys: + - v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} + - v1-yarn_cache-{{ arch }}- + - v1-yarn_cache- + - run: + name: Install dependencies + command: | + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + if [ $? -ne 0 ]; then + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + fi + - save_cache: + name: Save yarn cache + key: v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} + paths: + - ~/.cache/yarn + # The CircleCI API doesn't yet support triggering a specific workflow, but it # does support triggering a pipeline. So as a workaround you can triggger the # entire pipeline and use parameters to disable everything except the workflow @@ -115,28 +76,13 @@ parameters: default: '' jobs: - setup: - docker: *docker - environment: *environment - steps: - - checkout - - run: - name: NodeJS Version - command: node --version - - *restore_yarn_cache - - *restore_node_modules - - *yarn_install - - *yarn_install_retry - - *save_yarn_cache - - *save_node_modules - yarn_lint: docker: *docker environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: node ./scripts/prettier/index - run: node ./scripts/tasks/eslint - run: ./scripts/circleci/check_license.sh @@ -150,7 +96,7 @@ jobs: steps: - checkout - - *restore_node_modules + - setup_node_modules - run: node ./scripts/tasks/flow-ci scrape_warning_messages: @@ -159,7 +105,7 @@ jobs: steps: - checkout - - *restore_node_modules + - setup_node_modules - run: command: | mkdir -p ./build @@ -175,7 +121,7 @@ jobs: parallelism: 40 steps: - checkout - - *restore_node_modules + - setup_node_modules - run: yarn build-combined - persist_to_workspace: root: . @@ -190,7 +136,7 @@ jobs: type: string steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Download artifacts for revision command: | @@ -207,7 +153,7 @@ jobs: environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Download artifacts for base revision command: | @@ -235,7 +181,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: echo "<< pipeline.git.revision >>" >> build/COMMIT_SHA # Compress build directory into a single tarball for easy download - run: tar -zcvf ./build.tgz ./build @@ -254,7 +200,7 @@ jobs: - attach_workspace: at: . - run: echo "<< pipeline.git.revision >>" >> build/COMMIT_SHA - - *restore_node_modules + - setup_node_modules - run: command: node ./scripts/tasks/danger @@ -265,7 +211,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: environment: RELEASE_CHANNEL: experimental @@ -280,7 +226,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: name: Playwright install deps command: | @@ -302,7 +248,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: ./scripts/circleci/download_devtools_regression_build.js << parameters.version >> --replaceBuild - run: node ./scripts/jest/jest-cli.js --build --project devtools --release-channel=experimental --reactVersion << parameters.version >> --ci @@ -317,7 +263,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: name: Playwright install deps command: | @@ -341,7 +287,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: yarn lint-build yarn_check_release_dependencies: @@ -351,7 +297,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: yarn check-release-dependencies @@ -361,7 +307,7 @@ jobs: steps: - checkout - attach_workspace: *attach_workspace - - *restore_node_modules + - setup_node_modules - run: name: Search build artifacts for unminified errors command: | @@ -374,7 +320,7 @@ jobs: steps: - checkout - attach_workspace: *attach_workspace - - *restore_node_modules + - setup_node_modules - run: name: Confirm generated inline Fizz runtime is up to date command: | @@ -390,7 +336,7 @@ jobs: type: string steps: - checkout - - *restore_node_modules + - setup_node_modules - run: yarn test <> --ci yarn_test_build: @@ -404,7 +350,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: yarn test --build <> --ci RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: @@ -414,7 +360,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - *restore_yarn_cache_fixtures_dom - *yarn_install_fixtures_dom - *yarn_install_fixtures_dom_retry @@ -433,7 +379,7 @@ jobs: environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Run fuzz tests command: | @@ -452,7 +398,7 @@ jobs: environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Run publish script command: | @@ -465,27 +411,29 @@ jobs: workflows: version: 2 - # New workflow that will replace "stable" and "experimental" build_and_test: unless: << pipeline.parameters.prerelease_commit_sha >> jobs: - - setup: + - yarn_flow: filters: branches: ignore: - builds/facebook-www - - yarn_flow: - requires: - - setup - check_generated_fizz_runtime: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - yarn_lint: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - yarn_test: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www matrix: parameters: args: @@ -509,11 +457,15 @@ workflows: - '-r=stable --env=development --persistent' - '-r=experimental --env=development --persistent' - yarn_build_combined: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - scrape_warning_messages: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - process_artifacts_combined: requires: - scrape_warning_messages @@ -551,8 +503,7 @@ workflows: branches: ignore: - main - requires: - - setup + - builds/facebook-www - sizebot: filters: branches: @@ -591,10 +542,7 @@ workflows: only: - main jobs: - - setup - - test_fuzz: - requires: - - setup + - test_fuzz devtools_regression_tests: unless: << pipeline.parameters.prerelease_commit_sha >> @@ -607,10 +555,7 @@ workflows: only: - main jobs: - - setup - download_build: - requires: - - setup revision: << pipeline.git.revision >> - build_devtools_and_process_artifacts: requires: @@ -642,11 +587,8 @@ workflows: publish_preleases: when: << pipeline.parameters.prerelease_commit_sha >> jobs: - - setup - publish_prerelease: name: Publish to Next channel - requires: - - setup commit_sha: << pipeline.parameters.prerelease_commit_sha >> release_channel: stable dist_tag: "next" @@ -674,11 +616,8 @@ workflows: only: - main jobs: - - setup - publish_prerelease: name: Publish to Next channel - requires: - - setup commit_sha: << pipeline.git.revision >> release_channel: stable dist_tag: "next" diff --git a/fixtures/flight/server/handler.server.js b/fixtures/flight/server/handler.server.js index a51d653c0ab54..251833a52d15b 100644 --- a/fixtures/flight/server/handler.server.js +++ b/fixtures/flight/server/handler.server.js @@ -20,9 +20,10 @@ module.exports = function (req, res) { const App = m.default.default || m.default; res.setHeader('Access-Control-Allow-Origin', '*'); const moduleMap = JSON.parse(data); - const {pipe} = renderToPipeableStream(React.createElement(App), { - clientManifest: moduleMap, - }); + const {pipe} = renderToPipeableStream( + React.createElement(App), + moduleMap + ); pipe(res); } ); diff --git a/package.json b/package.json index 7e3e8930ff5ec..dc1c903370d94 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.196.3", - "flow-remove-types": "^2.196.1", + "flow-bin": "^0.199.0", + "flow-remove-types": "^2.198.2", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20200517.0.0", diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 2dd135ad656a9..624efb7428008 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -493,6 +493,12 @@ export function parseModelString( // When passed into React, we'll know how to suspend on this. return createLazyChunkWrapper(chunk); } + case '@': { + // Promise + const id = parseInt(value.substring(2), 16); + const chunk = getChunk(response, id); + return chunk; + } case 'S': { return Symbol.for(value.substring(2)); } diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js similarity index 100% rename from packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js rename to packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js similarity index 100% rename from packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js rename to packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js diff --git a/packages/react-reconciler/src/ReactFiberThenable.js b/packages/react-reconciler/src/ReactFiberThenable.js index 53d1c7176d588..81c59baf168d9 100644 --- a/packages/react-reconciler/src/ReactFiberThenable.js +++ b/packages/react-reconciler/src/ReactFiberThenable.js @@ -88,6 +88,9 @@ export function trackUsedThenable( // Only instrument the thenable if the status if not defined. If // it's defined, but an unknown value, assume it's been instrumented by // some custom userspace implementation. We treat it as "pending". + // Attach a dummy listener, to ensure that any lazy initialization can + // happen. Flight lazily parses JSON when the value is actually awaited. + thenable.then(noop, noop); } else { const pendingThenable: PendingThenable = (thenable: any); pendingThenable.status = 'pending'; @@ -107,17 +110,17 @@ export function trackUsedThenable( } }, ); + } - // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { - case 'fulfilled': { - const fulfilledThenable: FulfilledThenable = (thenable: any); - return fulfilledThenable.value; - } - case 'rejected': { - const rejectedThenable: RejectedThenable = (thenable: any); - throw rejectedThenable.reason; - } + // Check one more time in case the thenable resolved synchronously. + switch (thenable.status) { + case 'fulfilled': { + const fulfilledThenable: FulfilledThenable = (thenable: any); + return fulfilledThenable.value; + } + case 'rejected': { + const rejectedThenable: RejectedThenable = (thenable: any); + throw rejectedThenable.reason; } } diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-bun.js similarity index 100% rename from packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js rename to packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-bun.js diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-node.js similarity index 100% rename from packages/react-reconciler/src/forks/ReactFiberHostConfig.dom.js rename to packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-node.js diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js index b0d5d0a2a0beb..be2343121e2a0 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -27,12 +27,12 @@ type Options = { function renderToReadableStream( model: ReactModel, - webpackMaps: BundlerConfig, + webpackMap: BundlerConfig, options?: Options, ): ReadableStream { const request = createRequest( model, - webpackMaps, + webpackMap, options ? options.onError : undefined, options ? options.context : undefined, options ? options.identifierPrefix : undefined, diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index d05787fccf830..82ff956c99d72 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -37,12 +37,12 @@ type PipeableStream = { function renderToPipeableStream( model: ReactModel, - webpackMaps: BundlerConfig, + webpackMap: BundlerConfig, options?: Options, ): PipeableStream { const request = createRequest( model, - webpackMaps, + webpackMap, options ? options.onError : undefined, options ? options.context : undefined, options ? options.identifierPrefix : undefined, diff --git a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js index 500e79adf7b0a..c662d6d51f243 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -13,9 +13,7 @@ type WebpackMap = { }, }; -export type BundlerConfig = { - clientManifest: WebpackMap, -}; +export type BundlerConfig = WebpackMap; // eslint-disable-next-line no-unused-vars export type ClientReference = { @@ -56,7 +54,7 @@ export function resolveModuleMetaData( clientReference: ClientReference, ): ModuleMetaData { const resolvedModuleData = - config.clientManifest[clientReference.filepath][clientReference.name]; + config[clientReference.filepath][clientReference.name]; if (clientReference.async) { return { id: resolvedModuleData.id, diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js index d9512a69a5a07..d2045e9364f3b 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -33,8 +33,6 @@ module.exports = function register() { // reference. case 'defaultProps': return undefined; - case 'getDefaultProps': - return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; @@ -91,8 +89,6 @@ module.exports = function register() { // reference. case 'defaultProps': return undefined; - case 'getDefaultProps': - return undefined; // Avoid this attempting to be serialized. case 'toJSON': return undefined; @@ -106,9 +102,9 @@ module.exports = function register() { target.default = Object.defineProperties( (function () { throw new Error( - `Attempted to call the default export of ${moduleId} from the server` + + `Attempted to call the default export of ${moduleId} from the server ` + `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a` + + `the server, it can only be rendered as a Component or passed to props of a ` + `Client Component.`, ); }: any), @@ -132,24 +128,13 @@ module.exports = function register() { // we should resolve that with a client reference that unwraps the Promise on // the client. - const innerModuleId = target.filepath; - const clientReference: Function = Object.defineProperties( - (function () { - throw new Error( - `Attempted to call the module exports of ${innerModuleId} from the server` + - `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a` + - `Client Component.`, - ); - }: any), - { - // Represents the whole object instead of a particular import. - name: {value: '*'}, - $$typeof: {value: CLIENT_REFERENCE}, - filepath: {value: target.filepath}, - async: {value: true}, - }, - ); + const clientReference = Object.defineProperties(({}: any), { + // Represents the whole Module object instead of a particular import. + name: {value: '*'}, + $$typeof: {value: CLIENT_REFERENCE}, + filepath: {value: target.filepath}, + async: {value: true}, + }); const proxy = new Proxy(clientReference, proxyHandlers); // Treat this as a resolved Promise for React's use() @@ -221,23 +206,13 @@ module.exports = function register() { // $FlowFixMe[prop-missing] found when upgrading Flow Module._extensions['.client.js'] = function (module, path) { const moduleId: string = (url.pathToFileURL(path).href: any); - const clientReference: Function = Object.defineProperties( - (function () { - throw new Error( - `Attempted to call the module exports of ${moduleId} from the server` + - `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a` + - `Client Component.`, - ); - }: any), - { - // Represents the whole object instead of a particular import. - name: {value: '*'}, - $$typeof: {value: CLIENT_REFERENCE}, - filepath: {value: moduleId}, - async: {value: false}, - }, - ); + const clientReference = Object.defineProperties(({}: any), { + // Represents the whole Module object instead of a particular import. + name: {value: '*'}, + $$typeof: {value: CLIENT_REFERENCE}, + filepath: {value: moduleId}, + async: {value: false}, + }); // $FlowFixMe[incompatible-call] found when upgrading Flow module.exports = new Proxy(clientReference, proxyHandlers); }; 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 e6c0ca012fd15..89868fe82d055 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -246,6 +246,94 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

@div

'); }); + // @gate enableUseHook + it('should be able to esm compat test module references', async () => { + const ESMCompatModule = { + __esModule: true, + default: function ({greeting}) { + return greeting + ' World'; + }, + hi: 'Hello', + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + function interopWebpack(obj) { + // Basically what Webpack's ESM interop feature testing does. + if (typeof obj === 'object' && obj.__esModule) { + return obj; + } + return Object.assign({default: obj}, obj); + } + + const {default: Component, hi} = interopWebpack( + clientExports(ESMCompatModule), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + // @gate enableUseHook + it('should be able to render a named component export', async () => { + const Module = { + Component: function ({greeting}) { + return greeting + ' World'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {Component} = clientExports(Module); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + // @gate enableUseHook it('should unwrap async module references', async () => { const AsyncModule = Promise.resolve(function AsyncModule({text}) { @@ -848,8 +936,8 @@ describe('ReactFlightDOM', () => { }); // We simulate a bug in the Webpack bundler which causes an error on the server. - for (const id in webpackMap.clientManifest) { - Object.defineProperty(webpackMap.clientManifest, id, { + for (const id in webpackMap) { + Object.defineProperty(webpackMap, id, { get: () => { throw new Error('bug in the bundler'); }, @@ -905,4 +993,50 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual(['bug in the bundler']); }); + + // @gate enableUseHook + it('should pass a Promise through props and be able use() it on the client', async () => { + async function getData() { + return 'async hello'; + } + + function Component({data}) { + const text = use(data); + return

{text}

; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMReader.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.innerHTML).toBe('

async hello

'); + }); }); 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 b5600dc663767..a0056d65bdf51 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -473,14 +473,12 @@ describe('ReactFlightDOMBrowser', () => { const ClientComponentOnTheServer = clientExports(ClientComponent); // In the SSR bundle this module won't exist. We simulate this by deleting it. - const clientId = - webpackMap.clientManifest[ClientComponentOnTheClient.filepath]['*'].id; + const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id; delete webpackModules[clientId]; // Instead, we have to provide a translation from the client meta data to the SSR // meta data. - const ssrMetaData = - webpackMap.clientManifest[ClientComponentOnTheServer.filepath]['*']; + const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath]['*']; const translationMap = { [clientId]: { '*': ssrMetaData, diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 80617e9283bbf..607e6e0d6c76c 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -13,7 +13,7 @@ const Module = require('module'); let webpackModuleIdx = 0; const webpackModules = {}; const webpackErroredModules = {}; -const webpackMap = {clientManifest: {}}; +const webpackMap = {}; global.__webpack_require__ = function (id) { if (webpackErroredModules[id]) { throw webpackErroredModules[id]; @@ -44,7 +44,7 @@ exports.clientModuleError = function clientModuleError(moduleError) { const idx = '' + webpackModuleIdx++; webpackErroredModules[idx] = moduleError; const path = url.pathToFileURL(idx).href; - webpackMap.clientManifest[path] = { + webpackMap[path] = { '': { id: idx, chunks: [], @@ -65,7 +65,7 @@ exports.clientExports = function clientExports(moduleExports) { const idx = '' + webpackModuleIdx++; webpackModules[idx] = moduleExports; const path = url.pathToFileURL(idx).href; - webpackMap.clientManifest[path] = { + webpackMap[path] = { '': { id: idx, chunks: [], @@ -81,7 +81,7 @@ exports.clientExports = function clientExports(moduleExports) { moduleExports.then( asyncModuleExports => { for (const name in asyncModuleExports) { - webpackMap.clientManifest[path][name] = { + webpackMap[path][name] = { id: idx, chunks: [], name: name, @@ -92,7 +92,7 @@ exports.clientExports = function clientExports(moduleExports) { ); } for (const name in moduleExports) { - webpackMap.clientManifest[path][name] = { + webpackMap[path][name] = { id: idx, chunks: [], name: name, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 3d4bd59756cd9..7baa76220265a 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -216,6 +216,82 @@ const POP = {}; const jsxPropsParents: WeakMap = new WeakMap(); const jsxChildrenParents: WeakMap = new WeakMap(); +function serializeThenable(request: Request, thenable: Thenable): number { + request.pendingChunks++; + const newTask = createTask( + request, + null, + getActiveContext(), + request.abortableTasks, + ); + + switch (thenable.status) { + case 'fulfilled': { + // We have the resolved value, we can go ahead and schedule it for serialization. + newTask.model = thenable.value; + pingTask(request, newTask); + return newTask.id; + } + case 'rejected': { + const x = thenable.reason; + const digest = logRecoverableError(request, x); + if (__DEV__) { + const {message, stack} = getErrorMessageAndStackDev(x); + emitErrorChunkDev(request, newTask.id, digest, message, stack); + } else { + emitErrorChunkProd(request, newTask.id, digest); + } + return newTask.id; + } + default: { + if (typeof thenable.status === 'string') { + // Only instrument the thenable if the status if not defined. If + // it's defined, but an unknown value, assume it's been instrumented by + // some custom userspace implementation. We treat it as "pending". + break; + } + const pendingThenable: PendingThenable = (thenable: any); + pendingThenable.status = 'pending'; + pendingThenable.then( + fulfilledValue => { + if (thenable.status === 'pending') { + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = fulfilledValue; + } + }, + (error: mixed) => { + if (thenable.status === 'pending') { + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; + } + }, + ); + break; + } + } + + thenable.then( + value => { + newTask.model = value; + pingTask(request, newTask); + }, + reason => { + // TODO: Is it safe to directly emit these without being inside a retry? + const digest = logRecoverableError(request, reason); + if (__DEV__) { + const {message, stack} = getErrorMessageAndStackDev(reason); + emitErrorChunkDev(request, newTask.id, digest, message, stack); + } else { + emitErrorChunkProd(request, newTask.id, digest); + } + }, + ); + + return newTask.id; +} + function readThenable(thenable: Thenable): T { if (thenable.status === 'fulfilled') { return thenable.value; @@ -270,6 +346,7 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { } function attemptResolveElement( + request: Request, type: any, key: null | React$Key, ref: mixed, @@ -303,6 +380,14 @@ function attemptResolveElement( result !== null && typeof result.then === 'function' ) { + // When the return value is in children position we can resolve it immediately, + // to its value without a wrapper if it's synchronously available. + const thenable: Thenable = result; + if (thenable.status === 'fulfilled') { + return thenable.value; + } + // TODO: Once we accept Promises as children on the client, we can just return + // the thenable here. return createLazyWrapperAroundWakeable(result); } return result; @@ -331,6 +416,7 @@ function attemptResolveElement( const init = type._init; const wrappedType = init(payload); return attemptResolveElement( + request, wrappedType, key, ref, @@ -345,6 +431,7 @@ function attemptResolveElement( } case REACT_MEMO_TYPE: { return attemptResolveElement( + request, type.type, key, ref, @@ -414,10 +501,14 @@ function serializeByValueID(id: number): string { return '$' + id.toString(16); } -function serializeByRefID(id: number): string { +function serializeLazyID(id: number): string { return '$L' + id.toString(16); } +function serializePromiseID(id: number): string { + return '$@' + id.toString(16); +} + function serializeSymbolReference(name: string): string { return '$S' + name; } @@ -442,7 +533,7 @@ function serializeClientReference( // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. - return serializeByRefID(existingId); + return serializeLazyID(existingId); } return serializeByValueID(existingId); } @@ -461,7 +552,7 @@ function serializeClientReference( // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. - return serializeByRefID(moduleId); + return serializeLazyID(moduleId); } return serializeByValueID(moduleId); } catch (x) { @@ -835,6 +926,7 @@ export function resolveModelToJSON( const element: React$Element = (value: any); // Attempt to render the Server Component. value = attemptResolveElement( + request, element.type, element.key, element.ref, @@ -873,7 +965,7 @@ export function resolveModelToJSON( const ping = newTask.ping; x.then(ping, ping); newTask.thenableState = getThenableStateAfterSuspending(); - return serializeByRefID(newTask.id); + return serializeLazyID(newTask.id); } else { // Something errored. We'll still send everything we have up until this point. // We'll replace this element with a lazy reference that throws on the client @@ -887,7 +979,7 @@ export function resolveModelToJSON( } else { emitErrorChunkProd(request, errorId, digest); } - return serializeByRefID(errorId); + return serializeLazyID(errorId); } } } @@ -899,6 +991,11 @@ export function resolveModelToJSON( if (typeof value === 'object') { if (isClientReference(value)) { return serializeClientReference(request, parent, key, (value: any)); + } else if (typeof value.then === 'function') { + // We assume that any object with a .then property is a "Thenable" type, + // or a Promise type. Either of which can be represented by a Promise. + const promiseId = serializeThenable(request, (value: any)); + return serializePromiseID(promiseId); } else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) { const providerKey = ((value: any): ReactProviderType)._context ._globalName; @@ -1157,6 +1254,7 @@ function retryTask(request: Request, task: Task): void { // also suspends. task.model = value; value = attemptResolveElement( + request, element.type, element.key, element.ref, @@ -1180,6 +1278,7 @@ function retryTask(request: Request, task: Task): void { const nextElement: React$Element = (value: any); task.model = value; value = attemptResolveElement( + request, nextElement.type, nextElement.key, nextElement.ref, diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js similarity index 100% rename from packages/react-server/src/forks/ReactFlightServerConfig.bun.js rename to packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js similarity index 100% rename from packages/react-server/src/forks/ReactFlightServerConfig.dom.js rename to packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.bun.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-bun.js similarity index 100% rename from packages/react-server/src/forks/ReactServerFormatConfig.bun.js rename to packages/react-server/src/forks/ReactServerFormatConfig.dom-bun.js diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.dom.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-node.js similarity index 100% rename from packages/react-server/src/forks/ReactServerFormatConfig.dom.js rename to packages/react-server/src/forks/ReactServerFormatConfig.dom-node.js diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.bun.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-bun.js similarity index 100% rename from packages/react-server/src/forks/ReactServerStreamConfig.bun.js rename to packages/react-server/src/forks/ReactServerStreamConfig.dom-bun.js diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.dom.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-node.js similarity index 100% rename from packages/react-server/src/forks/ReactServerStreamConfig.dom.js rename to packages/react-server/src/forks/ReactServerStreamConfig.dom-node.js diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index f45ef1e508d08..e4dfeef98c6cd 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -138,10 +138,8 @@ function mapIntoArray( // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey( - // eslint-disable-next-line react-internal/safe-string-coercion - '' + - // $FlowFixMe Flow incorrectly thinks existing element's key can be a number - mappedChild.key, + // $FlowFixMe[unsafe-addition] + '' + mappedChild.key, // eslint-disable-line react-internal/safe-string-coercion ) + '/' : '') + childKey, diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 833337910f611..0451025b6c7f1 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -35,6 +35,8 @@ import {setExtraStackFrame} from './ReactDebugCurrentFrame'; import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame'; import hasOwnProperty from 'shared/hasOwnProperty'; +const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference'); + function setCurrentlyValidatingElement(element) { if (__DEV__) { if (element) { @@ -165,10 +167,12 @@ function validateExplicitKey(element, parentType) { * @param {*} parentType node's parent's type. */ function validateChildKeys(node, parentType) { - if (typeof node !== 'object') { + if (typeof node !== 'object' || !node) { return; } - if (isArray(node)) { + if (node.$$typeof === REACT_CLIENT_REFERENCE) { + // This is a reference to a client component so it's unknown. + } else if (isArray(node)) { for (let i = 0; i < node.length; i++) { const child = node[i]; if (isValidElement(child)) { @@ -180,7 +184,7 @@ function validateChildKeys(node, parentType) { if (node._store) { node._store.validated = true; } - } else if (node) { + } else { const iteratorFn = getIteratorFn(node); if (typeof iteratorFn === 'function') { // Entry iterators used to provide implicit keys, @@ -210,6 +214,9 @@ function validatePropTypes(element) { if (type === null || type === undefined || typeof type === 'string') { return; } + if (type.$$typeof === REACT_CLIENT_REFERENCE) { + return; + } let propTypes; if (typeof type === 'function') { propTypes = type.propTypes; diff --git a/packages/react/src/jsx/ReactJSXElementValidator.js b/packages/react/src/jsx/ReactJSXElementValidator.js index 947d806058732..da000079ee90f 100644 --- a/packages/react/src/jsx/ReactJSXElementValidator.js +++ b/packages/react/src/jsx/ReactJSXElementValidator.js @@ -32,6 +32,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; +const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference'); + function setCurrentlyValidatingElement(element) { if (__DEV__) { if (element) { @@ -179,10 +181,12 @@ function validateExplicitKey(element, parentType) { */ function validateChildKeys(node, parentType) { if (__DEV__) { - if (typeof node !== 'object') { + if (typeof node !== 'object' || !node) { return; } - if (isArray(node)) { + if (node.$$typeof === REACT_CLIENT_REFERENCE) { + // This is a reference to a client component so it's unknown. + } else if (isArray(node)) { for (let i = 0; i < node.length; i++) { const child = node[i]; if (isValidElement(child)) { @@ -194,7 +198,7 @@ function validateChildKeys(node, parentType) { if (node._store) { node._store.validated = true; } - } else if (node) { + } else { const iteratorFn = getIteratorFn(node); if (typeof iteratorFn === 'function') { // Entry iterators used to provide implicit keys, @@ -225,6 +229,9 @@ function validatePropTypes(element) { if (type === null || type === undefined || typeof type === 'string') { return; } + if (type.$$typeof === REACT_CLIENT_REFERENCE) { + return; + } let propTypes; if (typeof type === 'function') { propTypes = type.propTypes; diff --git a/scripts/flow/config/flowconfig b/scripts/flow/config/flowconfig index cd5aa2ccb15c9..4f27c1c552abc 100644 --- a/scripts/flow/config/flowconfig +++ b/scripts/flow/config/flowconfig @@ -49,4 +49,4 @@ munge_underscores=false %REACT_RENDERER_FLOW_OPTIONS% [version] -^0.196.3 +^0.199.0 diff --git a/scripts/release/publish-using-ci-workflow.js b/scripts/release/publish-using-ci-workflow.js index d7f87b48b094b..5d2cc68dba31d 100644 --- a/scripts/release/publish-using-ci-workflow.js +++ b/scripts/release/publish-using-ci-workflow.js @@ -99,6 +99,15 @@ async function main() { } ); + if (!pipelineResponse.ok) { + console.error( + theme.error( + `Failed to access CircleCI. Responded with status: ${pipelineResponse.status}` + ) + ); + process.exit(1); + } + const pipelineJSON = await pipelineResponse.json(); const pipelineID = pipelineJSON.id; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index ec03dd1647c3f..3f7a41021aa6e 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -8,7 +8,7 @@ module.exports = [ { - shortName: 'dom', + shortName: 'dom-node', entryPoints: [ 'react-dom', 'react-dom/unstable_testing', @@ -46,7 +46,7 @@ module.exports = [ isServerSupported: true, }, { - shortName: 'bun', + shortName: 'dom-bun', entryPoints: ['react-dom', 'react-dom/src/server/ReactDOMFizzServerBun.js'], paths: [ 'react-dom', diff --git a/yarn.lock b/yarn.lock index eddfc81b0d7d6..623c3ca158170 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8165,22 +8165,22 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-bin@^0.196.3: - version "0.196.3" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.196.3.tgz#b6df48986a2629f2c6a26fb79d73fc07c8056af0" - integrity sha512-pmvjlksi1CvkSnDHpcfhDFj/KC3hwSgE2OpzvugW57dfgqfHzqX1UfZIcScGWM5AmP/IeOsQCW383k3zIbEnrA== - -flow-parser@^0.196.1: - version "0.196.1" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.196.1.tgz#3c31f102454518f0c68eeb99f57501c2a0c9bff0" - integrity sha512-V3yaKHyBWhl+LF6sxgbfqxMlwoFKs8UKh2DYTrGj1AHi9ST7Zyp+9ToF4l9eoL6l/DxdFwCNF3MAJ1vCVrgJmw== - -flow-remove-types@^2.196.1: - version "2.196.1" - resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.196.1.tgz#c77ab53679beb1b1ba420c16865cea714a67defc" - integrity sha512-pAEe2B/fKtV96MVGWQgmjP5Z1nLeFFe++r83ql1Zj86+p+3IujsbvwxiXCiF/SS6ObbB6TmciCxxd+FsOUyY3Q== - dependencies: - flow-parser "^0.196.1" +flow-bin@^0.199.0: + version "0.199.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.199.0.tgz#e710c0834d4e1032529a633e0cf32d89a102fcfb" + integrity sha512-8N8jn59ghgtDSogFoy1Ld1P8NlfdlVUcXSRADDf8WyX3SMMA6b1SbqraTRXxJDNn0F3WdVBHKdufdUg73y4Nhw== + +flow-parser@^0.198.2: + version "0.198.2" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.198.2.tgz#4c225995896b6be75943e9358e606a4fd86c87d9" + integrity sha512-tCQzqXbRAz0ZadIhAXGwdp/xsusADo8IK9idgc/2qCK5RmazbKDGedyykfRtzWgy7Klt4f4NZxq0o/wFUg6plQ== + +flow-remove-types@^2.198.2: + version "2.198.2" + resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.198.2.tgz#1b9e0e32eead4554f0d325391bbeb0c7a2a2993f" + integrity sha512-l7ua9FnZwI0HUUUu2Khj3pYnGSlkY5vOE60bgG1ajPJ9P+KzRgXIjpCaS4iTbUEpu6olzRrAVJtJ5N8kxmvpiQ== + dependencies: + flow-parser "^0.198.2" pirates "^3.0.2" vlq "^0.2.1"