From b5eb40769ab303285a8464143a4a47f4a6625afd Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Mon, 2 Oct 2017 17:14:38 +0200 Subject: [PATCH] Bug 1399589 - Move NodeFront to its own module. r=jdescottes MozReview-Commit-ID: EVeaFmqAmKL --HG-- rename : devtools/shared/fronts/inspector.js => devtools/shared/fronts/node.js extra : rebase_source : 2791e99ca8e37f4b9e0ba6bd1bb4d0bb22601781 --- devtools/server/actors/inspector.js | 3 +- devtools/shared/fronts/inspector.js | 416 ----------------------- devtools/shared/fronts/moz.build | 1 + devtools/shared/fronts/node.js | 435 +++++++++++++++++++++++++ devtools/shared/specs/accessibility.js | 2 - devtools/shared/specs/index.js | 2 +- devtools/shared/specs/inspector.js | 3 - 7 files changed, 439 insertions(+), 423 deletions(-) create mode 100644 devtools/shared/fronts/node.js diff --git a/devtools/server/actors/inspector.js b/devtools/server/actors/inspector.js index d7e6570cbc01..3e4f60e4f03e 100644 --- a/devtools/server/actors/inspector.js +++ b/devtools/server/actors/inspector.js @@ -59,7 +59,8 @@ const defer = require("devtools/shared/defer"); const {Task} = require("devtools/shared/task"); const EventEmitter = require("devtools/shared/event-emitter"); -const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector"); +const {nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector"); +const {nodeSpec} = require("devtools/shared/specs/node"); loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils"); loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils"); diff --git a/devtools/shared/fronts/inspector.js b/devtools/shared/fronts/inspector.js index ec40e4f2593a..95e4762519f9 100644 --- a/devtools/shared/fronts/inspector.js +++ b/devtools/shared/fronts/inspector.js @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const { SimpleStringFront } = require("devtools/shared/fronts/string"); const { Front, FrontClassWithSpec, @@ -13,7 +12,6 @@ const { } = require("devtools/shared/protocol.js"); const { inspectorSpec, - nodeSpec, nodeListSpec, walkerSpec } = require("devtools/shared/specs/inspector"); @@ -25,420 +23,6 @@ loader.lazyRequireGetter(this, "nodeConstants", loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true); -const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; - -/** - * Convenience API for building a list of attribute modifications - * for the `modifyAttributes` request. - */ -class AttributeModificationList { - constructor(node) { - this.node = node; - this.modifications = []; - } - - apply() { - let ret = this.node.modifyAttributes(this.modifications); - return ret; - } - - destroy() { - this.node = null; - this.modification = null; - } - - setAttributeNS(ns, name, value) { - this.modifications.push({ - attributeNamespace: ns, - attributeName: name, - newValue: value - }); - } - - setAttribute(name, value) { - this.setAttributeNS(undefined, name, value); - } - - removeAttributeNS(ns, name) { - this.setAttributeNS(ns, name, undefined); - } - - removeAttribute(name) { - this.setAttributeNS(undefined, name, undefined); - } -} - -/** - * Client side of the node actor. - * - * Node fronts are strored in a tree that mirrors the DOM tree on the - * server, but with a few key differences: - * - Not all children will be necessary loaded for each node. - * - The order of children isn't guaranteed to be the same as the DOM. - * Children are stored in a doubly-linked list, to make addition/removal - * and traversal quick. - * - * Due to the order/incompleteness of the child list, it is safe to use - * the parent node from clients, but the `children` request should be used - * to traverse children. - */ -const NodeFront = FrontClassWithSpec(nodeSpec, { - initialize: function (conn, form, detail, ctx) { - // The parent node - this._parent = null; - // The first child of this node. - this._child = null; - // The next sibling of this node. - this._next = null; - // The previous sibling of this node. - this._prev = null; - Front.prototype.initialize.call(this, conn, form, detail, ctx); - }, - - /** - * Destroy a node front. The node must have been removed from the - * ownership tree before this is called, unless the whole walker front - * is being destroyed. - */ - destroy: function () { - Front.prototype.destroy.call(this); - }, - - // Update the object given a form representation off the wire. - form: function (form, detail, ctx) { - if (detail === "actorid") { - this.actorID = form; - return; - } - - // backward-compatibility: shortValue indicates we are connected to old server - if (form.shortValue) { - // If the value is not complete, set nodeValue to null, it will be fetched - // when calling getNodeValue() - form.nodeValue = form.incompleteValue ? null : form.shortValue; - } - - // Shallow copy of the form. We could just store a reference, but - // eventually we'll want to update some of the data. - this._form = Object.assign({}, form); - this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; - - if (form.parent) { - // Get the owner actor for this actor (the walker), and find the - // parent node of this actor from it, creating a standin node if - // necessary. - let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent); - this.reparent(parentNodeFront); - } - - if (form.inlineTextChild) { - this.inlineTextChild = - types.getType("domnode").read(form.inlineTextChild, ctx); - } else { - this.inlineTextChild = undefined; - } - }, - - /** - * Returns the parent NodeFront for this NodeFront. - */ - parentNode: function () { - return this._parent; - }, - - /** - * Process a mutation entry as returned from the walker's `getMutations` - * request. Only tries to handle changes of the node's contents - * themselves (character data and attribute changes), the walker itself - * will keep the ownership tree up to date. - */ - updateMutation: function (change) { - if (change.type === "attributes") { - // We'll need to lazily reparse the attributes after this change. - this._attrMap = undefined; - - // Update any already-existing attributes. - let found = false; - for (let i = 0; i < this.attributes.length; i++) { - let attr = this.attributes[i]; - if (attr.name == change.attributeName && - attr.namespace == change.attributeNamespace) { - if (change.newValue !== null) { - attr.value = change.newValue; - } else { - this.attributes.splice(i, 1); - } - found = true; - break; - } - } - // This is a new attribute. The null check is because of Bug 1192270, - // in the case of a newly added then removed attribute - if (!found && change.newValue !== null) { - this.attributes.push({ - name: change.attributeName, - namespace: change.attributeNamespace, - value: change.newValue - }); - } - } else if (change.type === "characterData") { - this._form.nodeValue = change.newValue; - } else if (change.type === "pseudoClassLock") { - this._form.pseudoClassLocks = change.pseudoClassLocks; - } else if (change.type === "events") { - this._form.hasEventListeners = change.hasEventListeners; - } - }, - - // Some accessors to make NodeFront feel more like an nsIDOMNode - - get id() { - return this.getAttribute("id"); - }, - - get nodeType() { - return this._form.nodeType; - }, - get namespaceURI() { - return this._form.namespaceURI; - }, - get nodeName() { - return this._form.nodeName; - }, - get displayName() { - let {displayName, nodeName} = this._form; - - // Keep `nodeName.toLowerCase()` for backward compatibility - return displayName || nodeName.toLowerCase(); - }, - get doctypeString() { - return ""; - }, - - get baseURI() { - return this._form.baseURI; - }, - - get className() { - return this.getAttribute("class") || ""; - }, - - get hasChildren() { - return this._form.numChildren > 0; - }, - get numChildren() { - return this._form.numChildren; - }, - get hasEventListeners() { - return this._form.hasEventListeners; - }, - - get isBeforePseudoElement() { - return this._form.isBeforePseudoElement; - }, - get isAfterPseudoElement() { - return this._form.isAfterPseudoElement; - }, - get isPseudoElement() { - return this.isBeforePseudoElement || this.isAfterPseudoElement; - }, - get isAnonymous() { - return this._form.isAnonymous; - }, - get isInHTMLDocument() { - return this._form.isInHTMLDocument; - }, - get tagName() { - return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null; - }, - - get isDocumentElement() { - return !!this._form.isDocumentElement; - }, - - // doctype properties - get name() { - return this._form.name; - }, - get publicId() { - return this._form.publicId; - }, - get systemId() { - return this._form.systemId; - }, - - getAttribute: function (name) { - let attr = this._getAttribute(name); - return attr ? attr.value : null; - }, - hasAttribute: function (name) { - this._cacheAttributes(); - return (name in this._attrMap); - }, - - get hidden() { - let cls = this.getAttribute("class"); - return cls && cls.indexOf(HIDDEN_CLASS) > -1; - }, - - get attributes() { - return this._form.attrs; - }, - - get pseudoClassLocks() { - return this._form.pseudoClassLocks || []; - }, - hasPseudoClassLock: function (pseudo) { - return this.pseudoClassLocks.some(locked => locked === pseudo); - }, - - get isDisplayed() { - // The NodeActor's form contains the isDisplayed information as a boolean - // starting from FF32. Before that, the property is missing - return "isDisplayed" in this._form ? this._form.isDisplayed : true; - }, - - get isTreeDisplayed() { - let parent = this; - while (parent) { - if (!parent.isDisplayed) { - return false; - } - parent = parent.parentNode(); - } - return true; - }, - - getNodeValue: custom(function () { - // backward-compatibility: if nodevalue is null and shortValue is defined, the actual - // value of the node needs to be fetched on the server. - if (this._form.nodeValue === null && this._form.shortValue) { - return this._getNodeValue(); - } - - let str = this._form.nodeValue || ""; - return promise.resolve(new SimpleStringFront(str)); - }, { - impl: "_getNodeValue" - }), - - // Accessors for custom form properties. - - getFormProperty: function (name) { - return this._form.props ? this._form.props[name] : null; - }, - - hasFormProperty: function (name) { - return this._form.props ? (name in this._form.props) : null; - }, - - get formProperties() { - return this._form.props; - }, - - /** - * Return a new AttributeModificationList for this node. - */ - startModifyingAttributes: function () { - return new AttributeModificationList(this); - }, - - _cacheAttributes: function () { - if (typeof this._attrMap != "undefined") { - return; - } - this._attrMap = {}; - for (let attr of this.attributes) { - this._attrMap[attr.name] = attr; - } - }, - - _getAttribute: function (name) { - this._cacheAttributes(); - return this._attrMap[name] || undefined; - }, - - /** - * Set this node's parent. Note that the children saved in - * this tree are unordered and incomplete, so shouldn't be used - * instead of a `children` request. - */ - reparent: function (parent) { - if (this._parent === parent) { - return; - } - - if (this._parent && this._parent._child === this) { - this._parent._child = this._next; - } - if (this._prev) { - this._prev._next = this._next; - } - if (this._next) { - this._next._prev = this._prev; - } - this._next = null; - this._prev = null; - this._parent = parent; - if (!parent) { - // Subtree is disconnected, we're done - return; - } - this._next = parent._child; - if (this._next) { - this._next._prev = this; - } - parent._child = this; - }, - - /** - * Return all the known children of this node. - */ - treeChildren: function () { - let ret = []; - for (let child = this._child; child != null; child = child._next) { - ret.push(child); - } - return ret; - }, - - /** - * Do we use a local target? - * Useful to know if a rawNode is available or not. - * - * This will, one day, be removed. External code should - * not need to know if the target is remote or not. - */ - isLocalToBeDeprecated: function () { - return !!this.conn._transport._serverConnection; - }, - - /** - * Get an nsIDOMNode for the given node front. This only works locally, - * and is only intended as a stopgap during the transition to the remote - * protocol. If you depend on this you're likely to break soon. - */ - rawNode: function (rawNode) { - if (!this.isLocalToBeDeprecated()) { - console.warn("Tried to use rawNode on a remote connection."); - return null; - } - const { DebuggerServer } = require("devtools/server/main"); - let actor = DebuggerServer.searchAllConnectionsForActor(this.actorID); - if (!actor) { - // Can happen if we try to get the raw node for an already-expired - // actor. - return null; - } - return actor.rawNode; - } -}); - -exports.NodeFront = NodeFront; - /** * Client side of a node list as returned by querySelectorAll() */ diff --git a/devtools/shared/fronts/moz.build b/devtools/shared/fronts/moz.build index 68a2636c1a24..5eadbccfc027 100644 --- a/devtools/shared/fronts/moz.build +++ b/devtools/shared/fronts/moz.build @@ -22,6 +22,7 @@ DevToolsModules( 'inspector.js', 'layout.js', 'memory.js', + 'node.js', 'performance-recording.js', 'performance.js', 'preference.js', diff --git a/devtools/shared/fronts/node.js b/devtools/shared/fronts/node.js new file mode 100644 index 000000000000..d724be8fc740 --- /dev/null +++ b/devtools/shared/fronts/node.js @@ -0,0 +1,435 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { + Front, + FrontClassWithSpec, + custom, + types +} = require("devtools/shared/protocol.js"); + +const { + nodeSpec, +} = require("devtools/shared/specs/node"); + +const promise = require("promise"); +const { SimpleStringFront } = require("devtools/shared/fronts/string"); + +loader.lazyRequireGetter(this, "nodeConstants", + "devtools/shared/dom-node-constants"); + +const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; + +/** + * Convenience API for building a list of attribute modifications + * for the `modifyAttributes` request. + */ +class AttributeModificationList { + constructor(node) { + this.node = node; + this.modifications = []; + } + + apply() { + let ret = this.node.modifyAttributes(this.modifications); + return ret; + } + + destroy() { + this.node = null; + this.modification = null; + } + + setAttributeNS(ns, name, value) { + this.modifications.push({ + attributeNamespace: ns, + attributeName: name, + newValue: value + }); + } + + setAttribute(name, value) { + this.setAttributeNS(undefined, name, value); + } + + removeAttributeNS(ns, name) { + this.setAttributeNS(ns, name, undefined); + } + + removeAttribute(name) { + this.setAttributeNS(undefined, name, undefined); + } +} + +/** + * Client side of the node actor. + * + * Node fronts are strored in a tree that mirrors the DOM tree on the + * server, but with a few key differences: + * - Not all children will be necessary loaded for each node. + * - The order of children isn't guaranteed to be the same as the DOM. + * Children are stored in a doubly-linked list, to make addition/removal + * and traversal quick. + * + * Due to the order/incompleteness of the child list, it is safe to use + * the parent node from clients, but the `children` request should be used + * to traverse children. + */ +const NodeFront = FrontClassWithSpec(nodeSpec, { + initialize: function (conn, form, detail, ctx) { + // The parent node + this._parent = null; + // The first child of this node. + this._child = null; + // The next sibling of this node. + this._next = null; + // The previous sibling of this node. + this._prev = null; + Front.prototype.initialize.call(this, conn, form, detail, ctx); + }, + + /** + * Destroy a node front. The node must have been removed from the + * ownership tree before this is called, unless the whole walker front + * is being destroyed. + */ + destroy: function () { + Front.prototype.destroy.call(this); + }, + + // Update the object given a form representation off the wire. + form: function (form, detail, ctx) { + if (detail === "actorid") { + this.actorID = form; + return; + } + + // backward-compatibility: shortValue indicates we are connected to old server + if (form.shortValue) { + // If the value is not complete, set nodeValue to null, it will be fetched + // when calling getNodeValue() + form.nodeValue = form.incompleteValue ? null : form.shortValue; + } + + // Shallow copy of the form. We could just store a reference, but + // eventually we'll want to update some of the data. + this._form = Object.assign({}, form); + this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; + + if (form.parent) { + // Get the owner actor for this actor (the walker), and find the + // parent node of this actor from it, creating a standin node if + // necessary. + let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent); + this.reparent(parentNodeFront); + } + + if (form.inlineTextChild) { + this.inlineTextChild = + types.getType("domnode").read(form.inlineTextChild, ctx); + } else { + this.inlineTextChild = undefined; + } + }, + + /** + * Returns the parent NodeFront for this NodeFront. + */ + parentNode: function () { + return this._parent; + }, + + /** + * Process a mutation entry as returned from the walker's `getMutations` + * request. Only tries to handle changes of the node's contents + * themselves (character data and attribute changes), the walker itself + * will keep the ownership tree up to date. + */ + updateMutation: function (change) { + if (change.type === "attributes") { + // We'll need to lazily reparse the attributes after this change. + this._attrMap = undefined; + + // Update any already-existing attributes. + let found = false; + for (let i = 0; i < this.attributes.length; i++) { + let attr = this.attributes[i]; + if (attr.name == change.attributeName && + attr.namespace == change.attributeNamespace) { + if (change.newValue !== null) { + attr.value = change.newValue; + } else { + this.attributes.splice(i, 1); + } + found = true; + break; + } + } + // This is a new attribute. The null check is because of Bug 1192270, + // in the case of a newly added then removed attribute + if (!found && change.newValue !== null) { + this.attributes.push({ + name: change.attributeName, + namespace: change.attributeNamespace, + value: change.newValue + }); + } + } else if (change.type === "characterData") { + this._form.nodeValue = change.newValue; + } else if (change.type === "pseudoClassLock") { + this._form.pseudoClassLocks = change.pseudoClassLocks; + } else if (change.type === "events") { + this._form.hasEventListeners = change.hasEventListeners; + } + }, + + // Some accessors to make NodeFront feel more like an nsIDOMNode + + get id() { + return this.getAttribute("id"); + }, + + get nodeType() { + return this._form.nodeType; + }, + get namespaceURI() { + return this._form.namespaceURI; + }, + get nodeName() { + return this._form.nodeName; + }, + get displayName() { + let {displayName, nodeName} = this._form; + + // Keep `nodeName.toLowerCase()` for backward compatibility + return displayName || nodeName.toLowerCase(); + }, + get doctypeString() { + return ""; + }, + + get baseURI() { + return this._form.baseURI; + }, + + get className() { + return this.getAttribute("class") || ""; + }, + + get hasChildren() { + return this._form.numChildren > 0; + }, + get numChildren() { + return this._form.numChildren; + }, + get hasEventListeners() { + return this._form.hasEventListeners; + }, + + get isBeforePseudoElement() { + return this._form.isBeforePseudoElement; + }, + get isAfterPseudoElement() { + return this._form.isAfterPseudoElement; + }, + get isPseudoElement() { + return this.isBeforePseudoElement || this.isAfterPseudoElement; + }, + get isAnonymous() { + return this._form.isAnonymous; + }, + get isInHTMLDocument() { + return this._form.isInHTMLDocument; + }, + get tagName() { + return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null; + }, + + get isDocumentElement() { + return !!this._form.isDocumentElement; + }, + + // doctype properties + get name() { + return this._form.name; + }, + get publicId() { + return this._form.publicId; + }, + get systemId() { + return this._form.systemId; + }, + + getAttribute: function (name) { + let attr = this._getAttribute(name); + return attr ? attr.value : null; + }, + hasAttribute: function (name) { + this._cacheAttributes(); + return (name in this._attrMap); + }, + + get hidden() { + let cls = this.getAttribute("class"); + return cls && cls.indexOf(HIDDEN_CLASS) > -1; + }, + + get attributes() { + return this._form.attrs; + }, + + get pseudoClassLocks() { + return this._form.pseudoClassLocks || []; + }, + hasPseudoClassLock: function (pseudo) { + return this.pseudoClassLocks.some(locked => locked === pseudo); + }, + + get isDisplayed() { + // The NodeActor's form contains the isDisplayed information as a boolean + // starting from FF32. Before that, the property is missing + return "isDisplayed" in this._form ? this._form.isDisplayed : true; + }, + + get isTreeDisplayed() { + let parent = this; + while (parent) { + if (!parent.isDisplayed) { + return false; + } + parent = parent.parentNode(); + } + return true; + }, + + getNodeValue: custom(function () { + // backward-compatibility: if nodevalue is null and shortValue is defined, the actual + // value of the node needs to be fetched on the server. + if (this._form.nodeValue === null && this._form.shortValue) { + return this._getNodeValue(); + } + + let str = this._form.nodeValue || ""; + return promise.resolve(new SimpleStringFront(str)); + }, { + impl: "_getNodeValue" + }), + + // Accessors for custom form properties. + + getFormProperty: function (name) { + return this._form.props ? this._form.props[name] : null; + }, + + hasFormProperty: function (name) { + return this._form.props ? (name in this._form.props) : null; + }, + + get formProperties() { + return this._form.props; + }, + + /** + * Return a new AttributeModificationList for this node. + */ + startModifyingAttributes: function () { + return new AttributeModificationList(this); + }, + + _cacheAttributes: function () { + if (typeof this._attrMap != "undefined") { + return; + } + this._attrMap = {}; + for (let attr of this.attributes) { + this._attrMap[attr.name] = attr; + } + }, + + _getAttribute: function (name) { + this._cacheAttributes(); + return this._attrMap[name] || undefined; + }, + + /** + * Set this node's parent. Note that the children saved in + * this tree are unordered and incomplete, so shouldn't be used + * instead of a `children` request. + */ + reparent: function (parent) { + if (this._parent === parent) { + return; + } + + if (this._parent && this._parent._child === this) { + this._parent._child = this._next; + } + if (this._prev) { + this._prev._next = this._next; + } + if (this._next) { + this._next._prev = this._prev; + } + this._next = null; + this._prev = null; + this._parent = parent; + if (!parent) { + // Subtree is disconnected, we're done + return; + } + this._next = parent._child; + if (this._next) { + this._next._prev = this; + } + parent._child = this; + }, + + /** + * Return all the known children of this node. + */ + treeChildren: function () { + let ret = []; + for (let child = this._child; child != null; child = child._next) { + ret.push(child); + } + return ret; + }, + + /** + * Do we use a local target? + * Useful to know if a rawNode is available or not. + * + * This will, one day, be removed. External code should + * not need to know if the target is remote or not. + */ + isLocalToBeDeprecated: function () { + return !!this.conn._transport._serverConnection; + }, + + /** + * Get an nsIDOMNode for the given node front. This only works locally, + * and is only intended as a stopgap during the transition to the remote + * protocol. If you depend on this you're likely to break soon. + */ + rawNode: function (rawNode) { + if (!this.isLocalToBeDeprecated()) { + console.warn("Tried to use rawNode on a remote connection."); + return null; + } + const { DebuggerServer } = require("devtools/server/main"); + let actor = DebuggerServer.searchAllConnectionsForActor(this.actorID); + if (!actor) { + // Can happen if we try to get the raw node for an already-expired + // actor. + return null; + } + return actor.rawNode; + } +}); + +exports.NodeFront = NodeFront; diff --git a/devtools/shared/specs/accessibility.js b/devtools/shared/specs/accessibility.js index f44c156f03f1..0d66af83aee3 100644 --- a/devtools/shared/specs/accessibility.js +++ b/devtools/shared/specs/accessibility.js @@ -6,8 +6,6 @@ const protocol = require("devtools/shared/protocol"); const { Arg, generateActorSpec, RetVal, types } = protocol; -// eslint-disable-next-line no-unused-vars -const { nodeSpec } = require("devtools/shared/specs/inspector"); types.addActorType("accessible"); diff --git a/devtools/shared/specs/index.js b/devtools/shared/specs/index.js index a98d86e79b2b..0fedd8a0cc78 100644 --- a/devtools/shared/specs/index.js +++ b/devtools/shared/specs/index.js @@ -130,7 +130,7 @@ const Types = exports.__TypesForTests = [ { types: ["domnode"], spec: "devtools/shared/specs/node", - front: "devtools/shared/fronts/inspector", + front: "devtools/shared/fronts/node", }, { types: ["performance"], diff --git a/devtools/shared/specs/inspector.js b/devtools/shared/specs/inspector.js index 163b10fe3da7..7c14b1f796f4 100644 --- a/devtools/shared/specs/inspector.js +++ b/devtools/shared/specs/inspector.js @@ -10,9 +10,6 @@ const { generateActorSpec, types } = require("devtools/shared/protocol"); -const { nodeSpec } = require("devtools/shared/specs/node"); - -exports.nodeSpec = nodeSpec; /** * Returned from any call that might return a node that isn't connected to root