diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index fe88fe0357b..c4d47bfecc5 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1969,6 +1969,44 @@ function createModel(response: Response, model: any): any { return model; } +const mightHaveStaticConstructor = /\bclass\b.*\bstatic\b/; + +function getInferredFunctionApproximate(code: string): () => void { + let slicedCode; + if (code.startsWith('Object.defineProperty(')) { + slicedCode = code.slice('Object.defineProperty('.length); + } else if (code.startsWith('(')) { + slicedCode = code.slice(1); + } else { + slicedCode = code; + } + if (slicedCode.startsWith('async function')) { + const idx = slicedCode.indexOf('(', 14); + if (idx !== -1) { + const name = slicedCode.slice(14, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':async function(){}})')[ + name + ]; + } + } else if (slicedCode.startsWith('function')) { + const idx = slicedCode.indexOf('(', 8); + if (idx !== -1) { + const name = slicedCode.slice(8, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':function(){}})')[name]; + } + } else if (slicedCode.startsWith('class')) { + const idx = slicedCode.indexOf('{', 5); + if (idx !== -1) { + const name = slicedCode.slice(5, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[name]; + } + } + return function () {}; +} + function parseModelString( response: Response, parentObject: Object, @@ -2158,41 +2196,37 @@ function parseModelString( // This should not compile to eval() because then it has local scope access. const code = value.slice(2); try { - // eslint-disable-next-line no-eval - return (0, eval)(code); + // If this might be a class constructor with a static initializer or + // static constructor then don't eval it. It might cause unexpected + // side-effects. Instead, fallback to parsing out the function type + // and name. + if (!mightHaveStaticConstructor.test(code)) { + // eslint-disable-next-line no-eval + return (0, eval)(code); + } } catch (x) { - // We currently use this to express functions so we fail parsing it, - // let's just return a blank function as a place holder. - if (code.startsWith('(async function')) { - const idx = code.indexOf('(', 15); - if (idx !== -1) { - const name = code.slice(15, idx).trim(); - // eslint-disable-next-line no-eval - return (0, eval)( - '({' + JSON.stringify(name) + ':async function(){}})', - )[name]; - } - } else if (code.startsWith('(function')) { - const idx = code.indexOf('(', 9); - if (idx !== -1) { - const name = code.slice(9, idx).trim(); - // eslint-disable-next-line no-eval - return (0, eval)( - '({' + JSON.stringify(name) + ':function(){}})', - )[name]; - } - } else if (code.startsWith('(class')) { - const idx = code.indexOf('{', 6); + // Fallthrough to fallback case. + } + // We currently use this to express functions so we fail parsing it, + // let's just return a blank function as a place holder. + let fn; + try { + fn = getInferredFunctionApproximate(code); + if (code.startsWith('Object.defineProperty(')) { + const DESCRIPTOR = ',"name",{value:"'; + const idx = code.lastIndexOf(DESCRIPTOR); if (idx !== -1) { - const name = code.slice(6, idx).trim(); - // eslint-disable-next-line no-eval - return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[ - name - ]; + const name = JSON.parse( + code.slice(idx + DESCRIPTOR.length - 1, code.length - 2), + ); + // $FlowFixMe[cannot-write] + Object.defineProperty(fn, 'name', {value: name}); } } - return function () {}; + } catch (_) { + fn = function () {}; } + return fn; } // Fallthrough } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 150997c1c37..9a60c3bd66b 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -3239,6 +3239,8 @@ describe('ReactFlight', () => { } Object.defineProperty(MyClass.prototype, 'y', {enumerable: true}); + Object.defineProperty(MyClass, 'name', {value: 'MyClassName'}); + function ServerComponent() { console.log('hi', { prop: 123, @@ -3341,6 +3343,7 @@ describe('ReactFlight', () => { const instance = mockConsoleLog.mock.calls[0][1].instance; expect(typeof Class).toBe('function'); expect(Class.prototype.constructor).toBe(Class); + expect(Class.name).toBe('MyClassName'); expect(instance instanceof Class).toBe(true); expect(Object.getPrototypeOf(instance)).toBe(Class.prototype); expect(instance.x).toBe(1); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 1a5cee7ba72..19a49dcd32b 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -4848,9 +4848,18 @@ function renderDebugModel( return existingReference; } + // $FlowFixMe[method-unbinding] + const functionBody: string = Function.prototype.toString.call(value); + + const name = value.name; const serializedValue = serializeEval( - // $FlowFixMe[method-unbinding] - '(' + Function.prototype.toString.call(value) + ')', + typeof name === 'string' + ? 'Object.defineProperty(' + + functionBody + + ',"name",{value:' + + JSON.stringify(name) + + '})' + : '(' + functionBody + ')', ); request.pendingDebugChunks++; const id = request.nextChunkId++;