@samitouri / QOS-React-1 / commits / 3958d5d84b

[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++;