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:
Dominic Gannaway 2018-07-25 11:13:40 -07:00 committed by Facebook Github Bot
parent 2810294260
commit b0fe89e863
11 changed files with 229 additions and 19 deletions

View File

@ -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 {

View File

@ -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));

View File

@ -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);

View File

@ -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,
};
}

View File

@ -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 = {

View File

@ -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;

View File

@ -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 }),

View File

@ -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;
}

View File

@ -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");
});

View 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;

View File

@ -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\\""`;