Add React functional component folding

Summary:
Release note: Adds experimental React functional component folding optimizations

This PR is stacked upon PRs #1118 and #1117. Thus, those PRs should be merged before this PR is merged to reduce noise in the diff.

This PR adds a new React Reconciler into Prepack's serialization process, so that React components trees can be folded/inlined into a single component at build time. To fold a component tree, it must be explicitly done via `__registerReactComponentRoot(nameOfComponent)`.

This PR only attempts to fold React functional components, not React ES2015 class components (that will come in another PR at a later date). Furthermore, the `props` parameter on a root component must contain Flow type annotations (otherwise we will have no idea what the values might be). Support flow `propTypes` might also be an addition, but not for this PR.

If the reconciler comes across a component that it cannot fold/inline, it will "bail-out" and try and continue the process without that particular component being folded into the tree.

An example of how this all works (input):

```jsx
function App(props: {title: string}) {
  return (
    <div>
      <ChildComponent title={props.title} />
    </div>
  );
}

function ChildComponent(props) {
  return (
    <span>
      <SubChildComponent {...props} />
    </span>
  );
}

function SubChildComponent(props) {
  return <span>{props.title.toString()}</span>
}

__registerReactComponentRoot(App);

global.App = App;
```

Output:
```jsx
(function () {
  "use strict";

  var _$1 = this;

  var _0 = function (props) {
    var _$0 = props.title;
    return <div><span><span>{_$0}</span></span></div>;
  };

  _$1.App = _0;
}).call(this);
```
Closes https://github.com/facebook/prepack/pull/1120

Differential Revision: D6237333

Pulled By: trueadm

fbshipit-source-id: b58c7d8979ca79a766bb2ee2eb01a380d37c3101
This commit is contained in:
Dominic Gannaway 2017-11-06 04:54:12 -08:00 committed by Facebook Github Bot
parent eebba38633
commit 810056d1ec
29 changed files with 875 additions and 213 deletions

0
.watchmanconfig Normal file
View File

View File

@ -42,7 +42,7 @@
"test-node-cli-mode": "bash < scripts/test-node-cli-mode.sh",
"test-std-in": "bash < scripts/test-std-in.sh",
"test-react": "jest scripts/test-react",
"test": "yarn test-residual && yarn test-serializer && yarn test-sourcemaps && yarn test-error-handler && yarn test-std-in && yarn test-test262 && yarn test-internal && yarn test-react",
"test": "yarn test-residual && yarn test-serializer && yarn test-sourcemaps && yarn test-error-handler && yarn test-std-in && yarn test-test262 && yarn test-react && yarn test-internal",
"test-coverage-most": "./node_modules/.bin/istanbul --stack_size=10000 --max_old_space_size=16384 cover ./lib/multi-runner.js --dir coverage.most && ./node_modules/.bin/remap-istanbul -i coverage.most/coverage.json -o coverage-sourcemapped -t html",
"test-all-coverage": "./node_modules/.bin/istanbul --stack_size=10000 --max_old_space_size=16384 cover ./lib/multi-runner.js --dir coverage.most && ./node_modules/.bin/istanbul --stack_size=10000 --max_old_space_size=16384 cover -- ./lib/test262-runner.js --timeout 50 --singleThreaded && ./node_modules/.bin/remap-istanbul -i coverage/coverage.json -i coverage.most/coverage.json -o coverage-sourcemapped -t html",
"repl": "node lib/repl-cli.js",

View File

