React/JSX code cleanup in preparation for React component folding

Summary:
Release note: none

This is one part of the component folding PRs (the original has been split up). This PR makes small changes to existing code and adds in:

- a new Prepack global hook `__registerReactComponentRoot` that works in a similar way to additional functions
- some tidy up and moving around of JSX/React code
- added `checkReactRootComponents` stub function in the functions serializer code that will be used in upcoming PRs
Closes https://github.com/facebook/prepack/pull/1117

Differential Revision: D6198875

Pulled By: trueadm

fbshipit-source-id: ea183e40b8e6519a88dc574a08804e9f706c5390
This commit is contained in:
Dominic Gannaway 2017-10-31 11:37:59 -07:00 committed by Facebook Github Bot
parent 4ca9ac0c4a
commit dc27e144e9
14 changed files with 141 additions and 71 deletions

View File

@ -24,7 +24,7 @@ import type {
BabelNodeJSXExpressionContainer,
} from "babel-types";
import { ArrayValue, StringValue, Value, NumberValue, ObjectValue, SymbolValue } from "../values/index.js";
import { convertJSXExpressionToIdentifier } from "../utils/jsx";
import { convertJSXExpressionToIdentifier } from "../react/jsx";
import * as t from "babel-types";
import {
Get,

View File

@ -126,7 +126,7 @@ export default function(realm: Realm): void {
configurable: true,
});
let uid = 0;
let additonalFunctionUid = 0;
// Allows dynamically registering additional functions.
// WARNING: these functions will get exposed at global scope and called there.
// NB: If we interpret one of these calls in an evaluateForEffects context
@ -143,7 +143,7 @@ export default function(realm: Realm): void {
realm.assignToGlobal(
t.memberExpression(
t.memberExpression(t.identifier("global"), t.identifier("__additionalFunctions")),
t.identifier("" + uid++)
t.identifier("" + additonalFunctionUid++)
),
functionValue
);
@ -155,6 +155,39 @@ export default function(realm: Realm): void {
configurable: true,
});
if (realm.react.enabled) {
global.$DefineOwnProperty("__reactComponentRoots", {
value: new ObjectValue(realm, realm.intrinsics.ObjectPrototype, "__reactComponentRoots", true),
writable: true,
enumerable: false,
configurable: true,
});
let reactComponentRootUid = 0;
// this is almost a copy of the additionalFunctions code above
global.$DefineOwnProperty("__registerReactComponentRoot", {
value: new NativeFunctionValue(
realm,
"global.__registerReactComponentRoot",
"__registerReactComponentRoot",
0,
(context, [functionValue]) => {
invariant(functionValue instanceof ECMAScriptSourceFunctionValue);
realm.assignToGlobal(
t.memberExpression(
t.memberExpression(t.identifier("global"), t.identifier("__reactComponentRoots")),
t.identifier("" + reactComponentRootUid++)
),
functionValue
);
return realm.intrinsics.undefined;
}
),
writable: true,
enumerable: false,
configurable: true,
});
}
// Maps from initialized moduleId to exports object
// NB: Changes to this shouldn't ever be serialized
global.$DefineOwnProperty("__initializedModules", {

View File

@ -20,20 +20,7 @@ import type {
BabelNodeMemberExpression,
} from "babel-types";
import invariant from "../invariant.js";
import { Value, ObjectValue, SymbolValue } from "../values/index.js";
import { Get } from "../methods/index.js";
export function isReactElement(val: Value): boolean {
if (val instanceof ObjectValue && val.properties.has("$$typeof")) {
let realm = val.$Realm;
let $$typeof = Get(realm, val, "$$typeof");
if ($$typeof instanceof SymbolValue) {
let symbolFromRegistry = realm.globalSymbolRegistry.find(e => e.$Symbol === $$typeof);
return symbolFromRegistry !== undefined && symbolFromRegistry.$Key === "react.element";
}
}
return false;
}
import { isReactComponent, getUniqueReactElementKey } from "./utils";
export function convertExpressionToJSXIdentifier(
expr: BabelNodeExpression,
@ -48,7 +35,7 @@ export function convertExpressionToJSXIdentifier(
invariant(
// ensure the 1st character of the string is uppercase
// for a component unless it is not the root
isRoot === false || (name.length > 0 && name[0] === name[0].toUpperCase()),
isRoot === false || isReactComponent(name),
"invalid JSXIdentifer from Identifier, Identifier name must be uppercase"
);
return t.jSXIdentifier(name);
@ -109,21 +96,6 @@ function addKeyToElement(astElement: BabelNodeJSXElement, key) {
}
}
// 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
function getUniqueJSXElementKey(index?: string, usedReactElementKeys: Set<string>) {
let key;
do {
key = Math.random().toString(36).replace(/[^a-z]+/g, "").substring(0, 2);
} while (usedReactElementKeys.has(key));
usedReactElementKeys.add(key);
if (index !== undefined) {
return `${key}${index}`;
}
return key;
}
export function applyKeysToNestedArray(
expr: BabelNodeArrayExpression,
isBase: boolean,
@ -137,20 +109,20 @@ export function applyKeysToNestedArray(
if (astElement != null) {
if (t.isJSXElement(astElement) && isBase === false) {
addKeyToElement((astElement: any), getUniqueJSXElementKey("" + i, usedReactElementKeys));
addKeyToElement((astElement: any), getUniqueReactElementKey("" + i, usedReactElementKeys));
} else if (t.isArrayExpression(astElement)) {
applyKeysToNestedArray((astElement: any), false, usedReactElementKeys);
} else if (astElement.type === "ConditionalExpression") {
let alternate = (astElement.alternate: any);
// it's common for conditions to be in an array, which means we need to check them for keys too
if (t.isJSXElement(alternate.type) && isBase === false) {
addKeyToElement(alternate, getUniqueJSXElementKey("0" + i, usedReactElementKeys));
addKeyToElement(alternate, getUniqueReactElementKey("0" + i, usedReactElementKeys));
} else if (t.isArrayExpression(alternate.type)) {
applyKeysToNestedArray(alternate, false, usedReactElementKeys);
}
let consequent = (astElement.consequent: any);
if (t.isJSXElement(consequent.type) && isBase === false) {
addKeyToElement(consequent, getUniqueJSXElementKey("1" + i, usedReactElementKeys));
addKeyToElement(consequent, getUniqueReactElementKey("1" + i, usedReactElementKeys));
} else if (t.isArrayExpression(consequent.type)) {
applyKeysToNestedArray(consequent, false, usedReactElementKeys);
}

49
src/react/utils.js Normal file
View File

@ -0,0 +1,49 @@
/**
* 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 { BabelNode, BabelNodeJSXIdentifier } from "babel-types";
import { Value, ObjectValue, SymbolValue } from "../values/index.js";
import { Get } from "../methods/index.js";
export function isReactElement(val: Value): boolean {
if (val instanceof ObjectValue && val.properties.has("$$typeof")) {
let realm = val.$Realm;
let $$typeof = Get(realm, val, "$$typeof");
if ($$typeof instanceof SymbolValue) {
let symbolFromRegistry = realm.globalSymbolRegistry.find(e => e.$Symbol === $$typeof);
return symbolFromRegistry !== undefined && symbolFromRegistry.$Key === "react.element";
}
}
return false;
}
export function isTagName(ast: BabelNode): boolean {
return ast.type === "JSXIdentifier" && /^[a-z]|\-/.test(((ast: any): BabelNodeJSXIdentifier).name);
}
export function isReactComponent(name: string) {
return name.length > 0 && name[0] === name[0].toUpperCase();
}
// 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
export function getUniqueReactElementKey(index?: string, usedReactElementKeys: Set<string>) {
let key;
do {
key = Math.random().toString(36).replace(/[^a-z]+/g, "").substring(0, 2);
} while (usedReactElementKeys.has(key));
usedReactElementKeys.add(key);
if (index !== undefined) {
return `${key}${index}`;
}
return key;
}

View File

@ -33,8 +33,8 @@ import {
convertExpressionToJSXIdentifier,
convertKeyValueToJSXAttribute,
applyKeysToNestedArray,
isReactElement,
} from "../utils/jsx";
} from "../react/jsx.js";
import { isReactElement } from "../react/utils.js";
import * as t from "babel-types";
import type {
BabelNodeArrayExpression,
@ -735,33 +735,17 @@ export class ResidualHeapSerializer {
return t.arrayExpression(initProperties);
}
_getPropertyValue(properties: Map<string, any>, key: string) {
if (properties.has(key)) {
let val = properties.get(key);
if (val !== undefined) {
let descriptor = val.descriptor;
invariant(!IsAccessorDescriptor(this.realm, descriptor), "expected descriptor to be a non-accessor property");
if (descriptor !== undefined) {
let descriptorValue = descriptor.value;
if (descriptorValue !== undefined) {
return descriptorValue;
}
}
}
}
return null;
}
_serializeValueReactElementChild(child: Value): BabelNode {
if (isReactElement(child)) {
// if we know it's a ReactElement, we add the value to the serializedValues
// and short cut to get back the JSX expression so we don't emit additional data
// we do this to ensure child JSXElements can get keys assigned if needed
this.serializedValues.add(child);
return this._serializeValueObject(((child: any): ObjectValue));
let reactChild = this._serializeValueObject(((child: any): ObjectValue));
if (reactChild.leadingComments !== null) {
return t.jSXExpressionContainer(reactChild);
}
return reactChild;
}
const expr = this.serializeValue(child);
@ -776,11 +760,10 @@ export class ResidualHeapSerializer {
}
_serializeValueReactElement(val: ObjectValue): BabelNodeExpression {
let objectProperties: Map<string, any> = val.properties;
let typeValue = this._getPropertyValue(objectProperties, "type");
let keyValue = this._getPropertyValue(objectProperties, "key");
let refValue = this._getPropertyValue(objectProperties, "ref");
let propsValue = this._getPropertyValue(objectProperties, "props");
let typeValue = Get(this.realm, val, "type");
let keyValue = Get(this.realm, val, "key");
let refValue = Get(this.realm, val, "ref");
let propsValue = Get(this.realm, val, "props");
invariant(typeValue !== null, "JSXElement type of null");
@ -818,12 +801,12 @@ export class ResidualHeapSerializer {
let childrenValue = desc.value;
if (childrenValue instanceof ArrayValue) {
this.serializedValues.add(childrenValue);
let childrenLength = this._getPropertyValue(childrenValue.properties, "length");
let childrenLength = Get(this.realm, childrenValue, "length");
let childrenLengthValue = 0;
if (childrenLength instanceof NumberValue) {
childrenLengthValue = childrenLength.value;
for (let i = 0; i < childrenLengthValue; i++) {
let child = this._getPropertyValue(childrenValue.properties, "" + i);
let child = Get(this.realm, childrenValue, "" + i);
if (child instanceof Value) {
children.push(this._serializeValueReactElementChild(child));
} else {

View File

@ -16,10 +16,17 @@ import invariant from "../invariant.js";
import { type Effects, type PropertyBindings, Realm } from "../realm.js";
import type { PropertyBinding } from "../types.js";
import { ignoreErrorsIn } from "../utils/errors.js";
import { AbstractObjectValue, FunctionValue, ObjectValue, AbstractValue } from "../values/index.js";
import {
AbstractObjectValue,
FunctionValue,
ObjectValue,
AbstractValue,
ECMAScriptSourceFunctionValue,
} from "../values/index.js";
import { Get } from "../methods/index.js";
import { ModuleTracer } from "./modules.js";
import buildTemplate from "babel-template";
import { type ReactSerializerState } from "./types";
import * as t from "babel-types";
export class Functions {
@ -69,11 +76,13 @@ export class Functions {
return calls;
}
_generateAdditionalFunctionCallsFromDirective(): Array<[FunctionValue, BabelNodeCallExpression]> {
// __reactComponentRoots
__generateAdditionalFunctions(globalKey: string) {
let recordedAdditionalFunctions: Map<FunctionValue, string> = new Map();
let realm = this.realm;
let globalRecordedAdditionalFunctionsMap = this.moduleTracer.modules.logger.tryQuery(
() => Get(realm, realm.$GlobalObject, "__additionalFunctions"),
() => Get(realm, realm.$GlobalObject, globalKey),
realm.intrinsics.undefined,
false
);
@ -97,6 +106,24 @@ export class Functions {
recordedAdditionalFunctions.set(funcValue, funcId);
}
}
return recordedAdditionalFunctions;
}
checkReactRootComponents(react: ReactSerializerState): void {
let recordedReactRootComponents = this.__generateAdditionalFunctions("__reactComponentRoots");
// Get write effects of the components
for (let [funcValue] of recordedReactRootComponents) {
invariant(
funcValue instanceof ECMAScriptSourceFunctionValue,
"only ECMAScriptSourceFunctionValue function values are supported as React root components"
);
throw new FatalError("TODO: implement functional component folding");
}
}
_generateAdditionalFunctionCallsFromDirective(): Array<[FunctionValue, BabelNodeCallExpression]> {
let recordedAdditionalFunctions = this.__generateAdditionalFunctions("__additionalFunctions");
// The additional functions we registered at runtime are recorded at:
// global.__additionalFunctions.id
@ -127,6 +154,9 @@ export class Functions {
// This may throw a FatalError if there is an unrecoverable error in the called function
// When that happens we cannot prepack the bundle.
// There may also be warnings reported for errors that happen inside imported modules that can be postponed.
// TODO check if the function is a React component
// for now we do this on all functions
let e = this.realm.evaluateNodeForEffectsInGlobalEnv(call, this.moduleTracer);
this.writeEffects.set(funcValue, e);
}

View File

@ -113,6 +113,9 @@ export class Serializer {
if (this.logger.hasErrors()) return undefined;
this.modules.resolveInitializedModules();
this.functions.checkThatFunctionsAreIndependent();
if (this.realm.react.enabled) {
this.functions.checkReactRootComponents(this.react);
}
if (this.options.initializeMoreModules) {
if (timingStats !== undefined) timingStats.initializeMoreModulesTime = Date.now();

View File

@ -12,7 +12,7 @@
import { Realm } from "../realm.js";
import { FunctionValue } from "../values/index.js";
import * as t from "babel-types";
import { convertExpressionToJSXIdentifier } from "../utils/jsx";
import { convertExpressionToJSXIdentifier } from "../react/jsx";
import type { BabelNodeExpression, BabelNodeCallExpression, BabelNodeFunctionExpression } from "babel-types";
import type { BabelTraversePath } from "babel-traverse";
import type { FunctionBodyAstNode } from "../types.js";

View File

@ -34,7 +34,7 @@ import {
UndefinedValue,
Value,
} from "./index.js";
import { isReactElement } from "../utils/jsx";
import { isReactElement } from "../react/utils.js";
import type { ECMAScriptSourceFunctionValue, NativeFunctionCallback } from "./index.js";
import {
joinValuesAsConditional,