Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 64 additions & 30 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 11 additions & 2 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down
Loading