From ffec9ec5b5c846f61d7b40e92f138e2a7b34f273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 27 Jun 2024 18:09:40 +0200 Subject: [PATCH] Add new package with renderToMarkup export (#30105) Name of the package is tbd (straw: `react-html`). It's a new package separate from `react-dom` though and can be used as a standalone package - e.g. also from a React Native app. ```js import {renderToMarkup} from '...'; const html = await renderToMarkup(); ``` The idea is that this is a helper for rendering HTML that is not intended to be hydrated. It's primarily intended to support a subset of HTML that can be used as embedding and not served as HTML documents from HTTP. For example as e-mails or in RSS/Atom feeds or other distributions. It's a successor to `renderToStaticMarkup`. A few differences: - This doesn't support "Client Components". It can only use the Server Components subset. No useEffect, no useState etc. since it will never be hydrated. Use of those are errors. - You also can't pass Client References so you can't use components marked with `"use client"`. - Unlike `renderToStaticMarkup` this does support async so you can suspend and use data from these components. - Unlike `renderToReadableStream` this does not support streaming or Suspense boundaries and any error rejects the promise. Since there's no feasible way to "client render" or patch up the document. - Form Actions are not supported since in an embedded environment there's no place to post back to across versions. You can render plain forms with fixed URLs though. - You can't use any resource preloading like `preload()` from `react-dom`. ## Implementation This first version in this PR only supports Server Components since that's the thing that doesn't have an existing API. Might add a Client Components version later that errors. We don't want to maintain a completely separate implementation for this use case so this uses the `dom-legacy` build dimension to wire up a build that encapsulates a Flight Server -> Flight Client -> Fizz stream to render Server Components that then get SSR:ed. There's no problem to use a Flight Client in a Server Component environment since it's already supported for Server-to-Server. Both of these use a bundler config that just errors for Client References though since we don't need any bundling integration and this is just a standalone package. Running Fizz in a Server Component environment is a problem though because it depends on "react" and it needs the client version. Therefore, for this build we embed the client version of "react" shared internals into the build. It doesn't need anything to be able to use those APIs since you can't call the client APIs anyway. One unfortunate thing though is that since Flight currently needs to go to binary and back, we need TextEncoder/TextDecoder to be available but this shouldn't really be necessary. Also since we use the legacy stream config, large strings that use byteLengthOfChunk errors atm. This needs to be fixed before shipping. I'm not sure what would be the best layering though that isn't unnecessarily burdensome to maintain. Maybe some kind of pass-through protocol that would also be useful in general - e.g. when Fizz and Flight are in the same process. --------- Co-authored-by: Sebastian Silbermann --- .../forks/ReactFlightClientConfig.dom-bun.js | 1 - .../ReactFlightClientConfig.dom-legacy.js | 92 +++++++-- packages/react-html/README.md | 32 ++++ packages/react-html/index.js | 5 + packages/react-html/npm/index.js | 5 + .../react-html/npm/react-html.react-server.js | 7 + packages/react-html/package.json | 38 ++++ .../react-html/react-html.react-server.js | 10 + .../src/ReactHTMLLegacyClientStreamConfig.js | 32 ++++ packages/react-html/src/ReactHTMLServer.js | 178 ++++++++++++++++++ .../src/__tests__/ReactHTMLServer-test.js | 112 +++++++++++ .../ReactFlightServerConfig.dom-legacy.js | 71 ++++++- scripts/error-codes/codes.json | 7 +- scripts/rollup/bundles.js | 13 ++ scripts/rollup/forks.js | 9 +- scripts/shared/inlinedHostConfigs.js | 35 ++++ 16 files changed, 627 insertions(+), 20 deletions(-) create mode 100644 packages/react-html/README.md create mode 100644 packages/react-html/index.js create mode 100644 packages/react-html/npm/index.js create mode 100644 packages/react-html/npm/react-html.react-server.js create mode 100644 packages/react-html/package.json create mode 100644 packages/react-html/react-html.react-server.js create mode 100644 packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js create mode 100644 packages/react-html/src/ReactHTMLServer.js create mode 100644 packages/react-html/src/__tests__/ReactHTMLServer-test.js diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 461996a2e0887..0a8027e3e12aa 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -11,7 +11,6 @@ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigPlain'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; -export type Response = any; export opaque type ModuleLoading = mixed; export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index ddf6440a20e48..8e91e3a8062fd 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -7,20 +7,82 @@ * @flow */ -export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactClientConsoleConfigBrowser'; -export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; - -export type Response = any; -export opaque type ModuleLoading = mixed; -export opaque type SSRModuleMap = mixed; -export opaque type ServerManifest = mixed; +import type {Thenable} from 'shared/ReactTypes'; + +export * from 'react-html/src/ReactHTMLLegacyClientStreamConfig.js'; +export * from 'react-client/src/ReactClientConsoleConfigPlain'; + +export type ModuleLoading = null; +export type SSRModuleMap = null; +export opaque type ServerManifest = null; export opaque type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = mixed; -export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars -export const resolveClientReference: any = null; -export const resolveServerReference: any = null; -export const preloadModule: any = null; -export const requireModule: any = null; -export const prepareDestinationForModule: any = null; +export opaque type ClientReferenceMetadata = null; +export opaque type ClientReference = null; // eslint-disable-line no-unused-vars + +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + throw new Error( + 'renderToMarkup should not have emitted Client References. This is a bug in React.', + ); +} + +export function resolveClientReference( + bundlerConfig: SSRModuleMap, + metadata: ClientReferenceMetadata, +): ClientReference { + throw new Error( + 'renderToMarkup should not have emitted Client References. This is a bug in React.', + ); +} + +export function resolveServerReference( + config: ServerManifest, + id: ServerReferenceId, +): ClientReference { + throw new Error( + 'renderToMarkup should not have emitted Server References. This is a bug in React.', + ); +} + +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + return null; +} + +export function requireModule(metadata: ClientReference): T { + throw new Error( + 'renderToMarkup should not have emitted Client References. This is a bug in React.', + ); +} + export const usedWithSSR = true; + +type HintCode = string; +type HintModel = null; // eslint-disable-line no-unused-vars + +export function dispatchHint( + code: Code, + model: HintModel, +): void { + // Should never happen. +} + +export function preinitModuleForSSR( + href: string, + nonce: ?string, + crossOrigin: ?string, +) { + // Should never happen. +} + +export function preinitScriptForSSR( + href: string, + nonce: ?string, + crossOrigin: ?string, +) { + // Should never happen. +} diff --git a/packages/react-html/README.md b/packages/react-html/README.md new file mode 100644 index 0000000000000..c0794aac104c5 --- /dev/null +++ b/packages/react-html/README.md @@ -0,0 +1,32 @@ +# `react-html` + +This package provides the ability to render standalone HTML from Server Components for use in embedded contexts such as e-mails and RSS/Atom feeds. It cannot use Client Components and does not hydrate. It is intended to be paired with the generic React package, which is shipped as `react` to npm. + +## Installation + +```sh +npm install react react-html +``` + +## Usage + +```js +import { renderToMarkup } from 'react-html'; +import EmailTemplate from './my-email-template-component.js' + +async function action(email, name) { + "use server"; + // ... in your server, e.g. a Server Action... + const htmlString = await renderToMarkup(); + // ... send e-mail using some e-mail provider + await sendEmail({ to: email, contentType: 'text/html', body: htmlString }); +} +``` + +Note that this is an async function that needs to be awaited - unlike the legacy `renderToString` in `react-dom`. + +## API + +### `react-html` + +See https://react.dev/reference/react-html diff --git a/packages/react-html/index.js b/packages/react-html/index.js new file mode 100644 index 0000000000000..a1818e8c3bba6 --- /dev/null +++ b/packages/react-html/index.js @@ -0,0 +1,5 @@ +'use strict'; + +throw new Error( + 'react-html is not supported outside a React Server Components environment.', +); diff --git a/packages/react-html/npm/index.js b/packages/react-html/npm/index.js new file mode 100644 index 0000000000000..e567bb2c0aa21 --- /dev/null +++ b/packages/react-html/npm/index.js @@ -0,0 +1,5 @@ +'use strict'; + +throw new Error( + 'react-html is not supported outside a React Server Components environment.' +); diff --git a/packages/react-html/npm/react-html.react-server.js b/packages/react-html/npm/react-html.react-server.js new file mode 100644 index 0000000000000..be2af1e0c5838 --- /dev/null +++ b/packages/react-html/npm/react-html.react-server.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-html.react-server.production.js'); +} else { + module.exports = require('./cjs/react-html.react-server.development.js'); +} diff --git a/packages/react-html/package.json b/packages/react-html/package.json new file mode 100644 index 0000000000000..92dfc38512338 --- /dev/null +++ b/packages/react-html/package.json @@ -0,0 +1,38 @@ +{ + "name": "react-html", + "version": "19.0.0", + "private": true, + "description": "React package generating embedded HTML markup such as e-mails using Server Components.", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react-html" + }, + "keywords": [ + "react" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://react.dev/", + "peerDependencies": { + "react": "^19.0.0" + }, + "files": [ + "LICENSE", + "README.md", + "index.js", + "react-html.react-server.js", + "cjs/" + ], + "exports": { + ".": { + "react-server": "./react-html.react-server.js", + "default": "./index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + } +} diff --git a/packages/react-html/react-html.react-server.js b/packages/react-html/react-html.react-server.js new file mode 100644 index 0000000000000..fb30cd4200836 --- /dev/null +++ b/packages/react-html/react-html.react-server.js @@ -0,0 +1,10 @@ +/** + * 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. + * + * @flow + */ + +export * from './src/ReactHTMLServer'; diff --git a/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js b/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js new file mode 100644 index 0000000000000..74b0503590462 --- /dev/null +++ b/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js @@ -0,0 +1,32 @@ +/** + * 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. + * + * @flow + */ + +// TODO: The legacy one should not use binary. + +export type StringDecoder = TextDecoder; + +export function createStringDecoder(): StringDecoder { + return new TextDecoder(); +} + +const decoderOptions = {stream: true}; + +export function readPartialStringChunk( + decoder: StringDecoder, + buffer: Uint8Array, +): string { + return decoder.decode(buffer, decoderOptions); +} + +export function readFinalStringChunk( + decoder: StringDecoder, + buffer: Uint8Array, +): string { + return decoder.decode(buffer); +} diff --git a/packages/react-html/src/ReactHTMLServer.js b/packages/react-html/src/ReactHTMLServer.js new file mode 100644 index 0000000000000..c4eebe3f51054 --- /dev/null +++ b/packages/react-html/src/ReactHTMLServer.js @@ -0,0 +1,178 @@ +/** + * 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. + * + * @flow + */ + +import type {ReactNodeList} from 'shared/ReactTypes'; +import type {LazyComponent} from 'react/src/ReactLazy'; + +import ReactVersion from 'shared/ReactVersion'; + +import { + createRequest as createFlightRequest, + startWork as startFlightWork, + startFlowing as startFlightFlowing, + abort as abortFlight, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse as createFlightResponse, + getRoot as getFlightRoot, + processBinaryChunk as processFlightBinaryChunk, + close as closeFlight, +} from 'react-client/src/ReactFlightClient'; + +import { + createRequest as createFizzRequest, + startWork as startFizzWork, + startFlowing as startFizzFlowing, + abort as abortFizz, +} from 'react-server/src/ReactFizzServer'; + +import { + createResumableState, + createRenderState, + createRootFormatContext, +} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; + +type ReactMarkupNodeList = + // This is the intersection of ReactNodeList and ReactClientValue minus + // Client/ServerReferences. + | React$Element> + | LazyComponent + | React$Element + | string + | boolean + | number + | symbol + | null + | void + | bigint + | $AsyncIterable + | $AsyncIterator + | Iterable + | Iterator + | Array + | Promise; // Thenable + +type MarkupOptions = { + identifierPrefix?: string, + signal?: AbortSignal, +}; + +function noServerCallOrFormAction() { + throw new Error( + 'renderToMarkup should not have emitted Server References. This is a bug in React.', + ); +} + +export function renderToMarkup( + children: ReactMarkupNodeList, + options?: MarkupOptions, +): Promise { + return new Promise((resolve, reject) => { + const textEncoder = new TextEncoder(); + const flightDestination = { + push(chunk: string | null): boolean { + if (chunk !== null) { + // TODO: Legacy should not use binary streams. + processFlightBinaryChunk(flightResponse, textEncoder.encode(chunk)); + } else { + closeFlight(flightResponse); + } + return true; + }, + destroy(error: mixed): void { + abortFizz(fizzRequest, error); + reject(error); + }, + }; + let buffer = ''; + const fizzDestination = { + // $FlowFixMe[missing-local-annot] + push(chunk) { + if (chunk !== null) { + buffer += chunk; + } else { + // null indicates that we finished + resolve(buffer); + } + return true; + }, + // $FlowFixMe[missing-local-annot] + destroy(error) { + abortFlight(flightRequest, error); + reject(error); + }, + }; + function onError(error: mixed) { + // Any error rejects the promise, regardless of where it happened. + // Unlike other React SSR we don't want to put Suspense boundaries into + // client rendering mode because there's no client rendering here. + reject(error); + } + const flightRequest = createFlightRequest( + // $FlowFixMe: This should be a subtype but not everything is typed covariant. + children, + null, + onError, + options ? options.identifierPrefix : undefined, + undefined, + 'Markup', + undefined, + ); + const flightResponse = createFlightResponse( + null, + null, + noServerCallOrFormAction, + noServerCallOrFormAction, + undefined, + undefined, + undefined, + ); + const resumableState = createResumableState( + options ? options.identifierPrefix : undefined, + undefined, + ); + const root = getFlightRoot(flightResponse); + const fizzRequest = createFizzRequest( + // $FlowFixMe: Thenables as children are supported. + root, + resumableState, + createRenderState(resumableState, true), + createRootFormatContext(), + Infinity, + onError, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abortFlight(flightRequest, (signal: any).reason); + abortFizz(fizzRequest, (signal: any).reason); + } else { + const listener = () => { + abortFlight(flightRequest, (signal: any).reason); + abortFizz(fizzRequest, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startFlightWork(flightRequest); + startFlightFlowing(flightRequest, flightDestination); + startFizzWork(fizzRequest); + startFizzFlowing(fizzRequest, fizzDestination); + }); +} + +export {ReactVersion as version}; diff --git a/packages/react-html/src/__tests__/ReactHTMLServer-test.js b/packages/react-html/src/__tests__/ReactHTMLServer-test.js new file mode 100644 index 0000000000000..503e233301c9a --- /dev/null +++ b/packages/react-html/src/__tests__/ReactHTMLServer-test.js @@ -0,0 +1,112 @@ +/** + * 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 + */ + +'use strict'; + +global.TextDecoder = require('util').TextDecoder; +global.TextEncoder = require('util').TextEncoder; + +let React; +let ReactHTML; + +describe('ReactHTML', () => { + beforeEach(() => { + jest.resetModules(); + // We run in the react-server condition. + jest.mock('react', () => require('react/react.react-server')); + jest.mock('react-html', () => + require('react-html/react-html.react-server'), + ); + + React = require('react'); + ReactHTML = require('react-html'); + }); + + it('should be able to render a simple component', async () => { + function Component() { + // We can't use JSX because that's client-JSX in our tests. + return React.createElement('div', null, 'hello world'); + } + + const html = await ReactHTML.renderToMarkup(React.createElement(Component)); + expect(html).toBe('
hello world
'); + }); + + it('supports the useId Hook', async () => { + function Component() { + const firstNameId = React.useId(); + const lastNameId = React.useId(); + // We can't use JSX because that's client-JSX in our tests. + return React.createElement( + 'div', + null, + React.createElement( + 'h2', + { + id: firstNameId, + }, + 'First', + ), + React.createElement( + 'p', + { + 'aria-labelledby': firstNameId, + }, + 'Sebastian', + ), + React.createElement( + 'h2', + { + id: lastNameId, + }, + 'Last', + ), + React.createElement( + 'p', + { + 'aria-labelledby': lastNameId, + }, + 'Smith', + ), + ); + } + + const html = await ReactHTML.renderToMarkup(React.createElement(Component)); + const container = document.createElement('div'); + container.innerHTML = html; + + expect(container.getElementsByTagName('h2')[0].id).toBe( + container.getElementsByTagName('p')[0].getAttribute('aria-labelledby'), + ); + expect(container.getElementsByTagName('h2')[1].id).toBe( + container.getElementsByTagName('p')[1].getAttribute('aria-labelledby'), + ); + + // It's not the same id between them. + expect(container.getElementsByTagName('h2')[0].id).not.toBe( + container.getElementsByTagName('p')[1].getAttribute('aria-labelledby'), + ); + }); + + // @gate enableCache + it('supports cache', async () => { + let counter = 0; + const getCount = React.cache(() => { + return counter++; + }); + function Component() { + const a = getCount(); + const b = getCount(); + return React.createElement('div', null, a, b); + } + + const html = await ReactHTML.renderToMarkup(React.createElement(Component)); + expect(html).toBe('
00
'); + }); +}); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index bfe794c9a2c55..99591bb954ea9 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -9,9 +9,15 @@ import type {Request} from 'react-server/src/ReactFlightServer'; import type {ReactComponentInfo} from 'shared/ReactTypes'; +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; -export * from '../ReactFlightServerConfigBundlerCustom'; -export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; +export type HintCode = string; +export type HintModel = null; // eslint-disable-line no-unused-vars +export type Hints = null; + +export function createHints(): Hints { + return null; +} export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any); @@ -21,3 +27,64 @@ export const componentStorage: AsyncLocalStorage = (null: any); export * from '../ReactFlightServerConfigDebugNoop'; + +export type ClientManifest = null; +export opaque type ClientReference = null; // eslint-disable-line no-unused-vars +export opaque type ServerReference = null; // eslint-disable-line no-unused-vars +export opaque type ClientReferenceMetadata: any = null; +export opaque type ServerReferenceId: string = string; +export opaque type ClientReferenceKey: any = string; + +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); + +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; +} + +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { + throw new Error( + 'Attempted to render a Client Component from renderToMarkup. ' + + 'This is not supported since it will never hydrate. ' + + 'Only render Server Components with renderToMarkup.', + ); +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + throw new Error( + 'Attempted to render a Client Component from renderToMarkup. ' + + 'This is not supported since it will never hydrate. ' + + 'Only render Server Components with renderToMarkup.', + ); +} + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + throw new Error( + 'Attempted to render a Server Action from renderToMarkup. ' + + 'This is not supported since it varies by version of the app. ' + + 'Use a fixed URL for any forms instead.', + ); +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + throw new Error( + 'Attempted to render a Server Action from renderToMarkup. ' + + 'This is not supported since it varies by version of the app. ' + + 'Use a fixed URL for any forms instead.', + ); +} diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index ef4ae75a6d634..9d8fb4ed3739f 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -515,5 +515,10 @@ "527": "Incompatible React versions: The \"react\" and \"react-dom\" packages must have the exact same version. Instead got:\n - react: %s\n - react-dom: %s\nLearn more: https://react.dev/warnings/version-mismatch", "528": "Expected not to update to be updated to a stylesheet with precedence. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.%s", "529": "Expected stylesheet with precedence to not be updated to a different kind of . Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.%s", - "530": "The render was aborted by the server with a promise." + "530": "The render was aborted by the server with a promise.", + "531": "react-html is not supported outside a React Server Components environment.", + "532": "Attempted to render a Client Component from renderToMarkup. This is not supported since it will never hydrate. Only render Server Components with renderToMarkup.", + "533": "Attempted to render a Server Action from renderToMarkup. This is not supported since it varies by version of the app. Use a fixed URL for any forms instead.", + "534": "renderToMarkup should not have emitted Client References. This is a bug in React.", + "535": "renderToMarkup should not have emitted Server References. This is a bug in React." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 66e59124ca2d5..67f1c237b209f 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -363,6 +363,19 @@ const bundles = [ externals: [], }, + /******* React HTML *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-html/src/ReactHTMLServer.js', + name: 'react-html.react-server', + condition: 'react-server', + global: 'ReactHTML', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react'], + }, + /******* React Server DOM Webpack Server *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index a83b8b1b1a969..0ba6827bba7a2 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -65,6 +65,12 @@ const forks = Object.freeze({ if (entry === 'react/src/ReactServer.js') { return './packages/react/src/ReactSharedInternalsServer.js'; } + if (entry === 'react-html/src/ReactHTMLServer.js') { + // Inside the ReactHTMLServer render we don't refer to any shared internals + // but instead use our own internal copy of the state because you cannot use + // any of this state from a component anyway. E.g. you can't use a client hook. + return './packages/react/src/ReactSharedInternalsClient.js'; + } if (bundle.condition === 'react-server') { return './packages/react-server/src/ReactSharedInternalsServer.js'; } @@ -93,7 +99,8 @@ const forks = Object.freeze({ entry === 'react-dom' || entry === 'react-dom/src/ReactDOMFB.js' || entry === 'react-dom/src/ReactDOMTestingFB.js' || - entry === 'react-dom/src/ReactDOMServer.js' + entry === 'react-dom/src/ReactDOMServer.js' || + entry === 'react-html/src/ReactHTMLServer.js' ) { if ( bundleType === FB_WWW_DEV || diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index a63da44c36f29..a21b624150aac 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -36,6 +36,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.node', 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-webpack', 'react-server-dom-webpack/client.node.unbundled', 'react-server-dom-webpack/server', @@ -71,6 +74,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.node', 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-webpack', 'react-server-dom-webpack/client.node', 'react-server-dom-webpack/server', @@ -108,6 +114,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.node', 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-turbopack', 'react-server-dom-turbopack/client.node.unbundled', 'react-server-dom-turbopack/server', @@ -145,6 +154,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.node', 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-turbopack', 'react-server-dom-turbopack/client.node', 'react-server-dom-turbopack/server', @@ -182,6 +194,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.bun', 'react-dom/src/server/ReactDOMFizzServerBun.js', 'react-dom-bindings', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'shared/ReactDOMSharedInternals', ], isFlowTyped: true, @@ -212,6 +227,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.browser', 'react-dom/src/server/ReactDOMFizzServerBrowser.js', // react-dom/server.browser 'react-dom/src/server/ReactDOMFizzStaticBrowser.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-webpack', 'react-server-dom-webpack/client', 'react-server-dom-webpack/client.browser', @@ -240,6 +258,9 @@ module.exports = [ 'react-dom/server', 'react-dom/server.node', 'react-dom-bindings', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-esm', 'react-server-dom-esm/client', 'react-server-dom-esm/client.browser', @@ -274,6 +295,9 @@ module.exports = [ 'react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-turbopack/server.browser 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js', 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -303,6 +327,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.edge', 'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge 'react-dom/src/server/ReactDOMFizzStaticEdge.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-webpack', 'react-server-dom-webpack/client.edge', 'react-server-dom-webpack/server.edge', @@ -338,6 +365,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.edge', 'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge 'react-dom/src/server/ReactDOMFizzStaticEdge.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-turbopack', 'react-server-dom-turbopack/client.edge', 'react-server-dom-turbopack/server.edge', @@ -374,6 +404,9 @@ module.exports = [ 'react-dom/src/server/react-dom-server.node', 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-esm', 'react-server-dom-esm/client.node', 'react-server-dom-esm/server', @@ -395,6 +428,7 @@ module.exports = [ entryPoints: [ 'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser 'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node + 'react-html/src/ReactHTMLServer.js', ], paths: [ 'react-dom', @@ -404,6 +438,7 @@ module.exports = [ 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Browser and *Node files 'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser 'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node + 'react-html', 'shared/ReactDOMSharedInternals', ], isFlowTyped: true,