Changes to React mocks for internal testing

Summary:
Release notes: none

This PR changes `react-mocks` -> `fb-www` and adds some `RelayModern` mocks. This is mainly for internal testing to unblock work.
Closes https://github.com/facebook/prepack/pull/1347

Differential Revision: D6798242

Pulled By: trueadm

fbshipit-source-id: 9c8a4aeef074e2a29e44fdd26352633f47bbc862
This commit is contained in:
Dominic Gannaway 2018-01-24 11:23:51 -08:00 committed by Facebook Github Bot
parent 577694ceb7
commit b891ee1166
15 changed files with 207 additions and 55 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <Component /> 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) {

View File

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

View File

@ -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<ReactSymbolTypes, SymbolValue>,
currentOwner?: ObjectValue,
reactLibraryObject?: ObjectValue,
hoistableReactElements: WeakMap<ObjectValue, boolean>,
hoistableFunctions: WeakMap<FunctionValue, boolean>,
};
fbLibraries: {
react: void | ObjectValue,
reactRelay: void | ObjectValue,
cx: void | ObjectValue,
fbt: void | ObjectValue,
jsResource: void | ObjectValue,
};
$GlobalObject: ObjectValue | AbstractObjectValue;
compatibility: Compatibility;

View File

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

View File

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

32
test/react/mocks/fb1.js Normal file
View File

@ -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 (
<QueryRenderer
environment={FBEnvironment}
query={graphql`
${query}
`}
variables={someVariables}
render={({error, props}) => {
return <span>Hello world</span>
}}
/>
);
}
App.getTrials = function(renderer, Root) {
renderer.update(<Root />);
return [['fb1 mocks', renderer.toJSON()]];
};
if (this.__registerReactComponentRoot) {
__registerReactComponentRoot(App);
}
module.exports = App;

View File

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