diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 8e91e3a8062fd..b992b01803260 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -7,82 +7,20 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes'; +export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; -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 type Response = any; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; +export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; -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 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 dispatchHint: any = null; +export const prepareDestinationForModule: any = null; 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-client/src/forks/ReactFlightClientConfig.markup.js b/packages/react-client/src/forks/ReactFlightClientConfig.markup.js new file mode 100644 index 0000000000000..8e91e3a8062fd --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.markup.js @@ -0,0 +1,88 @@ +/** + * 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 {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 = 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-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 731b6f2483d07..594051dc35c2c 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -108,6 +108,8 @@ export type HeadersDescriptor = { // E.g. this can be used to distinguish legacy renderers from this modern one. export const isPrimaryRenderer = true; +export const supportsClientAPIs = true; + export type StreamingFormat = 0 | 1; const ScriptStreamingFormat: StreamingFormat = 0; const DataStreamingFormat: StreamingFormat = 1; diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 92cd890976d22..725ee666f52af 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -166,6 +166,7 @@ export { resetResumableState, completeResumableState, emitEarlyPreloads, + supportsClientAPIs, } from './ReactFizzConfigDOM'; import escapeTextForBrowser from './escapeTextForBrowser'; diff --git a/packages/react-html/index.js b/packages/react-html/index.js index a1818e8c3bba6..01e5f5e51b08c 100644 --- a/packages/react-html/index.js +++ b/packages/react-html/index.js @@ -1,5 +1,10 @@ -'use strict'; +/** + * 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 + */ -throw new Error( - 'react-html is not supported outside a React Server Components environment.', -); +export * from './src/ReactHTMLClient'; diff --git a/packages/react-html/npm/index.js b/packages/react-html/npm/index.js index e567bb2c0aa21..753fdef93e42d 100644 --- a/packages/react-html/npm/index.js +++ b/packages/react-html/npm/index.js @@ -1,5 +1,7 @@ 'use strict'; -throw new Error( - 'react-html is not supported outside a React Server Components environment.' -); +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-html.production.js'); +} else { + module.exports = require('./cjs/react-html.development.js'); +} diff --git a/packages/react-html/src/ReactFizzConfigHTML.js b/packages/react-html/src/ReactFizzConfigHTML.js new file mode 100644 index 0000000000000..81528b0cdb81c --- /dev/null +++ b/packages/react-html/src/ReactFizzConfigHTML.js @@ -0,0 +1,188 @@ +/** + * 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 { + RenderState, + ResumableState, + HoistableState, + FormatContext, +} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +import {pushStartInstance as pushStartInstanceImpl} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +import type { + Destination, + Chunk, + PrecomputedChunk, +} from 'react-server/src/ReactServerStreamConfig'; + +import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; + +import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; + +import hasOwnProperty from 'shared/hasOwnProperty'; + +// Allow embedding inside another Fizz render. +export const isPrimaryRenderer = false; + +// Disable Client Hooks +export const supportsClientAPIs = false; + +import { + stringToChunk, + stringToPrecomputedChunk, +} from 'react-server/src/ReactServerStreamConfig'; + +// this chunk is empty on purpose because we do not want to emit the DOCTYPE +// when markup is rendering HTML +export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk(''); + +export type { + RenderState, + ResumableState, + HoistableState, + FormatContext, +} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export { + getChildFormatContext, + makeId, + pushEndInstance, + pushStartCompletedSuspenseBoundary, + pushEndCompletedSuspenseBoundary, + pushFormStateMarkerIsMatching, + pushFormStateMarkerIsNotMatching, + writeStartSegment, + writeEndSegment, + writeCompletedSegmentInstruction, + writeCompletedBoundaryInstruction, + writeClientRenderBoundaryInstruction, + writeStartPendingSuspenseBoundary, + writeEndPendingSuspenseBoundary, + writeHoistablesForBoundary, + writePlaceholder, + writeCompletedRoot, + createRootFormatContext, + createRenderState, + createResumableState, + createHoistableState, + writePreamble, + writeHoistables, + writePostamble, + hoistHoistables, + resetResumableState, + completeResumableState, + emitEarlyPreloads, +} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +import escapeTextForBrowser from 'react-dom-bindings/src/server/escapeTextForBrowser'; + +export function pushStartInstance( + target: Array, + type: string, + props: Object, + resumableState: ResumableState, + renderState: RenderState, + hoistableState: null | HoistableState, + formatContext: FormatContext, + textEmbedded: boolean, + isFallback: boolean, +): ReactNodeList { + for (const propKey in props) { + if (hasOwnProperty.call(props, propKey)) { + const propValue = props[propKey]; + if (propKey === 'ref' && propValue != null) { + throw new Error( + 'Cannot pass ref in renderToMarkup because they will never be hydrated.', + ); + } + if (typeof propValue === 'function') { + throw new Error( + 'Cannot pass event handlers (' + + propKey + + ') in renderToMarkup because ' + + 'the HTML will never be hydrated so they can never get called.', + ); + } + } + } + + return pushStartInstanceImpl( + target, + type, + props, + resumableState, + renderState, + hoistableState, + formatContext, + textEmbedded, + isFallback, + ); +} + +export function pushTextInstance( + target: Array, + text: string, + renderState: RenderState, + textEmbedded: boolean, +): boolean { + // Markup doesn't need any termination. + target.push(stringToChunk(escapeTextForBrowser(text))); + return false; +} + +export function pushSegmentFinale( + target: Array, + renderState: RenderState, + lastPushedText: boolean, + textEmbedded: boolean, +): void { + // Markup doesn't need any termination. + return; +} + +export function writeStartCompletedSuspenseBoundary( + destination: Destination, + renderState: RenderState, +): boolean { + // Markup doesn't have any instructions. + return true; +} +export function writeStartClientRenderedSuspenseBoundary( + destination: Destination, + renderState: RenderState, + // flushing these error arguments are not currently supported in this legacy streaming format. + errorDigest: ?string, + errorMessage: ?string, + errorStack: ?string, + errorComponentStack: ?string, +): boolean { + // Markup doesn't have any instructions. + return true; +} + +export function writeEndCompletedSuspenseBoundary( + destination: Destination, + renderState: RenderState, +): boolean { + // Markup doesn't have any instructions. + return true; +} +export function writeEndClientRenderedSuspenseBoundary( + destination: Destination, + renderState: RenderState, +): boolean { + // Markup doesn't have any instructions. + return true; +} + +export type TransitionStatus = FormStatus; +export const NotPendingTransition: TransitionStatus = NotPending; diff --git a/packages/react-html/src/ReactHTMLClient.js b/packages/react-html/src/ReactHTMLClient.js new file mode 100644 index 0000000000000..533ae7a3c3e7b --- /dev/null +++ b/packages/react-html/src/ReactHTMLClient.js @@ -0,0 +1,100 @@ +/** + * 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 ReactVersion from 'shared/ReactVersion'; + +import { + createRequest as createFizzRequest, + startWork as startFizzWork, + startFlowing as startFizzFlowing, + abort as abortFizz, +} from 'react-server/src/ReactFizzServer'; + +import { + createResumableState, + createRenderState, + createRootFormatContext, +} from './ReactFizzConfigHTML'; + +type MarkupOptions = { + identifierPrefix?: string, + signal?: AbortSignal, +}; + +export function renderToMarkup( + children: ReactNodeList, + options?: MarkupOptions, +): Promise { + return new Promise((resolve, reject) => { + let buffer = ''; + const fizzDestination = { + push(chunk: string | null): boolean { + if (chunk !== null) { + buffer += chunk; + } else { + // null indicates that we finished + resolve(buffer); + } + return true; + }, + destroy(error: mixed) { + 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 resumableState = createResumableState( + options ? options.identifierPrefix : undefined, + undefined, + ); + const fizzRequest = createFizzRequest( + children, + resumableState, + createRenderState( + resumableState, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + createRootFormatContext(), + Infinity, + onError, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abortFizz(fizzRequest, (signal: any).reason); + } else { + const listener = () => { + abortFizz(fizzRequest, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startFizzWork(fizzRequest); + startFizzFlowing(fizzRequest, fizzDestination); + }); +} + +export {ReactVersion as version}; diff --git a/packages/react-html/src/ReactHTMLServer.js b/packages/react-html/src/ReactHTMLServer.js index c4eebe3f51054..923881e4da755 100644 --- a/packages/react-html/src/ReactHTMLServer.js +++ b/packages/react-html/src/ReactHTMLServer.js @@ -37,7 +37,7 @@ import { createResumableState, createRenderState, createRootFormatContext, -} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; +} from './ReactFizzConfigHTML'; type ReactMarkupNodeList = // This is the intersection of ReactNodeList and ReactClientValue minus @@ -143,7 +143,14 @@ export function renderToMarkup( // $FlowFixMe: Thenables as children are supported. root, resumableState, - createRenderState(resumableState, true), + createRenderState( + resumableState, + undefined, + undefined, + undefined, + undefined, + undefined, + ), createRootFormatContext(), Infinity, onError, diff --git a/packages/react-html/src/__tests__/ReactHTMLClient-test.js b/packages/react-html/src/__tests__/ReactHTMLClient-test.js new file mode 100644 index 0000000000000..4f052288a8403 --- /dev/null +++ b/packages/react-html/src/__tests__/ReactHTMLClient-test.js @@ -0,0 +1,141 @@ +/** + * 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'; + +let React; +let ReactHTML; + +describe('ReactHTML', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactHTML = require('react-html'); + }); + + it('should be able to render a simple component', async () => { + function Component() { + return
hello world
; + } + + const html = await ReactHTML.renderToMarkup(); + expect(html).toBe('
hello world
'); + }); + + it('should error on useState', async () => { + function Component() { + const [state] = React.useState('hello'); + return
{state}
; + } + + await expect(async () => { + await ReactHTML.renderToMarkup(); + }).rejects.toThrow(); + }); + + it('should error on refs passed to host components', async () => { + function Component() { + const ref = React.createRef(); + return
; + } + + await expect(async () => { + await ReactHTML.renderToMarkup(); + }).rejects.toThrow(); + }); + + it('should error on callbacks passed to event handlers', async () => { + function Component() { + function onClick() { + // This won't be able to be called. + } + return
; + } + + await expect(async () => { + await ReactHTML.renderToMarkup(); + }).rejects.toThrow(); + }); + + it('supports the useId Hook', async () => { + function Component() { + const firstNameId = React.useId(); + const lastNameId = React.useId(); + 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(); + 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 disableClientCache + it('does NOT support cache yet because it is a client component', async () => { + let counter = 0; + const getCount = React.cache(() => { + return counter++; + }); + function Component() { + const a = getCount(); + const b = getCount(); + return ( +
+ {a} + {b} +
+ ); + } + + const html = await ReactHTML.renderToMarkup(); + expect(html).toBe('
01
'); + }); +}); diff --git a/packages/react-html/src/__tests__/ReactHTMLServer-test.js b/packages/react-html/src/__tests__/ReactHTMLServer-test.js index 503e233301c9a..ecb33c1c040d3 100644 --- a/packages/react-html/src/__tests__/ReactHTMLServer-test.js +++ b/packages/react-html/src/__tests__/ReactHTMLServer-test.js @@ -38,6 +38,44 @@ describe('ReactHTML', () => { expect(html).toBe('
hello world
'); }); + it('should error on useState', async () => { + function Component() { + const [state] = React.useState('hello'); + // We can't use JSX because that's client-JSX in our tests. + return React.createElement('div', null, state); + } + + await expect(async () => { + await ReactHTML.renderToMarkup(React.createElement(Component)); + }).rejects.toThrow(); + }); + + it('should error on refs passed to host components', async () => { + function Component() { + const ref = React.createRef(); + // We can't use JSX because that's client-JSX in our tests. + return React.createElement('div', {ref}); + } + + await expect(async () => { + await ReactHTML.renderToMarkup(React.createElement(Component)); + }).rejects.toThrow(); + }); + + it('should error on callbacks passed to event handlers', async () => { + function Component() { + function onClick() { + // This won't be able to be called. + } + // We can't use JSX because that's client-JSX in our tests. + return React.createElement('div', {onClick}); + } + + await expect(async () => { + await ReactHTML.renderToMarkup(React.createElement(Component)); + }).rejects.toThrow(); + }); + it('supports the useId Hook', async () => { function Component() { const firstNameId = React.useId(); diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.markup.js b/packages/react-reconciler/src/forks/ReactFiberConfig.markup.js new file mode 100644 index 0000000000000..9f70eeb70e5bc --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.markup.js @@ -0,0 +1,16 @@ +/** + * 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 + */ + +// Re-exported just because we always type check react-reconciler even in +// dimensions where it's not used. +export * from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; + +// eslint-disable-next-line react-internal/prod-error-codes +throw new Error('Fiber is not used in react-html'); diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 05cc1a2e1bead..f5965c4f69096 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -31,7 +31,11 @@ import { readPreviousThenable, } from './ReactFizzThenable'; -import {makeId, NotPendingTransition} from './ReactFizzConfig'; +import { + makeId, + NotPendingTransition, + supportsClientAPIs, +} from './ReactFizzConfig'; import {createFastHash} from './ReactServerStreamConfig'; import { @@ -803,29 +807,56 @@ function useMemoCache(size: number): Array { function noop(): void {} -export const HooksDispatcher: Dispatcher = { - readContext, - use, - useContext, - useMemo, - useReducer, - useRef, - useState, - useInsertionEffect: noop, - useLayoutEffect: noop, - useCallback, - // useImperativeHandle is not run in the server environment - useImperativeHandle: noop, - // Effects are not run in the server environment. - useEffect: noop, - // Debugging effect - useDebugValue: noop, - useDeferredValue, - useTransition, - useId, - // Subscriptions are not setup in a server environment. - useSyncExternalStore, -}; +function clientHookNotSupported() { + throw new Error( + 'Cannot use state or effect Hooks in renderToMarkup because ' + + 'this component will never be hydrated.', + ); +} + +export const HooksDispatcher: Dispatcher = supportsClientAPIs + ? { + readContext, + use, + useContext, + useMemo, + useReducer, + useRef, + useState, + useInsertionEffect: noop, + useLayoutEffect: noop, + useCallback, + // useImperativeHandle is not run in the server environment + useImperativeHandle: noop, + // Effects are not run in the server environment. + useEffect: noop, + // Debugging effect + useDebugValue: noop, + useDeferredValue, + useTransition, + useId, + // Subscriptions are not setup in a server environment. + useSyncExternalStore, + } + : { + readContext, + use, + useContext, + useMemo, + useReducer: clientHookNotSupported, + useRef: clientHookNotSupported, + useState: clientHookNotSupported, + useInsertionEffect: clientHookNotSupported, + useLayoutEffect: clientHookNotSupported, + useCallback, + useImperativeHandle: clientHookNotSupported, + useEffect: clientHookNotSupported, + useDebugValue: noop, + useDeferredValue: clientHookNotSupported, + useTransition: clientHookNotSupported, + useId, + useSyncExternalStore: clientHookNotSupported, + }; if (enableCache) { HooksDispatcher.useCacheRefresh = useCacheRefresh; diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js index c7964f187d42c..249292fd19519 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.custom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js @@ -37,6 +37,8 @@ export type {TransitionStatus}; export const isPrimaryRenderer = false; +export const supportsClientAPIs = true; + export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.markup.js b/packages/react-server/src/forks/ReactFizzConfig.markup.js new file mode 100644 index 0000000000000..15e35a2ef0b97 --- /dev/null +++ b/packages/react-server/src/forks/ReactFizzConfig.markup.js @@ -0,0 +1,16 @@ +/** + * 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 {Request} from 'react-server/src/ReactFizzServer'; + +export * from 'react-html/src/ReactFizzConfigHTML.js'; + +export * from 'react-client/src/ReactClientConsoleConfigPlain'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index 99591bb954ea9..15874c64e0858 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -9,15 +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 type HintCode = string; -export type HintModel = null; // eslint-disable-line no-unused-vars -export type Hints = null; +export * from '../ReactFlightServerConfigBundlerCustom'; -export function createHints(): Hints { - return null; -} +export * from '../ReactFlightServerConfigDebugNoop'; + +export type Hints = any; +export type HintCode = any; +// eslint-disable-next-line no-unused-vars +export type HintModel = any; export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any); @@ -26,65 +26,6 @@ export const supportsComponentStorage = false; 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.', - ); +export function createHints(): any { + return null; } diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.markup.js b/packages/react-server/src/forks/ReactFlightServerConfig.markup.js new file mode 100644 index 0000000000000..99591bb954ea9 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.markup.js @@ -0,0 +1,90 @@ +/** + * 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 {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; + +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); + +export const supportsComponentStorage = false; +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/packages/react-server/src/forks/ReactServerStreamConfig.markup.js b/packages/react-server/src/forks/ReactServerStreamConfig.markup.js new file mode 100644 index 0000000000000..80d5dcab2a5f2 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerStreamConfig.markup.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 'react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig'; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 9d8fb4ed3739f..46600256b0279 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -520,5 +520,8 @@ "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." + "535": "renderToMarkup should not have emitted Server References. This is a bug in React.", + "536": "Cannot pass ref in renderToMarkup because they will never be hydrated.", + "537": "Cannot pass event handlers (%s) in renderToMarkup because the HTML will never be hydrated so they can never get called.", + "538": "Cannot use state or effect Hooks in renderToMarkup because this component will never be hydrated." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 67f1c237b209f..db0a306ffcefc 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -363,7 +363,7 @@ const bundles = [ externals: [], }, - /******* React HTML *******/ + /******* React HTML RSC *******/ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RENDERER, @@ -376,6 +376,18 @@ const bundles = [ externals: ['react'], }, + /******* React HTML Client *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-html/src/ReactHTMLClient.js', + name: 'react-html', + 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 0ba6827bba7a2..cffc66d2324f3 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -100,6 +100,7 @@ const forks = Object.freeze({ entry === 'react-dom/src/ReactDOMFB.js' || entry === 'react-dom/src/ReactDOMTestingFB.js' || entry === 'react-dom/src/ReactDOMServer.js' || + entry === 'react-html/src/ReactHTMLClient.js' || entry === 'react-html/src/ReactHTMLServer.js' ) { if ( diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index a21b624150aac..d59a9bde49170 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -428,16 +428,29 @@ 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', 'react-dom/src/ReactDOMReactServer.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 'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser 'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, + { + shortName: 'markup', + entryPoints: [ + 'react-html/src/ReactHTMLClient.js', // react-html + 'react-html/src/ReactHTMLServer.js', // react-html/react-html.react-server + ], + paths: [ + 'react-dom', + 'react-dom/src/ReactDOMReactServer.js', + 'react-dom-bindings', 'react-html', 'shared/ReactDOMSharedInternals', ],