diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json index 757cfc38a..f329b14aa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/detect_bad_deps.js b/scripts/detect_bad_deps.js index 23583e2c9..10a9d8c14 100644 --- a/scripts/detect_bad_deps.js +++ b/scripts/detect_bad_deps.js @@ -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); diff --git a/scripts/test-react.js b/scripts/test-react.js index 5cbcd30a7..b3c530465 100644 --- a/scripts/test-react.js +++ b/scripts/test-react.js @@ -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"); }); diff --git a/src/flow/abstractObjectFactories.js b/src/flow/abstractObjectFactories.js new file mode 100644 index 000000000..d0aafcac2 --- /dev/null +++ b/src/flow/abstractObjectFactories.js @@ -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; +} diff --git a/src/flow/utils.js b/src/flow/utils.js index d5e7d5ff5..c0fb923ef 100644 --- a/src/flow/utils.js +++ b/src/flow/utils.js @@ -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) diff --git a/src/intrinsics/react-mocks/global.js b/src/intrinsics/react-mocks/global.js index 52ca7ad4a..35e5c4aeb 100644 --- a/src/intrinsics/react-mocks/global.js +++ b/src/intrinsics/react-mocks/global.js @@ -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, diff --git a/src/intrinsics/react-mocks/mocks.js b/src/intrinsics/react-mocks/mocks.js index a10ca533b..db22387a9 100644 --- a/src/intrinsics/react-mocks/mocks.js +++ b/src/intrinsics/react-mocks/mocks.js @@ -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; } diff --git a/src/react/branching.js b/src/react/branching.js new file mode 100644 index 000000000..628f2eac1 --- /dev/null +++ b/src/react/branching.js @@ -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, + }>; +} diff --git a/src/react/reconcilation.js b/src/react/reconcilation.js new file mode 100644 index 000000000..bdd404cb0 --- /dev/null +++ b/src/react/reconcilation.js @@ -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 = () =>
Hello world
+ 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 ReactElements + if (!(refValue instanceof NullValue)) { + this._assignBailOutMessage(reactElement, `Bail-out: refs are not supported on `); + return reactElement; + } + if (!(propsValue instanceof ObjectValue || propsValue instanceof AbstractValue)) { + this._assignBailOutMessage( + reactElement, + `Bail-out: props on was not not an ObjectValue or an AbstractValue` + ); + return reactElement; + } + if (!(typeValue instanceof ECMAScriptSourceFunctionValue)) { + this._assignBailOutMessage( + reactElement, + `Bail-out: type on 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); + }); + } +} diff --git a/src/react/utils.js b/src/react/utils.js index 8a4aa17c0..3f82552a7 100644 --- a/src/react/utils.js +++ b/src/react/utils.js @@ -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); + } + } +} diff --git a/src/serializer/ResidualHeapSerializer.js b/src/serializer/ResidualHeapSerializer.js index 92d05b09e..95f0305a7 100644 --- a/src/serializer/ResidualHeapSerializer.js +++ b/src/serializer/ResidualHeapSerializer.js @@ -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 { diff --git a/src/serializer/functions.js b/src/serializer/functions.js index 71eb39b38..998ba094b 100644 --- a/src/serializer/functions.js +++ b/src/serializer/functions.js @@ -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)); } } diff --git a/src/serializer/serializer.js b/src/serializer/serializer.js index 79b6da2c5..414950288 100644 --- a/src/serializer/serializer.js +++ b/src/serializer/serializer.js @@ -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) { diff --git a/src/values/ObjectValue.js b/src/values/ObjectValue.js index 1614a39cb..4d46607b8 100644 --- a/src/values/ObjectValue.js +++ b/src/values/ObjectValue.js @@ -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(); } diff --git a/test/react/functional-components/clone-element.js b/test/react/functional-components/clone-element.js index 5a4459f92..1a44c14c4 100644 --- a/test/react/functional-components/clone-element.js +++ b/test/react/functional-components/clone-element.js @@ -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 ( @@ -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; diff --git a/test/react/functional-components/conditional.js b/test/react/functional-components/conditional.js index 4edbe2204..9e20f8067 100644 --- a/test/react/functional-components/conditional.js +++ b/test/react/functional-components/conditional.js @@ -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; diff --git a/test/react/functional-components/dynamic-context.js b/test/react/functional-components/dynamic-context.js index f0c4d699e..0268484de 100644 --- a/test/react/functional-components/dynamic-context.js +++ b/test/react/functional-components/dynamic-context.js @@ -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 The context title is: {context.title}; } -function Child(props: any, context/*: {title: string}*/) { +function Child(props: any, context: {title: string}) { return ; } @@ -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; diff --git a/test/react/functional-components/dynamic-props.js b/test/react/functional-components/dynamic-props.js index 5977dbf35..79be24b8c 100644 --- a/test/react/functional-components/dynamic-props.js +++ b/test/react/functional-components/dynamic-props.js @@ -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
Hello {props[props.dynamicKey]}
; } -function App(props/*: {dynamicKey: string}*/) { +function App(props: {dynamicKey: string}) { return ; } @@ -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; diff --git a/test/react/functional-components/key-change.js b/test/react/functional-components/key-change.js index 8a19934d3..285f52847 100644 --- a/test/react/functional-components/key-change.js +++ b/test/react/functional-components/key-change.js @@ -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 (
@@ -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; diff --git a/test/react/functional-components/key-nesting.js b/test/react/functional-components/key-nesting.js index fe236e4fb..8a79499be 100644 --- a/test/react/functional-components/key-nesting.js +++ b/test/react/functional-components/key-nesting.js @@ -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
; } -function App(props/*: {switch: boolean}*/) { +function App(props: {switch: boolean}) { if (props.switch) { return (
@@ -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; diff --git a/test/react/functional-components/return-text.js b/test/react/functional-components/return-text.js index caad125bc..bf851c913 100644 --- a/test/react/functional-components/return-text.js +++ b/test/react/functional-components/return-text.js @@ -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; diff --git a/test/react/functional-components/return-undefined.js b/test/react/functional-components/return-undefined.js index b11e54fd5..72a8d6395 100644 --- a/test/react/functional-components/return-undefined.js +++ b/test/react/functional-components/return-undefined.js @@ -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; diff --git a/test/react/functional-components/simple-children.js b/test/react/functional-components/simple-children.js index 790813ac3..9985b91fa 100644 --- a/test/react/functional-components/simple-children.js +++ b/test/react/functional-components/simple-children.js @@ -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; diff --git a/test/react/functional-components/simple-refs.js b/test/react/functional-components/simple-refs.js index 88430a079..685e859a2 100644 --- a/test/react/functional-components/simple-refs.js +++ b/test/react/functional-components/simple-refs.js @@ -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 (
@@ -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; diff --git a/test/react/functional-components/simple.js b/test/react/functional-components/simple.js index cef3a220b..604f653ab 100644 --- a/test/react/functional-components/simple.js +++ b/test/react/functional-components/simple.js @@ -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 {props.x}
; @@ -27,14 +25,12 @@ function App() { } App.getTrials = function(renderer, Root) { - React.createElement("div") renderer.update(); 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; \ No newline at end of file diff --git a/test/react/functional-components/type-change.js b/test/react/functional-components/type-change.js index c5f8763e6..20812a1b5 100644 --- a/test/react/functional-components/type-change.js +++ b/test/react/functional-components/type-change.js @@ -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 Bye; } -function App(props/*: {switch: boolean}*/) { +function App(props: {switch: boolean}) { if (props.switch) { return (
@@ -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; diff --git a/test/react/functional-components/type-same.js b/test/react/functional-components/type-same.js new file mode 100644 index 000000000..7e723707f --- /dev/null +++ b/test/react/functional-components/type-same.js @@ -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
123
+} + +function App(props: {yar: boolean}) { + return
{props.yar ? : }
; +} + +App.getTrials = function(renderer, Root) { + renderer.update(); + let childKey = renderer.toTree().rendered.props.children.key + return [['no added keys to child components', childKey]]; +}; + +if (this.__registerReactComponentRoot) { + __registerReactComponentRoot(App); +} + +module.exports = App; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e9c6f0b07..1d79fc557 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" \ No newline at end of file