UNPKG

@adpt/core

Version:
540 lines 22.2 kB
"use strict"; /* * Copyright 2018-2019 Unbounded Systems, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const util = tslib_1.__importStar(require("util")); const ld = tslib_1.__importStar(require("lodash")); const utils_1 = require("@adpt/utils"); const graphql_1 = require("graphql"); const relations_1 = require("./deploy/relations"); const error_1 = require("./error"); const handle_1 = require("./handle"); const errors_1 = require("./observers/errors"); const query_transforms_1 = require("./observers/query_transforms"); const registry_1 = require("./observers/registry"); const reanimate_1 = require("./reanimate"); const state_1 = require("./state"); const status_1 = require("./status"); function isApplyStyle(el) { return el.componentType === exports.ApplyStyle; } exports.isApplyStyle = isApplyStyle; function isElement(val) { return utils_1.isInstance(val, AdaptElementImpl, "adapt"); } exports.isElement = isElement; function isElementImpl(val) { return isElement(val); } exports.isElementImpl = isElementImpl; function isMountedElement(val) { return isElementImpl(val) && val.mounted; } exports.isMountedElement = isMountedElement; function isDeferredElement(val) { return isDeferred(val.componentType.prototype); } exports.isDeferredElement = isDeferredElement; function isDeferredElementImpl(val) { return utils_1.isInstance(val, AdaptDeferredElementImpl, "adapt"); } exports.isDeferredElementImpl = isDeferredElementImpl; function isPrimitiveElement(elem) { return isPrimitive(elem.componentType.prototype); } exports.isPrimitiveElement = isPrimitiveElement; function isMountedPrimitiveElement(elem) { return isPrimitiveElement(elem) && isMountedElement(elem); } exports.isMountedPrimitiveElement = isMountedPrimitiveElement; function isComponentElement(val) { return isElement(val) && isComponent(val.componentType.prototype); } exports.isComponentElement = isComponentElement; function isSFCElement(val) { return isElement(val) && !isComponentElement(val); } exports.isSFCElement = isSFCElement; exports.isFinalDomElement = isMountedPrimitiveElement; exports.isPartialFinalDomElement = isMountedElement; function componentStateNow(c) { try { return c.state; } catch (_a) { return undefined; } } exports.componentStateNow = componentStateNow; class Component { // NOTE(mark): This really should be just BuiltinProps with both key // and handle required, but there's some strange interaction between // ElementClass and LibraryManagedAttributes in the JSX namespace that // I don't understand at the moment where I can't seem to make the // Component constructor require it without also making key and handle // unintentionally required in all JSX expressions. constructor(props) { this.props = props; reanimate_1.registerConstructor(this.constructor); const cData = getComponentConstructorData(); const curState = cData.getState(); if (curState === undefined && this.initialState != null) { const init = this.initialState(); if (init == null || !ld.isObject(init)) { throw new Error(`initialState function returned invalid value ` + `'${init}'. initialState must return an object.`); } cData.setInitialState(init); } this.stateUpdates = cData.stateUpdates; this.deployInfo = cData.deployInfo; // Prevent subclass constructors from accessing this.state too early // by waiting to init getState. this.getState = cData.getState; } get state() { if (this.getState == null) { throw new Error(`this.state cannot be accessed before calling super()`); } if (this.initialState == null) { throw new Error(`cannot access this.state in a Component that ` + `lacks an initialState method`); } return this.getState(); } set state(_) { throw new Error(`State for a component can only be changed by calling this.setState`); } setState(stateUpdate) { if (this.initialState == null) { throw new Error(`Component ${this.constructor.name}: cannot access ` + `this.setState in a Component that lacks an ` + `initialState method`); } this.stateUpdates.push(ld.isFunction(stateUpdate) ? stateUpdate : () => stateUpdate); } status(observeForStatus, buildData) { return status_1.defaultStatus(this.props, observeForStatus, buildData); } } exports.Component = Component; utils_1.tagConstructor(Component, "adapt"); class DeferredComponent extends Component { } exports.DeferredComponent = DeferredComponent; utils_1.tagConstructor(DeferredComponent, "adapt"); function isDeferred(component) { return utils_1.isInstance(component, DeferredComponent, "adapt"); } exports.isDeferred = isDeferred; class PrimitiveComponent extends DeferredComponent { build() { throw new error_1.BuildNotImplemented(); } validate() { return; } } exports.PrimitiveComponent = PrimitiveComponent; utils_1.tagConstructor(PrimitiveComponent, "adapt"); function isPrimitive(component) { return utils_1.isInstance(component, PrimitiveComponent, "adapt"); } exports.isPrimitive = isPrimitive; function isComponent(func) { return utils_1.isInstance(func, Component, "adapt"); } exports.isComponent = isComponent; /** * @internal */ class AdaptElementImpl { constructor(componentType, props, children) { this.componentType = componentType; this.stateNamespace = []; this.mounted = false; this.instanceMethods = {}; this.buildData = {}; this.buildState = BuildState.initial; this.reanimated = false; this.stateUpdates = []; this.setBuilt = () => this.buildState = BuildState.built; this.shouldBuild = () => this.buildState === BuildState.initial; this.built = () => this.buildState === BuildState.built; this.setState = (stateUpdate) => { this.stateUpdates.push(stateUpdate); }; this.getStatusMethod = () => { if (isSFCElement(this)) { const customStatus = this.componentType.status; if (customStatus) { return (observeForStatus, buildData) => customStatus(this.props, observeForStatus, buildData); } return (observeForStatus, buildData) => status_1.defaultStatus(this.props, observeForStatus, buildData); } return (o, b) => { if (!this.component) throw new status_1.NoStatusAvailable(`element.component === ${this.component}`); return this.component.status(o, b); }; }; this.statusCommon = async (observeForStatus) => { if (this.reanimated && !this.built()) { throw new status_1.NoStatusAvailable("status for reanimated elements not supported without a DOM build"); } if (!this.mounted) throw new status_1.NoStatusAvailable(`element is not mounted`); const buildData = this.buildData; //After build, this type assertion should hold if (buildData === undefined) throw new Error(`Status requested but no buildData: ${this}`); const statusMethod = this.getStatusMethod(); return statusMethod(observeForStatus, buildData); }; this.statusWithMgr = async (mgr) => { const observeForStatus = async (observer, query, variables) => { const result = await mgr.executeQuery(observer, query, variables); if (result.errors) { const badErrors = result.errors.filter((e) => !e.message.startsWith("Adapt Observer Needs Data:")); if (badErrors.length !== 0) { const msgs = badErrors.map((e) => e.originalError ? e.stack : graphql_1.printError(e)).join("\n"); throw new Error(msgs); } const needMsgs = result.errors.map((e) => e.originalError ? e.stack : graphql_1.printError(e)).join("\n"); throw new errors_1.ObserverNeedsData(needMsgs); } return result.data; }; return this.statusCommon(observeForStatus); }; this.status = async (o) => { const observeForStatus = async (observer, query, variables) => { //FIXME(manishv) Make this collect all queries and then observe only once - may require interface change const plugin = registry_1.findObserver(observer); if (!plugin) throw new Error(`Cannot find observer ${observer.observerName}`); const observations = await plugin.observe([{ query, variables }]); const schema = plugin.schema; const result = await query_transforms_1.adaptGqlExecute(schema, query, observations.data, observations.context, variables); if (result.errors) { const msgs = result.errors.map((e) => e.originalError ? e.stack : graphql_1.printError(e)).join("\n"); throw new Error(msgs); } return result.data; }; return this.statusCommon(o || observeForStatus); }; const hand = props.handle || handle_1.handle(); if (!handle_1.isHandleInternal(hand)) throw new error_1.InternalError(`handle is not a HandleImpl`); hand.associate(this); this.props = Object.assign({}, props, { handle: hand }); // Children passed as explicit parameter replace any on props if (children.length > 0) this.props.children = children; // Validate and flatten children. this.props.children = simplifyChildren(this.props.children); if (this.props.children === undefined) { delete this.props.children; } Object.freeze(this.props); } mount(parentNamespace, path, keyPath, deployID, deployOpID) { if (this.mounted) { throw new Error("Cannot remount elements!"); } if ("key" in this.props) { const propsWithKey = this.props; this.stateNamespace = [...parentNamespace, propsWithKey.key]; } else { throw new error_1.InternalError(`props has no key at mount: ${util.inspect(this)}`); } this.path = path; this.keyPath = keyPath; this.mounted = true; this.buildData.id = this.id; this.buildData.deployID = deployID; this.buildData.deployOpID = deployOpID; } async postBuild(stateStore) { return { stateChanged: await state_1.applyStateUpdates(this.stateNamespace, stateStore, this.props, this.stateUpdates) }; } /** * Add one or more deploy dependencies to this Element. * @remarks * Intended to be called during the DOM build process. */ addDependency(dependencies) { const hands = utils_1.toArray(dependencies); if (hands.length === 0) return; if (!this.addlDependencies) this.addlDependencies = new Set(); for (const h of hands) this.addlDependencies.add(h); } /** * Return all the dependencies of an Element. * @remarks * Intended to be called during the deployment phase from the execution * plan code only. * @internal */ dependsOn(goalStatus, helpers) { if (!this.mounted) { throw new error_1.InternalError(`dependsOn requested but element is not mounted`); } const method = this.instance.dependsOn; const methodDeps = method && method(goalStatus, helpers); if (!this.addlDependencies) return methodDeps; const finalHandles = [...this.addlDependencies] .map((h) => h.mountedOrig) .filter(utils_1.notNull) .map((el) => el.props.handle); const addlDeps = helpers.dependsOn(finalHandles); if (methodDeps === undefined) return addlDeps; return relations_1.And(methodDeps, addlDeps); } /** * Returns whether this Element (and ONLY this element) has completed * deployment. * @remarks * Intended to be called during the deployment phase from the execution * plan code only. * @internal */ deployedWhen(goalStatus, helpers) { if (!this.mounted) { throw new error_1.InternalError(`deployedWhen requested but element is not mounted`); } const method = this.instance.deployedWhen || defaultDeployedWhen(this); return method(goalStatus, helpers); } /** * True if the Element's `deployedWhen` is considered trivial. * @remarks * This flag is a hint for user interfaces, such as the Adapt CLI. It * tells the user interface that this Element's `deployedWhen` function * is "trivial" and therefore its status should not typically be shown in * user interfaces unless the user has requested more detailed status * information on all components, or if there's an active action for * this component. * * This flag is `true` if the component does not have a custom * `deployedWhen` method or if the trivial flag was specifically set via * {@link useDeployedWhen} options (function component) or via * {@link Component.deployedWhenIsTrivial} (class component). */ get deployedWhenIsTrivial() { if (!this.mounted || !this.built()) { throw new error_1.InternalError(`deployedWhenIsTrivial can only be ` + `accessed on a built Element`); } const inst = this.instance; return inst.deployedWhen == null || this.instance.deployedWhenIsTrivial === true; } get componentName() { return this.componentType.name || "anonymous"; } get displayName() { return this.componentType.displayName || this.componentName; } get id() { return JSON.stringify(this.stateNamespace); } get instance() { return this.component || this.instanceMethods; } } exports.AdaptElementImpl = AdaptElementImpl; utils_1.tagConstructor(AdaptElementImpl, "adapt"); /** * Creates a function that implements the default `deployedWhen` behavior for * an Element. * @remarks * When a component has not specified a custom `deployedWhen` * method, it will use this function to generate the default `deployedWhen` * method. * * The default `deployedWhen` behavior, implemented by this function, is to * return `true` (meaning the Element has reached the `goalStatus` and is deployed) * once the successor Element of `el` is deployed. If `el` has no successor, * it will return `true` once all of its children have become deployed. * * @param el - The Element for which a default `deployedWhen` function should * be created. * * @returns A `deployedWhen` function for Element `el` that implements the * default behavior. * @public */ function defaultDeployedWhen(el) { if (!isElement(el)) throw new Error(`Parameter must be an AdaptElement`); return (_goalStatus, helpers) => { if (!isElementImpl(el)) throw new error_1.InternalError(`Element is not an ElementImpl`); const succ = el.buildData.successor; // null means there's no successor and nothing in the final DOM for // this element--no primitive element and no children. if (succ === null) return true; // undefined means this element is the final one in the chain. If // this element has children, it's deployed when they are. if (succ === undefined) { const unready = childrenToArray(el.props.children) .filter(isMountedElement) .filter((k) => !helpers.isDeployed(k.props.handle)) .map((e) => e.props.handle); return unready.length === 0 ? true : relations_1.waiting(`Waiting on ${unready.length} child elements`, unready); } return helpers.isDeployed(succ.props.handle) ? true : relations_1.waiting(`Waiting on successor element`, [succ.props.handle]); }; } exports.defaultDeployedWhen = defaultDeployedWhen; var BuildState; (function (BuildState) { BuildState["initial"] = "initial"; BuildState["deferred"] = "deferred"; BuildState["built"] = "built"; })(BuildState || (BuildState = {})); class AdaptDeferredElementImpl extends AdaptElementImpl { constructor() { super(...arguments); this.setDeferred = () => this.buildState = BuildState.deferred; this.shouldBuild = () => this.buildState === BuildState.deferred; //Build if we've deferred once } } exports.AdaptDeferredElementImpl = AdaptDeferredElementImpl; utils_1.tagConstructor(AdaptDeferredElementImpl, "adapt"); class AdaptPrimitiveElementImpl extends AdaptDeferredElementImpl { constructor(componentType, props, children) { super(componentType, props, children); this.componentType = componentType; } validate() { if (!this.mounted) { throw new error_1.InternalError(`validate called on unmounted component at ${this.path}`); } if (this.component == null) { throw new error_1.InternalError(`validate called but component instance not created at ${this.path}`); } let ret = this.component.validate(); if (ret === undefined) ret = []; else if (typeof ret === "string") ret = [ret]; else if (!Array.isArray(ret)) { throw new Error(`Incorrect type '${typeof ret}' returned from ` + `component validate at ${this.path}`); } return ret.map((m) => ({ type: utils_1.MessageType.warning, timestamp: Date.now(), from: "DOM validate", content: `Component validation error. [${this.path}] cannot be ` + `built with current props: ${m}`, })); } } exports.AdaptPrimitiveElementImpl = AdaptPrimitiveElementImpl; utils_1.tagConstructor(AdaptPrimitiveElementImpl, "adapt"); function createElement(ctor, //props should never be null, but tsc will pass null when Props = {} in .js //See below for null workaround, exclude null here for explicit callers props, ...children) { if (typeof ctor === "string") { throw new Error("createElement cannot called with string element type"); } let fixedProps = ((props === null) ? {} : props); if (ctor.defaultProps) { // The 'as any' below is due to open TS bugs/PR: // https://github.com/Microsoft/TypeScript/pull/13288 fixedProps = Object.assign({}, ctor.defaultProps, props); } if (isPrimitive(ctor.prototype)) { return new AdaptPrimitiveElementImpl(ctor, fixedProps, children); } else if (isDeferred(ctor.prototype)) { return new AdaptDeferredElementImpl(ctor, fixedProps, children); } else { return new AdaptElementImpl(ctor, fixedProps, children); } } exports.createElement = createElement; function cloneElement(element, props, ...children) { const elProps = Object.assign({}, element.props); // handle cannot be cloned delete elProps.handle; const newProps = Object.assign({}, elProps, props); if (isPrimitiveElement(element)) { return new AdaptPrimitiveElementImpl(element.componentType, newProps, children); } else if (isDeferredElement(element)) { return new AdaptDeferredElementImpl(element.componentType, newProps, children); } else { return new AdaptElementImpl(element.componentType, newProps, children); } } exports.cloneElement = cloneElement; function childrenToArray(propsChildren) { const ret = simplifyChildren(propsChildren); if (ret == null) return []; if (!Array.isArray(ret)) return [ret]; return ret; } exports.childrenToArray = childrenToArray; function simplifyChildren(children) { if (ld.isArray(children)) { const flatChildren = ld.flatten(children); children = flatChildren.filter((e) => e != null); if (children.length === 0) { return undefined; } else if (children.length === 1) { return children[0]; } } return children; } exports.simplifyChildren = simplifyChildren; let componentConstructorStack = []; // exported as support utility for testing only function setComponentConstructorStack_(stack) { const old = componentConstructorStack; componentConstructorStack = stack; return old; } exports.setComponentConstructorStack_ = setComponentConstructorStack_; function pushComponentConstructorData(d) { componentConstructorStack.push(d); } exports.pushComponentConstructorData = pushComponentConstructorData; function popComponentConstructorData() { componentConstructorStack.pop(); } exports.popComponentConstructorData = popComponentConstructorData; function getComponentConstructorData() { const data = ld.last(componentConstructorStack); if (data == null) { throw new error_1.InternalError(`componentConstructorStack is empty`); } return data; } exports.getComponentConstructorData = getComponentConstructorData; //# sourceMappingURL=jsx.js.map