mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Flight] Support classes in renderDebugModel (#33590)
This adds better support for serializing class instances as Debug
values.
It adds a new marker on the object `{ "": "$P...", ... }` which
indicates which constructor's prototype to use for this object's
prototype. It doesn't encode arbitrary prototypes and it doesn't encode
any of the properties on the prototype. It might get some of the
properties from the prototype by virtue of `toString` on a `class`
constructor will include the whole class's body.
This will ensure that the instance gets the right name in logs.
Additionally, this now also invokes getters if they're enumerable on the
prototype. This lets us reify values that can only be read from native
classes.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
This commit is contained in:
co-authored by
Hendrik Liebau
parent
1d1b26c701
commit
18ee505e77
+56
-1
@@ -1430,6 +1430,17 @@ function createFormData(
|
||||
return formData;
|
||||
}
|
||||
|
||||
function applyConstructor(
|
||||
response: Response,
|
||||
model: Function,
|
||||
parentObject: Object,
|
||||
key: string,
|
||||
): void {
|
||||
Object.setPrototypeOf(parentObject, model.prototype);
|
||||
// Delete the property. It was just a placeholder.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
|
||||
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
|
||||
return model[Symbol.iterator]();
|
||||
@@ -1606,16 +1617,60 @@ function parseModelString(
|
||||
// BigInt
|
||||
return BigInt(value.slice(2));
|
||||
}
|
||||
case 'P': {
|
||||
if (__DEV__) {
|
||||
// In DEV mode we allow debug objects to specify themselves as instances of
|
||||
// another constructor.
|
||||
const ref = value.slice(2);
|
||||
return getOutlinedModel(
|
||||
response,
|
||||
ref,
|
||||
parentObject,
|
||||
key,
|
||||
applyConstructor,
|
||||
);
|
||||
}
|
||||
//Fallthrough
|
||||
}
|
||||
case 'E': {
|
||||
if (__DEV__) {
|
||||
// In DEV mode we allow indirect eval to produce functions for logging.
|
||||
// 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)(value.slice(2));
|
||||
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);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(6, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
|
||||
name
|
||||
];
|
||||
}
|
||||
}
|
||||
return function () {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3208,6 +3208,20 @@ describe('ReactFlight', () => {
|
||||
return 'hello';
|
||||
}
|
||||
|
||||
class MyClass {
|
||||
constructor() {
|
||||
this.x = 1;
|
||||
}
|
||||
method() {}
|
||||
get y() {
|
||||
return this.x + 1;
|
||||
}
|
||||
get z() {
|
||||
return this.x + 5;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
|
||||
|
||||
function ServerComponent() {
|
||||
console.log('hi', {
|
||||
prop: 123,
|
||||
@@ -3215,6 +3229,8 @@ describe('ReactFlight', () => {
|
||||
map: new Map([['foo', foo]]),
|
||||
promise: Promise.resolve('yo'),
|
||||
infinitePromise: new Promise(() => {}),
|
||||
Class: MyClass,
|
||||
instance: new MyClass(),
|
||||
});
|
||||
throw new Error('err');
|
||||
}
|
||||
@@ -3304,6 +3320,19 @@ describe('ReactFlight', () => {
|
||||
// This should not reject upon aborting the stream.
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
const Class = mockConsoleLog.mock.calls[0][1].Class;
|
||||
const instance = mockConsoleLog.mock.calls[0][1].instance;
|
||||
expect(typeof Class).toBe('function');
|
||||
expect(Class.prototype.constructor).toBe(Class);
|
||||
expect(instance instanceof Class).toBe(true);
|
||||
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
|
||||
expect(instance.x).toBe(1);
|
||||
expect(instance.hasOwnProperty('y')).toBe(true);
|
||||
expect(instance.y).toBe(2); // Enumerable getter was reified
|
||||
expect(instance.hasOwnProperty('z')).toBe(false);
|
||||
expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class
|
||||
expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString()
|
||||
|
||||
expect(ownerStacks).toEqual(['\n in App (at **)']);
|
||||
});
|
||||
|
||||
|
||||
+42
@@ -139,6 +139,7 @@ import {
|
||||
|
||||
import {
|
||||
describeObjectForErrorMessage,
|
||||
isGetter,
|
||||
isSimpleObject,
|
||||
jsxPropsParents,
|
||||
jsxChildrenParents,
|
||||
@@ -148,6 +149,7 @@ import {
|
||||
import ReactSharedInternals from './ReactSharedInternalsServer';
|
||||
import isArray from 'shared/isArray';
|
||||
import getPrototypeOf from 'shared/getPrototypeOf';
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import binaryToComparableString from 'shared/binaryToComparableString';
|
||||
|
||||
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
|
||||
@@ -3906,6 +3908,8 @@ function serializeEval(source: string): string {
|
||||
return '$E' + source;
|
||||
}
|
||||
|
||||
const CONSTRUCTOR_MARKER: symbol = __DEV__ ? Symbol() : (null: any);
|
||||
|
||||
let debugModelRoot: mixed = null;
|
||||
let debugNoOutline: mixed = null;
|
||||
// This is a forked version of renderModel which should never error, never suspend and is limited
|
||||
@@ -3941,6 +3945,16 @@ function renderDebugModel(
|
||||
(value: any),
|
||||
);
|
||||
}
|
||||
if (value.$$typeof === CONSTRUCTOR_MARKER) {
|
||||
const constructor: Function = (value: any).constructor;
|
||||
let ref = request.writtenDebugObjects.get(constructor);
|
||||
if (ref === undefined) {
|
||||
const id = outlineDebugModel(request, counter, constructor);
|
||||
ref = serializeByValueID(id);
|
||||
}
|
||||
return '$P' + ref.slice(1);
|
||||
}
|
||||
|
||||
if (request.temporaryReferences !== undefined) {
|
||||
const tempRef = resolveTemporaryReference(
|
||||
request.temporaryReferences,
|
||||
@@ -4141,6 +4155,34 @@ function renderDebugModel(
|
||||
return Array.from((value: any));
|
||||
}
|
||||
|
||||
const proto = getPrototypeOf(value);
|
||||
if (proto !== ObjectPrototype && proto !== null) {
|
||||
const object: Object = value;
|
||||
const instanceDescription: Object = Object.create(null);
|
||||
for (const propName in object) {
|
||||
if (hasOwnProperty.call(value, propName) || isGetter(proto, propName)) {
|
||||
// We intentionally invoke getters on the prototype to read any enumerable getters.
|
||||
instanceDescription[propName] = object[propName];
|
||||
}
|
||||
}
|
||||
const constructor = proto.constructor;
|
||||
if (
|
||||
typeof constructor === 'function' &&
|
||||
constructor.prototype === proto
|
||||
) {
|
||||
// This is a simple class shape.
|
||||
if (hasOwnProperty.call(object, '') || isGetter(proto, '')) {
|
||||
// This object already has an empty property name. Skip encoding its prototype.
|
||||
} else {
|
||||
instanceDescription[''] = {
|
||||
$$typeof: CONSTRUCTOR_MARKER,
|
||||
constructor: constructor,
|
||||
};
|
||||
}
|
||||
}
|
||||
return instanceDescription;
|
||||
}
|
||||
|
||||
// $FlowFixMe[incompatible-return]
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,18 @@ function isObjectPrototype(object: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isGetter(object: any, name: string): boolean {
|
||||
const ObjectPrototype = Object.prototype;
|
||||
if (object === ObjectPrototype || object === null) {
|
||||
return false;
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(object, name);
|
||||
if (descriptor === undefined) {
|
||||
return isGetter(getPrototypeOf(object), name);
|
||||
}
|
||||
return typeof descriptor.get === 'function';
|
||||
}
|
||||
|
||||
export function isSimpleObject(object: any): boolean {
|
||||
if (!isObjectPrototype(getPrototypeOf(object))) {
|
||||
return false;
|
||||
@@ -80,9 +92,8 @@ export function isSimpleObject(object: any): boolean {
|
||||
export function objectName(object: mixed): string {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const name = Object.prototype.toString.call(object);
|
||||
return name.replace(/^\[object (.*)\]$/, function (m, p0) {
|
||||
return p0;
|
||||
});
|
||||
// Extract 'Object' from '[object Object]':
|
||||
return name.slice(8, name.length - 1);
|
||||
}
|
||||
|
||||
function describeKeyForErrorMessage(key: string): string {
|
||||
|
||||
Reference in New Issue
Block a user