diff --git a/src/react/components.js b/src/react/components.js index 6b572d7ab..7e8e304cf 100644 --- a/src/react/components.js +++ b/src/react/components.js @@ -34,9 +34,10 @@ import { Get, Construct } from "../methods/index.js"; import { Properties } from "../singletons.js"; import invariant from "../invariant.js"; import traverse from "@babel/traverse"; -import type { ClassComponentMetadata } from "../types.js"; +import type { ClassComponentMetadata, ReactComponentTreeConfig } from "../types.js"; import type { ReactEvaluatedNode } from "../serializer/types.js"; import { FatalError } from "../errors.js"; +import { type ComponentModel, ShapeInformation } from "../utils/ShapeInformation.js"; const lifecycleMethods = new Set([ "componentWillUnmount", @@ -52,8 +53,11 @@ const whitelistedProperties = new Set(["props", "context", "refs", "setState"]); export function getInitialProps( realm: Realm, - componentType: ECMAScriptSourceFunctionValue | null + componentType: ECMAScriptSourceFunctionValue | null, + { modelString }: ReactComponentTreeConfig ): AbstractObjectValue { + let componentModel = modelString !== undefined ? (JSON.parse(modelString): ComponentModel) : undefined; + let shape = ShapeInformation.createForReactComponentProps(componentModel); let propsName = null; if (componentType !== null) { if (valueIsClassComponent(realm, componentType)) { @@ -68,11 +72,11 @@ export function getInitialProps( } } } - let value = AbstractValue.createAbstractObject(realm, propsName || "props"); - invariant(value instanceof AbstractObjectValue); - flagPropsWithNoPartialKeyOrRef(realm, value); - value.makeFinal(); - return value; + let abstractPropsObject = AbstractValue.createAbstractObject(realm, propsName || "props", shape); + invariant(abstractPropsObject instanceof AbstractObjectValue); + flagPropsWithNoPartialKeyOrRef(realm, abstractPropsObject); + abstractPropsObject.makeFinal(); + return abstractPropsObject; } export function getInitialContext(realm: Realm, componentType: ECMAScriptSourceFunctionValue): AbstractObjectValue { diff --git a/src/react/experimental-server-rendering/rendering.js b/src/react/experimental-server-rendering/rendering.js index afe35fdc4..a0208c986 100644 --- a/src/react/experimental-server-rendering/rendering.js +++ b/src/react/experimental-server-rendering/rendering.js @@ -483,7 +483,11 @@ export function renderToString( staticMarkup: boolean ): StringValue | AbstractValue { let reactStatistics = new ReactStatistics(); - let reconciler = new Reconciler(realm, { firstRenderOnly: true, isRoot: true }, reactStatistics); + let reconciler = new Reconciler( + realm, + { firstRenderOnly: true, isRoot: true, modelString: undefined }, + reactStatistics + ); let typeValue = getProperty(realm, reactElement, "type"); let propsValue = getProperty(realm, reactElement, "props"); let evaluatedRootNode = createReactEvaluatedNode("ROOT", getComponentName(realm, typeValue)); diff --git a/src/react/reconcilation.js b/src/react/reconcilation.js index 47172bb48..fa8e3fde3 100644 --- a/src/react/reconcilation.js +++ b/src/react/reconcilation.js @@ -173,7 +173,7 @@ export class Reconciler { ): Effects { const resolveComponentTree = () => { try { - let initialProps = props || getInitialProps(this.realm, componentType); + let initialProps = props || getInitialProps(this.realm, componentType, this.componentTreeConfig); let initialContext = context || getInitialContext(this.realm, componentType); this.alreadyEvaluatedRootNodes.set(componentType, evaluatedRootNode); let { result } = this._resolveComponent(componentType, initialProps, initialContext, "ROOT", evaluatedRootNode); diff --git a/src/react/utils.js b/src/react/utils.js index 9765915df..251800e9a 100644 --- a/src/react/utils.js +++ b/src/react/utils.js @@ -775,6 +775,7 @@ export function convertConfigObjectToReactComponentTreeConfig( // defaults let firstRenderOnly = false; let isRoot = false; + let modelString; if (!(config instanceof UndefinedValue)) { for (let [key] of config.properties) { @@ -782,13 +783,33 @@ export function convertConfigObjectToReactComponentTreeConfig( if (propValue instanceof StringValue || propValue instanceof NumberValue || propValue instanceof BooleanValue) { let value = propValue.value; - // boolean options if (typeof value === "boolean") { + // boolean options if (key === "firstRenderOnly") { firstRenderOnly = value; } else if (key === "isRoot") { isRoot = value; } + } else if (typeof value === "string") { + try { + // result here is ignored as the main point here is to + // check and produce error + JSON.parse(value); + } catch (e) { + let componentModelError = new CompilerDiagnostic( + "Failed to parse model for component", + realm.currentLocation, + "PP1008", + "FatalError" + ); + if (realm.handleError(componentModelError) !== "Recover") { + throw new FatalError(); + } + } + // string options + if (key === "model") { + modelString = value; + } } } else { let diagnostic = new CompilerDiagnostic( @@ -805,6 +826,7 @@ export function convertConfigObjectToReactComponentTreeConfig( return { firstRenderOnly, isRoot, + modelString, }; } diff --git a/src/types.js b/src/types.js index c202dcf43..f9104547e 100644 --- a/src/types.js +++ b/src/types.js @@ -351,6 +351,7 @@ export type ReactHint = {| firstRenderValue: Value, object: ObjectValue, propert export type ReactComponentTreeConfig = { firstRenderOnly: boolean, isRoot: boolean, + modelString: void | string, }; export type DebugServerType = { diff --git a/src/utils/ShapeInformation.js b/src/utils/ShapeInformation.js index a2ec91126..43571647b 100644 --- a/src/utils/ShapeInformation.js +++ b/src/utils/ShapeInformation.js @@ -73,6 +73,11 @@ export type ArgModel = { arguments: { [string]: string }, }; +export type ComponentModel = { + universe: ShapeUniverse, + component: { props: string }, +}; + export class ShapeInformation implements ShapeInformationInterface { constructor( descriptor: ShapeDescriptorNonLink, @@ -126,6 +131,17 @@ export class ShapeInformation implements ShapeInformationInterface { : undefined; } + static createForReactComponentProps(model: void | ComponentModel): void | ShapeInformation { + return model !== undefined + ? ShapeInformation._resolveLinksAndWrap( + model.universe[model.component.props], + undefined, + undefined, + model.universe + ) + : undefined; + } + _isOptional(): void | boolean { if (this._parentDescriptor === undefined) { return undefined; diff --git a/src/values/AbstractObjectValue.js b/src/values/AbstractObjectValue.js index 28bb465be..cacccf1c4 100644 --- a/src/values/AbstractObjectValue.js +++ b/src/values/AbstractObjectValue.js @@ -206,6 +206,7 @@ export default class AbstractObjectValue extends AbstractValue { } makeFinal(): void { + if (this.shape) return; if (this.values.isTop()) { AbstractValue.reportIntrospectionError(this); throw new FatalError(); @@ -506,9 +507,10 @@ export default class AbstractObjectValue extends AbstractValue { let shapeContainer = this.kind === "explicit conversion to object" ? this.args[0] : this; invariant(shapeContainer instanceof AbstractValue); invariant(typeof P === "string"); + let realm = this.$Realm; let shape = shapeContainer.shape; let propertyShape, propertyGetter; - if (this.$Realm.instantRender.enabled && shape !== undefined) { + if ((realm.instantRender.enabled || realm.react.enabled) && shape !== undefined) { propertyShape = shape.getPropertyShape(P); if (propertyShape !== undefined) { type = propertyShape.getAbstractType(); @@ -516,7 +518,7 @@ export default class AbstractObjectValue extends AbstractValue { } } let propAbsVal = AbstractValue.createTemporalFromBuildFunction( - this.$Realm, + realm, type, [ob, new StringValue(this.$Realm, P)], createOperationDescriptor("ABSTRACT_OBJECT_GET", { propertyGetter }), diff --git a/src/values/AbstractValue.js b/src/values/AbstractValue.js index 62a5987ca..8b9b128e9 100644 --- a/src/values/AbstractValue.js +++ b/src/values/AbstractValue.js @@ -942,13 +942,15 @@ export default class AbstractValue extends Value { return realm.reportIntrospectionError(message); } - static createAbstractObject(realm: Realm, name: string, template?: ObjectValue): AbstractObjectValue { + static createAbstractObject( + realm: Realm, + name: string, + templateOrShape?: ObjectValue | ShapeInformationInterface + ): AbstractObjectValue { let value; - if (template === undefined) { - template = new ObjectValue(realm, realm.intrinsics.ObjectPrototype); + if (templateOrShape === undefined) { + templateOrShape = new ObjectValue(realm, realm.intrinsics.ObjectPrototype); } - template.makePartial(); - template.makeSimple(); value = AbstractValue.createFromTemplate(realm, buildExpressionTemplate(name), ObjectValue, [], name); if (!realm.isNameStringUnique(name)) { value.hashValue = ++realm.objectCount; @@ -956,8 +958,14 @@ export default class AbstractValue extends Value { realm.saveNameString(name); } value.intrinsicName = name; - value.values = new ValuesDomain(new Set([template])); - realm.rebuildNestedProperties(value, name); + if (templateOrShape instanceof ObjectValue) { + templateOrShape.makePartial(); + templateOrShape.makeSimple(); + value.values = new ValuesDomain(new Set([templateOrShape])); + realm.rebuildNestedProperties(value, name); + } else { + value.shape = templateOrShape; + } invariant(value instanceof AbstractObjectValue); return value; } diff --git a/test/react/FunctionalComponents-test.js b/test/react/FunctionalComponents-test.js index 8836f9bee..00b1bedd6 100644 --- a/test/react/FunctionalComponents-test.js +++ b/test/react/FunctionalComponents-test.js @@ -323,3 +323,7 @@ it("Hoist Fragment", () => { it("Pathological case", () => { runTest(__dirname + "/FunctionalComponents/pathological-case.js"); }); + +it("Model props", () => { + runTest(__dirname + "/FunctionalComponents/model-props.js"); +}); diff --git a/test/react/FunctionalComponents/model-props.js b/test/react/FunctionalComponents/model-props.js new file mode 100644 index 000000000..2d676b688 --- /dev/null +++ b/test/react/FunctionalComponents/model-props.js @@ -0,0 +1,81 @@ +var React = require("React"); + +function App(props) { + return ( +
+

{props.header.toString()}

+ +
+ ); +} + +App.getTrials = function(renderer, Root) { + var items = [{ id: 0, title: "Item 1" }, { id: 1, title: "Item 2" }, { id: 2, title: "Item 3" }]; + renderer.update(); + return [["render simple with props model", renderer.toJSON()]]; +}; + +if (this.__optimizeReactComponentTree) { + let universe = { + Item: { + kind: "object", + jsType: "object", + properties: { + id: { + shape: { + kind: "scalar", + jsType: "integral", + }, + optional: false, + }, + title: { + shape: { + kind: "scalar", + jsType: "string", + }, + optional: false, + }, + }, + }, + Props: { + kind: "object", + jsType: "object", + properties: { + header: { + shape: { + kind: "scalar", + jsType: "string", + }, + optional: false, + }, + items: { + shape: { + kind: "array", + jsType: "array", + elementShape: { + shape: { + kind: "link", + shapeName: "Item", + }, + optional: false, + }, + }, + optional: true, + }, + }, + }, + }; + + let appModel = { + component: { + props: "Props", + }, + universe, + }; + + __optimizeReactComponentTree(App, { + model: JSON.stringify(appModel), + }); +} + +module.exports = App; diff --git a/test/react/__snapshots__/FunctionalComponents-test.js.snap b/test/react/__snapshots__/FunctionalComponents-test.js.snap index 4bd5654e0..0cd75762e 100644 --- a/test/react/__snapshots__/FunctionalComponents-test.js.snap +++ b/test/react/__snapshots__/FunctionalComponents-test.js.snap @@ -3040,6 +3040,74 @@ ReactStatistics { } `; +exports[`Model props: (JSX => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 1, + "evaluatedRootNodes": Array [ + Object { + "children": Array [], + "message": "", + "name": "App", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 1, +} +`; + +exports[`Model props: (JSX => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 1, + "evaluatedRootNodes": Array [ + Object { + "children": Array [], + "message": "", + "name": "App", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 1, +} +`; + +exports[`Model props: (createElement => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 1, + "evaluatedRootNodes": Array [ + Object { + "children": Array [], + "message": "", + "name": "App", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 1, +} +`; + +exports[`Model props: (createElement => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 1, + "evaluatedRootNodes": Array [ + Object { + "children": Array [], + "message": "", + "name": "App", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 1, +} +`; + exports[`Mutations - not-safe 1: (JSX => JSX) 1`] = `"Failed to render React component root \\"Bar\\" due to side-effects from mutating the binding \\"x\\""`; exports[`Mutations - not-safe 1: (JSX => createElement) 1`] = `"Failed to render React component root \\"Bar\\" due to side-effects from mutating the binding \\"x\\""`;