diff --git a/scripts/test-react.js b/scripts/test-react.js index 22165f3ce..e24a04b2c 100644 --- a/scripts/test-react.js +++ b/scripts/test-react.js @@ -22,7 +22,7 @@ let { expect, describe, it } = global; function runTestSuite(outputJsx) { let reactTestRoot = path.join(__dirname, "../test/react/"); let prepackOptions = { - compatibility: "react-mocks", + compatibility: "fb-www", internalDebug: true, serialize: true, uniqueSuffix: "", @@ -57,8 +57,20 @@ function runTestSuite(outputJsx) { let moduleShim = { exports: null }; let requireShim = name => { switch (name) { + case "React": case "react": return React; + case "RelayModern": + return { + QueryRenderer(props) { + return props.render({ props: {}, error: null }); + }, + graphql() { + return null; + }, + }; + case "FBEnvironment": + return {}; default: throw new Error(`Unrecognized import: "${name}".`); } @@ -230,6 +242,14 @@ function runTestSuite(outputJsx) { await runTest(directory, "classes-with-state.js"); }); }); + + describe("fb-www mocks", () => { + let directory = "mocks"; + + it("fb-www", async () => { + await runTest(directory, "fb1.js"); + }); + }); }); } diff --git a/src/globals.js b/src/globals.js index 609947bdd..d8627769f 100644 --- a/src/globals.js +++ b/src/globals.js @@ -13,14 +13,14 @@ import type { Realm } from "./realm.js"; import initializePrepackGlobals from "./intrinsics/prepack/global.js"; import initializeDOMGlobals from "./intrinsics/dom/global.js"; import initializeReactNativeGlobals from "./intrinsics/react-native/global.js"; -import initializeReactMocks from "./intrinsics/react-mocks/global.js"; +import initializeReactMocks from "./intrinsics/fb-www/global.js"; export default function(realm: Realm): Realm { initializePrepackGlobals(realm); if (realm.isCompatibleWith("browser")) { initializeDOMGlobals(realm); } - if (realm.isCompatibleWith("react-mocks")) { + if (realm.isCompatibleWith("fb-www")) { initializeDOMGlobals(realm); initializeReactMocks(realm); } diff --git a/src/intrinsics/react-mocks/global.js b/src/intrinsics/fb-www/global.js similarity index 66% rename from src/intrinsics/react-mocks/global.js rename to src/intrinsics/fb-www/global.js index 0b1a93c70..24099e035 100644 --- a/src/intrinsics/react-mocks/global.js +++ b/src/intrinsics/fb-www/global.js @@ -12,7 +12,8 @@ import type { Realm } from "../../realm.js"; import { AbstractValue, NativeFunctionValue, Value, StringValue } from "../../values/index.js"; import buildExpressionTemplate from "../../utils/builder.js"; -import { createMockReact } from "./mocks.js"; +import { createMockReact } from "./react-mocks.js"; +import { createMockReactRelay } from "./relay-mocks.js"; import invariant from "../../invariant"; export default function(realm: Realm): void { @@ -31,13 +32,22 @@ export default function(realm: Realm): void { global.$DefineOwnProperty("require", { value: new NativeFunctionValue(realm, "global.require", "require", 0, (context, [requireNameVal]) => { invariant(requireNameVal instanceof StringValue); - if (requireNameVal.value === "react" || requireNameVal.value === "React") { - if (realm.react.reactLibraryObject === undefined) { - let reactLibraryObject = createMockReact(realm); - realm.react.reactLibraryObject = reactLibraryObject; - return reactLibraryObject; + let requireNameValValue = requireNameVal.value; + + if (requireNameValValue === "react" || requireNameValValue === "React") { + if (realm.fbLibraries.react === undefined) { + let react = createMockReact(realm, requireNameValValue); + realm.fbLibraries.react = react; + return react; } - return realm.react.reactLibraryObject; + return realm.fbLibraries.react; + } else if (requireNameValValue === "react-relay" || requireNameValValue === "RelayModern") { + if (realm.fbLibraries.reactRelay === undefined) { + let reactRelay = createMockReactRelay(realm, requireNameValValue); + realm.fbLibraries.reactRelay = reactRelay; + return reactRelay; + } + return realm.fbLibraries.reactRelay; } let requireName = `require("${requireNameVal.value}")`; let type = Value.getTypeFromName("function"); diff --git a/src/intrinsics/react-mocks/mocks.js b/src/intrinsics/fb-www/react-mocks.js similarity index 91% rename from src/intrinsics/react-mocks/mocks.js rename to src/intrinsics/fb-www/react-mocks.js index a831644ac..f230bf506 100644 --- a/src/intrinsics/react-mocks/mocks.js +++ b/src/intrinsics/fb-www/react-mocks.js @@ -251,7 +251,7 @@ let reactCode = ` `; let reactAst = parseExpression(reactCode, { plugins: ["flow"] }); -export function createMockReact(realm: Realm): ObjectValue { +export function createMockReact(realm: Realm, reactRequireName: string): ObjectValue { let reactFactory = Environment.GetValue(realm, realm.$GlobalEnv.evaluate(reactAst, false)); invariant(reactFactory instanceof ECMAScriptSourceFunctionValue); @@ -268,35 +268,35 @@ export function createMockReact(realm: Realm): ObjectValue { getReactSymbol("react.symbol", realm), currentOwner, ]); - reactValue.intrinsicName = `require("react")`; + reactValue.intrinsicName = `require("${reactRequireName}")`; invariant(reactValue instanceof ObjectValue); let reactComponentValue = Get(realm, reactValue, "Component"); - reactComponentValue.intrinsicName = `require("react").Component`; + reactComponentValue.intrinsicName = `require("${reactRequireName}").Component`; invariant(reactComponentValue instanceof ECMAScriptFunctionValue); let reactPureComponentValue = Get(realm, reactValue, "PureComponent"); - reactPureComponentValue.intrinsicName = `require("react").PureComponent`; + reactPureComponentValue.intrinsicName = `require("${reactRequireName}").PureComponent`; invariant(reactPureComponentValue instanceof ECMAScriptFunctionValue); reactComponentValue.$FunctionKind = "normal"; invariant(reactComponentValue instanceof ObjectValue); let reactComponentPrototypeValue = Get(realm, reactComponentValue, "prototype"); - reactComponentPrototypeValue.intrinsicName = `require("react").Component.prototype`; + reactComponentPrototypeValue.intrinsicName = `require("${reactRequireName}").Component.prototype`; let reactPureComponentPrototypeValue = Get(realm, reactPureComponentValue, "prototype"); - reactPureComponentPrototypeValue.intrinsicName = `require("react").PureComponent.prototype`; + reactPureComponentPrototypeValue.intrinsicName = `require("${reactRequireName}").PureComponent.prototype`; let reactCloneElementValue = Get(realm, reactValue, "cloneElement"); - reactCloneElementValue.intrinsicName = `require("react").cloneElement`; + reactCloneElementValue.intrinsicName = `require("${reactRequireName}").cloneElement`; let reactCreateElementValue = Get(realm, reactValue, "createElement"); - reactCreateElementValue.intrinsicName = `require("react").createElement`; + reactCreateElementValue.intrinsicName = `require("${reactRequireName}").createElement`; let reactIsValidElementValue = Get(realm, reactValue, "isValidElement"); - reactIsValidElementValue.intrinsicName = `require("react").isValidElement`; + reactIsValidElementValue.intrinsicName = `require("${reactRequireName}").isValidElement`; let reactChildrenValue = Get(realm, reactValue, "Children"); - reactChildrenValue.intrinsicName = `require("react").Children`; + reactChildrenValue.intrinsicName = `require("${reactRequireName}").Children`; return reactValue; } diff --git a/src/intrinsics/fb-www/relay-mocks.js b/src/intrinsics/fb-www/relay-mocks.js new file mode 100644 index 000000000..50985dd7f --- /dev/null +++ b/src/intrinsics/fb-www/relay-mocks.js @@ -0,0 +1,33 @@ +/** + * 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 type { Realm } from "../../realm.js"; +import { ObjectValue } from "../../values/index.js"; +import { Create } from "../../singletons.js"; +import { createAbstract } from "../prepack/utils.js"; + +export function createMockReactRelay(realm: Realm, relayRequireName: string): ObjectValue { + let reactRelay = new ObjectValue(realm, realm.intrinsics.ObjectPrototype, `require("${relayRequireName}")`, true); + // for QueryRenderer, we want to leave the component alone but process it's "render" prop + let queryRendererComponent = createAbstract(realm, "function", `require("${relayRequireName}").QueryRenderer`); + Create.CreateDataPropertyOrThrow(realm, reactRelay, "QueryRenderer", queryRendererComponent); + + let graphql = createAbstract(realm, "function", `require("${relayRequireName}").graphql`); + Create.CreateDataPropertyOrThrow(realm, reactRelay, "graphql", graphql); + + let createFragmentContainer = createAbstract( + realm, + "function", + `require("${relayRequireName}").createFragmentContainer` + ); + Create.CreateDataPropertyOrThrow(realm, reactRelay, "createFragmentContainer", createFragmentContainer); + return reactRelay; +} diff --git a/src/options.js b/src/options.js index ef0a8eea9..760bee9b4 100644 --- a/src/options.js +++ b/src/options.js @@ -11,15 +11,8 @@ import type { ErrorHandler } from "./errors.js"; -export type Compatibility = "browser" | "jsc-600-1-4-17" | "mobile" | "node-source-maps" | "node-cli" | "react-mocks"; -export const CompatibilityValues = [ - "browser", - "jsc-600-1-4-17", - "mobile", - "node-source-maps", - "node-cli", - "react-mocks", -]; +export type Compatibility = "browser" | "jsc-600-1-4-17" | "mobile" | "node-source-maps" | "node-cli" | "fb-www"; +export const CompatibilityValues = ["browser", "jsc-600-1-4-17", "mobile", "node-source-maps", "node-cli", "fb-www"]; export type ReactOutputTypes = "create-element" | "jsx"; export type RealmOptions = { diff --git a/src/react/branching.js b/src/react/branching.js index 8de8bae8e..9f4694bc3 100644 --- a/src/react/branching.js +++ b/src/react/branching.js @@ -25,6 +25,7 @@ import { import { type ReactSerializerState } from "../serializer/types.js"; import { isReactElement, addKeyToReactElement, mapOverArrayValue } from "./utils"; import { ExpectedBailOut } from "./errors.js"; +import invariant from "../invariant"; // 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 @@ -90,7 +91,8 @@ export class BranchState { } } } - captureBranchedValue(type: StringValue | ECMAScriptSourceFunctionValue, value: Value): Value { + captureBranchedValue(type: Value, value: Value): Value { + invariant(type instanceof ECMAScriptSourceFunctionValue || type instanceof StringValue); this._branchesToValidate.push({ type, value }); return value; } diff --git a/src/react/components.js b/src/react/components.js index 31e293ba5..41f5e535e 100644 --- a/src/react/components.js +++ b/src/react/components.js @@ -29,16 +29,21 @@ const lifecycleMethods = new Set([ "componentWillReceiveProps", ]); -export function getInitialProps(realm: Realm, componentType: ECMAScriptSourceFunctionValue): AbstractObjectValue { +export function getInitialProps( + realm: Realm, + componentType: ECMAScriptSourceFunctionValue | null +): AbstractObjectValue { let propsName = null; - if (valueIsClassComponent(realm, componentType)) { - propsName = "this.props"; - } 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; + if (componentType !== null) { + if (valueIsClassComponent(realm, componentType)) { + propsName = "this.props"; + } 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; + } } } } diff --git a/src/react/reconcilation.js b/src/react/reconcilation.js index 02a4c7c94..a7b4d2747 100644 --- a/src/react/reconcilation.js +++ b/src/react/reconcilation.js @@ -33,6 +33,8 @@ import { BranchState, type BranchStatusEnum } from "./branching.js"; import { getInitialProps, getInitialContext, createClassInstance, createSimpleClassInstance } from "./components.js"; import { ExpectedBailOut, SimpleClassBailOut } from "./errors.js"; +type RenderStrategy = "NORMAL" | "RELAY_QUERY_RENDERER"; + export class Reconciler { constructor( realm: Realm, @@ -149,13 +151,26 @@ export class Reconciler { return componentType.$Call(this.realm.intrinsics.undefined, [props, context]); } + _renderRelayQueryRendererComponent( + reactElement: ObjectValue, + props: ObjectValue | AbstractObjectValue, + context: ObjectValue | AbstractObjectValue + ) { + // TODO: for now we do nothing, in the future we want to evaluate the render prop of this component + return { + result: reactElement, + childContext: context, + }; + } + _renderComponent( - componentType: ECMAScriptSourceFunctionValue, + componentType: Value, props: ObjectValue | AbstractObjectValue, context: ObjectValue | AbstractObjectValue, branchStatus: BranchStatusEnum, branchState: BranchState | null ) { + invariant(componentType instanceof ECMAScriptSourceFunctionValue); let value; let childContext = context; @@ -207,6 +222,17 @@ export class Reconciler { }; } + _getRenderStrategy(func: Value): RenderStrategy { + // check if it's a ReactRelay.QueryRenderer + if (this.realm.fbLibraries.reactRelay !== undefined) { + let QueryRenderer = Get(this.realm, this.realm.fbLibraries.reactRelay, "QueryRenderer"); + if (func === QueryRenderer) { + return "RELAY_QUERY_RENDERER"; + } + } + return "NORMAL"; + } + _resolveDeeply( value: Value, context: ObjectValue | AbstractObjectValue, @@ -275,7 +301,9 @@ export class Reconciler { ); return reactElement; } - if (!(typeValue instanceof ECMAScriptSourceFunctionValue)) { + let renderStrategy = this._getRenderStrategy(typeValue); + + if (renderStrategy === "NORMAL" && !(typeValue instanceof ECMAScriptSourceFunctionValue)) { this._assignBailOutMessage( reactElement, `Bail-out: type on was not a ECMAScriptSourceFunctionValue` @@ -283,13 +311,28 @@ export class Reconciler { return reactElement; } try { - let { result } = this._renderComponent( - typeValue, - propsValue, - context, - branchStatus === "NEW_BRANCH" ? "BRANCH" : branchStatus, - null - ); + let result; + switch (renderStrategy) { + case "NORMAL": { + let render = this._renderComponent( + typeValue, + propsValue, + context, + branchStatus === "NEW_BRANCH" ? "BRANCH" : branchStatus, + null + ); + result = render.result; + break; + } + case "RELAY_QUERY_RENDERER": { + let render = this._renderRelayQueryRendererComponent(reactElement, propsValue, context); + result = render.result; + break; + } + default: + invariant(false, "unsupported render strategy"); + } + if (result instanceof UndefinedValue) { this._assignBailOutMessage(reactElement, `Bail-out: undefined was returned from render`); if (branchStatus === "NEW_BRANCH" && branchState) { diff --git a/src/react/utils.js b/src/react/utils.js index 344066a6a..4a77712c6 100644 --- a/src/react/utils.js +++ b/src/react/utils.js @@ -97,7 +97,7 @@ export function valueIsClassComponent(realm: Realm, value: Value): boolean { // logger isn't typed otherwise it will increase flow cycle length :() export function valueIsReactLibraryObject(realm: Realm, value: ObjectValue, logger: any): boolean { - if (realm.react.reactLibraryObject === value) { + if (realm.fbLibraries.react === value) { return true; } // we check that the object is the React or React-like library by checking for diff --git a/src/realm.js b/src/realm.js index 20d60409e..7d6a4cfab 100644 --- a/src/realm.js +++ b/src/realm.js @@ -176,11 +176,18 @@ export class Realm { output: opts.reactOutput || "create-element", symbols: new Map(), currentOwner: undefined, - reactLibraryObject: undefined, hoistableReactElements: new WeakMap(), hoistableFunctions: new WeakMap(), }; + this.fbLibraries = { + react: undefined, + reactRelay: undefined, + cx: undefined, + fbt: undefined, + jsResource: undefined, + }; + this.errorHandler = opts.errorHandler; this.globalSymbolRegistry = []; @@ -227,11 +234,18 @@ export class Realm { output?: ReactOutputTypes, symbols: Map, currentOwner?: ObjectValue, - reactLibraryObject?: ObjectValue, hoistableReactElements: WeakMap, hoistableFunctions: WeakMap, }; + fbLibraries: { + react: void | ObjectValue, + reactRelay: void | ObjectValue, + cx: void | ObjectValue, + fbt: void | ObjectValue, + jsResource: void | ObjectValue, + }; + $GlobalObject: ObjectValue | AbstractObjectValue; compatibility: Compatibility; diff --git a/src/serializer/ResidualHeapVisitor.js b/src/serializer/ResidualHeapVisitor.js index b4cf9e071..fbc0652fe 100644 --- a/src/serializer/ResidualHeapVisitor.js +++ b/src/serializer/ResidualHeapVisitor.js @@ -606,7 +606,7 @@ export class ResidualHeapVisitor { this.logger.logError(val, `Arguments object is not supported in residual heap.`); } if (this.realm.react.enabled && valueIsReactLibraryObject(this.realm, val, this.logger)) { - this.realm.react.reactLibraryObject = val; + this.realm.fbLibraries.react = val; } return; } @@ -920,7 +920,7 @@ export class ResidualHeapVisitor { _visitReactLibrary() { // find and visit the React library - let reactLibraryObject = this.realm.react.reactLibraryObject; + let reactLibraryObject = this.realm.fbLibraries.react; if (this.realm.react.output === "jsx") { // React might not be defined in scope, i.e. another library is using JSX // we don't throw an error as we should support JSX stand-alone diff --git a/src/serializer/ResidualReactElements.js b/src/serializer/ResidualReactElements.js index 55fd671f1..9e4490e23 100644 --- a/src/serializer/ResidualReactElements.js +++ b/src/serializer/ResidualReactElements.js @@ -125,7 +125,7 @@ export class ResidualReactElements { } } } - let reactLibraryObject = this.realm.react.reactLibraryObject; + let reactLibraryObject = this.realm.fbLibraries.react; let shouldHoist = this.residualHeapSerializer.currentFunctionBody !== this.residualHeapSerializer.mainBody && canHoistReactElement(this.realm, val); diff --git a/test/react/mocks/fb1.js b/test/react/mocks/fb1.js new file mode 100644 index 000000000..0b5a51f0e --- /dev/null +++ b/test/react/mocks/fb1.js @@ -0,0 +1,32 @@ +var React = require('React'); +// the JSX transform converts to React, so we need to add it back in +this['React'] = React; +var {QueryRenderer, graphql} = require('RelayModern'); + +var FBEnvironment = require('FBEnvironment'); + +function App({ initialNumComments, someVariables, query, pageSize, onCommit }) { + return ( + { + return Hello world + }} + /> + ); +} + +App.getTrials = function(renderer, Root) { + renderer.update(); + return [['fb1 mocks', renderer.toJSON()]]; +}; + +if (this.__registerReactComponentRoot) { + __registerReactComponentRoot(App); +} + +module.exports = App; diff --git a/website/js/repl.js b/website/js/repl.js index 751dd2c16..d24598744 100644 --- a/website/js/repl.js +++ b/website/js/repl.js @@ -40,7 +40,7 @@ var optionsConfig = [ { type: "choice", name: "compatibility", - choices: ["browser", "jsc-600-1-4-17", "node-source-maps", "node-cli", "react-mocks"], + choices: ["browser", "jsc-600-1-4-17", "node-source-maps", "node-cli", "fb-wwww"], defaultVal: "browser", description: "The target environment for Prepack" },