@ -41,7 +41,7 @@ exec("flow check --profile", function(error, stdout, stderr) {
process.exit(1);
}
console.log("Biggest cycle: " + cycle_len);
let MAX_CYCLE_LEN = 59;
let MAX_CYCLE_LEN = 61;
if (cycle_len > MAX_CYCLE_LEN) {
console.log("Error: You increased cycle length from the previous high of " + MAX_CYCLE_LEN);
process.exit(1);

View File

@ -35,15 +35,11 @@ let prepackOptions = {
function compileSourceWithPrepack(source) {
let code = `(function(){${source}})()`;
let serialized = prepackSources([{ filePath: "", fileContents: code, sourceMapContents: "" }], prepackOptions);
// add the React require back in, as we've removed it with our Prepack mock
// the regex checks for any Prepack variable that matches "_$**any digit**.React"
let compiledSource = serialized.code.replace(/_\$[\d].React/, "React = require('react')");
if (serialized == null || serialized.reactStatistics == null) {
throw new Error("React test runner failed during serialization");
}
return {
// replace the code to put back the generator (Prepack doesn't serialize them yet)
compiledSource,
compiledSource: serialized.code,
statistics: serialized.reactStatistics,
};
}
@ -145,6 +141,10 @@ describe("Test React", () => {
await runTest(directory, "type-change.js");
});
it("Component type same", async () => {
await runTest(directory, "type-same.js");
});
it("Dynamic props", async () => {
await runTest(directory, "dynamic-props.js");
});

View File

@ -0,0 +1,123 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import { Realm } from "../realm.js";
import buildExpressionTemplate from "../utils/builder.js";
import { ObjectCreate, ArrayCreate } from "../methods/index.js";
import { ValuesDomain } from "../domains/index.js";
import { Value, AbstractValue, ObjectValue, ArrayValue } from "../values/index.js";
import invariant from "../invariant.js";
import { type ObjectTypeTemplate } from "./utils.js";
export function createObject(realm: Realm, shape: ObjectTypeTemplate | null, name: string | null): ObjectValue {
let obj = ObjectCreate(realm, realm.intrinsics.ObjectPrototype);
if (shape != null) {
// to get around Flow complaining that shape could be null
let shapeThatIsNotNull = shape;
Object.keys(shape).forEach((id: string) => {
let value = shapeThatIsNotNull[id];
invariant(value instanceof Value, "creation of object failed due to object containing non-value properties");
obj.$Set(id, value, obj);
if (name !== null) {
value.intrinsicName = `${name}.${id}`;
}
});
}
if (name !== null) {
obj.intrinsicName = name;
}
return obj;
}
export function createArray(realm: Realm, name: string | null): ArrayValue {
let obj = ArrayCreate(realm, 0, realm.intrinsics.ArrayPrototype);
if (name !== null) {
obj.intrinsicName = name;
}
return ((obj: any): ArrayValue);
}
function _createAbstractArray(realm: Realm, name: string): AbstractValue {
let value = AbstractValue.createFromTemplate(realm, buildExpressionTemplate(name), ArrayValue, [], name);
value.intrinsicName = name;
let template = createArray(realm, name);
template.makePartial();
template.makeSimple();
value.values = new ValuesDomain(new Set([template]));
realm.rebuildNestedProperties(value, name);
return value;
}
function _createAbstractObject(
realm: Realm,
name: string | null,
objectTypes: ObjectTypeTemplate | null
): AbstractValue {
if (name === null) {
name = "unknown";
}
let value = AbstractValue.createFromTemplate(realm, buildExpressionTemplate(name), ObjectValue, [], name);
value.intrinsicName = name;
let template = createObject(realm, objectTypes, name);
template.makePartial();
template.makeSimple();
value.values = new ValuesDomain(new Set([template]));
realm.rebuildNestedProperties(value, name);
return value;
}
export function createAbstractObject(
realm: Realm,
name: string | null,
objectTypes: ObjectTypeTemplate | null | string
): ObjectValue | AbstractValue {
if (typeof objectTypes === "string") {
invariant(
objectTypes === "empty" || objectTypes === "object",
`Expected an object or a string of "empty" or "object" for createAbstractObject() paramater "objectTypes"`
);
return _createAbstractObject(realm, name, null);
}
if (objectTypes !== null) {
let propTypeObject = {};
let objTypes = objectTypes;
invariant(objTypes);
Object.keys(objTypes).forEach(key => {
let value = objTypes[key];
let propertyName = name !== null ? `${name}.${key}` : key;
if (typeof value === "string") {
if (value === "array") {
propTypeObject[key] = _createAbstractArray(realm, propertyName);
} else if (value === "object") {
propTypeObject[key] = _createAbstractObject(realm, propertyName, null);
} else {
propTypeObject[key] = createAbstractByType(realm, value, propertyName);
}
} else if (typeof value === "object" && value !== null) {
propTypeObject[key] = createAbstractObject(realm, propertyName, value);
} else {
invariant(false, `Unknown propType value of "${value}" for "${key}"`);
}
});
return _createAbstractObject(realm, name, propTypeObject);
} else {
return _createAbstractObject(realm, name, null);
}
}
export function createAbstractByType(realm: Realm, typeNameString: string, name: string): Value {
let type = Value.getTypeFromName(typeNameString);
invariant(type !== undefined, "createAbstractByType() cannot be undefined");
let value = AbstractValue.createFromTemplate(realm, buildExpressionTemplate(name), type, [], name);
value.intrinsicName = name;
return value;
}

View File

@ -9,10 +9,73 @@
/* @flow */
import type { typeAnnotation } from "babel-types";
import invariant from "../invariant.js";
import traverse from "babel-traverse";
import { BabelNode } from "babel-types";
import * as t from "babel-types";
export type ObjectTypeTemplate = {
[key: string]: string | ObjectTypeTemplate,
};
export function flowAnnotationToObjectTypeTemplate(annotation: typeAnnotation): string | ObjectTypeTemplate {
if (annotation.type === "TypeAnnotation") {
return flowAnnotationToObjectTypeTemplate(annotation.typeAnnotation);
} else if (annotation.type === "GenericTypeAnnotation") {
if (annotation.id.type === "Identifier") {
let identifier = annotation.id.name;
switch (identifier) {
case "Function":
return "function";
case "Object":
return "object";
case "Array":
return "array";
case "any":
case "empty":
return "empty";
default:
// get the Flow type
invariant(false, "Flow types are currently not supported");
}
} else {
invariant(false, "unknown generic Flow type annotation node");
}
} else if (annotation.type === "EmptyTypeAnnotation") {
return "empty";
} else if (annotation.type === "BooleanTypeAnnotation") {
return "boolean";
} else if (annotation.type === "StringTypeAnnotation") {
return "string";
} else if (annotation.type === "NumberTypeAnnotation") {
return "number";
} else if (annotation.type === "FunctionTypeAnnotation") {
return "function";
} else if (annotation.type === "ArrayTypeAnnotation") {
return "array";
} else if (annotation.type === "ObjectTypeAnnotation") {
let obj = {};
annotation.properties.forEach(property => {
if (property.type === "ObjectTypeProperty") {
if (property.key.type === "Identifier") {
obj[property.key.name] = flowAnnotationToObjectTypeTemplate(property.value);
} else {
invariant(false, "only Identifier nodes are supported in ObjectTypeProperty keys");
}
} else {
invariant(false, "only ObjectTypeProperty properties are supported in ObjectTypeAnnotation");
}
});
return obj;
} else if (annotation.type === "AnyTypeAnnotation") {
return "empty";
} else {
invariant(false, "unknown Flow type annotation node");
}
}
// Taken directly from Babel:
// https://github.com/babel/babel/blob/cde005422701a69ff21044c138c29a5ad23b6d0a/packages/babel-plugin-transform-flow-strip-types/src/index.js#L32-L107
// Copyright 2015-present Sebastian McKenzie / Babel project (https://github.com/babel)

View File

@ -10,10 +10,11 @@
/* @flow */
import type { Realm } from "../../realm.js";
import { AbstractValue, NativeFunctionValue, ObjectValue, Value } from "../../values/index.js";
import { ObjectCreate, CreateDataPropertyOrThrow, GetValue } from "../../methods/index.js";
import { AbstractValue, NativeFunctionValue, Value, StringValue } from "../../values/index.js";
import { ObjectCreate } from "../../methods/index.js";
import buildExpressionTemplate from "../../utils/builder.js";
import { createMockReactComponent, createMockReactCloneElement } from "./mocks.js";
import { createMockReact } from "./mocks.js";
import invariant from "../../invariant";
export default function(realm: Realm): void {
let global = realm.$GlobalObject;
@ -31,41 +32,25 @@ export default function(realm: Realm): void {
enumerable: false,
configurable: true,
});
// require("SomeModule") support (makes them abstract)
let type = Value.getTypeFromName("function");
let requireValue = AbstractValue.createFromTemplate(
realm,
buildExpressionTemplate("require"),
((type: any): typeof Value),
[],
"require"
);
requireValue.intrinsicName = "require";
global.$DefineOwnProperty("require", {
value: requireValue,
writable: true,
enumerable: false,
configurable: true,
});
// apply React mock (for now just React.Component)
global.$DefineOwnProperty("__createReactMock", {
value: new NativeFunctionValue(realm, "global.__createReactMock", "__createReactMock", 0, (context, []) => {
// React object
let reactValue = ObjectCreate(realm, realm.intrinsics.ObjectPrototype);
reactValue.intrinsicName = "React";
// React.Component
let reactComponent = GetValue(realm, realm.$GlobalEnv.evaluate(createMockReactComponent(), false));
reactComponent.intrinsicName = "React.Component";
let prototypeValue = ((reactComponent: any): ObjectValue).properties.get("prototype");
if (prototypeValue && prototypeValue.descriptor) {
((prototypeValue.descriptor.value: any): Value).intrinsicName = `React.Component.prototype`;
global.$DefineOwnProperty("require", {
value: new NativeFunctionValue(realm, "global.require", "require", 0, (context, [requireNameVal]) => {
invariant(requireNameVal instanceof StringValue);
if (requireNameVal.value === "react" || requireNameVal.value === "React") {
return createMockReact(realm);
}
CreateDataPropertyOrThrow(realm, reactValue, "Component", reactComponent);
// React.cloneElement
let reactCloneElement = GetValue(realm, realm.$GlobalEnv.evaluate(createMockReactCloneElement(), false));
reactCloneElement.intrinsicName = "React.cloneElement";
CreateDataPropertyOrThrow(realm, reactValue, "cloneElement", reactCloneElement);
return reactValue;
let requireName = `require("${requireNameVal.value}")`;
let type = Value.getTypeFromName("function");
let requireValue = AbstractValue.createFromTemplate(
realm,
buildExpressionTemplate(requireName),
((type: any): typeof Value),
[],
requireName
);
requireValue.intrinsicName = requireName;
return requireValue;
}),
writable: true,
enumerable: false,

View File

@ -9,89 +9,109 @@
/* @flow */
import type { Realm } from "../../realm.js";
import { parseExpression } from "babylon";
import { ObjectValue } from "../../values/index.js";
import { Get, GetValue } from "../../methods/index.js";
import invariant from "../../invariant";
// this a mock of React.Component, to be used for tests
export function createMockReactComponent() {
let componentCode = `
class Component {
let reactCode = `
{
Component: class Component {
constructor(props, context) {
this.props = props || {};
this.context = context || {};
this.refs = {};
this.state = {};
}
isReactComponent() {
return true;
}
getChildContext() {}
}
`;
return parseExpression(componentCode, { plugins: ["flow"] });
}
// this a mock of React.Component, to be used for tests
export function createMockReactCloneElement() {
let cloneElementCode = `
function cloneElement(element, config, children) {
var propName;
var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
var hasOwnProperty = Object.prototype.hasOwnProperty;
var props = Object.assign({}, element.props);
var key = element.key;
var ref = element.ref;
var self = element._self;
var source = element._source;
var owner = element._owner;
if (config != null) {
if (config.ref !== undefined) {
// owner = ReactCurrentOwner.current;
}
if (config.key !== undefined) {
key = '' + config.key;
}
var defaultProps;
if (element.type && element.type.defaultProps) {
defaultProps = element.type.defaultProps;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
if (config[propName] === undefined && defaultProps !== undefined) {
// Resolve default props
props[propName] = defaultProps[propName];
} else {
props[propName] = config[propName];
},
createElement: function() {
// TODO
},
cloneElement(element, config, children) {
var propName;
var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
var hasOwnProperty = Object.prototype.hasOwnProperty;
var props = Object.assign({}, element.props);
var key = element.key;
var ref = element.ref;
var self = element._self;
var source = element._source;
var owner = element._owner;
if (config != null) {
if (config.ref !== undefined) {
// owner = ReactCurrentOwner.current;
}
if (config.key !== undefined) {
key = '' + config.key;
}
var defaultProps;
if (element.type && element.type.defaultProps) {
defaultProps = element.type.defaultProps;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
if (config[propName] === undefined && defaultProps !== undefined) {
// Resolve default props
props[propName] = defaultProps[propName];
} else {
props[propName] = config[propName];
}
}
}
}
}
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
props.children = childArray;
}
return {
$$typeof: element.$$typeof,
type: element.type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return {
$$typeof: element.$$typeof,
type: element.type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
},
}
`;
return parseExpression(cloneElementCode, { plugins: ["flow"] });
`;
let reactAst = parseExpression(reactCode, { plugins: ["flow"] });
export function createMockReact(realm: Realm): ObjectValue {
let reactValue = GetValue(realm, realm.$GlobalEnv.evaluate(reactAst, false));
reactValue.intrinsicName = `require("react")`;
invariant(reactValue instanceof ObjectValue);
let reactComponentValue = Get(realm, reactValue, "Component");
reactComponentValue.intrinsicName = `require("react").Component`;
invariant(reactComponentValue instanceof ObjectValue);
let reactComponentPrototypeValue = Get(realm, reactComponentValue, "prototype");
reactComponentPrototypeValue.intrinsicName = `require("react").Component.prototype`;
let reactCloneElementValue = Get(realm, reactValue, "cloneElement");
reactCloneElementValue.intrinsicName = `require("react").cloneElement`;
return reactValue;
}

101
src/react/branching.js Normal file
View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import { Realm } from "../realm.js";
import {
ECMAScriptSourceFunctionValue,
Value,
UndefinedValue,
StringValue,
NumberValue,
BooleanValue,
NullValue,
AbstractValue,
ArrayValue,
ObjectValue,
} from "../values/index.js";
import { type ReactSerializerState } from "../serializer/types.js";
import { isReactElement, addKeyToReactElement, mapOverArrayValue } from "./utils";
import { ExpectedBailOut } from "./reconcilation.js";
// Branch status is used for when Prepack returns an abstract value from a render
// that results in a conditional path occuring. This can be problematic for reconcilation
// as the reconciler then needs to understand if this is the start of a new branch, or if
// it's actually deep into an existing branch. If it's a new branch, we need to apply
// keys to the root JSX element so that it keeps it identity (because we're folding trees).
// Furthermore, we also need to bail-out of folding class components where they have lifecycle
// events, as we can't merge lifecycles of mutliple trees when branched reliably
export type BranchStatusEnum = "NO_BRANCH" | "NEW_BRANCH" | "BRANCH";
// Branch state is used to capture branched ReactElements so they can be analyzed and compared
// once all branches have been processed. This allows us to add keys to the respective ReactElement
// objects depending on various heuristics (if they have the same "type" for example)
// A new branch state is created on a branch status of "NEW_BRANCH" and is reset to null once the branch is no
// longer new
export class BranchState {
constructor() {
this._branchesToValidate = [];
}
_applyBranchedLogicValue(realm: Realm, reactSerializerState: ReactSerializerState, value: Value): void {
if (
value instanceof StringValue ||
value instanceof NumberValue ||
value instanceof BooleanValue ||
value instanceof NullValue ||
value instanceof UndefinedValue
) {
// terminal values
} else if (value instanceof ObjectValue && isReactElement(value)) {
addKeyToReactElement(realm, reactSerializerState, value);
} else if (value instanceof ArrayValue) {
mapOverArrayValue(realm, value, elementValue => {
this._applyBranchedLogicValue(realm, reactSerializerState, elementValue);
});
} else if (value instanceof AbstractValue) {
let length = value.args.length;
if (length > 0) {
for (let i = 0; i < length; i++) {
this._applyBranchedLogicValue(realm, reactSerializerState, value.args[i]);
}
}
} else {
throw new ExpectedBailOut("Unsupported value encountered when applying branched logic to values");
}
}
applyBranchedLogic(realm: Realm, reactSerializerState: ReactSerializerState): void {
let reactElementType;
let applyBranchedLogic = false;
for (let i = 0; i < this._branchesToValidate.length; i++) {
let { type } = this._branchesToValidate[i];
if (reactElementType === undefined) {
reactElementType = type;
} else if (type !== reactElementType) {
// the types of the ReactElements do not match, so apply branch logic
applyBranchedLogic = true;
break;
}
}
if (applyBranchedLogic) {
for (let i = 0; i < this._branchesToValidate.length; i++) {
this._applyBranchedLogicValue(realm, reactSerializerState, this._branchesToValidate[i].value);
}
}
}
captureBranchedValue(type: StringValue | ECMAScriptSourceFunctionValue, value: Value): Value {
this._branchesToValidate.push({ type, value });
return value;
}
_branchesToValidate: Array<{
type: StringValue | ECMAScriptSourceFunctionValue,
value: Value,
}>;
}

315
src/react/reconcilation.js Normal file
View File

@ -0,0 +1,315 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import { Realm, type Effects } from "../realm.js";
import { ModuleTracer } from "../serializer/modules.js";
import {
ECMAScriptSourceFunctionValue,
Value,
UndefinedValue,
StringValue,
NumberValue,
BooleanValue,
NullValue,
AbstractValue,
ArrayValue,
ObjectValue,
} from "../values/index.js";
import { ReactStatistics, type ReactSerializerState } from "../serializer/types.js";
import { isReactElement, valueIsClassComponent, mapOverArrayValue } from "./utils";
import { Get } from "../methods/index.js";
import invariant from "../invariant.js";
import { flowAnnotationToObjectTypeTemplate } from "../flow/utils.js";
import * as t from "babel-types";
import type { BabelNodeIdentifier } from "babel-types";
import { createAbstractObject } from "../flow/abstractObjectFactories.js";
import { CompilerDiagnostic, FatalError } from "../errors.js";
import { BranchState, type BranchStatusEnum } from "./branching.js";
// ExpectedBailOut is like an error, that gets thrown during the reconcilation phase
// allowing the reconcilation to continue on other branches of the tree, the message
// given to ExpectedBailOut will be assigned to the value.$BailOutReason property and serialized
// as a comment in the output source to give the user hints as to what they need to do
// to fix the bail-out case
export class ExpectedBailOut {
message: string;
constructor(message: string) {
this.message = message;
}
}
function getInitialProps(realm: Realm, componentType: ECMAScriptSourceFunctionValue): ObjectValue | AbstractValue {
let propsName = null;
let propTypes = null;
if (valueIsClassComponent(realm, componentType)) {
// it's a class component, so we need to check the type on for props of the component prototype
// as we don't support class components yet, throw a fatal error
throw new ExpectedBailOut("class components not yet supported");
} else {
// otherwise it's a functional component, where the first paramater of the function is "props" (if it exists)
if (componentType.$FormalParameters.length > 0) {
let firstParam = componentType.$FormalParameters[0];
if (t.isIdentifier(firstParam)) {
propsName = ((firstParam: any): BabelNodeIdentifier).name;
}
let propsTypeAnnotation = firstParam.typeAnnotation !== undefined && firstParam.typeAnnotation;
// we expect that if there's a props paramater, it should always have Flow annotations
if (!propsTypeAnnotation) {
throw new ExpectedBailOut(`root component missing Flow type annotations for the "props" paramater`);
}
propTypes = flowAnnotationToObjectTypeTemplate(propsTypeAnnotation);
}
}
return createAbstractObject(realm, propsName, propTypes);
}
function getInitialContext(realm: Realm, componentType: ECMAScriptSourceFunctionValue): ObjectValue | AbstractValue {
let contextName = null;
let contextTypes = null;
if (valueIsClassComponent(realm, componentType)) {
// it's a class component, so we need to check the type on for context of the component prototype
// as we don't support class components yet, throw a fatal error
throw new ExpectedBailOut("class components not yet supported");
} else {
// otherwise it's a functional component, where the second paramater of the function is "context" (if it exists)
if (componentType.$FormalParameters.length > 1) {
let secondParam = componentType.$FormalParameters[1];
if (t.isIdentifier(secondParam)) {
contextName = ((secondParam: any): BabelNodeIdentifier).name;
}
let contextTypeAnnotation = secondParam.typeAnnotation !== undefined && secondParam.typeAnnotation;
// we expect that if there's a context param, it should always have Flow annotations
if (!contextTypeAnnotation) {
throw new ExpectedBailOut(`root component missing Flow type annotations for the "context" paramater`);
}
contextTypes = flowAnnotationToObjectTypeTemplate(contextTypeAnnotation);
}
}
return createAbstractObject(realm, contextName, contextTypes);
}
export class Reconciler {
constructor(
realm: Realm,
moduleTracer: ModuleTracer,
statistics: ReactStatistics,
reactSerializerState: ReactSerializerState
) {
this.realm = realm;
this.moduleTracer = moduleTracer;
this.statistics = statistics;
this.reactSerializerState = reactSerializerState;
}
realm: Realm;
moduleTracer: ModuleTracer;
statistics: ReactStatistics;
reactSerializerState: ReactSerializerState;
render(componentType: ECMAScriptSourceFunctionValue): Effects {
return this.realm.wrapInGlobalEnv(() =>
// TODO: (sebmarkbage): You could use the return value of this to detect if there are any mutations on objects other
// than newly created ones. Then log those to the error logger. That'll help us track violations in
// components. :)
this.realm.evaluateForEffects(() => {
// initialProps and initialContext are created from Flow types from:
// - if a functional component, the 1st and 2nd paramater of function
// - if a class component, use this.props and this.context
// if there are no Flow types for props or context, we will throw a
// FatalError, unless it's a functional component that has no paramater
// i.e let MyComponent = () => <div>Hello world</div>
try {
let initialProps = getInitialProps(this.realm, componentType);
let initialContext = getInitialContext(this.realm, componentType);
let { result } = this._renderAsDeepAsPossible(componentType, initialProps, initialContext, "NO_BRANCH", null);
this.statistics.optimizedTrees++;
return result;
} catch (error) {
// if there was a bail-out on the root component in this reconcilation process, then this
// should be an invariant as the user has explicitly asked for this component to get folded
if (error instanceof ExpectedBailOut) {
let diagnostic = new CompilerDiagnostic(
`__registerReactComponentRoot() failed due to - ${error.message}`,
this.realm.currentLocation,
"PP0019",
"FatalError"
);
this.realm.handleError(diagnostic);
throw new FatalError();
}
throw error;
}
})
);
}
_renderAsDeepAsPossible(
componentType: ECMAScriptSourceFunctionValue,
props: ObjectValue | AbstractValue,
context: ObjectValue | AbstractValue,
branchStatus: BranchStatusEnum,
branchState: BranchState | null
) {
let { value, childContext } = this._renderOneLevel(componentType, props, context);
let result = this._resolveDeeply(value, childContext, branchStatus, branchState);
return {
result,
childContext,
};
}
_renderOneLevel(
componentType: ECMAScriptSourceFunctionValue,
props: ObjectValue | AbstractValue,
context: ObjectValue | AbstractValue
) {
if (valueIsClassComponent(this.realm, componentType)) {
// for now we don't support class components, so we throw a ExpectedBailOut
throw new ExpectedBailOut("class components not yet supported");
} else {
invariant(componentType.$Call, "Expected componentType to be a FunctionValue with $Call method");
let value = componentType.$Call(this.realm.intrinsics.undefined, [props, context]);
return { value, childContext: context };
}
}
_resolveDeeply(
value: Value,
context: ObjectValue | AbstractValue,
branchStatus: BranchStatusEnum,
branchState: BranchState | null
) {
if (
value instanceof StringValue ||
value instanceof NumberValue ||
value instanceof BooleanValue ||
value instanceof NullValue ||
value instanceof UndefinedValue
) {
// terminal values
return value;
} else if (value instanceof AbstractValue) {
let length = value.args.length;
if (length > 0) {
let newBranchState = new BranchState();
// TODO investigate what other kinds than "conditional" might be safe to deeply resolve
for (let i = 0; i < length; i++) {
value.args[i] = this._resolveDeeply(value.args[i], context, "NEW_BRANCH", newBranchState);
}
newBranchState.applyBranchedLogic(this.realm, this.reactSerializerState);
}
return value;
}
// TODO investigate what about other iterables type objects
if (value instanceof ArrayValue) {
this._resolveFragment(value, context, branchStatus, branchState);
return value;
}
if (value instanceof ObjectValue && isReactElement(value)) {
// we call value reactElement, to make it clearer what we're dealing with in this block
let reactElement = value;
let typeValue = Get(this.realm, reactElement, "type");
let propsValue = Get(this.realm, reactElement, "props");
let refValue = Get(this.realm, reactElement, "ref");
if (typeValue instanceof StringValue) {
// terminal host component. Start evaluating its children.
if (propsValue instanceof ObjectValue) {
let childrenProperty = propsValue.properties.get("children");
if (childrenProperty) {
let childrenPropertyDescriptor = childrenProperty.descriptor;
// if the descriptor is undefined, the property is likely deleted, if it exists
// proceed to resolve the children
if (childrenPropertyDescriptor !== undefined) {
let childrenPropertyValue = childrenPropertyDescriptor.value;
invariant(childrenPropertyValue instanceof Value, `Bad "children" prop passed in JSXElement`);
let resolvedChildren = this._resolveDeeply(childrenPropertyValue, context, branchStatus, branchState);
childrenPropertyDescriptor.value = resolvedChildren;
}
}
}
return reactElement;
}
// we do not support "ref" on <Component /> ReactElements
if (!(refValue instanceof NullValue)) {
this._assignBailOutMessage(reactElement, `Bail-out: refs are not supported on <Components />`);
return reactElement;
}
if (!(propsValue instanceof ObjectValue || propsValue instanceof AbstractValue)) {
this._assignBailOutMessage(
reactElement,
`Bail-out: props on <Component /> was not not an ObjectValue or an AbstractValue`
);
return reactElement;
}
if (!(typeValue instanceof ECMAScriptSourceFunctionValue)) {
this._assignBailOutMessage(
reactElement,
`Bail-out: type on <Component /> was not a ECMAScriptSourceFunctionValue`
);
return reactElement;
}
try {
let { result } = this._renderAsDeepAsPossible(
typeValue,
propsValue,
context,
branchStatus === "NEW_BRANCH" ? "BRANCH" : branchStatus,
null
);
if (result instanceof UndefinedValue) {
this._assignBailOutMessage(reactElement, `Bail-out: undefined was returned from render`);
if (branchStatus === "NEW_BRANCH" && branchState) {
return branchState.captureBranchedValue(typeValue, reactElement);
}
return reactElement;
}
this.statistics.inlinedComponents++;
if (branchStatus === "NEW_BRANCH" && branchState) {
return branchState.captureBranchedValue(typeValue, result);
}
return result;
} catch (error) {
// assign a bail out message
if (error instanceof ExpectedBailOut) {
this._assignBailOutMessage(reactElement, "Bail-out: " + error.message);
} else if (error instanceof FatalError) {
this._assignBailOutMessage(reactElement, "Evaluation bail-out");
} else {
throw error;
}
// a child component bailed out during component folding, so return the function value and continue
if (branchStatus === "NEW_BRANCH" && branchState) {
return branchState.captureBranchedValue(typeValue, reactElement);
}
return reactElement;
}
} else {
throw new ExpectedBailOut("unsupported value type during reconcilation");
}
}
_assignBailOutMessage(reactElement: ObjectValue, message: string): void {
// $BailOutReason is a field on ObjectValue that allows us to specify a message
// that gets serialized as a comment node during the ReactElement serialization stage
if (reactElement.$BailOutReason !== undefined) {
// merge bail out messages if one already exists
reactElement.$BailOutReason += `, ${message}`;
} else {
reactElement.$BailOutReason = message;
}
}
_resolveFragment(
arrayValue: ArrayValue,
context: ObjectValue | AbstractValue,
branchStatus: BranchStatusEnum,
branchState: BranchState | null
) {
mapOverArrayValue(this.realm, arrayValue, (elementValue, elementPropertyDescriptor) => {
elementPropertyDescriptor.value = this._resolveDeeply(elementValue, context, branchStatus, branchState);
});
}
}

View File

@ -9,9 +9,21 @@
/* @flow */
import { Realm } from "../realm.js";
import type { BabelNode, BabelNodeJSXIdentifier } from "babel-types";
import { Value, ObjectValue, SymbolValue } from "../values/index.js";
import {
Value,
NumberValue,
ObjectValue,
SymbolValue,
FunctionValue,
StringValue,
ArrayValue,
} from "../values/index.js";
import { Get } from "../methods/index.js";
import { computeBinary } from "../evaluators/BinaryExpression.js";
import { type ReactSerializerState } from "../serializer/types.js";
import invariant from "../invariant.js";
export function isReactElement(val: Value): boolean {
if (val instanceof ObjectValue && val.properties.has("$$typeof")) {
@ -33,6 +45,35 @@ export function isReactComponent(name: string) {
return name.length > 0 && name[0] === name[0].toUpperCase();
}
export function valueIsClassComponent(realm: Realm, value: Value) {
if (!(value instanceof FunctionValue)) {
return false;
}
if (value.$Prototype instanceof ObjectValue) {
let prototype = Get(realm, value.$Prototype, "prototype");
if (prototype instanceof ObjectValue) {
return prototype.properties.has("isReactComponent");
}
}
return false;
}
export function addKeyToReactElement(
realm: Realm,
reactSerializerState: ReactSerializerState,
reactElement: ObjectValue
): void {
// we need to apply a key when we're branched
let currentKeyValue = Get(realm, reactElement, "key") || realm.intrinsics.null;
let uniqueKey = getUniqueReactElementKey("", reactSerializerState.usedReactElementKeys);
let newKeyValue = new StringValue(realm, uniqueKey);
if (currentKeyValue !== realm.intrinsics.null) {
newKeyValue = computeBinary(realm, "+", currentKeyValue, newKeyValue);
}
// TODO: This might not be safe in DEV because these objects are frozen (Object.freeze).
// We should probably go behind the scenes in this case to by-pass that.
reactElement.$Set("key", newKeyValue, reactElement);
}
// we create a unique key for each JSXElement to prevent collisions
// otherwise React will detect a missing/conflicting key at runtime and
// this can break the reconcilation of JSXElements in arrays
@ -47,3 +88,19 @@ export function getUniqueReactElementKey(index?: string, usedReactElementKeys: S
}
return key;
}
// a helper function to map over ArrayValues
export function mapOverArrayValue(realm: Realm, arrayValue: ArrayValue, mapFunc: Function): void {
let lengthValue = Get(realm, arrayValue, "length");
invariant(lengthValue instanceof NumberValue, "Invalid length on ArrayValue during reconcilation");
let length = lengthValue.value;
for (let i = 0; i < length; i++) {
let elementProperty = arrayValue.properties.get("" + i);
let elementPropertyDescriptor = elementProperty && elementProperty.descriptor;
invariant(elementPropertyDescriptor, `Invalid ArrayValue[${i}] descriptor`);
let elementValue = elementPropertyDescriptor.value;
if (elementValue instanceof Value) {
mapFunc(elementValue, elementPropertyDescriptor);
}
}
}

View File

@ -830,7 +830,13 @@ export class ResidualHeapSerializer {
let openingElement = t.jSXOpeningElement(identifier, (attributes: any), children.length === 0);
let closingElement = t.jSXClosingElement(identifier);
return t.jSXElement(openingElement, closingElement, children, children.length === 0);
let jsxElement = t.jSXElement(openingElement, closingElement, children, children.length === 0);
// if there has been a bail-out, we create an inline BlockComment node before the JSX element
if (val.$BailOutReason !== undefined) {
// $BailOutReason contains an optional string of what to print out in the comment
jsxElement.leadingComments = [({ type: "BlockComment", value: `${val.$BailOutReason}` }: any)];
}
return jsxElement;
}
_serializeValueMap(val: ObjectValue): BabelNodeExpression {

View File

@ -26,7 +26,8 @@ import {
import { Get } from "../methods/index.js";
import { ModuleTracer } from "./modules.js";
import buildTemplate from "babel-template";
import { type ReactSerializerState } from "./types";
import { ReactStatistics, type ReactSerializerState } from "./types";
import { Reconciler } from "../react/reconcilation.js";
import * as t from "babel-types";
export class Functions {
@ -109,16 +110,17 @@ export class Functions {
return recordedAdditionalFunctions;
}
checkReactRootComponents(react: ReactSerializerState): void {
checkReactRootComponents(statistics: ReactStatistics, react: ReactSerializerState): void {
let recordedReactRootComponents = this.__generateAdditionalFunctions("__reactComponentRoots");
// Get write effects of the components
for (let [funcValue] of recordedReactRootComponents) {
let reconciler = new Reconciler(this.realm, this.moduleTracer, statistics, react);
invariant(
funcValue instanceof ECMAScriptSourceFunctionValue,
"only ECMAScriptSourceFunctionValue function values are supported as React root components"
);
throw new FatalError("TODO: implement functional component folding");
this.writeEffects.set(funcValue, reconciler.render(funcValue));
}
}

View File

@ -117,7 +117,7 @@ export class Serializer {
let reactStatistics = null;
if (this.realm.react.enabled) {
reactStatistics = new ReactStatistics();
this.functions.checkReactRootComponents(this.react);
this.functions.checkReactRootComponents(reactStatistics, this.react);
}
if (this.options.initializeMoreModules) {

View File

@ -244,6 +244,9 @@ export default class ObjectValue extends ConcreteValue {
intrinsicNameGenerated: void | true;
hashValue: void | number;
// ReactElement
$BailOutReason: void | string;
equals(x: Value): boolean {
return x instanceof ObjectValue && this.getHash() === x.getHash();
}

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function MaybeShow(props) {
if (props.show) {
@ -19,7 +17,7 @@ function Override(props) {
});
}
function App(props/*: {show: boolean}*/) {
function App(props: {show: boolean}) {
return (
<Override overrideShow={props.show}>
<MaybeShow show={true}>
@ -40,8 +38,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function MaybeShow(props) {
if (props.show) {
@ -25,8 +23,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,14 +1,12 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function SubChild(props, context) {
return <span>The context title is: {context.title}</span>;
}
function Child(props: any, context/*: {title: string}*/) {
function Child(props: any, context: {title: string}) {
return <span><SubChild /></span>;
}
@ -49,8 +47,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(Child);
}
module.exports = App;

View File

@ -1,14 +1,12 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function Fn(props) {
return <div>Hello {props[props.dynamicKey]}</div>;
}
function App(props/*: {dynamicKey: string}*/) {
function App(props: {dynamicKey: string}) {
return <Fn foo="World" dynamicKey={props.dynamicKey} />;
}
@ -18,8 +16,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
// we can't use ES2015 classes in Prepack yet (they don't serialize)
// so we have to use ES5 instead
@ -32,7 +30,7 @@ var Stateful = (function (superclass) {
return Stateful;
}(React.Component));
function App(props/*: {switch: boolean}*/) {
function App(props: {switch: boolean}) {
if (props.switch) {
return (
<div>
@ -67,8 +65,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
// we can't use ES2015 classes in Prepack yet (they don't serialize)
// so we have to use ES5 instead
@ -39,7 +37,7 @@ function SettingsPane() {
return <div key='ha'><Stateful /></div>;
}
function App(props/*: {switch: boolean}*/) {
function App(props: {switch: boolean}) {
if (props.switch) {
return (
<div>
@ -74,8 +72,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function A(props) {
return 'Hello, ';
@ -25,8 +23,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function A() {
}
@ -26,8 +24,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,7 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function A(props) {
return props.children;
}
@ -23,8 +22,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
let refB = false;
@ -15,7 +13,7 @@ function A(foo) {
);
}
function App({rootRef}/*: {rootRef: Function}*/) {
function App({rootRef}: {rootRef: Function}) {
return (
<div>
<A rootRef={rootRef} />
@ -37,8 +35,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function A(props) {
return <div>Hello {props.x}</div>;
@ -27,14 +25,12 @@ function App() {
}
App.getTrials = function(renderer, Root) {
React.createElement("div")
renderer.update(<Root />);
return [['simple render', renderer.toJSON()]];
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -1,8 +1,6 @@
if (this.__createReactMock) {
var React = __createReactMock();
} else {
var React = require('react');
}
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
// we can't use ES2015 classes in Prepack yet (they don't serialize)
// so we have to use ES5 instead
@ -40,7 +38,7 @@ function SettingsPane() {
return <Stateful>Bye</Stateful>;
}
function App(props/*: {switch: boolean}*/) {
function App(props: {switch: boolean}) {
if (props.switch) {
return (
<div>
@ -75,8 +73,7 @@ App.getTrials = function(renderer, Root) {
};
if (this.__registerReactComponentRoot) {
// to be used when component folding is added in separate PR
// __registerReactComponentRoot(App);
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -0,0 +1,23 @@
var React = require('react');
// the JSX transform converts to React, so we need to add it back in
this['React'] = React;
function Foo() {
return <div>123</div>
}
function App(props: {yar: boolean}) {
return <div>{props.yar ? <Foo arg={1} /> : <Foo arg={2} />}</div>;
}
App.getTrials = function(renderer, Root) {
renderer.update(<Root />);
let childKey = renderer.toTree().rendered.props.children.key
return [['no added keys to child components', childKey]];
};
if (this.__registerReactComponentRoot) {
__registerReactComponentRoot(App);
}
module.exports = App;

View File

@ -6273,4 +6273,4 @@ yauzl@^2.2.1:
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2"
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.0.1"
fd-slicer "~1.0.1"