mirror of
https://github.com/facebookarchive/prepack.git
synced 2024-08-17 10:30:32 +03:00
Add shape model functionality to React components (#2320)
Summary: Release notes: React components can have their props modelled via `__optimizeReactComponentTree` This extends the current shape modelling functionality added for InstantRender (great work by hotsnr ) and provides the same powers to the React compiler. An example of usage: ```js var React = require("React"); function App(props) { return <div> <h1>{props.header.toString()}</h1> <ul> { props.items && props.items.map(item => <li key={item.id}>{item.title.toString()}</li> ) } </ul> </div>; } App.getTrials = function(renderer, Root) { var items = [{ id: 0, title: "Item 1" }, { id: 1, title: "Item 2" }, { id: 2, title: "Item 3" }]; renderer.update(<Root items={items} header={"Hello world!"} />); return [["render simple with props model", renderer.toJSON()]]; }; if (this.__optimizeReactComponentTree) { let universe = { Item: { kind: "object", jsType: "object", properties: { id: { shape: { kind: "scalar", jsType: "integral", }, optional: false, }, title: { shape: { kind: "scalar", jsType: "string", }, optional: false, }, }, }, Props: { kind: "object", jsType: "object", properties: { header: { shape: { kind: "scalar", jsType: "string", }, optional: false, }, items: { shape: { kind: "array", jsType: "array", elementShape: { shape: { kind: "link", shapeName: "Item", }, optional: false, }, }, optional: true, }, }, }, }; let appModel = { component: { props: "Props", }, universe, }; __optimizeReactComponentTree(App, { model: JSON.stringify(appModel), }); } module.exports = App; ``` Pull Request resolved: https://github.com/facebook/prepack/pull/2320 Differential Revision: D8996429 Pulled By: trueadm fbshipit-source-id: 31eb4b42fcfab4aec785492ed0baecfb5533a0e2
This commit is contained in:
parent
2810294260
commit
b0fe89e863
@ -34,9 +34,10 @@ import { Get, Construct } from "../methods/index.js";
|
||||
import { Properties } from "../singletons.js";
|
||||
import invariant from "../invariant.js";
|
||||
import traverse from "@babel/traverse";
|
||||
import type { ClassComponentMetadata } from "../types.js";
|
||||
import type { ClassComponentMetadata, ReactComponentTreeConfig } from "../types.js";
|
||||
import type { ReactEvaluatedNode } from "../serializer/types.js";
|
||||
import { FatalError } from "../errors.js";
|
||||
import { type ComponentModel, ShapeInformation } from "../utils/ShapeInformation.js";
|
||||
|
||||
const lifecycleMethods = new Set([
|
||||
"componentWillUnmount",
|
||||
@ -52,8 +53,11 @@ const whitelistedProperties = new Set(["props", "context", "refs", "setState"]);
|
||||
|
||||
export function getInitialProps(
|
||||
realm: Realm,
|
||||
componentType: ECMAScriptSourceFunctionValue | null
|
||||
componentType: ECMAScriptSourceFunctionValue | null,
|
||||
{ modelString }: ReactComponentTreeConfig
|
||||
): AbstractObjectValue {
|
||||
let componentModel = modelString !== undefined ? (JSON.parse(modelString): ComponentModel) : undefined;
|
||||
let shape = ShapeInformation.createForReactComponentProps(componentModel);
|
||||
let propsName = null;
|
||||
if (componentType !== null) {
|
||||
if (valueIsClassComponent(realm, componentType)) {
|
||||
@ -68,11 +72,11 @@ export function getInitialProps(
|
||||
}
|
||||
}
|
||||
}
|
||||
let value = AbstractValue.createAbstractObject(realm, propsName || "props");
|
||||
invariant(value instanceof AbstractObjectValue);
|
||||
flagPropsWithNoPartialKeyOrRef(realm, value);
|
||||
value.makeFinal();
|
||||
return value;
|
||||
let abstractPropsObject = AbstractValue.createAbstractObject(realm, propsName || "props", shape);
|
||||
invariant(abstractPropsObject instanceof AbstractObjectValue);
|
||||
flagPropsWithNoPartialKeyOrRef(realm, abstractPropsObject);
|
||||
abstractPropsObject.makeFinal();
|
||||
return abstractPropsObject;
|
||||
}
|
||||
|
||||
export function getInitialContext(realm: Realm, componentType: ECMAScriptSourceFunctionValue): AbstractObjectValue {
|
||||
|
@ -483,7 +483,11 @@ export function renderToString(
|
||||
staticMarkup: boolean
|
||||
): StringValue | AbstractValue {
|
||||
let reactStatistics = new ReactStatistics();
|
||||
let reconciler = new Reconciler(realm, { firstRenderOnly: true, isRoot: true }, reactStatistics);
|
||||
let reconciler = new Reconciler(
|
||||
realm,
|
||||
{ firstRenderOnly: true, isRoot: true, modelString: undefined },
|
||||
reactStatistics
|
||||
);
|
||||
let typeValue = getProperty(realm, reactElement, "type");
|
||||
let propsValue = getProperty(realm, reactElement, "props");
|
||||
let evaluatedRootNode = createReactEvaluatedNode("ROOT", getComponentName(realm, typeValue));
|
||||
|
@ -173,7 +173,7 @@ export class Reconciler {
|
||||
): Effects {
|
||||
const resolveComponentTree = () => {
|
||||
try {
|
||||
let initialProps = props || getInitialProps(this.realm, componentType);
|
||||
let initialProps = props || getInitialProps(this.realm, componentType, this.componentTreeConfig);
|
||||
let initialContext = context || getInitialContext(this.realm, componentType);
|
||||
this.alreadyEvaluatedRootNodes.set(componentType, evaluatedRootNode);
|
||||
let { result } = this._resolveComponent(componentType, initialProps, initialContext, "ROOT", evaluatedRootNode);
|
||||
|
@ -775,6 +775,7 @@ export function convertConfigObjectToReactComponentTreeConfig(
|
||||
// defaults
|
||||
let firstRenderOnly = false;
|
||||
let isRoot = false;
|
||||
let modelString;
|
||||
|
||||
if (!(config instanceof UndefinedValue)) {
|
||||
for (let [key] of config.properties) {
|
||||
@ -782,13 +783,33 @@ export function convertConfigObjectToReactComponentTreeConfig(
|
||||
if (propValue instanceof StringValue || propValue instanceof NumberValue || propValue instanceof BooleanValue) {
|
||||
let value = propValue.value;
|
||||
|
||||
// boolean options
|
||||
if (typeof value === "boolean") {
|
||||
// boolean options
|
||||
if (key === "firstRenderOnly") {
|
||||
firstRenderOnly = value;
|
||||
} else if (key === "isRoot") {
|
||||
isRoot = value;
|
||||
}
|
||||
} else if (typeof value === "string") {
|
||||
try {
|
||||
// result here is ignored as the main point here is to
|
||||
// check and produce error
|
||||
JSON.parse(value);
|
||||
} catch (e) {
|
||||
let componentModelError = new CompilerDiagnostic(
|
||||
"Failed to parse model for component",
|
||||
realm.currentLocation,
|
||||
"PP1008",
|
||||
"FatalError"
|
||||
);
|
||||
if (realm.handleError(componentModelError) !== "Recover") {
|
||||
throw new FatalError();
|
||||
}
|
||||
}
|
||||
// string options
|
||||
if (key === "model") {
|
||||
modelString = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let diagnostic = new CompilerDiagnostic(
|
||||
@ -805,6 +826,7 @@ export function convertConfigObjectToReactComponentTreeConfig(
|
||||
return {
|
||||
firstRenderOnly,
|
||||
isRoot,
|
||||
modelString,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -351,6 +351,7 @@ export type ReactHint = {| firstRenderValue: Value, object: ObjectValue, propert
|
||||
export type ReactComponentTreeConfig = {
|
||||
firstRenderOnly: boolean,
|
||||
isRoot: boolean,
|
||||
modelString: void | string,
|
||||
};
|
||||
|
||||
export type DebugServerType = {
|
||||
|
@ -73,6 +73,11 @@ export type ArgModel = {
|
||||
arguments: { [string]: string },
|
||||
};
|
||||
|
||||
export type ComponentModel = {
|
||||
universe: ShapeUniverse,
|
||||
component: { props: string },
|
||||
};
|
||||
|
||||
export class ShapeInformation implements ShapeInformationInterface {
|
||||
constructor(
|
||||
descriptor: ShapeDescriptorNonLink,
|
||||
@ -126,6 +131,17 @@ export class ShapeInformation implements ShapeInformationInterface {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
static createForReactComponentProps(model: void | ComponentModel): void | ShapeInformation {
|
||||
return model !== undefined
|
||||
? ShapeInformation._resolveLinksAndWrap(
|
||||
model.universe[model.component.props],
|
||||
undefined,
|
||||
undefined,
|
||||
model.universe
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
_isOptional(): void | boolean {
|
||||
if (this._parentDescriptor === undefined) {
|
||||
return undefined;
|
||||
|
@ -206,6 +206,7 @@ export default class AbstractObjectValue extends AbstractValue {
|
||||
}
|
||||
|
||||
makeFinal(): void {
|
||||
if (this.shape) return;
|
||||
if (this.values.isTop()) {
|
||||
AbstractValue.reportIntrospectionError(this);
|
||||
throw new FatalError();
|
||||
@ -506,9 +507,10 @@ export default class AbstractObjectValue extends AbstractValue {
|
||||
let shapeContainer = this.kind === "explicit conversion to object" ? this.args[0] : this;
|
||||
invariant(shapeContainer instanceof AbstractValue);
|
||||
invariant(typeof P === "string");
|
||||
let realm = this.$Realm;
|
||||
let shape = shapeContainer.shape;
|
||||
let propertyShape, propertyGetter;
|
||||
if (this.$Realm.instantRender.enabled && shape !== undefined) {
|
||||
if ((realm.instantRender.enabled || realm.react.enabled) && shape !== undefined) {
|
||||
propertyShape = shape.getPropertyShape(P);
|
||||
if (propertyShape !== undefined) {
|
||||
type = propertyShape.getAbstractType();
|
||||
@ -516,7 +518,7 @@ export default class AbstractObjectValue extends AbstractValue {
|
||||
}
|
||||
}
|
||||
let propAbsVal = AbstractValue.createTemporalFromBuildFunction(
|
||||
this.$Realm,
|
||||
realm,
|
||||
type,
|
||||
[ob, new StringValue(this.$Realm, P)],
|
||||
createOperationDescriptor("ABSTRACT_OBJECT_GET", { propertyGetter }),
|
||||
|
@ -942,13 +942,15 @@ export default class AbstractValue extends Value {
|
||||
return realm.reportIntrospectionError(message);
|
||||
}
|
||||
|
||||
static createAbstractObject(realm: Realm, name: string, template?: ObjectValue): AbstractObjectValue {
|
||||
static createAbstractObject(
|
||||
realm: Realm,
|
||||
name: string,
|
||||
templateOrShape?: ObjectValue | ShapeInformationInterface
|
||||
): AbstractObjectValue {
|
||||
let value;
|
||||
if (template === undefined) {
|
||||
template = new ObjectValue(realm, realm.intrinsics.ObjectPrototype);
|
||||
if (templateOrShape === undefined) {
|
||||
templateOrShape = new ObjectValue(realm, realm.intrinsics.ObjectPrototype);
|
||||
}
|
||||
template.makePartial();
|
||||
template.makeSimple();
|
||||
value = AbstractValue.createFromTemplate(realm, buildExpressionTemplate(name), ObjectValue, [], name);
|
||||
if (!realm.isNameStringUnique(name)) {
|
||||
value.hashValue = ++realm.objectCount;
|
||||
@ -956,8 +958,14 @@ export default class AbstractValue extends Value {
|
||||
realm.saveNameString(name);
|
||||
}
|
||||
value.intrinsicName = name;
|
||||
value.values = new ValuesDomain(new Set([template]));
|
||||
realm.rebuildNestedProperties(value, name);
|
||||
if (templateOrShape instanceof ObjectValue) {
|
||||
templateOrShape.makePartial();
|
||||
templateOrShape.makeSimple();
|
||||
value.values = new ValuesDomain(new Set([templateOrShape]));
|
||||
realm.rebuildNestedProperties(value, name);
|
||||
} else {
|
||||
value.shape = templateOrShape;
|
||||
}
|
||||
invariant(value instanceof AbstractObjectValue);
|
||||
return value;
|
||||
}
|
||||
|
@ -323,3 +323,7 @@ it("Hoist Fragment", () => {
|
||||
it("Pathological case", () => {
|
||||
runTest(__dirname + "/FunctionalComponents/pathological-case.js");
|
||||
});
|
||||
|
||||
it("Model props", () => {
|
||||
runTest(__dirname + "/FunctionalComponents/model-props.js");
|
||||
});
|
||||
|
81
test/react/FunctionalComponents/model-props.js
Normal file
81
test/react/FunctionalComponents/model-props.js
Normal file
@ -0,0 +1,81 @@
|
||||
var React = require("React");
|
||||
|
||||
function App(props) {
|
||||
return (
|
||||
<div>
|
||||
<h1>{props.header.toString()}</h1>
|
||||
<ul>{props.items && props.items.map(item => <li key={item.id}>{item.title.toString()}</li>)}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
App.getTrials = function(renderer, Root) {
|
||||
var items = [{ id: 0, title: "Item 1" }, { id: 1, title: "Item 2" }, { id: 2, title: "Item 3" }];
|
||||
renderer.update(<Root items={items} header={"Hello world!"} />);
|
||||
return [["render simple with props model", renderer.toJSON()]];
|
||||
};
|
||||
|
||||
if (this.__optimizeReactComponentTree) {
|
||||
let universe = {
|
||||
Item: {
|
||||
kind: "object",
|
||||
jsType: "object",
|
||||
properties: {
|
||||
id: {
|
||||
shape: {
|
||||
kind: "scalar",
|
||||
jsType: "integral",
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
title: {
|
||||
shape: {
|
||||
kind: "scalar",
|
||||
jsType: "string",
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Props: {
|
||||
kind: "object",
|
||||
jsType: "object",
|
||||
properties: {
|
||||
header: {
|
||||
shape: {
|
||||
kind: "scalar",
|
||||
jsType: "string",
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
items: {
|
||||
shape: {
|
||||
kind: "array",
|
||||
jsType: "array",
|
||||
elementShape: {
|
||||
shape: {
|
||||
kind: "link",
|
||||
shapeName: "Item",
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let appModel = {
|
||||
component: {
|
||||
props: "Props",
|
||||
},
|
||||
universe,
|
||||
};
|
||||
|
||||
__optimizeReactComponentTree(App, {
|
||||
model: JSON.stringify(appModel),
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = App;
|
@ -3040,6 +3040,74 @@ ReactStatistics {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Model props: (JSX => JSX) 1`] = `
|
||||
ReactStatistics {
|
||||
"componentsEvaluated": 1,
|
||||
"evaluatedRootNodes": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"message": "",
|
||||
"name": "App",
|
||||
"status": "ROOT",
|
||||
},
|
||||
],
|
||||
"inlinedComponents": 0,
|
||||
"optimizedNestedClosures": 0,
|
||||
"optimizedTrees": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Model props: (JSX => createElement) 1`] = `
|
||||
ReactStatistics {
|
||||
"componentsEvaluated": 1,
|
||||
"evaluatedRootNodes": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"message": "",
|
||||
"name": "App",
|
||||
"status": "ROOT",
|
||||
},
|
||||
],
|
||||
"inlinedComponents": 0,
|
||||
"optimizedNestedClosures": 0,
|
||||
"optimizedTrees": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Model props: (createElement => JSX) 1`] = `
|
||||
ReactStatistics {
|
||||
"componentsEvaluated": 1,
|
||||
"evaluatedRootNodes": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"message": "",
|
||||
"name": "App",
|
||||
"status": "ROOT",
|
||||
},
|
||||
],
|
||||
"inlinedComponents": 0,
|
||||
"optimizedNestedClosures": 0,
|
||||
"optimizedTrees": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Model props: (createElement => createElement) 1`] = `
|
||||
ReactStatistics {
|
||||
"componentsEvaluated": 1,
|
||||
"evaluatedRootNodes": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"message": "",
|
||||
"name": "App",
|
||||
"status": "ROOT",
|
||||
},
|
||||
],
|
||||
"inlinedComponents": 0,
|
||||
"optimizedNestedClosures": 0,
|
||||
"optimizedTrees": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Mutations - not-safe 1: (JSX => JSX) 1`] = `"Failed to render React component root \\"Bar\\" due to side-effects from mutating the binding \\"x\\""`;
|
||||
|
||||
exports[`Mutations - not-safe 1: (JSX => createElement) 1`] = `"Failed to render React component root \\"Bar\\" due to side-effects from mutating the binding \\"x\\""`;
|
||||
|
Loading…
Reference in New Issue
Block a user