UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,707 lines (1,679 loc) 186 kB
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