[Flight] Copy the name field of a serialized function debug value (#34085)
This ensures that if the name is set manually after the declaration, then we get that name when we log the value. For example Node.js `Response` is declared as `_Response` and then later assigned a new name. We should probably really serialize all static enumerable properties but "name" is non-enumerable so it's still a special case.
Sebastian Markbåge committed
Aug 7, 2025 at 10:55 UTC
3958d5d84b3d3e6ae5c1caef9c8cf0dc58e862e6
3 files changed
+78
-32
packages/react-client/src/ReactFlightClient.js
+64
-30
@@ -1969,6 +1969,44 @@ function createModel(response: Response, model: any): any {
1969
return model;
1970
}
1971
1972
+const mightHaveStaticConstructor = /\bclass\b.*\bstatic\b/;
1973
+
1974
+function getInferredFunctionApproximate(code: string): () => void {
1975
+ let slicedCode;
1976
+ if (code.startsWith('Object.defineProperty(')) {
1977
+ slicedCode = code.slice('Object.defineProperty('.length);
1978
+ } else if (code.startsWith('(')) {
1979
+ slicedCode = code.slice(1);
1980
+ } else {
1981
+ slicedCode = code;
1982
+ }
1983
+ if (slicedCode.startsWith('async function')) {
1984
+ const idx = slicedCode.indexOf('(', 14);
1985
+ if (idx !== -1) {
1986
+ const name = slicedCode.slice(14, idx).trim();
1987
+ // eslint-disable-next-line no-eval
1988
+ return (0, eval)('({' + JSON.stringify(name) + ':async function(){}})')[
1989
+ name
1990
+ ];
1991
+ }
1992
+ } else if (slicedCode.startsWith('function')) {
1993
+ const idx = slicedCode.indexOf('(', 8);
1994
+ if (idx !== -1) {
1995
+ const name = slicedCode.slice(8, idx).trim();
1996
+ // eslint-disable-next-line no-eval
1997
+ return (0, eval)('({' + JSON.stringify(name) + ':function(){}})')[name];
1998
+ }
1999
+ } else if (slicedCode.startsWith('class')) {
2000
+ const idx = slicedCode.indexOf('{', 5);
2001
+ if (idx !== -1) {
2002
+ const name = slicedCode.slice(5, idx).trim();
2003
+ // eslint-disable-next-line no-eval
2004
+ return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[name];
2005
+ }
2006
+ }
2007
+ return function () {};
2008
+}
2009
+
2010
function parseModelString(
2011
response: Response,
2012
parentObject: Object,
@@ -2158,41 +2196,37 @@ function parseModelString(
2196
// This should not compile to eval() because then it has local scope access.
2197
const code = value.slice(2);
2198
try {
2161
- // eslint-disable-next-line no-eval
2162
- return (0, eval)(code);
2199
+ // If this might be a class constructor with a static initializer or
2200
+ // static constructor then don't eval it. It might cause unexpected
2201
+ // side-effects. Instead, fallback to parsing out the function type
2202
+ // and name.
2203
+ if (!mightHaveStaticConstructor.test(code)) {
2204
+ // eslint-disable-next-line no-eval
2205
+ return (0, eval)(code);
2206
+ }
2207
} catch (x) {
2164
- // We currently use this to express functions so we fail parsing it,
2165
- // let's just return a blank function as a place holder.
2166
- if (code.startsWith('(async function')) {
2167
- const idx = code.indexOf('(', 15);
2168
- if (idx !== -1) {
2169
- const name = code.slice(15, idx).trim();
2170
- // eslint-disable-next-line no-eval
2171
- return (0, eval)(
2172
- '({' + JSON.stringify(name) + ':async function(){}})',
2173
- )[name];
2174
- }
2175
- } else if (code.startsWith('(function')) {
2176
- const idx = code.indexOf('(', 9);
2177
- if (idx !== -1) {
2178
- const name = code.slice(9, idx).trim();
2179
- // eslint-disable-next-line no-eval
2180
- return (0, eval)(
2181
- '({' + JSON.stringify(name) + ':function(){}})',
2182
- )[name];
2183
- }
2184
- } else if (code.startsWith('(class')) {
2185
- const idx = code.indexOf('{', 6);
2208
+ // Fallthrough to fallback case.
2209
+ }
2210
+ // We currently use this to express functions so we fail parsing it,
2211
+ // let's just return a blank function as a place holder.
2212
+ let fn;
2213
+ try {
2214
+ fn = getInferredFunctionApproximate(code);
2215
+ if (code.startsWith('Object.defineProperty(')) {
2216
+ const DESCRIPTOR = ',"name",{value:"';
2217
+ const idx = code.lastIndexOf(DESCRIPTOR);
2218
if (idx !== -1) {
2187
- const name = code.slice(6, idx).trim();
2188
- // eslint-disable-next-line no-eval
2189
- return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
2190
- name
2191
- ];
2219
+ const name = JSON.parse(
2220
+ code.slice(idx + DESCRIPTOR.length - 1, code.length - 2),
2221
+ );
2222
+ // $FlowFixMe[cannot-write]
2223
+ Object.defineProperty(fn, 'name', {value: name});
2224
}
2225
}
2194
- return function () {};
2226
+ } catch (_) {
2227
+ fn = function () {};
2228
}
2229
+ return fn;
2230
}
2231
// Fallthrough
2232
}
packages/react-client/src/__tests__/ReactFlight-test.js
+3
@@ -3239,6 +3239,8 @@ describe('ReactFlight', () => {
3239
}
3240
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
3241
3242
+ Object.defineProperty(MyClass, 'name', {value: 'MyClassName'});
3243
+
3244
function ServerComponent() {
3245
console.log('hi', {
3246
prop: 123,
@@ -3341,6 +3343,7 @@ describe('ReactFlight', () => {
3343
const instance = mockConsoleLog.mock.calls[0][1].instance;
3344
expect(typeof Class).toBe('function');
3345
expect(Class.prototype.constructor).toBe(Class);
3346
+ expect(Class.name).toBe('MyClassName');
3347
expect(instance instanceof Class).toBe(true);
3348
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
3349
expect(instance.x).toBe(1);
packages/react-server/src/ReactFlightServer.js
+11
-2
@@ -4848,9 +4848,18 @@ function renderDebugModel(
4848
return existingReference;
4849
}
4850
4851
+ // $FlowFixMe[method-unbinding]
4852
+ const functionBody: string = Function.prototype.toString.call(value);
4853
+
4854
+ const name = value.name;
4855
const serializedValue = serializeEval(
4852
- // $FlowFixMe[method-unbinding]
4853
- '(' + Function.prototype.toString.call(value) + ')',
4856
+ typeof name === 'string'
4857
+ ? 'Object.defineProperty(' +
4858
+ functionBody +
4859
+ ',"name",{value:' +
4860
+ JSON.stringify(name) +
4861
+ '})'
4862
+ : '(' + functionBody + ')',
4863
);
4864
request.pendingDebugChunks++;
4865
const id = request.nextChunkId++;