From ed4bd540ca67f1c4db65f009ad726a5ae1a0af01 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 2 Feb 2026 12:56:14 +0100 Subject: [PATCH] [Flight] Warn once if `eval` is disabled in dev environment (#35661) --- .../src/ReactClientDebugConfigBrowser.js | 36 +++++++++++ .../src/ReactClientDebugConfigNode.js | 36 +++++++++++ .../src/ReactClientDebugConfigPlain.js | 34 +++++++++++ .../react-client/src/ReactFlightClient.js | 9 +++ .../src/ReactFlightReplyClient.js | 13 +++- .../src/__tests__/ReactFlight-test.js | 6 ++ .../forks/ReactFlightClientConfig.custom.js | 3 + ...ReactFlightClientConfig.dom-browser-esm.js | 1 + ...ctFlightClientConfig.dom-browser-parcel.js | 1 + ...lightClientConfig.dom-browser-turbopack.js | 1 + .../ReactFlightClientConfig.dom-browser.js | 1 + .../forks/ReactFlightClientConfig.dom-bun.js | 1 + ...ReactFlightClientConfig.dom-edge-parcel.js | 1 + ...ctFlightClientConfig.dom-edge-turbopack.js | 1 + ...eactFlightClientConfig.dom-edge-webpack.js | 1 + .../ReactFlightClientConfig.dom-legacy.js | 1 + .../ReactFlightClientConfig.dom-node-esm.js | 1 + ...ReactFlightClientConfig.dom-node-parcel.js | 1 + ...ctFlightClientConfig.dom-node-turbopack.js | 1 + ...ctFlightClientConfig.dom-node-unbundled.js | 1 + .../forks/ReactFlightClientConfig.dom-node.js | 1 + .../forks/ReactFlightClientConfig.markup.js | 1 + .../src/ReactNoopFlightClient.js | 25 ++++++++ .../ReactFlightTurbopackDOMReply-test.js | 32 ++++++++++ .../src/__tests__/ReactFlightDOMNode-test.js | 60 +++++++++++++++---- .../__tests__/ReactFlightDOMReplyEdge-test.js | 31 ++++++++++ 26 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 packages/react-client/src/ReactClientDebugConfigBrowser.js create mode 100644 packages/react-client/src/ReactClientDebugConfigNode.js create mode 100644 packages/react-client/src/ReactClientDebugConfigPlain.js diff --git a/packages/react-client/src/ReactClientDebugConfigBrowser.js b/packages/react-client/src/ReactClientDebugConfigBrowser.js new file mode 100644 index 0000000000..1bc8ad0a16 --- /dev/null +++ b/packages/react-client/src/ReactClientDebugConfigBrowser.js @@ -0,0 +1,36 @@ +/** + * 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 + */ + +let hasConfirmedEval = false; +export function checkEvalAvailabilityOnceDev(): void { + if (__DEV__) { + if (!hasConfirmedEval) { + hasConfirmedEval = true; + try { + // eslint-disable-next-line no-eval + (0, eval)('null'); + } catch { + console.error( + 'eval() is not supported in this environment. ' + + 'If this page was served with a `Content-Security-Policy` header, ' + + 'make sure that `unsafe-eval` is included. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ); + } + } + } else { + // These errors should never make it into a build so we don't need to encode them in codes.json + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'checkEvalAvailabilityOnceDev should never be called in production mode. This is a bug in React.', + ); + } +} diff --git a/packages/react-client/src/ReactClientDebugConfigNode.js b/packages/react-client/src/ReactClientDebugConfigNode.js new file mode 100644 index 0000000000..2579ec1cc1 --- /dev/null +++ b/packages/react-client/src/ReactClientDebugConfigNode.js @@ -0,0 +1,36 @@ +/** + * 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 + */ + +let hasConfirmedEval = false; +export function checkEvalAvailabilityOnceDev(): void { + if (__DEV__) { + if (!hasConfirmedEval) { + hasConfirmedEval = true; + try { + // eslint-disable-next-line no-eval + (0, eval)('null'); + } catch { + console.error( + 'eval() is not supported in this environment. ' + + 'This can happen if you started the Node.js process with --disallow-code-generation-from-strings, ' + + 'or if `eval` was patched by other means. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ); + } + } + } else { + // These errors should never make it into a build so we don't need to encode them in codes.json + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'checkEvalAvailabilityOnceDev should never be called in production mode. This is a bug in React.', + ); + } +} diff --git a/packages/react-client/src/ReactClientDebugConfigPlain.js b/packages/react-client/src/ReactClientDebugConfigPlain.js new file mode 100644 index 0000000000..c5e3b002c2 --- /dev/null +++ b/packages/react-client/src/ReactClientDebugConfigPlain.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +let hasConfirmedEval = false; +export function checkEvalAvailabilityOnceDev(): void { + if (__DEV__) { + if (!hasConfirmedEval) { + hasConfirmedEval = true; + try { + // eslint-disable-next-line no-eval + (0, eval)('null'); + } catch { + console.error( + 'eval() is not supported in this environment. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ); + } + } + } else { + // These errors should never make it into a build so we don't need to encode them in codes.json + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'checkEvalAvailabilityOnceDev should never be called in production mode. This is a bug in React.', + ); + } +} diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 4dc316de36..12c9528b8d 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -61,6 +61,7 @@ import { bindToConsole, rendererVersion, rendererPackageName, + checkEvalAvailabilityOnceDev, } from './ReactFlightClientConfig'; import { @@ -2768,6 +2769,14 @@ export function createResponse( debugEndTime: void | number, // DEV-only debugChannel: void | DebugChannel, // DEV-only ): WeakResponse { + if (__DEV__) { + // We use eval to create fake function stacks which includes Component stacks. + // A warning would be noise if you used Flight without Components and don't encounter + // errors. We're warning eagerly so that you configure your environment accordingly + // before you encounter an error. + checkEvalAvailabilityOnceDev(); + } + return getWeakResponse( // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors new ResponseInstance( diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 9a1b665186..0661f78246 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -39,7 +39,10 @@ import getPrototypeOf from 'shared/getPrototypeOf'; const ObjectPrototype = Object.prototype; -import {usedWithSSR} from './ReactFlightClientConfig'; +import { + usedWithSSR, + checkEvalAvailabilityOnceDev, +} from './ReactFlightClientConfig'; type ReactJSONValue = | string @@ -190,6 +193,14 @@ export function processReply( const writtenObjects: WeakMap = new WeakMap(); let modelRoot: null | ReactServerValue = root; + if (__DEV__) { + // We use eval to create fake function stacks which includes Component stacks. + // A warning would be noise if you used Flight without Components and don't encounter + // errors. We're warning eagerly so that you configure your environment accordingly + // before you encounter an error. + checkEvalAvailabilityOnceDev(); + } + function serializeTypedArray( tag: string, typedArray: $ArrayBufferView, diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index f62ec32150..0c482f72cd 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -3715,6 +3715,12 @@ describe('ReactFlight', () => { '\n in b (at **)' + '\n in a (at **)', ); + assertConsoleErrorDev([ + 'eval() is not supported in this environment. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ]); } else { expect(receivedError.message).toEqual( 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.', diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index a6c0c933d2..319f28b778 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -53,3 +53,6 @@ export const bindToConsole = $$$config.bindToConsole; export const rendererVersion = $$$config.rendererVersion; export const rendererPackageName = $$$config.rendererPackageName; + +export const checkEvalAvailabilityOnceDev = + $$$config.checkEvalAvailabilityOnceDev; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js index dbc89a2677..a428cb065d 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-esm'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientDebugConfigBrowser'; export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigBundlerESM'; export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigTargetESMBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js index 89b834784f..fc7ccce22c 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-parcel'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientDebugConfigBrowser'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js index 6a071981be..252cb61880 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-turbopack'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientDebugConfigBrowser'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackBrowser'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackBrowser'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js index 73d27adefa..1d94b14f8d 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-webpack'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientDebugConfigBrowser'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackBrowser'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 24caf0df88..07b49a2de5 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-bun'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigPlain'; +export * from 'react-client/src/ReactClientDebugConfigPlain'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export opaque type ModuleLoading = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js index 626f26903e..7b698b85c7 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-parcel'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigPlain'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js index fbdb9fc683..95a96d1edc 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-turbopack'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigPlain'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js index f328a3e2ed..519eb9b647 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-webpack'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigPlain'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 0f7381fc5a..6a3a2c43d9 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'not-used'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientDebugConfigBrowser'; export type Response = any; export opaque type ModuleLoading = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js index 8cb512ea44..9cb3164704 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-esm'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigNode'; export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigBundlerESM'; export * from 'react-server-dom-esm/src/client/ReactFlightClientConfigTargetESMServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js index 466d21164c..dcb5af3435 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-parcel'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigNode'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js index ec97d45077..094658ecc9 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-turbopack'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigNode'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js index c95a3ba07f..838a187a7f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js @@ -12,6 +12,7 @@ export const rendererPackageName = 'react-server-dom-unbundled'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigNode'; export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode'; export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js index 9840d5bc91..30878a7e58 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js @@ -11,6 +11,7 @@ export const rendererPackageName = 'react-server-dom-webpack'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientDebugConfigNode'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.markup.js b/packages/react-client/src/forks/ReactFlightClientConfig.markup.js index 52f96ef9cb..1c5eae7eba 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.markup.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.markup.js @@ -14,6 +14,7 @@ import type {Thenable} from 'shared/ReactTypes'; export * from 'react-markup/src/ReactMarkupLegacyClientStreamConfig.js'; export * from 'react-client/src/ReactClientConsoleConfigPlain'; +export * from 'react-client/src/ReactClientDebugConfigPlain'; export type ModuleLoading = null; export type ServerConsumerModuleMap = null; diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index fb6259f1a6..45edbd6f00 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -53,6 +53,7 @@ const {createResponse, createStreamState, processBinaryChunk, getRoot, close} = [console].concat(args), ); }, + checkEvalAvailabilityOnceDev, }); type ReadOptions = {| @@ -87,4 +88,28 @@ function read(source: Source, options: ReadOptions): Thenable { return getRoot(response); } +let hasConfirmedEval = false; +function checkEvalAvailabilityOnceDev(): void { + if (__DEV__) { + if (!hasConfirmedEval) { + hasConfirmedEval = true; + try { + // eslint-disable-next-line no-eval + (0, eval)('null'); + } catch { + console.error( + 'eval() is not supported in this environment. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ); + } + } + } else { + throw new Error( + 'checkEvalAvailabilityOnceDev should never be called in production mode. This is a bug in React.', + ); + } +} + export {read}; diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js index 04abb546df..9e6cb3c780 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js @@ -18,6 +18,7 @@ global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; // let serverExports; +let assertConsoleErrorDev; let turbopackServerMap; let ReactServerDOMServer; let ReactServerDOMClient; @@ -41,6 +42,9 @@ describe('ReactFlightTurbopackDOMReply', () => { ReactServerDOMServer = require('react-server-dom-turbopack/server.browser'); jest.resetModules(); ReactServerDOMClient = require('react-server-dom-turbopack/client'); + + const InternalTestUtils = require('internal-test-utils'); + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); it('can encode a reply', async () => { @@ -52,4 +56,32 @@ describe('ReactFlightTurbopackDOMReply', () => { expect(decoded).toEqual({some: 'object'}); }); + + it('warns with a tailored message if eval is not available in dev', async () => { + // eslint-disable-next-line no-eval + const previousEval = globalThis.eval.bind(globalThis); + // eslint-disable-next-line no-eval + globalThis.eval = () => { + throw new Error('eval is disabled'); + }; + + try { + const body = await ReactServerDOMClient.encodeReply({some: 'object'}); + assertConsoleErrorDev([ + 'eval() is not supported in this environment. ' + + 'If this page was served with a `Content-Security-Policy` header, ' + + 'make sure that `unsafe-eval` is included. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ]); + + await ReactServerDOMServer.decodeReply(body, turbopackServerMap); + + assertConsoleErrorDev([]); + } finally { + // eslint-disable-next-line no-eval + globalThis.eval = previousEval; + } + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 3fb34e0ba4..bd2f96609a 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -28,6 +28,7 @@ let ReactServerDOMStaticServer; let ReactServerDOMClient; let Stream; let use; +let assertConsoleErrorDev; let serverAct; // We test pass-through without encoding strings but it should work without it too. @@ -73,6 +74,9 @@ describe('ReactFlightDOMNode', () => { ReactServerDOMClient = require('react-server-dom-webpack/client'); Stream = require('stream'); use = React.use; + + const InternalTestUtils = require('internal-test-utils'); + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function filterStackFrame(filename, functionName) { @@ -955,10 +959,10 @@ describe('ReactFlightDOMNode', () => { // The concrete location may change as this test is updated. // Just make sure they still point at React.use(p2) (gate(flags => flags.enableAsyncDebugInfo) - ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)' + ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:817:7)' : '') + - '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)' + - '\n at App (file://./ReactFlightDOMNode-test.js:852:25)', + '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:839:26)' + + '\n at App (file://./ReactFlightDOMNode-test.js:856:25)', ); } else { expect(ownerStack).toBeNull(); @@ -1545,12 +1549,12 @@ describe('ReactFlightDOMNode', () => { '\n' + ' in Dynamic' + (gate(flags => flags.enableAsyncDebugInfo) - ? ' (file://ReactFlightDOMNode-test.js:1419:27)\n' + ? ' (file://ReactFlightDOMNode-test.js:1423:27)\n' : '\n') + ' in body\n' + ' in html\n' + - ' in App (file://ReactFlightDOMNode-test.js:1432:25)\n' + - ' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)', + ' in App (file://ReactFlightDOMNode-test.js:1436:25)\n' + + ' in ClientRoot (ReactFlightDOMNode-test.js:1511:16)', ); } else { expect( @@ -1559,7 +1563,7 @@ describe('ReactFlightDOMNode', () => { '\n' + ' in body\n' + ' in html\n' + - ' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)', + ' in ClientRoot (ReactFlightDOMNode-test.js:1511:16)', ); } @@ -1569,8 +1573,8 @@ describe('ReactFlightDOMNode', () => { normalizeCodeLocInfo(ownerStack, {preserveLocation: true}), ).toBe( '\n' + - ' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n' + - ' in App (file://ReactFlightDOMNode-test.js:1432:25)', + ' in Dynamic (file://ReactFlightDOMNode-test.js:1423:27)\n' + + ' in App (file://ReactFlightDOMNode-test.js:1436:25)', ); } else { expect( @@ -1578,7 +1582,7 @@ describe('ReactFlightDOMNode', () => { ).toBe( '' + '\n' + - ' in App (file://ReactFlightDOMNode-test.js:1432:25)', + ' in App (file://ReactFlightDOMNode-test.js:1436:25)', ); } } else { @@ -1586,4 +1590,40 @@ describe('ReactFlightDOMNode', () => { } }); }); + + it('warns with a tailored message if eval is not available in dev', async () => { + // eslint-disable-next-line no-eval + const previousEval = globalThis.eval.bind(globalThis); + // eslint-disable-next-line no-eval + globalThis.eval = () => { + throw new Error('eval is disabled'); + }; + + try { + const readable = await serverAct(() => + ReactServerDOMServer.renderToReadableStream({}, webpackMap), + ); + + assertConsoleErrorDev([]); + + await ReactServerDOMClient.createFromReadableStream(readable, { + serverConsumerManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); + + assertConsoleErrorDev([ + 'eval() is not supported in this environment. ' + + 'This can happen if you started the Node.js process with --disallow-code-generation-from-strings, ' + + 'or if `eval` was patched by other means. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ]); + } finally { + // eslint-disable-next-line no-eval + globalThis.eval = previousEval; + } + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index ac5aff0d87..476d3d5b66 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -10,6 +10,7 @@ 'use strict'; +let assertConsoleErrorDev; let serverExports; let webpackServerMap; let ReactServerDOMServer; @@ -29,6 +30,9 @@ describe('ReactFlightDOMReplyEdge', () => { ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); jest.resetModules(); ReactServerDOMClient = require('react-server-dom-webpack/client.edge'); + + const InternalTestUtils = require('internal-test-utils'); + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); it('can encode a reply', async () => { @@ -373,4 +377,31 @@ describe('ReactFlightDOMReplyEdge', () => { expect(replyResult.method).toBe(greet); expect(replyResult.boundMethod()).toBe('hi, there'); }); + + it('warns with a tailored message if eval is not available in dev', async () => { + // eslint-disable-next-line no-eval + const previousEval = globalThis.eval.bind(globalThis); + // eslint-disable-next-line no-eval + globalThis.eval = () => { + throw new Error('eval is disabled'); + }; + + try { + const body = await ReactServerDOMClient.encodeReply({some: 'object'}); + + assertConsoleErrorDev([ + 'eval() is not supported in this environment. ' + + 'React requires eval() in development mode for various debugging features ' + + 'like reconstructing callstacks from a different environment.\n' + + 'React will never use eval() in production mode', + ]); + + await ReactServerDOMServer.decodeReply(body, webpackServerMap); + + assertConsoleErrorDev([]); + } finally { + // eslint-disable-next-line no-eval + globalThis.eval = previousEval; + } + }); });