Skip to content

Commit 3958d5d

Browse files
authored
[Flight] Copy the name field of a serialized function debug value (facebook#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.
1 parent 738aebd commit 3958d5d

File tree

3 files changed

+78
-32
lines changed

3 files changed

+78
-32
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,6 +1969,44 @@ function createModel(response: Response, model: any): any {
19691969
return model;
19701970
}
19711971

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+
19722010
function parseModelString(
19732011
response: Response,
19742012
parentObject: Object,
@@ -2158,41 +2196,37 @@ function parseModelString(
21582196
// This should not compile to eval() because then it has local scope access.
21592197
const code = value.slice(2);
21602198
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+
}
21632207
} 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);
21862218
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});
21922224
}
21932225
}
2194-
return function () {};
2226+
} catch (_) {
2227+
fn = function () {};
21952228
}
2229+
return fn;
21962230
}
21972231
// Fallthrough
21982232
}

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3239,6 +3239,8 @@ describe('ReactFlight', () => {
32393239
}
32403240
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
32413241

3242+
Object.defineProperty(MyClass, 'name', {value: 'MyClassName'});
3243+
32423244
function ServerComponent() {
32433245
console.log('hi', {
32443246
prop: 123,
@@ -3341,6 +3343,7 @@ describe('ReactFlight', () => {
33413343
const instance = mockConsoleLog.mock.calls[0][1].instance;
33423344
expect(typeof Class).toBe('function');
33433345
expect(Class.prototype.constructor).toBe(Class);
3346+
expect(Class.name).toBe('MyClassName');
33443347
expect(instance instanceof Class).toBe(true);
33453348
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
33463349
expect(instance.x).toBe(1);

packages/react-server/src/ReactFlightServer.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4848,9 +4848,18 @@ function renderDebugModel(
48484848
return existingReference;
48494849
}
48504850

4851+
// $FlowFixMe[method-unbinding]
4852+
const functionBody: string = Function.prototype.toString.call(value);
4853+
4854+
const name = value.name;
48514855
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 + ')',
48544863
);
48554864
request.pendingDebugChunks++;
48564865
const id = request.nextChunkId++;

0 commit comments

Comments
 (0)