[Flight] Warn once if eval is disabled in dev environment (#35661)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-02-02 12:56:14 +01:00
committed by GitHub
parent 64b4605cb8
commit ed4bd540ca
26 changed files with 289 additions and 11 deletions

View File

@@ -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.',
);
}
}

View File

@@ -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.',
);
}
}

View File

@@ -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.',
);
}
}

View File

@@ -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(

View File

@@ -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<Reference, string> = 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,

View File

@@ -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.',

View File

@@ -53,3 +53,6 @@ export const bindToConsole = $$$config.bindToConsole;
export const rendererVersion = $$$config.rendererVersion;
export const rendererPackageName = $$$config.rendererPackageName;
export const checkEvalAvailabilityOnceDev =
$$$config.checkEvalAvailabilityOnceDev;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -53,6 +53,7 @@ const {createResponse, createStreamState, processBinaryChunk, getRoot, close} =
[console].concat(args),
);
},
checkEvalAvailabilityOnceDev,
});
type ReadOptions = {|
@@ -87,4 +88,28 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
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};

View File

@@ -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;
}
});
});

View File

@@ -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;
}
});
});

View File

@@ -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;
}
});
});