ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,707 lines (1,679 loc) • 186 kB
JavaScript
import { check, CheckPrimitive, CheckNumber, CheckOption, CheckHandle, CheckBlockSymbolTable, CheckInstanceof, CheckString, CheckElement, CheckMaybe, CheckNode, CheckOr, CheckProgramSymbolTable, CheckInterface, CheckFunction, wrap, CheckArray, CheckDict, CheckUnknown, CheckObject, CheckSafeString, CheckDocumentFragment, recordStackSize, CheckBoolean, CheckUndefined } from '@glimmer/debug';
import { createConstRef, createPrimitiveRef, valueForRef, isConstRef, createComputeRef, UNDEFINED_REFERENCE, createDebugAliasRef, childRefFor, TRUE_REFERENCE, FALSE_REFERENCE, createIteratorRef, NULL_REFERENCE, REFERENCE, isInvokableRef, updateRef, createIteratorItemRef } from '@glimmer/reference';
import { decodeHandle, isHandle, decodeImmediate, assert, expect, unwrap, isObject, debugToString, EMPTY_STRING_ARRAY, assign, dict, unwrapTemplate, enumerate, emptyArray, castToSimple, buildUntouchableThis, NS_SVG, castToBrowser, Stack, clearElement, INSERT_AFTER_BEGIN, INSERT_BEFORE_END, isDict, unwrapHandle, reverse, COMMENT_NODE, INSERT_BEFORE_BEGIN } from '@glimmer/util';
import { Op, $t0, CurriedTypes, $t1, InternalComponentCapabilities, $sp, $v0, ContentType, CurriedType, $pc, isLowLevelRegister, $s1, $s0, $fp, $ra, MachineOp } from '@glimmer/vm';
import { registerDestructor, destroy, associateDestroyableChild, _hasDestroyableChildren, isDestroying, isDestroyed, destroyChildren } from '@glimmer/destroyable';
export { destroy, isDestroyed, isDestroying, registerDestructor } from '@glimmer/destroyable';
import { DEBUG } from '@glimmer/env';
import { toBool, warnIfStyleNotTrusted, getPath, setPath, assertGlobalContextWasSet } from '@glimmer/global-context';
import { getInternalModifierManager, managerHasCapability, getInternalHelperManager, setInternalComponentManager, setInternalModifierManager, hasInternalComponentManager, hasInternalHelperManager, setInternalHelperManager, hasValue, hasDestroyable } from '@glimmer/manager';
import { consumeTag, valueForTag, validateTag, CURRENT_TAG, COMPUTE, CONSTANT_TAG, createCache, debug, resetTracking, beginTrackFrame, endTrackFrame, track, updateTag, createUpdatableTag, INITIAL, getValue } from '@glimmer/validator';
import { RuntimeProgramImpl } from '@glimmer/program';
import { getOwner } from '@glimmer/owner';
class DynamicScopeImpl {
bucket;
constructor(bucket) {
if (bucket) {
this.bucket = assign({}, bucket);
} else {
this.bucket = {};
}
}
get(key) {
return unwrap(this.bucket[key]);
}
set(key, reference) {
return this.bucket[key] = reference;
}
child() {
return new DynamicScopeImpl(this.bucket);
}
}
class PartialScopeImpl {
static root(self, size = 0, owner) {
let refs = new Array(size + 1).fill(UNDEFINED_REFERENCE);
return new PartialScopeImpl(refs, owner, null, null, null).init({
self
});
}
static sized(size = 0, owner) {
let refs = new Array(size + 1).fill(UNDEFINED_REFERENCE);
return new PartialScopeImpl(refs, owner, null, null, null);
}
constructor(
// the 0th slot is `self`
slots, owner, callerScope,
// named arguments and blocks passed to a layout that uses eval
evalScope,
// locals in scope when the partial was invoked
partialMap) {
this.slots = slots;
this.owner = owner;
this.callerScope = callerScope;
this.evalScope = evalScope;
this.partialMap = partialMap;
}
init({
self
}) {
this.slots[0] = self;
return this;
}
getSelf() {
return this.get(0);
}
getSymbol(symbol) {
return this.get(symbol);
}
getBlock(symbol) {
let block = this.get(symbol);
return block === UNDEFINED_REFERENCE ? null : block;
}
getEvalScope() {
return this.evalScope;
}
getPartialMap() {
return this.partialMap;
}
bind(symbol, value) {
this.set(symbol, value);
}
bindSelf(self) {
this.set(0, self);
}
bindSymbol(symbol, value) {
this.set(symbol, value);
}
bindBlock(symbol, value) {
this.set(symbol, value);
}
bindEvalScope(map) {
this.evalScope = map;
}
bindPartialMap(map) {
this.partialMap = map;
}
bindCallerScope(scope) {
this.callerScope = scope;
}
getCallerScope() {
return this.callerScope;
}
child() {
return new PartialScopeImpl(this.slots.slice(), this.owner, this.callerScope, this.evalScope, this.partialMap);
}
get(index) {
if (index >= this.slots.length) {
throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`);
}
return this.slots[index];
}
set(index, value) {
if (index >= this.slots.length) {
throw new RangeError(`BUG: cannot get $${index} from scope; length=${this.slots.length}`);
}
this.slots[index] = value;
}
}
// These symbols represent "friend" properties that are used inside of
// the VM in other classes, but are not intended to be a part of
// Glimmer's API.
const INNER_VM = Symbol('INNER_VM');
const DESTROYABLE_STACK = Symbol('DESTROYABLE_STACK');
const STACKS = Symbol('STACKS');
const REGISTERS = Symbol('REGISTERS');
const HEAP = Symbol('HEAP');
const CONSTANTS = Symbol('CONSTANTS');
const ARGS$1 = Symbol('ARGS');
class CursorImpl {
constructor(element, nextSibling) {
this.element = element;
this.nextSibling = nextSibling;
}
}
class ConcreteBounds {
constructor(parentNode, first, last) {
this.parentNode = parentNode;
this.first = first;
this.last = last;
}
parentElement() {
return this.parentNode;
}
firstNode() {
return this.first;
}
lastNode() {
return this.last;
}
}
function move(bounds, reference) {
let parent = bounds.parentElement();
let first = bounds.firstNode();
let last = bounds.lastNode();
let current = first;
// eslint-disable-next-line no-constant-condition
while (true) {
let next = current.nextSibling;
parent.insertBefore(current, reference);
if (current === last) {
return next;
}
current = expect(next, 'invalid bounds');
}
}
function clear(bounds) {
let parent = bounds.parentElement();
let first = bounds.firstNode();
let last = bounds.lastNode();
let current = first;
// eslint-disable-next-line no-constant-condition
while (true) {
let next = current.nextSibling;
parent.removeChild(current);
if (current === last) {
return next;
}
current = expect(next, 'invalid bounds');
}
}
function normalizeStringValue(value) {
if (isEmpty$2(value)) {
return '';
}
return String(value);
}
function shouldCoerce(value) {
return isString(value) || isEmpty$2(value) || typeof value === 'boolean' || typeof value === 'number';
}
function isEmpty$2(value) {
return value === null || value === undefined || typeof value.toString !== 'function';
}
function isSafeString(value) {
return typeof value === 'object' && value !== null && typeof value.toHTML === 'function';
}
function isNode(value) {
return typeof value === 'object' && value !== null && typeof value.nodeType === 'number';
}
function isFragment(value) {
return isNode(value) && value.nodeType === 11;
}
function isString(value) {
return typeof value === 'string';
}
/*
* @method normalizeProperty
* @param element {HTMLElement}
* @param slotName {String}
* @returns {Object} { name, type }
*/
function normalizeProperty(element, slotName) {
let type, normalized;
if (slotName in element) {
normalized = slotName;
type = 'prop';
} else {
let lower = slotName.toLowerCase();
if (lower in element) {
type = 'prop';
normalized = lower;
} else {
type = 'attr';
normalized = slotName;
}
}
if (type === 'prop' && (normalized.toLowerCase() === 'style' || preferAttr(element.tagName, normalized))) {
type = 'attr';
}
return {
normalized,
type
};
}
// properties that MUST be set as attributes, due to:
// * browser bug
// * strange spec outlier
const ATTR_OVERRIDES = {
INPUT: {
form: true,
// Chrome 46.0.2464.0: 'autocorrect' in document.createElement('input') === false
// Safari 8.0.7: 'autocorrect' in document.createElement('input') === false
// Mobile Safari (iOS 8.4 simulator): 'autocorrect' in document.createElement('input') === true
autocorrect: true,
// Chrome 54.0.2840.98: 'list' in document.createElement('input') === true
// Safari 9.1.3: 'list' in document.createElement('input') === false
list: true
},
// element.form is actually a legitimate readOnly property, that is to be
// mutated, but must be mutated by setAttribute...
SELECT: {
form: true
},
OPTION: {
form: true
},
TEXTAREA: {
form: true
},
LABEL: {
form: true
},
FIELDSET: {
form: true
},
LEGEND: {
form: true
},
OBJECT: {
form: true
},
OUTPUT: {
form: true
},
BUTTON: {
form: true
}
};
function preferAttr(tagName, propName) {
let tag = ATTR_OVERRIDES[tagName.toUpperCase()];
return tag && tag[propName.toLowerCase()] || false;
}
const badProtocols = ['javascript:', 'vbscript:'];
const badTags = ['A', 'BODY', 'LINK', 'IMG', 'IFRAME', 'BASE', 'FORM'];
const badTagsForDataURI = ['EMBED'];
const badAttributes = ['href', 'src', 'background', 'action'];
const badAttributesForDataURI = ['src'];
function has(array, item) {
return array.indexOf(item) !== -1;
}
function checkURI(tagName, attribute) {
return (tagName === null || has(badTags, tagName)) && has(badAttributes, attribute);
}
function checkDataURI(tagName, attribute) {
if (tagName === null) return false;
return has(badTagsForDataURI, tagName) && has(badAttributesForDataURI, attribute);
}
function requiresSanitization(tagName, attribute) {
return checkURI(tagName, attribute) || checkDataURI(tagName, attribute);
}
function findProtocolForURL() {
if (typeof URL === 'object' && URL !== null &&
// this is super annoying, TS thinks that URL **must** be a function so `URL.parse` check
// thinks it is `never` without this `as unknown as any`
typeof URL.parse === 'function') {
// In Ember-land the `fastboot` package sets the `URL` global to `require('url')`
// ultimately, this should be changed (so that we can either rely on the natural `URL` global
// that exists) but for now we have to detect the specific `FastBoot` case first
//
// a future version of `fastboot` will detect if this legacy URL setup is required (by
// inspecting Ember version) and if new enough, it will avoid shadowing the `URL` global
// constructor with `require('url')`.
let nodeURL = URL;
return url => {
let protocol = null;
if (typeof url === 'string') {
protocol = nodeURL.parse(url).protocol;
}
return protocol === null ? ':' : protocol;
};
} else if (typeof URL === 'function') {
return _url => {
try {
let url = new URL(_url);
return url.protocol;
} catch (error) {
// any non-fully qualified url string will trigger an error (because there is no
// baseURI that we can provide; in that case we **know** that the protocol is
// "safe" because it isn't specifically one of the `badProtocols` listed above
// (and those protocols can never be the default baseURI)
return ':';
}
};
} else {
throw new Error(`@glimmer/runtime needs a valid "globalThis.URL"`);
}
}
let _protocolForUrlImplementation;
function protocolForUrl(url) {
if (!_protocolForUrlImplementation) {
_protocolForUrlImplementation = findProtocolForURL();
}
return _protocolForUrlImplementation(url);
}
function sanitizeAttributeValue(element, attribute, value) {
let tagName = null;
if (value === null || value === undefined) {
return value;
}
if (isSafeString(value)) {
return value.toHTML();
}
if (!element) {
tagName = null;
} else {
tagName = element.tagName.toUpperCase();
}
let str = normalizeStringValue(value);
if (checkURI(tagName, attribute)) {
let protocol = protocolForUrl(str);
if (has(badProtocols, protocol)) {
return `unsafe:${str}`;
}
}
if (checkDataURI(tagName, attribute)) {
return `unsafe:${str}`;
}
return str;
}
function dynamicAttribute(element, attr, namespace, isTrusting = false) {
const {
tagName,
namespaceURI
} = element;
const attribute = {
element,
name: attr,
namespace
};
if (DEBUG && attr === 'style' && !isTrusting) {
return new DebugStyleAttributeManager(attribute);
}
if (namespaceURI === NS_SVG) {
return buildDynamicAttribute(tagName, attr, attribute);
}
const {
type,
normalized
} = normalizeProperty(element, attr);
if (type === 'attr') {
return buildDynamicAttribute(tagName, normalized, attribute);
} else {
return buildDynamicProperty(tagName, normalized, attribute);
}
}
function buildDynamicAttribute(tagName, name, attribute) {
if (requiresSanitization(tagName, name)) {
return new SafeDynamicAttribute(attribute);
} else {
return new SimpleDynamicAttribute(attribute);
}
}
function buildDynamicProperty(tagName, name, attribute) {
if (requiresSanitization(tagName, name)) {
return new SafeDynamicProperty(name, attribute);
}
if (isUserInputValue(tagName, name)) {
return new InputValueDynamicAttribute(name, attribute);
}
if (isOptionSelected(tagName, name)) {
return new OptionSelectedDynamicAttribute(name, attribute);
}
return new DefaultDynamicProperty(name, attribute);
}
class DynamicAttribute {
constructor(attribute) {
this.attribute = attribute;
}
}
class SimpleDynamicAttribute extends DynamicAttribute {
set(dom, value, _env) {
const normalizedValue = normalizeValue(value);
if (normalizedValue !== null) {
const {
name,
namespace
} = this.attribute;
dom.__setAttribute(name, normalizedValue, namespace);
}
}
update(value, _env) {
const normalizedValue = normalizeValue(value);
const {
element,
name
} = this.attribute;
if (normalizedValue === null) {
element.removeAttribute(name);
} else {
element.setAttribute(name, normalizedValue);
}
}
}
class DefaultDynamicProperty extends DynamicAttribute {
constructor(normalizedName, attribute) {
super(attribute);
this.normalizedName = normalizedName;
}
value;
set(dom, value, _env) {
if (value !== null && value !== undefined) {
this.value = value;
dom.__setProperty(this.normalizedName, value);
}
}
update(value, _env) {
const {
element
} = this.attribute;
if (this.value !== value) {
element[this.normalizedName] = this.value = value;
if (value === null || value === undefined) {
this.removeAttribute();
}
}
}
removeAttribute() {
// TODO this sucks but to preserve properties first and to meet current
// semantics we must do this.
const {
element,
namespace
} = this.attribute;
if (namespace) {
element.removeAttributeNS(namespace, this.normalizedName);
} else {
element.removeAttribute(this.normalizedName);
}
}
}
class SafeDynamicProperty extends DefaultDynamicProperty {
set(dom, value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.set(dom, sanitized, env);
}
update(value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.update(sanitized, env);
}
}
class SafeDynamicAttribute extends SimpleDynamicAttribute {
set(dom, value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.set(dom, sanitized, env);
}
update(value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.update(sanitized, env);
}
}
class InputValueDynamicAttribute extends DefaultDynamicProperty {
set(dom, value) {
dom.__setProperty('value', normalizeStringValue(value));
}
update(value) {
const input = castToBrowser(this.attribute.element, ['input', 'textarea']);
const currentValue = input.value;
const normalizedValue = normalizeStringValue(value);
if (currentValue !== normalizedValue) {
input.value = normalizedValue;
}
}
}
class OptionSelectedDynamicAttribute extends DefaultDynamicProperty {
set(dom, value) {
if (value !== null && value !== undefined && value !== false) {
dom.__setProperty('selected', true);
}
}
update(value) {
const option = castToBrowser(this.attribute.element, 'option');
if (value) {
option.selected = true;
} else {
option.selected = false;
}
}
}
function isOptionSelected(tagName, attribute) {
return tagName === 'OPTION' && attribute === 'selected';
}
function isUserInputValue(tagName, attribute) {
return (tagName === 'INPUT' || tagName === 'TEXTAREA') && attribute === 'value';
}
function normalizeValue(value) {
if (value === false || value === undefined || value === null || typeof value.toString === 'undefined') {
return null;
}
if (value === true) {
return '';
}
// onclick function etc in SSR
if (typeof value === 'function') {
return null;
}
return String(value);
}
let DebugStyleAttributeManager;
if (DEBUG) {
DebugStyleAttributeManager = class extends SimpleDynamicAttribute {
set(dom, value, env) {
warnIfStyleNotTrusted(value);
super.set(dom, value, env);
}
update(value, env) {
warnIfStyleNotTrusted(value);
super.update(value, env);
}
};
}
class First {
constructor(node) {
this.node = node;
}
firstNode() {
return this.node;
}
}
class Last {
constructor(node) {
this.node = node;
}
lastNode() {
return this.node;
}
}
const CURSOR_STACK = Symbol('CURSOR_STACK');
class NewElementBuilder {
dom;
updateOperations;
constructing = null;
operations = null;
env;
[CURSOR_STACK] = new Stack();
modifierStack = new Stack();
blockStack = new Stack();
static forInitialRender(env, cursor) {
return new this(env, cursor.element, cursor.nextSibling).initialize();
}
static resume(env, block) {
let parentNode = block.parentElement();
let nextSibling = block.reset(env);
let stack = new this(env, parentNode, nextSibling).initialize();
stack.pushLiveBlock(block);
return stack;
}
constructor(env, parentNode, nextSibling) {
this.pushElement(parentNode, nextSibling);
this.env = env;
this.dom = env.getAppendOperations();
this.updateOperations = env.getDOM();
}
initialize() {
this.pushSimpleBlock();
return this;
}
debugBlocks() {
return this.blockStack.toArray();
}
get element() {
return this[CURSOR_STACK].current.element;
}
get nextSibling() {
return this[CURSOR_STACK].current.nextSibling;
}
get hasBlocks() {
return this.blockStack.size > 0;
}
block() {
return expect(this.blockStack.current, 'Expected a current live block');
}
popElement() {
this[CURSOR_STACK].pop();
expect(this[CURSOR_STACK].current, "can't pop past the last element");
}
pushSimpleBlock() {
return this.pushLiveBlock(new SimpleLiveBlock(this.element));
}
pushUpdatableBlock() {
return this.pushLiveBlock(new UpdatableBlockImpl(this.element));
}
pushBlockList(list) {
return this.pushLiveBlock(new LiveBlockList(this.element, list));
}
pushLiveBlock(block, isRemote = false) {
let current = this.blockStack.current;
if (current !== null) {
if (!isRemote) {
current.didAppendBounds(block);
}
}
this.__openBlock();
this.blockStack.push(block);
return block;
}
popBlock() {
this.block().finalize(this);
this.__closeBlock();
return expect(this.blockStack.pop(), 'Expected popBlock to return a block');
}
__openBlock() {}
__closeBlock() {}
// todo return seems unused
openElement(tag) {
let element = this.__openElement(tag);
this.constructing = element;
return element;
}
__openElement(tag) {
return this.dom.createElement(tag, this.element);
}
flushElement(modifiers) {
let parent = this.element;
let element = expect(this.constructing, `flushElement should only be called when constructing an element`);
this.__flushElement(parent, element);
this.constructing = null;
this.operations = null;
this.pushModifiers(modifiers);
this.pushElement(element, null);
this.didOpenElement(element);
}
__flushElement(parent, constructing) {
this.dom.insertBefore(parent, constructing, this.nextSibling);
}
closeElement() {
this.willCloseElement();
this.popElement();
return this.popModifiers();
}
pushRemoteElement(element, guid, insertBefore) {
return this.__pushRemoteElement(element, guid, insertBefore);
}
__pushRemoteElement(element, _guid, insertBefore) {
this.pushElement(element, insertBefore);
if (insertBefore === undefined) {
while (element.lastChild) {
element.removeChild(element.lastChild);
}
}
let block = new RemoteLiveBlock(element);
return this.pushLiveBlock(block, true);
}
popRemoteElement() {
const block = this.popBlock();
assert(block instanceof RemoteLiveBlock, '[BUG] expecting a RemoteLiveBlock');
this.popElement();
return block;
}
pushElement(element, nextSibling = null) {
this[CURSOR_STACK].push(new CursorImpl(element, nextSibling));
}
pushModifiers(modifiers) {
this.modifierStack.push(modifiers);
}
popModifiers() {
return this.modifierStack.pop();
}
didAppendBounds(bounds) {
this.block().didAppendBounds(bounds);
return bounds;
}
didAppendNode(node) {
this.block().didAppendNode(node);
return node;
}
didOpenElement(element) {
this.block().openElement(element);
return element;
}
willCloseElement() {
this.block().closeElement();
}
appendText(string) {
return this.didAppendNode(this.__appendText(string));
}
__appendText(text) {
let {
dom,
element,
nextSibling
} = this;
let node = dom.createTextNode(text);
dom.insertBefore(element, node, nextSibling);
return node;
}
__appendNode(node) {
this.dom.insertBefore(this.element, node, this.nextSibling);
return node;
}
__appendFragment(fragment) {
let first = fragment.firstChild;
if (first) {
let ret = new ConcreteBounds(this.element, first, fragment.lastChild);
this.dom.insertBefore(this.element, fragment, this.nextSibling);
return ret;
} else {
const comment = this.__appendComment('');
return new ConcreteBounds(this.element, comment, comment);
}
}
__appendHTML(html) {
return this.dom.insertHTMLBefore(this.element, this.nextSibling, html);
}
appendDynamicHTML(value) {
let bounds = this.trustedContent(value);
this.didAppendBounds(bounds);
}
appendDynamicText(value) {
let node = this.untrustedContent(value);
this.didAppendNode(node);
return node;
}
appendDynamicFragment(value) {
let bounds = this.__appendFragment(value);
this.didAppendBounds(bounds);
}
appendDynamicNode(value) {
let node = this.__appendNode(value);
let bounds = new ConcreteBounds(this.element, node, node);
this.didAppendBounds(bounds);
}
trustedContent(value) {
return this.__appendHTML(value);
}
untrustedContent(value) {
return this.__appendText(value);
}
appendComment(string) {
return this.didAppendNode(this.__appendComment(string));
}
__appendComment(string) {
let {
dom,
element,
nextSibling
} = this;
let node = dom.createComment(string);
dom.insertBefore(element, node, nextSibling);
return node;
}
__setAttribute(name, value, namespace) {
this.dom.setAttribute(this.constructing, name, value, namespace);
}
__setProperty(name, value) {
this.constructing[name] = value;
}
setStaticAttribute(name, value, namespace) {
this.__setAttribute(name, value, namespace);
}
setDynamicAttribute(name, value, trusting, namespace) {
let element = this.constructing;
let attribute = dynamicAttribute(element, name, namespace, trusting);
attribute.set(this, value, this.env);
return attribute;
}
}
class SimpleLiveBlock {
first = null;
last = null;
nesting = 0;
constructor(parent) {
this.parent = parent;
}
parentElement() {
return this.parent;
}
firstNode() {
let first = expect(this.first, 'cannot call `firstNode()` while `SimpleLiveBlock` is still initializing');
return first.firstNode();
}
lastNode() {
let last = expect(this.last, 'cannot call `lastNode()` while `SimpleLiveBlock` is still initializing');
return last.lastNode();
}
openElement(element) {
this.didAppendNode(element);
this.nesting++;
}
closeElement() {
this.nesting--;
}
didAppendNode(node) {
if (this.nesting !== 0) return;
if (!this.first) {
this.first = new First(node);
}
this.last = new Last(node);
}
didAppendBounds(bounds) {
if (this.nesting !== 0) return;
if (!this.first) {
this.first = bounds;
}
this.last = bounds;
}
finalize(stack) {
if (this.first === null) {
stack.appendComment('');
}
}
}
class RemoteLiveBlock extends SimpleLiveBlock {
constructor(parent) {
super(parent);
registerDestructor(this, () => {
// In general, you only need to clear the root of a hierarchy, and should never
// need to clear any child nodes. This is an important constraint that gives us
// a strong guarantee that clearing a subtree is a single DOM operation.
//
// Because remote blocks are not normally physically nested inside of the tree
// that they are logically nested inside, we manually clear remote blocks when
// a logical parent is cleared.
//
// HOWEVER, it is currently possible for a remote block to be physically nested
// inside of the block it is logically contained inside of. This happens when
// the remote block is appended to the end of the application's entire element.
//
// The problem with that scenario is that Glimmer believes that it owns more of
// the DOM than it actually does. The code is attempting to write past the end
// of the Glimmer-managed root, but Glimmer isn't aware of that.
//
// The correct solution to that problem is for Glimmer to be aware of the end
// of the bounds that it owns, and once we make that change, this check could
// be removed.
//
// For now, a more targeted fix is to check whether the node was already removed
// and avoid clearing the node if it was. In most cases this shouldn't happen,
// so this might hide bugs where the code clears nested nodes unnecessarily,
// so we should eventually try to do the correct fix.
if (this.parentElement() === this.firstNode().parentNode) {
clear(this);
}
});
}
}
class UpdatableBlockImpl extends SimpleLiveBlock {
reset() {
destroy(this);
let nextSibling = clear(this);
this.first = null;
this.last = null;
this.nesting = 0;
return nextSibling;
}
}
// FIXME: All the noops in here indicate a modelling problem
class LiveBlockList {
constructor(parent, boundList) {
this.parent = parent;
this.boundList = boundList;
this.parent = parent;
this.boundList = boundList;
}
parentElement() {
return this.parent;
}
firstNode() {
let head = expect(this.boundList[0], 'cannot call `firstNode()` while `LiveBlockList` is still initializing');
return head.firstNode();
}
lastNode() {
let boundList = this.boundList;
let tail = expect(boundList[boundList.length - 1], 'cannot call `lastNode()` while `LiveBlockList` is still initializing');
return tail.lastNode();
}
openElement(_element) {
assert(false, 'Cannot openElement directly inside a block list');
}
closeElement() {
assert(false, 'Cannot closeElement directly inside a block list');
}
didAppendNode(_node) {
assert(false, 'Cannot create a new node directly inside a block list');
}
didAppendBounds(_bounds) {}
finalize(_stack) {
assert(this.boundList.length > 0, 'boundsList cannot be empty');
}
}
function clientBuilder(env, cursor) {
return NewElementBuilder.forInitialRender(env, cursor);
}
class AppendOpcodes {
evaluateOpcode = new Array(Op.Size).fill(null);
add(name, evaluate, kind = 'syscall') {
this.evaluateOpcode[name] = {
syscall: kind !== 'machine',
evaluate
};
}
debugBefore(vm, opcode) {
let params = undefined;
let opName = undefined;
let sp;
recordStackSize(vm.fetchValue($sp));
return {
sp: sp,
pc: vm.fetchValue($pc),
name: opName,
params,
type: opcode.type,
isMachine: opcode.isMachine,
size: opcode.size,
state: undefined
};
}
debugAfter(vm, pre) {
}
evaluate(vm, opcode, type) {
let operation = unwrap(this.evaluateOpcode[type]);
if (operation.syscall) {
assert(!opcode.isMachine, `BUG: Mismatch between operation.syscall (${operation.syscall}) and opcode.isMachine (${opcode.isMachine}) for ${opcode.type}`);
operation.evaluate(vm, opcode);
} else {
assert(opcode.isMachine, `BUG: Mismatch between operation.syscall (${operation.syscall}) and opcode.isMachine (${opcode.isMachine}) for ${opcode.type}`);
operation.evaluate(vm[INNER_VM], opcode);
}
}
}
const APPEND_OPCODES = new AppendOpcodes();
const TYPE = Symbol('TYPE');
const INNER = Symbol('INNER');
const OWNER = Symbol('OWNER');
const ARGS = Symbol('ARGS');
const RESOLVED = Symbol('RESOLVED');
const CURRIED_VALUES = new WeakSet();
function isCurriedValue(value) {
return CURRIED_VALUES.has(value);
}
function isCurriedType(value, type) {
return isCurriedValue(value) && value[TYPE] === type;
}
class CurriedValue {
[TYPE];
[INNER];
[OWNER];
[ARGS];
[RESOLVED];
/** @internal */
constructor(type, inner, owner, args, resolved = false) {
CURRIED_VALUES.add(this);
this[TYPE] = type;
this[INNER] = inner;
this[OWNER] = owner;
this[ARGS] = args;
this[RESOLVED] = resolved;
}
}
function resolveCurriedValue(curriedValue) {
let currentWrapper = curriedValue;
let positional;
let named;
let definition, owner, resolved;
// eslint-disable-next-line no-constant-condition
while (true) {
let {
[ARGS]: curriedArgs,
[INNER]: inner
} = currentWrapper;
if (curriedArgs !== null) {
let {
named: curriedNamed,
positional: curriedPositional
} = curriedArgs;
if (curriedPositional.length > 0) {
positional = positional === undefined ? curriedPositional : curriedPositional.concat(positional);
}
if (named === undefined) {
named = [];
}
named.unshift(curriedNamed);
}
if (!isCurriedValue(inner)) {
// Save off the owner that this helper was curried with. Later on,
// we'll fetch the value of this register and set it as the owner on the
// new root scope.
definition = inner;
owner = currentWrapper[OWNER];
resolved = currentWrapper[RESOLVED];
break;
}
currentWrapper = inner;
}
return {
definition,
owner,
resolved,
positional,
named
};
}
function curry(type, spec, owner, args, resolved = false) {
return new CurriedValue(type, spec, owner, args, resolved);
}
function createCurryRef(type, inner, owner, args, resolver, isStrict) {
let lastValue, curriedDefinition;
return createComputeRef(() => {
let value = valueForRef(inner);
if (value === lastValue) {
return curriedDefinition;
}
if (isCurriedType(value, type)) {
curriedDefinition = args ? curry(type, value, owner, args) : args;
} else if (type === CurriedTypes.Component && typeof value === 'string' && value) {
// Only components should enter this path, as helpers and modifiers do not
// support string based resolution
if (DEBUG) {
if (isStrict) {
throw new Error(`Attempted to resolve a dynamic component with a string definition, \`${value}\` in a strict mode template. In strict mode, using strings to resolve component definitions is prohibited. You can instead import the component definition and use it directly.`);
}
let resolvedDefinition = expect(resolver, 'BUG: expected resolver for curried component definitions').lookupComponent(value, owner);
if (!resolvedDefinition) {
throw new Error(`Attempted to resolve \`${value}\`, which was expected to be a component, but nothing was found.`);
}
}
curriedDefinition = curry(type, value, owner, args);
} else if (isObject(value)) {
curriedDefinition = curry(type, value, owner, args);
} else {
curriedDefinition = null;
}
lastValue = value;
return curriedDefinition;
});
}
/** @internal */
function hasCustomDebugRenderTreeLifecycle(manager) {
return 'getDebugCustomRenderTree' in manager;
}
function resolveComponent(resolver, constants, name, owner) {
let definition = resolver.lookupComponent(name, expect(owner, 'BUG: expected owner when looking up component'));
if (DEBUG && !definition) {
throw new Error(`Attempted to resolve \`${name}\`, which was expected to be a component, but nothing was found.`);
}
return constants.resolvedComponent(definition, name);
}
function createClassListRef(list) {
return createComputeRef(() => {
let ret = [];
for (const ref of list) {
let value = normalizeStringValue(typeof ref === 'string' ? ref : valueForRef(ref));
if (value) ret.push(value);
}
return ret.length === 0 ? null : ret.join(' ');
});
}
function stackAssert(name, top) {
return `Expected top of stack to be ${name}, was ${String(top)}`;
}
APPEND_OPCODES.add(Op.ChildScope, vm => vm.pushChildScope());
APPEND_OPCODES.add(Op.PopScope, vm => vm.popScope());
APPEND_OPCODES.add(Op.PushDynamicScope, vm => vm.pushDynamicScope());
APPEND_OPCODES.add(Op.PopDynamicScope, vm => vm.popDynamicScope());
APPEND_OPCODES.add(Op.Constant, (vm, {
op1: other
}) => {
vm.stack.push(vm[CONSTANTS].getValue(decodeHandle(other)));
});
APPEND_OPCODES.add(Op.ConstantReference, (vm, {
op1: other
}) => {
vm.stack.push(createConstRef(vm[CONSTANTS].getValue(decodeHandle(other)), false));
});
APPEND_OPCODES.add(Op.Primitive, (vm, {
op1: primitive
}) => {
let stack = vm.stack;
if (isHandle(primitive)) {
// it is a handle which does not already exist on the stack
let value = vm[CONSTANTS].getValue(decodeHandle(primitive));
stack.push(value);
} else {
// is already an encoded immediate or primitive handle
stack.push(decodeImmediate(primitive));
}
});
APPEND_OPCODES.add(Op.PrimitiveReference, vm => {
let stack = vm.stack;
let value = check(stack.pop(), CheckPrimitive);
let ref;
if (value === undefined) {
ref = UNDEFINED_REFERENCE;
} else if (value === null) {
ref = NULL_REFERENCE;
} else if (value === true) {
ref = TRUE_REFERENCE;
} else if (value === false) {
ref = FALSE_REFERENCE;
} else {
ref = createPrimitiveRef(value);
}
stack.push(ref);
});
APPEND_OPCODES.add(Op.Dup, (vm, {
op1: register,
op2: offset
}) => {
let position = check(vm.fetchValue(register), CheckNumber) - offset;
vm.stack.dup(position);
});
APPEND_OPCODES.add(Op.Pop, (vm, {
op1: count
}) => {
vm.stack.pop(count);
});
APPEND_OPCODES.add(Op.Load, (vm, {
op1: register
}) => {
vm.load(register);
});
APPEND_OPCODES.add(Op.Fetch, (vm, {
op1: register
}) => {
vm.fetch(register);
});
APPEND_OPCODES.add(Op.BindDynamicScope, (vm, {
op1: _names
}) => {
let names = vm[CONSTANTS].getArray(_names);
vm.bindDynamicScope(names);
});
APPEND_OPCODES.add(Op.Enter, (vm, {
op1: args
}) => {
vm.enter(args);
});
APPEND_OPCODES.add(Op.Exit, vm => {
vm.exit();
});
APPEND_OPCODES.add(Op.PushSymbolTable, (vm, {
op1: _table
}) => {
let stack = vm.stack;
stack.push(vm[CONSTANTS].getValue(_table));
});
APPEND_OPCODES.add(Op.PushBlockScope, vm => {
let stack = vm.stack;
stack.push(vm.scope());
});
APPEND_OPCODES.add(Op.CompileBlock, vm => {
let stack = vm.stack;
let block = stack.pop();
if (block) {
stack.push(vm.compile(block));
} else {
stack.push(null);
}
});
APPEND_OPCODES.add(Op.InvokeYield, vm => {
let {
stack
} = vm;
let handle = check(stack.pop(), CheckOption(CheckHandle));
let scope = check(stack.pop(), CheckOption(CheckScope));
let table = check(stack.pop(), CheckOption(CheckBlockSymbolTable));
assert(table === null || table && typeof table === 'object' && Array.isArray(table.parameters), stackAssert('Option<BlockSymbolTable>', table));
let args = check(stack.pop(), CheckInstanceof(VMArgumentsImpl));
if (table === null) {
// To balance the pop{Frame,Scope}
vm.pushFrame();
vm.pushScope(scope ?? vm.scope());
return;
}
let invokingScope = expect(scope, 'BUG: expected scope');
// If necessary, create a child scope
{
let locals = table.parameters;
let localsCount = locals.length;
if (localsCount > 0) {
invokingScope = invokingScope.child();
for (let i = 0; i < localsCount; i++) {
invokingScope.bindSymbol(unwrap(locals[i]), args.at(i));
}
}
}
vm.pushFrame();
vm.pushScope(invokingScope);
vm.call(handle);
});
APPEND_OPCODES.add(Op.JumpIf, (vm, {
op1: target
}) => {
let reference = check(vm.stack.pop(), CheckReference);
let value = Boolean(valueForRef(reference));
if (isConstRef(reference)) {
if (value === true) {
vm.goto(target);
}
} else {
if (value === true) {
vm.goto(target);
}
vm.updateWith(new Assert(reference));
}
});
APPEND_OPCODES.add(Op.JumpUnless, (vm, {
op1: target
}) => {
let reference = check(vm.stack.pop(), CheckReference);
let value = Boolean(valueForRef(reference));
if (isConstRef(reference)) {
if (value === false) {
vm.goto(target);
}
} else {
if (value === false) {
vm.goto(target);
}
vm.updateWith(new Assert(reference));
}
});
APPEND_OPCODES.add(Op.JumpEq, (vm, {
op1: target,
op2: comparison
}) => {
let other = check(vm.stack.peek(), CheckNumber);
if (other === comparison) {
vm.goto(target);
}
});
APPEND_OPCODES.add(Op.AssertSame, vm => {
let reference = check(vm.stack.peek(), CheckReference);
if (isConstRef(reference) === false) {
vm.updateWith(new Assert(reference));
}
});
APPEND_OPCODES.add(Op.ToBoolean, vm => {
let {
stack
} = vm;
let valueRef = check(stack.pop(), CheckReference);
stack.push(createComputeRef(() => toBool(valueForRef(valueRef))));
});
class Assert {
last;
constructor(ref) {
this.ref = ref;
this.last = valueForRef(ref);
}
evaluate(vm) {
let {
last,
ref
} = this;
let current = valueForRef(ref);
if (last !== current) {
vm.throw();
}
}
}
class AssertFilter {
last;
constructor(ref, filter) {
this.ref = ref;
this.filter = filter;
this.last = filter(valueForRef(ref));
}
evaluate(vm) {
let {
last,
ref,
filter
} = this;
let current = filter(valueForRef(ref));
if (last !== current) {
vm.throw();
}
}
}
class JumpIfNotModifiedOpcode {
tag = CONSTANT_TAG;
lastRevision = INITIAL;
target;
finalize(tag, target) {
this.target = target;
this.didModify(tag);
}
evaluate(vm) {
let {
tag,
target,
lastRevision
} = this;
if (!vm.alwaysRevalidate && validateTag(tag, lastRevision)) {
consumeTag(tag);
vm.goto(expect(target, 'VM BUG: Target must be set before attempting to jump'));
}
}
didModify(tag) {
this.tag = tag;
this.lastRevision = valueForTag(this.tag);
consumeTag(tag);
}
}
class BeginTrackFrameOpcode {
constructor(debugLabel) {
this.debugLabel = debugLabel;
}
evaluate() {
beginTrackFrame(this.debugLabel);
}
}
class EndTrackFrameOpcode {
constructor(target) {
this.target = target;
}
evaluate() {
let tag = endTrackFrame();
this.target.didModify(tag);
}
}
APPEND_OPCODES.add(Op.Text, (vm, {
op1: text
}) => {
vm.elements().appendText(vm[CONSTANTS].getValue(text));
});
APPEND_OPCODES.add(Op.Comment, (vm, {
op1: text
}) => {
vm.elements().appendComment(vm[CONSTANTS].getValue(text));
});
APPEND_OPCODES.add(Op.OpenElement, (vm, {
op1: tag
}) => {
vm.elements().openElement(vm[CONSTANTS].getValue(tag));
});
APPEND_OPCODES.add(Op.OpenDynamicElement, vm => {
let tagName = check(valueForRef(check(vm.stack.pop(), CheckReference)), CheckString);
vm.elements().openElement(tagName);
});
APPEND_OPCODES.add(Op.PushRemoteElement, vm => {
let elementRef = check(vm.stack.pop(), CheckReference);
let insertBeforeRef = check(vm.stack.pop(), CheckReference);
let guidRef = check(vm.stack.pop(), CheckReference);
let element = check(valueForRef(elementRef), CheckElement);
let insertBefore = check(valueForRef(insertBeforeRef), CheckMaybe(CheckOption(CheckNode)));
let guid = valueForRef(guidRef);
if (!isConstRef(elementRef)) {
vm.updateWith(new Assert(elementRef));
}
if (insertBefore !== undefined && !isConstRef(insertBeforeRef)) {
vm.updateWith(new Assert(insertBeforeRef));
}
let block = vm.elements().pushRemoteElement(element, guid, insertBefore);
if (block) vm.associateDestroyable(block);
if (vm.env.debugRenderTree !== undefined) {
// Note that there is nothing to update – when the args for an
// {{#in-element}} changes it gets torn down and a new one is
// re-created/rendered in its place (see the `Assert`s above)
let args = createCapturedArgs(insertBefore === undefined ? {} : {
insertBefore: insertBeforeRef
}, [elementRef]);
vm.env.debugRenderTree.create(block, {
type: 'keyword',
name: 'in-element',
args,
instance: null
});
registerDestructor(block, () => {
vm.env.debugRenderTree?.willDestroy(block);
});
}
});
APPEND_OPCODES.add(Op.PopRemoteElement, vm => {
let bounds = vm.elements().popRemoteElement();
if (vm.env.debugRenderTree !== undefined) {
// The RemoteLiveBlock is also its bounds
vm.env.debugRenderTree.didRender(bounds, bounds);
}
});
APPEND_OPCODES.add(Op.FlushElement, vm => {
let operations = check(vm.fetchValue($t0), CheckOperations);
let modifiers = null;
if (operations) {
modifiers = operations.flush(vm);
vm.loadValue($t0, null);
}
vm.elements().flushElement(modifiers);
});
APPEND_OPCODES.add(Op.CloseElement, vm => {
let modifiers = vm.elements().closeElement();
if (modifiers !== null) {
modifiers.forEach(modifier => {
vm.env.scheduleInstallModifier(modifier);
const d = modifier.manager.getDestroyable(modifier.state);
if (d !== null) {
vm.associateDestroyable(d);
}
});
}
});
APPEND_OPCODES.add(Op.Modifier, (vm, {
op1: handle
}) => {
if (vm.env.isInteractive === false) {
return;
}
let owner = vm.getOwner();
let args = check(vm.stack.pop(), CheckArguments);
let definition = vm[CONSTANTS].getValue(handle);
let {
manager
} = definition;
let {
constructing
} = vm.elements();
let capturedArgs = args.capture();
let state = manager.create(owner, expect(constructing, 'BUG: ElementModifier could not find the element it applies to'), definition.state, capturedArgs);
let instance = {
manager,
state,
definition
};
let operations = expect(check(vm.fetchValue($t0), CheckOperations), 'BUG: ElementModifier could not find operations to append to');
operations.addModifier(vm, instance, capturedArgs);
let tag = manager.getTag(state);
if (tag !== null) {
consumeTag(tag);
return vm.updateWith(new UpdateModifierOpcode(tag, instance));
}
});
APPEND_OPCODES.add(Op.DynamicModifier, vm => {
if (vm.env.isInteractive === false) {
return;
}
let {
stack
} = vm;
let ref = check(stack.pop(), CheckReference);
let args = check(stack.pop(), CheckArguments).capture();
let {
positional: outerPositional,
named: outerNamed
} = args;
let {
constructing
} = vm.elements();
let initialOwner = vm.getOwner();
let instanceRef = createComputeRef(() => {
let value = valueForRef(ref);
let owner;
if (!isObject(value)) {
return;
}
let hostDefinition;
if (isCurriedType(value, CurriedTypes.Modifier)) {
let {
definition: resolvedDefinition,
owner: curriedOwner,
positional,
named
} = resolveCurriedValue(value);
hostDefinition = resolvedDefinition;
owner = curriedOwner;
if (positional !== undefined) {
args.positional = positional.concat(outerPositional);
}
if (named !== undefined) {
args.named = Object.assign({}, ...named, outerNamed);
}
} else {
hostDefinition = value;
owner = initialOwner;
}
let manager = getInternalModifierManager(hostDefinition, true);
if (manager === null) {
if (DEBUG) {
throw new Error(`Expected a dynamic modifier definition, but received an object or function that did not have a modifier manager associated with it. The dynamic invocation was \`{{${ref.debugLabel}}}\`, and the incorrect definition is the value at the path \`${ref.debugLabel}\`, which was: ${debugToString(hostDefinition)}`);
} else {
throw new Error('BUG: modifier manager expected');
}
}
let definition = {
resolvedName: null,
manager,
state: hostDefinition
};
let state = manager.create(owner, expect(constructing, 'BUG: ElementModifier could not find the element it applies to'), definition.state, args);
return {
manager,
state,
definition
};
});
let instance = valueForRef(instanceRef);
let tag = null;
if (instance !== undefined) {
let operations = expect(check(vm.fetchValue($t0), CheckOperations), 'BUG: ElementModifier could not find operations to append to');
operations.addModifier(vm, instance, args);
tag = instance.manager.getTag(instance.state);
if (tag !== null) {
consumeTag(tag);
}
}
if (!isConstRef(ref) || tag) {
return vm.updateWith(new UpdateDynamicModifierOpcode(tag, instance, instanceRef));
}
});
class UpdateModifierOpcode {
lastUpdated;
constructor(tag, modifier) {
this.tag = tag;
this.modifier = modifier;
this.lastUpdated = valueForTag(tag);
}
evaluate(vm) {
let {
modifier,
tag,
lastUpdated
} = this;
consumeTag(tag);
if (!validateTag(tag, lastUpdated)) {
vm.env.scheduleUpdateModifier(modifier);
this.lastUpdated = valueForTag(tag);
}
}
}
class UpdateDynamicModifierOpcode {
lastUpdated;
constructor(tag, instance, instanceRef) {
this.tag = tag;
this.instance = instance;
this.instanceRef = instanceRef;
this.lastUpdated = valueForTag(tag ?? CURRENT_TAG);
}
evaluate(vm) {
let {
tag,
lastUpdated,
instance,
instanceRef
} = this;
let newInstance = valueForRef(instanceRef);
if (newInstance !== instance) {
if (instance !== undefined) {
let destroyable = instance.manager.getDestroyable(instance.state);
if (destroyable !== null) {
destroy(destroyable);
}
}
if (newInstance !== undefined) {
let {
manager,
state
} = newInstance;
let destroyable = manager.getDestroyable(state);
if (destroyable !== null) {
associateDestroyableChild(this, destroyable);
}
tag = manager.getTag(state);
if (tag !== null) {
this.lastUpdated = valueForTag(tag);
}
this.tag = tag;
vm.env.scheduleInstallModifier(newInstance);
}
this.instance = newInstance;
} else if (tag !== null && !validateTag(tag, lastUpdated)) {
vm.env.scheduleUpdateModifier(instance);
this.lastUpdated = valueForTag(tag);
}
if (tag !== null) {
consumeTag(tag);
}
}
}
APPEND_OPCODES.add(Op.StaticAttr, (vm, {
op1: _name,
op2: _value,
op3: _namespace
}) => {
let name = vm[CONSTANTS].getValue(_name);
let value = vm[CONSTANTS].getValue(_value);
let namespace = _namespace ? vm[CONSTANTS].getValue(_namespace) : null;
vm.elements().setStaticAttribute(name, value, namespace);
});
APPEND_OPCODES.add(Op.DynamicAttr, (vm, {
op1: _name,
op2: _trusting,
op3: _namespace
}) => {
let name = vm[CONSTANTS].getValue(_name);
let trusting = vm[CONSTANTS].getValue(_trusting);
let reference = check(vm.stack.pop(), CheckReference);
let value = valueForRef(reference);
let namespace = _namespace ? vm[CONSTANTS].getValue(_namespace) : null;
let attribute = vm.elements().setDynamicAttribute(name, value, trusting, namespace);
if (!isConstRef(reference)) {
vm.updateWith(new UpdateDynamicAttributeOpcode(reference, attribute, vm.env));
}
});
class UpdateDynamicAttributeOpcode {
updateRef;
constructor(reference, attribute, env) {
let initialized = false;
this.updateRef = createComputeRef(() => {
let value = valueForRef(reference);
if (initialized === true) {
attribute.update(value, env);
} else {
initialized = true;
}
});
valueForRef(this.updateRef);
}
evaluate() {
valueForRef(this.updateRef);
}
}
/**
* The VM creates a new ComponentInstance data structure for every component
* invocation it encounters.
*
* Similar to how a ComponentDefinition contains state about all c