@adpt/core
Version:
AdaptJS core library
540 lines • 22.2 kB
JavaScript
"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