diff --git a/package.json b/package.json index 3dc91e972e1..03edceeb942 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,34 @@ }, "./package.json": "./package.json" }, - "sideEffects": false, + "sideEffects": [ + "./dist/dev/**", + "**/@ember/-internals/glimmer/lib/setup-registry.js", + "**/@ember/-internals/glimmer/lib/register-curly-component.js", + "**/@ember/-internals/glimmer/lib/syntax/register-routing-keywords.js", + "**/@ember/-internals/glimmer/lib/environment.js", + "**/@ember/-internals/glimmer/lib/renderer.js", + "**/@ember/-internals/glimmer/lib/helper.js", + "**/@ember/-internals/glimmer/lib/helpers/element.js", + "**/@ember/-internals/glimmer/lib/components/input.js", + "**/@ember/-internals/glimmer/lib/components/textarea.js", + "**/@ember/-internals/glimmer/lib/components/link-to.js", + "**/@ember/-internals/runtime/lib/ext/rsvp.js", + "**/@ember/-internals/metal/lib/observer.js", + "**/@ember/-internals/metal/lib/decorator.js", + "**/@glimmer/runtime/lib/debug-render-tree-register.js", + "**/@glimmer/runtime/lib/component/template-only.js", + "**/@glimmer/runtime/lib/compiled/opcodes/expressions.js", + "**/@glimmer/runtime/lib/compiled/opcodes/vm.js", + "**/@glimmer/runtime/lib/compiled/opcodes/debugger.js", + "**/@glimmer/runtime/lib/compiled/opcodes/content.js", + "**/@glimmer/runtime/lib/compiled/opcodes/component.js", + "**/@glimmer/runtime/lib/compiled/opcodes/dom.js", + "**/@glimmer/runtime/lib/compiled/opcodes/lists.js", + "**/@glimmer/runtime/lib/vm/low-level.js", + "**/@glimmer/validator/index.js", + "**/@glimmer/validator/lib/validators.js" + ], "homepage": "https://emberjs.com/", "bugs": { "url": "https://github.com/emberjs/ember.js/issues" @@ -192,6 +219,7 @@ "@ember/-internals/runtime/lib/mixins/action_handler.js": "ember-source/@ember/-internals/runtime/lib/mixins/action_handler.js", "@ember/-internals/runtime/lib/mixins/comparable.js": "ember-source/@ember/-internals/runtime/lib/mixins/comparable.js", "@ember/-internals/runtime/lib/mixins/container_proxy.js": "ember-source/@ember/-internals/runtime/lib/mixins/container_proxy.js", + "@ember/-internals/runtime/lib/mixins/content-for.js": "ember-source/@ember/-internals/runtime/lib/mixins/content-for.js", "@ember/-internals/runtime/lib/mixins/registry_proxy.js": "ember-source/@ember/-internals/runtime/lib/mixins/registry_proxy.js", "@ember/-internals/runtime/lib/mixins/target_action_support.js": "ember-source/@ember/-internals/runtime/lib/mixins/target_action_support.js", "@ember/-internals/string/index.js": "ember-source/@ember/-internals/string/index.js", @@ -239,9 +267,11 @@ "@ember/enumerable/mutable.js": "ember-source/@ember/enumerable/mutable.js", "@ember/helper/index.js": "ember-source/@ember/helper/index.js", "@ember/instrumentation/index.js": "ember-source/@ember/instrumentation/index.js", + "@ember/instrumentation/lib/internal-instrument.js": "ember-source/@ember/instrumentation/lib/internal-instrument.js", "@ember/modifier/index.js": "ember-source/@ember/modifier/index.js", "@ember/modifier/on.js": "ember-source/@ember/modifier/on.js", "@ember/object/-internals.js": "ember-source/@ember/object/-internals.js", + "@ember/object/action.js": "ember-source/@ember/object/action.js", "@ember/object/compat.js": "ember-source/@ember/object/compat.js", "@ember/object/computed.js": "ember-source/@ember/object/computed.js", "@ember/object/core.js": "ember-source/@ember/object/core.js", diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index 66afa93812e..b6b5c188813 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -467,13 +467,8 @@ export { htmlSafe, isHTMLSafe, } from './lib/utils/string'; -export { - Renderer, - _resetRenderers, - renderSettled, - renderComponent, - type View, -} from './lib/renderer'; +export { _resetRenderers, renderSettled, renderComponent } from './lib/renderer'; +export { Renderer, type View } from './lib/classic-renderer'; export { getTemplate, setTemplate, diff --git a/packages/@ember/-internals/glimmer/lib/classic-renderer.ts b/packages/@ember/-internals/glimmer/lib/classic-renderer.ts new file mode 100644 index 00000000000..87486c421e7 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/classic-renderer.ts @@ -0,0 +1,372 @@ +import { privatize as P } from '@ember/-internals/container/lib/registry'; +import type { InternalOwner } from '@ember/-internals/owner'; +import { getOwner } from '@ember/-internals/owner'; +import type { Nullable } from '@ember/-internals/utility-types'; +import { guidFor } from '@ember/-internals/utils/lib/guid'; +import { getViewElement, getViewId } from '@ember/-internals/views/lib/system/utils'; +import { assert } from '@ember/debug'; +import { + associateDestroyableChild, + destroy, + isDestroyed, + isDestroying, +} from '@glimmer/destroyable'; +import type { + Bounds, + CurriedComponent, + DynamicScope as GlimmerDynamicScope, + Environment, + EvaluationContext, + RenderResult as GlimmerRenderResult, + Template, + TemplateFactory, +} from '@glimmer/interfaces'; +import type { Reference } from '@glimmer/reference/lib/reference'; +import { createConstRef, UNDEFINED_REFERENCE, valueForRef } from '@glimmer/reference/lib/reference'; +import type { CurriedValue } from '@glimmer/runtime/lib/curried-value'; +import { curry } from '@glimmer/runtime/lib/curried-value'; +import { createCapturedArgs, EMPTY_POSITIONAL } from '@glimmer/runtime/lib/vm/arguments'; +import { clientBuilder } from '@glimmer/runtime/lib/vm/element-builder'; +import { inTransaction } from '@glimmer/runtime/lib/environment'; +import { renderMain } from '@glimmer/runtime/lib/render'; +import { dict } from '@glimmer/util/lib/collections'; +import type { SimpleDocument, SimpleElement, SimpleNode } from '@simple-dom/interface'; + +import { hasDOM } from '../../browser-environment'; +import type Component from './component'; +import type ClassicComponent from './component'; +import { BOUNDS } from './component-managers/curly-symbols'; +import { createRootOutlet } from './component-managers/outlet'; +import { RootComponentDefinition } from './component-managers/root'; +import { makeRouteTemplate } from './component-managers/route-template'; +import { unwrapTemplate } from './component-managers/unwrap-template'; +import { BaseRenderer, errorLoopTransaction, type IBuilder, type RootState } from './renderer'; +import ResolverImpl from './resolver'; +import type { OutletState } from './utils/outlet'; +import OutletView from './views/outlet'; + +export interface View { + parentView: Nullable; + renderer: Renderer; + tagName: string | null; + elementId: string | null; + isDestroying: boolean; + isDestroyed: boolean; + [BOUNDS]: Bounds | null; +} + +export class DynamicScope implements GlimmerDynamicScope { + constructor( + public view: View | null, + public outletState: Reference + ) {} + + child() { + return new DynamicScope(this.view, this.outletState); + } + + get(key: 'outletState'): Reference { + assert( + `Using \`-get-dynamic-scope\` is only supported for \`outletState\` (you used \`${key}\`).`, + key === 'outletState' + ); + return this.outletState; + } + + set(key: 'outletState', value: Reference) { + assert( + `Using \`-with-dynamic-scope\` is only supported for \`outletState\` (you used \`${key}\`).`, + key === 'outletState' + ); + this.outletState = value; + return value; + } +} + +class ClassicRootState implements RootState { + readonly type = 'classic'; + public id: string; + public result: GlimmerRenderResult | undefined; + public destroyed: boolean; + public render: () => void; + readonly env: Environment; + + constructor( + public root: Component | OutletView, + context: EvaluationContext, + owner: object, + template: Template, + self: Reference, + parentElement: SimpleElement, + dynamicScope: DynamicScope, + builder: IBuilder + ) { + assert( + `You cannot render \`${valueForRef(self)}\` without a template.`, + template !== undefined + ); + + this.id = root instanceof OutletView ? guidFor(root) : getViewId(root); + this.result = undefined; + this.destroyed = false; + this.env = context.env; + + this.render = errorLoopTransaction(() => { + let layout = unwrapTemplate(template).asLayout(); + + let iterator = renderMain( + context, + owner, + self, + builder(context.env, { element: parentElement, nextSibling: null }), + layout, + dynamicScope + ); + + let result = (this.result = iterator.sync()); + + associateDestroyableChild(this, result); + + this.render = errorLoopTransaction(() => { + if (isDestroying(result) || isDestroyed(result)) return; + + return result.rerender({ + alwaysRevalidate: false, + }); + }); + }); + } + + isFor(possibleRoot: unknown): boolean { + return this.root === possibleRoot; + } + + destroy() { + let { result, env } = this; + + this.destroyed = true; + + this.root = null as any; + this.result = undefined; + this.render = undefined as any; + + if (result !== undefined) { + /* + Handles these scenarios: + + * When roots are removed during standard rendering process, a transaction exists already + `.begin()` / `.commit()` are not needed. + * When roots are being destroyed manually (`component.append(); component.destroy() case), no + transaction exists already. + * When roots are being destroyed during `Renderer#destroy`, no transaction exists + + */ + + inTransaction(env, () => destroy(result!)); + } + } +} + +interface ViewRegistry { + [viewId: string]: unknown; +} + +export class Renderer extends BaseRenderer { + static override strict( + owner: object, + document: SimpleDocument | Document, + options: { isInteractive: boolean; hasDOM?: boolean } + ): BaseRenderer { + return new BaseRenderer( + owner, + { hasDOM: hasDOM, ...options }, + document as SimpleDocument, + new ResolverImpl(), + clientBuilder + ); + } + + private _rootTemplate: Template; + private _viewRegistry: ViewRegistry; + + static create(props: { _viewRegistry: any }): Renderer { + let { _viewRegistry } = props; + let owner = getOwner(props); + assert('Renderer is unexpectedly missing an owner', owner); + let document = owner.lookup('service:-document') as SimpleDocument; + let env = owner.lookup('-environment:main') as { + isInteractive: boolean; + hasDOM: boolean; + }; + let rootTemplate = owner.lookup(P`template:-root`) as TemplateFactory; + let builder = owner.lookup('service:-dom-builder') as IBuilder; + return new this(owner, document, env, rootTemplate, _viewRegistry, builder); + } + + constructor( + owner: InternalOwner, + document: SimpleDocument, + env: { isInteractive: boolean; hasDOM: boolean }, + rootTemplate: TemplateFactory, + viewRegistry: ViewRegistry, + builder = clientBuilder, + resolver = new ResolverImpl() + ) { + super(owner, env, document, resolver, builder); + this._rootTemplate = rootTemplate(owner); + this._viewRegistry = viewRegistry || owner.lookup('-view-registry:main'); + } + + // renderer HOOKS + + appendOutletView(view: OutletView, target: SimpleElement): void { + // TODO: This bypasses the {{outlet}} syntax so logically duplicates + // some of the set up code. Since this is all internal (or is it?), + // we can refactor this to do something more direct/less convoluted + // and with less setup, but get it working first + let outlet = createRootOutlet(view); + let { name, /* controller, */ template } = view.state; + + let named = dict(); + + named['Component'] = createConstRef( + makeRouteTemplate(view.owner, name, template as Template), + '@Component' + ); + + // TODO: is this guaranteed to be undefined? It seems to be the + // case in the `OutletView` class. Investigate how much that class + // exists as an internal implementation detail only, or if it was + // used outside of core. As far as I can tell, test-helpers uses + // it but only for `setOutletState`. + // named['controller'] = createConstRef(controller, '@controller'); + // Update: at least according to the debug render tree tests, we + // appear to always expect this to be undefined. Not a definitive + // source by any means, but is useful evidence + named['controller'] = UNDEFINED_REFERENCE; + named['model'] = UNDEFINED_REFERENCE; + + let args = createCapturedArgs(named, EMPTY_POSITIONAL); + + this._appendDefinition( + view, + curry(0 as CurriedComponent, outlet, view.owner, args, true), + target + ); + } + + appendTo(view: ClassicComponent, target: SimpleElement): void { + let definition = new RootComponentDefinition(view); + this._appendDefinition( + view, + curry(0 as CurriedComponent, definition, this.state.owner, null, true), + target + ); + } + + _appendDefinition( + root: OutletView | ClassicComponent, + definition: CurriedValue, + target: SimpleElement + ): void { + let self = createConstRef(definition, 'this'); + let dynamicScope = new DynamicScope(null, UNDEFINED_REFERENCE); + let rootState = new ClassicRootState( + root, + this.state.context, + this.state.owner, + this._rootTemplate, + self, + target, + dynamicScope, + this.state.builder + ); + this.state.renderRoot(rootState, this); + } + + cleanupRootFor(component: ClassicComponent): void { + // no need to cleanup roots if we have already been destroyed + if (isDestroyed(this)) { + return; + } + + let roots = this.state.roots; + + // traverse in reverse so we can remove items + // without mucking up the index + let i = roots.length; + while (i--) { + let root = roots[i]; + assert('has root', root); + if (root.type === 'classic' && (root as ClassicRootState).isFor(component)) { + root.destroy(); + roots.splice(i, 1); + } + } + } + + remove(view: ClassicComponent): void { + view._transitionTo('destroying'); + + this.cleanupRootFor(view); + + if (this.state.isInteractive) { + view.trigger('didDestroyElement'); + } + } + + get _roots() { + return this.state.debug.roots; + } + + get _inRenderTransaction() { + return this.state.debug.inRenderTransaction; + } + + get _isInteractive() { + return this.state.debug.isInteractive; + } + + get _context() { + return this.state.context; + } + + register(view: any): void { + let id = getViewId(view); + assert( + 'Attempted to register a view with an id already in use: ' + id, + !this._viewRegistry[id] + ); + this._viewRegistry[id] = view; + } + + unregister(view: any): void { + delete this._viewRegistry[getViewId(view)]; + } + + getElement(component: View): Nullable { + if (this._isInteractive) { + return getViewElement(component); + } else { + throw new Error( + 'Accessing `this.element` is not allowed in non-interactive environments (such as FastBoot).' + ); + } + } + + getBounds(component: View): { + parentElement: SimpleElement; + firstNode: SimpleNode; + lastNode: SimpleNode; + } { + let bounds: Bounds | null = component[BOUNDS]; + + assert('object passed to getBounds must have the BOUNDS symbol as a property', bounds); + + let parentElement = bounds.parentElement(); + let firstNode = bounds.firstNode(); + let lastNode = bounds.lastNode(); + + return { parentElement, firstNode, lastNode }; + } +} diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly-symbols.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly-symbols.ts new file mode 100644 index 00000000000..6df8d6280c1 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly-symbols.ts @@ -0,0 +1,9 @@ +export const DIRTY_TAG: unique symbol = Symbol('DIRTY_TAG'); +export const IS_DISPATCHING_ATTRS: unique symbol = Symbol('IS_DISPATCHING_ATTRS'); +export const BOUNDS: unique symbol = Symbol('BOUNDS'); + +export const CURLY_COMPONENT_BRAND: unique symbol = Symbol('CURLY_COMPONENT_BRAND'); + +export function isCurlyManager(manager: object): boolean { + return (manager as { [CURLY_COMPONENT_BRAND]?: boolean })[CURLY_COMPONENT_BRAND] === true; +} diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index 585a9b8d63d..c74f7543908 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -12,7 +12,7 @@ import { } from '@ember/-internals/views/lib/system/utils'; import type { Nullable } from '@ember/-internals/utility-types'; import { assert, debugFreeze } from '@ember/debug'; -import { _instrumentStart } from '@ember/instrumentation'; +import { _instrumentStart } from '@ember/instrumentation/lib/internal-instrument'; import { DEBUG } from '@glimmer/env'; import type { Bounds, @@ -60,6 +60,7 @@ import { import ComponentStateBucket from '../utils/curly-component-state-bucket'; import { processComponentArgs } from '../utils/process-args'; +import { BOUNDS, CURLY_COMPONENT_BRAND, DIRTY_TAG, IS_DISPATCHING_ATTRS } from './curly-symbols'; const COMPONENT_ARGS_MAP = new WeakMap(); @@ -69,9 +70,7 @@ export function getComponentCapturedArgs( return COMPONENT_ARGS_MAP.get(component); } -export const DIRTY_TAG = Symbol('DIRTY_TAG'); -export const IS_DISPATCHING_ATTRS = Symbol('IS_DISPATCHING_ATTRS'); -export const BOUNDS = Symbol('BOUNDS'); +export { BOUNDS, DIRTY_TAG, IS_DISPATCHING_ATTRS } from './curly-symbols'; const EMBER_VIEW_REF = createPrimitiveRef('ember-view'); @@ -560,7 +559,6 @@ const CURLY_CAPABILITIES: InternalComponentCapabilities = { }; export const CURLY_COMPONENT_MANAGER = new CurlyComponentManager(); +(CURLY_COMPONENT_MANAGER as unknown as Record)[CURLY_COMPONENT_BRAND] = true; -export function isCurlyManager(manager: object): boolean { - return manager === CURLY_COMPONENT_MANAGER; -} +export { CURLY_COMPONENT_BRAND, isCurlyManager } from './curly-symbols'; diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts index 823e1239f51..b56684b04de 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts @@ -2,7 +2,7 @@ import type { InternalOwner } from '@ember/-internals/owner'; import type { Nullable } from '@ember/-internals/utility-types'; import { assert } from '@ember/debug'; import EngineInstance from '@ember/engine/instance'; -import { _instrumentStart } from '@ember/instrumentation'; +import { _instrumentStart } from '@ember/instrumentation/lib/internal-instrument'; import { precompileTemplate } from '@ember/template-compilation'; import type { CompilableProgram, diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts index 2ca09c5e158..c05e0500a91 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts @@ -1,6 +1,6 @@ import { getFactoryFor } from '@ember/-internals/container/lib/container'; import { assert } from '@ember/debug'; -import { _instrumentStart } from '@ember/instrumentation'; +import { _instrumentStart } from '@ember/instrumentation/lib/internal-instrument'; import { DEBUG } from '@glimmer/env'; import type { ComponentDefinition, diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts b/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts index 8e6b12278aa..1d578b97e40 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/route-template.ts @@ -1,5 +1,5 @@ import type { InternalOwner } from '@ember/-internals/owner'; -import { _instrumentStart } from '@ember/instrumentation'; +import { _instrumentStart } from '@ember/instrumentation/lib/internal-instrument'; import type { CapturedArguments, CompilableProgram, diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index f61e446f829..2040dc62592 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -18,19 +18,13 @@ import { guidFor } from '@ember/-internals/utils/lib/guid'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import type { Environment, Template, TemplateFactory } from '@glimmer/interfaces'; -import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import { isUpdatableRef, updateRef } from '@glimmer/reference/lib/reference'; import { normalizeProperty } from '@glimmer/runtime/lib/dom/props'; import type { DirtyableTag } from '@glimmer/interfaces'; import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; import type { SimpleElement } from '@simple-dom/interface'; -import { - BOUNDS, - CURLY_COMPONENT_MANAGER, - DIRTY_TAG, - IS_DISPATCHING_ATTRS, - getComponentCapturedArgs, -} from './component-managers/curly'; +import { getComponentCapturedArgs } from './component-managers/curly'; +import { BOUNDS, DIRTY_TAG, IS_DISPATCHING_ATTRS } from './component-managers/curly-symbols'; import hasDOM from '@ember/-internals/browser-environment/lib/has-dom'; // Keep track of which component classes have already been processed for lazy event setup. @@ -1682,11 +1676,4 @@ class Component } } -// We continue to use reopenClass here so that positionalParams can be overridden with reopenClass in subclasses. -Component.reopenClass({ - positionalParams: [], -}); - -setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component); - export default Component; diff --git a/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts b/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts index 0c63b2793ac..5f84f710373 100644 --- a/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts +++ b/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts @@ -1,6 +1,6 @@ import { tracked } from '@ember/-internals/metal/lib/tracked'; import { assert } from '@ember/debug'; -import { action } from '@ember/object'; +import { action } from '@ember/object/action'; import type { Reference } from '@glimmer/reference/lib/reference'; import { isConstRef, diff --git a/packages/@ember/-internals/glimmer/lib/components/input.ts b/packages/@ember/-internals/glimmer/lib/components/input.ts index cd9898555dc..0bb706c7dce 100644 --- a/packages/@ember/-internals/glimmer/lib/components/input.ts +++ b/packages/@ember/-internals/glimmer/lib/components/input.ts @@ -4,7 +4,7 @@ import hasDOM from '@ember/-internals/browser-environment/lib/has-dom'; import { type Opaque } from '@ember/-internals/utility-types'; import { assert, warn } from '@ember/debug'; -import { action } from '@ember/object'; +import { action } from '@ember/object/action'; import { valueForRef } from '@glimmer/reference/lib/reference'; import { untrack } from '@glimmer/validator/lib/tracking'; import InputTemplate from '../templates/input'; diff --git a/packages/@ember/-internals/glimmer/lib/components/link-to.ts b/packages/@ember/-internals/glimmer/lib/components/link-to.ts index 4f11670388c..3199f0e064b 100644 --- a/packages/@ember/-internals/glimmer/lib/components/link-to.ts +++ b/packages/@ember/-internals/glimmer/lib/components/link-to.ts @@ -5,8 +5,8 @@ import inspect from '@ember/debug/lib/inspect'; import { assert, debugFreeze, warn } from '@ember/debug'; import { getEngineParent } from '@ember/engine/parent'; import type EngineInstance from '@ember/engine/instance'; -import { flaggedInstrument } from '@ember/instrumentation'; -import { action } from '@ember/object'; +import { flaggedInstrument } from '@ember/instrumentation/lib/internal-instrument'; +import { action } from '@ember/object/action'; import { service } from '@ember/service'; import { DEBUG } from '@glimmer/env'; import type { Maybe } from '@glimmer/interfaces'; diff --git a/packages/@ember/-internals/glimmer/lib/components/textarea.ts b/packages/@ember/-internals/glimmer/lib/components/textarea.ts index 3ab5f40c178..c50beee6abf 100644 --- a/packages/@ember/-internals/glimmer/lib/components/textarea.ts +++ b/packages/@ember/-internals/glimmer/lib/components/textarea.ts @@ -2,7 +2,7 @@ @module @ember/component */ import { type Opaque } from '@ember/-internals/utility-types'; -import { action } from '@ember/object'; +import { action } from '@ember/object/action'; import TextareaTemplate from '../templates/textarea'; import AbstractInput from './abstract-input'; import { type OpaqueInternalComponentConstructor, opaquify } from './internal'; diff --git a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts index 1e4c51bc083..a591a16005f 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -2,7 +2,7 @@ @module ember */ import { tagForObject } from '@ember/-internals/metal/lib/tags'; -import { contentFor as _contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy'; +import { contentFor as _contentFor } from '@ember/-internals/runtime/lib/mixins/content-for'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import { assert } from '@ember/debug'; import type { CapturedArguments } from '@glimmer/interfaces'; diff --git a/packages/@ember/-internals/glimmer/lib/register-classic-helper.ts b/packages/@ember/-internals/glimmer/lib/register-classic-helper.ts new file mode 100644 index 00000000000..5018abe63ea --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/register-classic-helper.ts @@ -0,0 +1,28 @@ +import { DEBUG } from '@glimmer/env'; +import { setInternalHelperManager } from '@glimmer/manager/lib/internal/api'; + +import { CLASSIC_HELPER_MANAGER, isClassicHelper } from './helper'; +import { registerClassicHelperHandler } from './resolver'; + +const CLASSIC_HELPER_MANAGER_ASSOCIATED = new WeakSet(); + +registerClassicHelperHandler((definition, factory) => { + if (!isClassicHelper(definition)) return null; + + // For classic class based helpers, we need to pass the factoryFor result + // itself rather than the raw value (`factoryFor(...).class`). This is + // because injections are already bound in the factoryFor result, including + // type-based injections. + if (DEBUG) { + // In DEBUG we need to only set the associated value once, otherwise + // we'll trigger an assertion. + if (!CLASSIC_HELPER_MANAGER_ASSOCIATED.has(factory)) { + CLASSIC_HELPER_MANAGER_ASSOCIATED.add(factory); + setInternalHelperManager(CLASSIC_HELPER_MANAGER, factory); + } + } else { + setInternalHelperManager(CLASSIC_HELPER_MANAGER, factory); + } + + return factory; +}); diff --git a/packages/@ember/-internals/glimmer/lib/register-curly-component.ts b/packages/@ember/-internals/glimmer/lib/register-curly-component.ts new file mode 100644 index 00000000000..cec33796b74 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/register-curly-component.ts @@ -0,0 +1,10 @@ +import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; + +import Component from './component'; +import { CURLY_COMPONENT_MANAGER } from './component-managers/curly'; + +Component.reopenClass({ + positionalParams: [], +}); + +setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component); diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index bf90ff27c9e..07409bcf277 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -1,9 +1,5 @@ -import { privatize as P } from '@ember/-internals/container/lib/registry'; import { ENV } from '@ember/-internals/environment/lib/env'; import type { InternalOwner } from '@ember/-internals/owner'; -import { getOwner } from '@ember/-internals/owner'; -import { guidFor } from '@ember/-internals/utils/lib/guid'; -import { getViewElement, getViewId } from '@ember/-internals/views/lib/system/utils'; import { assert } from '@ember/debug'; import { _backburner, _getCurrentRunLoop } from '@ember/runloop'; import { @@ -15,87 +11,36 @@ import { } from '@glimmer/destroyable'; import { DEBUG } from '@glimmer/env'; import type { - Bounds, + ClassicResolver, Cursor, DebugRenderTree, Environment, - DynamicScope as GlimmerDynamicScope, - RenderResult as GlimmerRenderResult, - Template, - TemplateFactory, EvaluationContext, - CurriedComponent, + RenderResult as GlimmerRenderResult, TreeBuilder, - ClassicResolver, } from '@glimmer/interfaces'; - -import type { Nullable } from '@ember/-internals/utility-types'; import { artifacts } from '@glimmer/program/lib/helpers'; import { RuntimeOpImpl } from '@glimmer/program/lib/opcode'; -import type { Reference } from '@glimmer/reference/lib/reference'; -import { createConstRef, UNDEFINED_REFERENCE, valueForRef } from '@glimmer/reference/lib/reference'; -import type { CurriedValue } from '@glimmer/runtime/lib/curried-value'; +import { renderComponent as glimmerRenderComponent } from '@glimmer/runtime/lib/render'; import { clientBuilder } from '@glimmer/runtime/lib/vm/element-builder'; -import { createCapturedArgs, EMPTY_POSITIONAL } from '@glimmer/runtime/lib/vm/arguments'; -import { curry } from '@glimmer/runtime/lib/curried-value'; import { inTransaction, runtimeOptions } from '@glimmer/runtime/lib/environment'; -import { renderComponent as glimmerRenderComponent, renderMain } from '@glimmer/runtime/lib/render'; -import { dict } from '@glimmer/util/lib/collections'; -import { unwrapTemplate } from './component-managers/unwrap-template'; +import { EvaluationContextImpl } from '@glimmer/opcode-compiler/lib/program-context'; import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; -import type { SimpleDocument, SimpleElement, SimpleNode } from '@simple-dom/interface'; -import RSVP from 'rsvp'; -import type Component from './component'; +import type { SimpleDocument, SimpleElement } from '@simple-dom/interface'; + import { hasDOM } from '../../browser-environment'; -import type ClassicComponent from './component'; -import { BOUNDS } from './component-managers/curly'; -import { createRootOutlet } from './component-managers/outlet'; -import { RootComponentDefinition } from './component-managers/root'; import { EmberEnvironmentDelegate } from './environment'; import ResolverImpl from './resolver'; -import type { OutletState } from './utils/outlet'; -import OutletView from './views/outlet'; -import { makeRouteTemplate } from './component-managers/route-template'; -import { EvaluationContextImpl } from '@glimmer/opcode-compiler/lib/program-context'; export type IBuilder = (env: Environment, cursor: Cursor) => TreeBuilder; -export interface View { - parentView: Nullable; - renderer: Renderer; - tagName: string | null; - elementId: string | null; - isDestroying: boolean; - isDestroyed: boolean; - [BOUNDS]: Bounds | null; -} - -export class DynamicScope implements GlimmerDynamicScope { - constructor( - public view: View | null, - public outletState: Reference - ) {} - - child() { - return new DynamicScope(this.view, this.outletState); - } - - get(key: 'outletState'): Reference { - assert( - `Using \`-get-dynamic-scope\` is only supported for \`outletState\` (you used \`${key}\`).`, - key === 'outletState' - ); - return this.outletState; - } - - set(key: 'outletState', value: Reference) { - assert( - `Using \`-with-dynamic-scope\` is only supported for \`outletState\` (you used \`${key}\`).`, - key === 'outletState' - ); - this.outletState = value; - return value; - } +export interface RootState { + readonly type: 'classic' | 'component'; + readonly destroyed: boolean; + readonly result: GlimmerRenderResult | undefined; + render(): void; + destroy(): void; + isFor(possibleRoot: unknown): boolean; } const NO_OP = () => {}; @@ -103,7 +48,7 @@ const NO_OP = () => {}; // This wrapper logic prevents us from rerendering in case of a hard failure // during render. This prevents infinite revalidation type loops from occuring, // and ensures that errors are not swallowed by subsequent follow on failures. -function errorLoopTransaction(fn: () => void) { +export function errorLoopTransaction(fn: () => void) { if (DEBUG) { return () => { let didError = true; @@ -129,9 +74,7 @@ function errorLoopTransaction(fn: () => void) { } } -type RootState = ClassicRootState | ComponentRootState; - -class ComponentRootState { +class ComponentRootState implements RootState { readonly type = 'component'; #result: GlimmerRenderResult | undefined; @@ -165,7 +108,7 @@ class ComponentRootState { }); } - isFor(_component: ClassicComponent): boolean { + isFor(_root: unknown): boolean { return false; } @@ -186,90 +129,6 @@ class ComponentRootState { } } -class ClassicRootState { - readonly type = 'classic'; - public id: string; - public result: GlimmerRenderResult | undefined; - public destroyed: boolean; - public render: () => void; - readonly env: Environment; - - constructor( - public root: Component | OutletView, - context: EvaluationContext, - owner: object, - template: Template, - self: Reference, - parentElement: SimpleElement, - dynamicScope: DynamicScope, - builder: IBuilder - ) { - assert( - `You cannot render \`${valueForRef(self)}\` without a template.`, - template !== undefined - ); - - this.id = root instanceof OutletView ? guidFor(root) : getViewId(root); - this.result = undefined; - this.destroyed = false; - this.env = context.env; - - this.render = errorLoopTransaction(() => { - let layout = unwrapTemplate(template).asLayout(); - - let iterator = renderMain( - context, - owner, - self, - builder(context.env, { element: parentElement, nextSibling: null }), - layout, - dynamicScope - ); - - let result = (this.result = iterator.sync()); - - associateDestroyableChild(this, result); - - this.render = errorLoopTransaction(() => { - if (isDestroying(result) || isDestroyed(result)) return; - - return result.rerender({ - alwaysRevalidate: false, - }); - }); - }); - } - - isFor(possibleRoot: unknown): boolean { - return this.root === possibleRoot; - } - - destroy() { - let { result, env } = this; - - this.destroyed = true; - - this.root = null as any; - this.result = undefined; - this.render = undefined as any; - - if (result !== undefined) { - /* - Handles these scenarios: - - * When roots are removed during standard rendering process, a transaction exists already - `.begin()` / `.commit()` are not needed. - * When roots are being destroyed manually (`component.append(); component.destroy() case), no - transaction exists already. - * When roots are being destroyed during `Renderer#destroy`, no transaction exists - - */ - - inTransaction(env, () => destroy(result!)); - } - } -} - const renderers: BaseRenderer[] = []; export function _resetRenderers() { @@ -293,7 +152,17 @@ function loopBegin(): void { } } -let renderSettledDeferred: RSVP.Deferred | null = null; +type Deferred = { promise: Promise; resolve: () => void }; + +function defer(): Deferred { + let resolve!: () => void; + let promise = new Promise((r) => { + resolve = r; + }); + return { promise, resolve }; +} + +let renderSettledDeferred: Deferred | null = null; /* Returns a promise which will resolve when rendering has settled. Settled in this context is defined as when all of the tags in use are "current" (e.g. @@ -305,7 +174,7 @@ let renderSettledDeferred: RSVP.Deferred | null = null; */ export function renderSettled() { if (renderSettledDeferred === null) { - renderSettledDeferred = RSVP.defer(); + renderSettledDeferred = defer(); // if there is no current runloop, the promise created above will not have // a chance to resolve (because its resolved in backburner's "end" event) if (!_getCurrentRunLoop()) { @@ -347,10 +216,6 @@ function loopEnd() { _backburner.on('begin', loopBegin); _backburner.on('end', loopEnd); -interface ViewRegistry { - [viewId: string]: unknown; -} - type Resolver = ClassicResolver; interface RendererData { @@ -713,12 +578,12 @@ export function renderComponent( const RENDER_CACHE = new WeakMap(); const RENDERER_CACHE = new WeakMap(); -class BaseRenderer { +export class BaseRenderer { static strict( owner: object, document: SimpleDocument | Document, options: { isInteractive: boolean; hasDOM?: boolean } - ) { + ): BaseRenderer { return new BaseRenderer( owner, { hasDOM: hasDOM, ...options }, @@ -797,208 +662,6 @@ class BaseRenderer { rerender(): void { this.state.scheduleRevalidate(this); } - - // render(component: Component, options: { into: Cursor; args?: Record }): void { - // this.state.renderRoot(component); - // } } -export class Renderer extends BaseRenderer { - static strict( - owner: object, - document: SimpleDocument | Document, - options: { isInteractive: boolean; hasDOM?: boolean } - ): BaseRenderer { - return new BaseRenderer( - owner, - { hasDOM: hasDOM, ...options }, - document as SimpleDocument, - new ResolverImpl(), - clientBuilder - ); - } - - private _rootTemplate: Template; - private _viewRegistry: ViewRegistry; - - static create(props: { _viewRegistry: any }): Renderer { - let { _viewRegistry } = props; - let owner = getOwner(props); - assert('Renderer is unexpectedly missing an owner', owner); - let document = owner.lookup('service:-document') as SimpleDocument; - let env = owner.lookup('-environment:main') as { - isInteractive: boolean; - hasDOM: boolean; - }; - let rootTemplate = owner.lookup(P`template:-root`) as TemplateFactory; - let builder = owner.lookup('service:-dom-builder') as IBuilder; - return new this(owner, document, env, rootTemplate, _viewRegistry, builder); - } - - constructor( - owner: InternalOwner, - document: SimpleDocument, - env: { isInteractive: boolean; hasDOM: boolean }, - rootTemplate: TemplateFactory, - viewRegistry: ViewRegistry, - builder = clientBuilder, - resolver = new ResolverImpl() - ) { - super(owner, env, document, resolver, builder); - this._rootTemplate = rootTemplate(owner); - this._viewRegistry = viewRegistry || owner.lookup('-view-registry:main'); - } - - // renderer HOOKS - - appendOutletView(view: OutletView, target: SimpleElement): void { - // TODO: This bypasses the {{outlet}} syntax so logically duplicates - // some of the set up code. Since this is all internal (or is it?), - // we can refactor this to do something more direct/less convoluted - // and with less setup, but get it working first - let outlet = createRootOutlet(view); - let { name, /* controller, */ template } = view.state; - - let named = dict(); - - named['Component'] = createConstRef( - makeRouteTemplate(view.owner, name, template as Template), - '@Component' - ); - - // TODO: is this guaranteed to be undefined? It seems to be the - // case in the `OutletView` class. Investigate how much that class - // exists as an internal implementation detail only, or if it was - // used outside of core. As far as I can tell, test-helpers uses - // it but only for `setOutletState`. - // named['controller'] = createConstRef(controller, '@controller'); - // Update: at least according to the debug render tree tests, we - // appear to always expect this to be undefined. Not a definitive - // source by any means, but is useful evidence - named['controller'] = UNDEFINED_REFERENCE; - named['model'] = UNDEFINED_REFERENCE; - - let args = createCapturedArgs(named, EMPTY_POSITIONAL); - - this._appendDefinition( - view, - curry(0 as CurriedComponent, outlet, view.owner, args, true), - target - ); - } - - appendTo(view: ClassicComponent, target: SimpleElement): void { - let definition = new RootComponentDefinition(view); - this._appendDefinition( - view, - curry(0 as CurriedComponent, definition, this.state.owner, null, true), - target - ); - } - - _appendDefinition( - root: OutletView | ClassicComponent, - definition: CurriedValue, - target: SimpleElement - ): void { - let self = createConstRef(definition, 'this'); - let dynamicScope = new DynamicScope(null, UNDEFINED_REFERENCE); - let rootState = new ClassicRootState( - root, - this.state.context, - this.state.owner, - this._rootTemplate, - self, - target, - dynamicScope, - this.state.builder - ); - this.state.renderRoot(rootState, this); - } - - cleanupRootFor(component: ClassicComponent): void { - // no need to cleanup roots if we have already been destroyed - if (isDestroyed(this)) { - return; - } - - let roots = this.state.roots; - - // traverse in reverse so we can remove items - // without mucking up the index - let i = roots.length; - while (i--) { - let root = roots[i]; - assert('has root', root); - if (root.type === 'classic' && root.isFor(component)) { - root.destroy(); - roots.splice(i, 1); - } - } - } - - remove(view: ClassicComponent): void { - view._transitionTo('destroying'); - - this.cleanupRootFor(view); - - if (this.state.isInteractive) { - view.trigger('didDestroyElement'); - } - } - - get _roots() { - return this.state.debug.roots; - } - - get _inRenderTransaction() { - return this.state.debug.inRenderTransaction; - } - - get _isInteractive() { - return this.state.debug.isInteractive; - } - - get _context() { - return this.state.context; - } - - register(view: any): void { - let id = getViewId(view); - assert( - 'Attempted to register a view with an id already in use: ' + id, - !this._viewRegistry[id] - ); - this._viewRegistry[id] = view; - } - - unregister(view: any): void { - delete this._viewRegistry[getViewId(view)]; - } - - getElement(component: View): Nullable { - if (this._isInteractive) { - return getViewElement(component); - } else { - throw new Error( - 'Accessing `this.element` is not allowed in non-interactive environments (such as FastBoot).' - ); - } - } - - getBounds(component: View): { - parentElement: SimpleElement; - firstNode: SimpleNode; - lastNode: SimpleNode; - } { - let bounds: Bounds | null = component[BOUNDS]; - - assert('object passed to getBounds must have the BOUNDS symbol as a property', bounds); - - let parentElement = bounds.parentElement(); - let firstNode = bounds.firstNode(); - let lastNode = bounds.lastNode(); - - return { parentElement, firstNode, lastNode }; - } -} +export type { DynamicScope, Renderer, View } from './classic-renderer'; diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index 43c2ce19ecf..0ba9e0ac322 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -1,7 +1,7 @@ import type { InternalFactory, InternalOwner } from '@ember/-internals/owner'; import { isFactory } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; -import { _instrumentStart } from '@ember/instrumentation'; +import { _instrumentStart } from '@ember/instrumentation/lib/internal-instrument'; import { DEBUG } from '@glimmer/env'; import type { ClassicResolver, @@ -13,10 +13,7 @@ import type { } from '@glimmer/interfaces'; import type { Nullable } from '@ember/-internals/utility-types'; import { getComponentTemplate } from '@glimmer/manager/lib/public/template'; -import { - getInternalComponentManager, - setInternalHelperManager, -} from '@glimmer/manager/lib/internal/api'; +import { getInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import { array } from '@glimmer/runtime/lib/helpers/array'; import { concat } from '@glimmer/runtime/lib/helpers/concat'; import { fn } from '@glimmer/runtime/lib/helpers/fn'; @@ -27,8 +24,7 @@ import { templateOnlyComponent, TEMPLATE_ONLY_COMPONENT_MANAGER, } from '@glimmer/runtime/lib/component/template-only'; -import { isCurlyManager } from './component-managers/curly'; -import { CLASSIC_HELPER_MANAGER, isClassicHelper } from './helper'; +import { isCurlyManager } from './component-managers/curly-symbols'; import { default as disallowDynamicResolution } from './helpers/-disallow-dynamic-resolution'; import { default as inElementNullCheckHelper } from './helpers/-in-element-null-check'; import { default as normalizeClassHelper } from './helpers/-normalize-class'; @@ -40,9 +36,6 @@ import { default as readonly } from './helpers/readonly'; import { default as unbound } from './helpers/unbound'; import { default as uniqueId } from './helpers/unique-id'; -import { mountHelper } from './syntax/mount'; -import { outletHelper } from './syntax/outlet'; - function instrumentationPayload(name: string) { return { object: `component:${name}` }; } @@ -96,11 +89,14 @@ const BUILTIN_KEYWORD_HELPERS: Record = { '-normalize-class': normalizeClassHelper, '-resolve': resolve, '-track-array': trackArray, - '-mount': mountHelper, - '-outlet': outletHelper, '-in-el-null': inElementNullCheckHelper, }; +export function registerBuiltInKeywordHelper(name: string, helper: object): void { + BUILTIN_KEYWORD_HELPERS[name] = helper; + BUILTIN_HELPERS[name] = helper; +} + const BUILTIN_HELPERS: Record = { ...BUILTIN_KEYWORD_HELPERS, array, @@ -134,7 +130,17 @@ const BUILTIN_MODIFIERS: Record = { on, }; -const CLASSIC_HELPER_MANAGER_ASSOCIATED = new WeakSet(); +type ClassicHelperHandler = ( + definition: object, + + factory: any +) => HelperDefinitionState | null; + +let classicHelperHandler: ClassicHelperHandler | null = null; + +export function registerClassicHelperHandler(handler: ClassicHelperHandler): void { + classicHelperHandler = handler; +} export default class ResolverImpl implements ClassicResolver { private componentDefinitionCache: Map = new Map(); @@ -166,23 +172,11 @@ export default class ResolverImpl implements ClassicResolver { return null; } - if (typeof definition === 'function' && isClassicHelper(definition)) { - // For classic class based helpers, we need to pass the factoryFor result itself rather - // than the raw value (`factoryFor(...).class`). This is because injections are already - // bound in the factoryFor result, including type-based injections - - if (DEBUG) { - // In DEBUG we need to only set the associated value once, otherwise - // we'll trigger an assertion - if (!CLASSIC_HELPER_MANAGER_ASSOCIATED.has(factory)) { - CLASSIC_HELPER_MANAGER_ASSOCIATED.add(factory); - setInternalHelperManager(CLASSIC_HELPER_MANAGER, factory); - } - } else { - setInternalHelperManager(CLASSIC_HELPER_MANAGER, factory); + if (typeof definition === 'function' && classicHelperHandler !== null) { + let result = classicHelperHandler(definition, factory); + if (result !== null) { + return result; } - - return factory; } return definition; diff --git a/packages/@ember/-internals/glimmer/lib/setup-registry.ts b/packages/@ember/-internals/glimmer/lib/setup-registry.ts index 75fb2760855..62783b9ce90 100644 --- a/packages/@ember/-internals/glimmer/lib/setup-registry.ts +++ b/packages/@ember/-internals/glimmer/lib/setup-registry.ts @@ -6,10 +6,14 @@ import Input from './components/input'; import LinkTo from './components/link-to'; import Textarea from './components/textarea'; import { clientBuilder, rehydrationBuilder, serializeBuilder } from './dom'; -import { Renderer } from './renderer'; +import { Renderer } from './classic-renderer'; import OutletTemplate from './templates/outlet'; import RootTemplate from './templates/root'; import OutletView from './views/outlet'; +import './syntax/register-routing-keywords'; +import '@glimmer/runtime/lib/debug-render-tree-register'; +import './register-curly-component'; +import './register-classic-helper'; export function setupApplicationRegistry(registry: Registry): void { // because we are using injections we can't use instantiate false diff --git a/packages/@ember/-internals/glimmer/lib/syntax/register-routing-keywords.ts b/packages/@ember/-internals/glimmer/lib/syntax/register-routing-keywords.ts new file mode 100644 index 00000000000..dc336be8a1e --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/syntax/register-routing-keywords.ts @@ -0,0 +1,6 @@ +import { registerBuiltInKeywordHelper } from '../resolver'; +import { mountHelper } from './mount'; +import { outletHelper } from './outlet'; + +registerBuiltInKeywordHelper('-mount', mountHelper); +registerBuiltInKeywordHelper('-outlet', outletHelper); diff --git a/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts b/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts index 64933a3e22a..ded35d037d6 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts @@ -1,16 +1,20 @@ import { isHTMLSafe } from './string'; import { get } from '@ember/-internals/metal/lib/property_get'; import { tagForProperty } from '@ember/-internals/metal/lib/tags'; -import { isArray } from '@ember/array'; +import { isEmberArray } from '@ember/array/-internals'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import { consumeTag } from '@glimmer/validator/lib/tracking'; +function isArrayLike(obj: unknown): obj is ArrayLike { + return Array.isArray(obj) || isEmberArray(obj); +} + export default function toBool(predicate: unknown): boolean { if (isProxy(predicate)) { consumeTag(tagForProperty(predicate, 'content')); return Boolean(get(predicate, 'isTruthy')); - } else if (isArray(predicate)) { + } else if (isArrayLike(predicate)) { consumeTag(tagForProperty(predicate as object, '[]')); return (predicate as { length: number }).length !== 0; diff --git a/packages/@ember/-internals/metal/lib/decorator.ts b/packages/@ember/-internals/metal/lib/decorator.ts index c5a305b2ce3..ee6e9229858 100644 --- a/packages/@ember/-internals/metal/lib/decorator.ts +++ b/packages/@ember/-internals/metal/lib/decorator.ts @@ -2,6 +2,7 @@ import type { Meta } from '@ember/-internals/meta/lib/meta'; import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; +import { registerComputedSetterCheck } from './property_set'; export type DecoratorPropertyDescriptor = (PropertyDescriptor & { initializer?: any }) | undefined; @@ -109,6 +110,8 @@ function DESCRIPTOR_SETTER_FUNCTION( export const COMPUTED_SETTERS = new WeakSet(); +registerComputedSetterCheck((setter) => COMPUTED_SETTERS.has(setter)); + export function makeComputedDecorator( desc: ComputedDescriptor, DecoratorClass: { prototype: object } diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index 08c5148cd9c..2cb14d2d44e 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -1,6 +1,6 @@ import { ENV } from '@ember/-internals/environment/lib/env'; import { peekMeta } from '@ember/-internals/meta/lib/meta'; -import type { schedule } from '@ember/runloop'; +import { registerAsyncObserverFlush, type schedule } from '@ember/runloop'; import { registerDestructor } from '@glimmer/destroyable'; import type { Tag } from '@glimmer/interfaces'; import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; @@ -8,6 +8,7 @@ import { tagMetaFor } from '@glimmer/validator/lib/meta'; import { getChainTagsForKey } from './chain-tags'; import changeEvent from './change_event'; import { addListener, removeListener, sendEvent } from './events'; +import { registerObserverDeactivationHooks, registerObserverFlushSync } from './property_events'; interface ActiveObserver { tag: Tag; @@ -270,3 +271,7 @@ function destroyObservers(target: object) { if (SYNC_OBSERVERS.size > 0) SYNC_OBSERVERS.delete(target); if (ASYNC_OBSERVERS.size > 0) ASYNC_OBSERVERS.delete(target); } + +registerObserverFlushSync(flushSyncObservers); +registerObserverDeactivationHooks(suspendedObserverDeactivation, resumeObserverDeactivation); +registerAsyncObserverFlush(flushAsyncObservers); diff --git a/packages/@ember/-internals/metal/lib/property_events.ts b/packages/@ember/-internals/metal/lib/property_events.ts index b4420bc952b..6b501f86467 100644 --- a/packages/@ember/-internals/metal/lib/property_events.ts +++ b/packages/@ember/-internals/metal/lib/property_events.ts @@ -1,13 +1,21 @@ import type { Meta } from '@ember/-internals/meta/lib/meta'; import { peekMeta } from '@ember/-internals/meta/lib/meta'; import { assert } from '@ember/debug'; -import { - flushSyncObservers, - resumeObserverDeactivation, - suspendedObserverDeactivation, -} from './observer'; import { markObjectAsDirty } from './tags'; +let observerFlushSync: (() => void) | null = null; +let observerSuspendDeactivation: (() => void) | null = null; +let observerResumeDeactivation: (() => void) | null = null; + +export function registerObserverFlushSync(fn: () => void): void { + observerFlushSync = fn; +} + +export function registerObserverDeactivationHooks(suspend: () => void, resume: () => void): void { + observerSuspendDeactivation = suspend; + observerResumeDeactivation = resume; +} + /** @module ember @private @@ -61,8 +69,8 @@ function notifyPropertyChange( markObjectAsDirty(obj, keyName); - if (deferred <= 0) { - flushSyncObservers(); + if (deferred <= 0 && observerFlushSync !== null) { + observerFlushSync(); } if (PROPERTY_DID_CHANGE in obj) { @@ -87,7 +95,9 @@ function notifyPropertyChange( */ function beginPropertyChanges(): void { deferred++; - suspendedObserverDeactivation(); + if (observerSuspendDeactivation !== null) { + observerSuspendDeactivation(); + } } /** @@ -97,8 +107,12 @@ function beginPropertyChanges(): void { function endPropertyChanges(): void { deferred--; if (deferred <= 0) { - flushSyncObservers(); - resumeObserverDeactivation(); + if (observerFlushSync !== null) { + observerFlushSync(); + } + if (observerResumeDeactivation !== null) { + observerResumeDeactivation(); + } } } diff --git a/packages/@ember/-internals/metal/lib/property_set.ts b/packages/@ember/-internals/metal/lib/property_set.ts index 7502923e85c..56280e1b9f2 100644 --- a/packages/@ember/-internals/metal/lib/property_set.ts +++ b/packages/@ember/-internals/metal/lib/property_set.ts @@ -3,11 +3,19 @@ import { setWithMandatorySetter } from '@ember/-internals/utils/lib/mandatory-se import toString from '@ember/-internals/utils/lib/to-string'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { COMPUTED_SETTERS } from './decorator'; import { isPath } from './path_cache'; import { notifyPropertyChange } from './property_events'; import { getPossibleMandatoryProxyValue, _getPath as getPath } from './property_get'; +let computedSetterCheck: ((setter: (this: object, value: unknown) => void) => boolean) | null = + null; + +export function registerComputedSetterCheck( + fn: (setter: (this: object, value: unknown) => void) => boolean +): void { + computedSetterCheck = fn; +} + interface ExtendedObject { isDestroyed?: boolean; setUnknownProperty?: (keyName: string, value: any) => any; @@ -70,7 +78,12 @@ export function set(obj: object, keyName: string, value: T, tolerant?: boolea export function _setProp(obj: object, keyName: string, value: any) { let descriptor = lookupDescriptor(obj, keyName); - if (descriptor !== null && COMPUTED_SETTERS.has(descriptor.set!)) { + if ( + descriptor !== null && + descriptor.set !== undefined && + computedSetterCheck !== null && + computedSetterCheck(descriptor.set) + ) { (obj as any)[keyName] = value; return value; } diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts index ddb31a6c18d..19adf728f97 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts @@ -15,18 +15,12 @@ import { isObject } from '@ember/-internals/utils/lib/spec'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; -import type { UpdatableTag, Tag } from '@glimmer/interfaces'; -import { combine, UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; +import type { Tag } from '@glimmer/interfaces'; +import { combine } from '@glimmer/validator/lib/validators'; import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; -export function contentFor(proxy: ProxyMixin): T | null { - let content = get(proxy, 'content'); - // SAFETY: Ideally we'd assert instead of casting, but @glimmer/validator doesn't give us - // sufficient public types for this. Previously this code was .js and worked correctly so - // hopefully this is sufficiently reliable. - updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); - return content; -} +import { contentFor } from './content-for'; +export { contentFor }; function customTagForProxy(proxy: object, key: string, addMandatorySetter?: boolean): Tag { assert('Expected a proxy', isProxy(proxy)); diff --git a/packages/@ember/-internals/runtime/lib/mixins/content-for.ts b/packages/@ember/-internals/runtime/lib/mixins/content-for.ts new file mode 100644 index 00000000000..34f3b7f8723 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/content-for.ts @@ -0,0 +1,13 @@ +import { get } from '@ember/-internals/metal/lib/property_get'; +import { tagForObject } from '@ember/-internals/metal/lib/tags'; +import type { UpdatableTag } from '@glimmer/interfaces'; +import { UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; +import type ProxyMixin from './-proxy'; + +export function contentFor(proxy: ProxyMixin): T | null { + let content = get(proxy, 'content'); + // SAFETY: @glimmer/validator doesn't expose enough public types for an + // assertion here. + updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); + return content; +} diff --git a/packages/@ember/-internals/views/lib/views/states.ts b/packages/@ember/-internals/views/lib/views/states.ts index aba69ba106b..b38ae20ddfd 100644 --- a/packages/@ember/-internals/views/lib/views/states.ts +++ b/packages/@ember/-internals/views/lib/views/states.ts @@ -1,7 +1,7 @@ import { teardownMandatorySetter } from '@ember/-internals/utils/lib/mandatory-setter'; import type Component from '@ember/-internals/glimmer/lib/component'; import { assert } from '@ember/debug'; -import { flaggedInstrument } from '@ember/instrumentation'; +import { flaggedInstrument } from '@ember/instrumentation/lib/internal-instrument'; import { join } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; diff --git a/packages/@ember/instrumentation/index.ts b/packages/@ember/instrumentation/index.ts index f3fb35d4775..1fe451298f6 100644 --- a/packages/@ember/instrumentation/index.ts +++ b/packages/@ember/instrumentation/index.ts @@ -1,28 +1,19 @@ -/* eslint no-console:off */ -/* global console */ - -import { ENV } from '@ember/-internals/environment/lib/env'; -import { assert } from '@ember/debug'; - -export interface Listener { - before: (name: string, timestamp: number, payload: object) => T; - after: (name: string, timestamp: number, payload: object, beforeValue: T) => void; -} - -export interface Subscriber { - pattern: string; - regex: RegExp; - object: Listener; -} +import { + NOOP, + _instrumentStart, + resetCache, + subscribers, + type Listener, + type Subscriber, +} from './lib/internal-instrument'; + +export { _instrumentStart, flaggedInstrument, subscribers } from './lib/internal-instrument'; +export type { Listener, Subscriber, StructuredProfilePayload } from './lib/internal-instrument'; export interface PayloadWithException { exception?: any; } -export interface StructuredProfilePayload { - object: string | object; -} - /** @module @ember/instrumentation @private @@ -78,23 +69,6 @@ export interface StructuredProfilePayload { @static @private */ -export let subscribers: Subscriber[] = []; -let cache: { [key: string]: Listener[] } = {}; - -function populateListeners(name: string) { - let listeners: Listener[] = []; - - for (let subscriber of subscribers) { - if (subscriber.regex.test(name)) { - listeners.push(subscriber.object); - } - } - - cache[name] = listeners; - return listeners; -} - -const time = (): number => performance.now(); type InstrumentCallback = (this: Binding) => Result; @@ -172,14 +146,6 @@ export function instrument( } } -export function flaggedInstrument( - _name: string, - _payload: object, - callback: () => Result -): Result { - return callback(); -} - function withFinalizer( callback: InstrumentCallback, finalizer: () => void, @@ -196,67 +162,6 @@ function withFinalizer( } } -function NOOP() {} - -// private for now -export function _instrumentStart(name: string, payloadFunc: () => object): () => void; -export function _instrumentStart( - name: string, - payloadFunc: (arg: Arg) => object, - payloadArg: Arg -): () => void; -export function _instrumentStart( - name: string, - payloadFunc: ((arg: Arg) => object) | (() => object), - payloadArg?: Arg -): () => void { - if (subscribers.length === 0) { - return NOOP; - } - - let listeners = cache[name]; - - if (!listeners) { - listeners = populateListeners(name); - } - - if (listeners.length === 0) { - return NOOP; - } - - let payload = payloadFunc(payloadArg!); - - let STRUCTURED_PROFILE = ENV.STRUCTURED_PROFILE; - let timeName: string; - if (STRUCTURED_PROFILE) { - timeName = `${name}: ${(payload as StructuredProfilePayload).object}`; - console.time(timeName); - } - - let beforeValues: any[] = []; - let timestamp = time(); - for (let listener of listeners) { - beforeValues.push(listener.before(name, timestamp, payload)); - } - - const constListeners = listeners; - - return function _instrumentEnd(): void { - let timestamp = time(); - for (let i = 0; i < constListeners.length; i++) { - let listener = constListeners[i]; - assert('has listener', listener); // Iterating over values - if (typeof listener.after === 'function') { - listener.after(name, timestamp, payload, beforeValues[i]); - } - } - - if (STRUCTURED_PROFILE) { - console.timeEnd(timeName); - } - }; -} - /** Subscribes to a particular event or instrumented block of code. @@ -292,7 +197,7 @@ export function subscribe(pattern: string, object: Listener): Subscriber): void { } subscribers.splice(index, 1); - cache = {}; + resetCache(); } /** @@ -330,5 +235,5 @@ export function unsubscribe(subscriber: Subscriber): void { */ export function reset(): void { subscribers.length = 0; - cache = {}; + resetCache(); } diff --git a/packages/@ember/instrumentation/lib/internal-instrument.ts b/packages/@ember/instrumentation/lib/internal-instrument.ts new file mode 100644 index 00000000000..10cab44910e --- /dev/null +++ b/packages/@ember/instrumentation/lib/internal-instrument.ts @@ -0,0 +1,114 @@ +import { ENV } from '@ember/-internals/environment/lib/env'; +import { assert } from '@ember/debug'; + +export interface Listener { + before: (name: string, timestamp: number, payload: object) => T; + after: (name: string, timestamp: number, payload: object, beforeValue: T) => void; +} + +export interface Subscriber { + pattern: string; + regex: RegExp; + object: Listener; +} + +export interface StructuredProfilePayload { + object: string | object; +} + +export const subscribers: Subscriber[] = []; + +export const cache: { [key: string]: Listener[] } = {}; + +export function resetCache(): void { + for (const key of Object.keys(cache)) { + delete cache[key]; + } +} + +function populateListeners(name: string): Listener[] { + let listeners: Listener[] = []; + + for (let subscriber of subscribers) { + if (subscriber.regex.test(name)) { + listeners.push(subscriber.object); + } + } + + cache[name] = listeners; + return listeners; +} + +const time = (): number => performance.now(); + +export const NOOP = (): void => {}; + +// `flaggedInstrument` historically wrapped a callback in conditionally- +// enabled instrumentation; today it's a thin pass-through. +export function flaggedInstrument( + _name: string, + _payload: object, + callback: () => Result +): Result { + return callback(); +} + +export function _instrumentStart(name: string, payloadFunc: () => object): () => void; +export function _instrumentStart( + name: string, + payloadFunc: (arg: Arg) => object, + payloadArg: Arg +): () => void; +export function _instrumentStart( + name: string, + payloadFunc: ((arg: Arg) => object) | (() => object), + payloadArg?: Arg +): () => void { + if (subscribers.length === 0) { + return NOOP; + } + + let listeners = cache[name]; + + if (!listeners) { + listeners = populateListeners(name); + } + + if (listeners.length === 0) { + return NOOP; + } + + let payload = payloadFunc(payloadArg!); + + let STRUCTURED_PROFILE = ENV.STRUCTURED_PROFILE; + let timeName: string; + if (STRUCTURED_PROFILE) { + timeName = `${name}: ${(payload as StructuredProfilePayload).object}`; + // eslint-disable-next-line no-console + console.time(timeName); + } + + let beforeValues: any[] = []; + let timestamp = time(); + for (let listener of listeners) { + beforeValues.push(listener.before(name, timestamp, payload)); + } + + const constListeners = listeners; + + return function _instrumentEnd(): void { + let timestamp = time(); + for (let i = 0; i < constListeners.length; i++) { + let listener = constListeners[i]; + assert('has listener', listener); + if (typeof listener.after === 'function') { + listener.after(name, timestamp, payload, beforeValues[i]); + } + } + + if (STRUCTURED_PROFILE) { + // eslint-disable-next-line no-console + console.timeEnd(timeName); + } + }; +} diff --git a/packages/@ember/object/action.ts b/packages/@ember/object/action.ts new file mode 100644 index 00000000000..e514c005b81 --- /dev/null +++ b/packages/@ember/object/action.ts @@ -0,0 +1,120 @@ +import { assert } from '@ember/debug'; +import type { + ElementDescriptor, + ExtendedMethodDecorator, +} from '@ember/-internals/metal/lib/decorator'; +import { isElementDescriptor, setClassicDecorator } from '@ember/-internals/metal/lib/decorator'; + +const BINDINGS_MAP = new WeakMap(); + +interface HasProto { + constructor: { + proto(): void; + }; +} + +function hasProto(obj: unknown): obj is HasProto { + return ( + obj != null && + (obj as any).constructor !== undefined && + typeof ((obj as any).constructor as any).proto === 'function' + ); +} + +interface HasActions { + actions: Record; +} + +function setupAction( + target: Partial, + key: string | symbol, + actionFn: Function +): TypedPropertyDescriptor { + if (hasProto(target)) { + target.constructor.proto(); + } + + if (!Object.prototype.hasOwnProperty.call(target, 'actions')) { + let parentActions = target.actions; + // we need to assign because of the way mixins copy actions down when inheriting + target.actions = parentActions ? Object.assign({}, parentActions) : {}; + } + + assert("[BUG] Somehow the target doesn't have actions!", target.actions != null); + + target.actions[key] = actionFn; + + return { + get() { + let bindings = BINDINGS_MAP.get(this); + + if (bindings === undefined) { + bindings = new Map(); + BINDINGS_MAP.set(this, bindings); + } + + let fn = bindings.get(actionFn); + + if (fn === undefined) { + fn = actionFn.bind(this); + bindings.set(actionFn, fn); + } + + return fn; + }, + }; +} + +export function action( + target: ElementDescriptor[0], + key: ElementDescriptor[1], + desc: ElementDescriptor[2] +): PropertyDescriptor; +export function action(desc: PropertyDescriptor): ExtendedMethodDecorator; +export function action( + ...args: ElementDescriptor | [PropertyDescriptor] +): PropertyDescriptor | ExtendedMethodDecorator { + let actionFn: object | Function; + + if (!isElementDescriptor(args)) { + actionFn = args[0]; + + let decorator: ExtendedMethodDecorator = function ( + target, + key, + _desc, + _meta, + isClassicDecorator + ) { + assert( + 'The @action decorator may only be passed a method when used in classic classes. You should decorate methods directly in native classes', + isClassicDecorator + ); + + assert( + 'The action() decorator must be passed a method when used in classic classes', + typeof actionFn === 'function' + ); + + return setupAction(target, key, actionFn); + }; + + setClassicDecorator(decorator); + + return decorator; + } + + let [target, key, desc] = args; + + actionFn = desc?.value; + + assert( + 'The @action decorator must be applied to methods when used in native classes', + typeof actionFn === 'function' + ); + + // SAFETY: TS types are weird with decorators. This should work. + return setupAction(target, key, actionFn); +} + +setClassicDecorator(action as ExtendedMethodDecorator); diff --git a/packages/@ember/object/index.ts b/packages/@ember/object/index.ts index f664c95ef0b..11a3a745e36 100644 --- a/packages/@ember/object/index.ts +++ b/packages/@ember/object/index.ts @@ -1,10 +1,5 @@ import { assert } from '@ember/debug'; import { ENV } from '@ember/-internals/environment/lib/env'; -import type { - ElementDescriptor, - ExtendedMethodDecorator, -} from '@ember/-internals/metal/lib/decorator'; -import { isElementDescriptor, setClassicDecorator } from '@ember/-internals/metal/lib/decorator'; import expandProperties from '@ember/-internals/metal/lib/expand_properties'; import { getFactoryFor } from '@ember/-internals/container/lib/container'; import { setObservers } from '@ember/-internals/utils/lib/super'; @@ -109,120 +104,7 @@ export default EmberObject; @return {PropertyDecorator} property decorator instance */ -const BINDINGS_MAP = new WeakMap(); - -interface HasProto { - constructor: { - proto(): void; - }; -} - -function hasProto(obj: unknown): obj is HasProto { - return ( - obj != null && - (obj as any).constructor !== undefined && - typeof ((obj as any).constructor as any).proto === 'function' - ); -} - -interface HasActions { - actions: Record; -} - -function setupAction( - target: Partial, - key: string | symbol, - actionFn: Function -): TypedPropertyDescriptor { - if (hasProto(target)) { - target.constructor.proto(); - } - - if (!Object.prototype.hasOwnProperty.call(target, 'actions')) { - let parentActions = target.actions; - // we need to assign because of the way mixins copy actions down when inheriting - target.actions = parentActions ? Object.assign({}, parentActions) : {}; - } - - assert("[BUG] Somehow the target doesn't have actions!", target.actions != null); - - target.actions[key] = actionFn; - - return { - get() { - let bindings = BINDINGS_MAP.get(this); - - if (bindings === undefined) { - bindings = new Map(); - BINDINGS_MAP.set(this, bindings); - } - - let fn = bindings.get(actionFn); - - if (fn === undefined) { - fn = actionFn.bind(this); - bindings.set(actionFn, fn); - } - - return fn; - }, - }; -} - -export function action( - target: ElementDescriptor[0], - key: ElementDescriptor[1], - desc: ElementDescriptor[2] -): PropertyDescriptor; -export function action(desc: PropertyDescriptor): ExtendedMethodDecorator; -export function action( - ...args: ElementDescriptor | [PropertyDescriptor] -): PropertyDescriptor | ExtendedMethodDecorator { - let actionFn: object | Function; - - if (!isElementDescriptor(args)) { - actionFn = args[0]; - - let decorator: ExtendedMethodDecorator = function ( - target, - key, - _desc, - _meta, - isClassicDecorator - ) { - assert( - 'The @action decorator may only be passed a method when used in classic classes. You should decorate methods directly in native classes', - isClassicDecorator - ); - - assert( - 'The action() decorator must be passed a method when used in classic classes', - typeof actionFn === 'function' - ); - - return setupAction(target, key, actionFn); - }; - - setClassicDecorator(decorator); - - return decorator; - } - - let [target, key, desc] = args; - - actionFn = desc?.value; - - assert( - 'The @action decorator must be applied to methods when used in native classes', - typeof actionFn === 'function' - ); - - // SAFETY: TS types are weird with decorators. This should work. - return setupAction(target, key, actionFn); -} - -// SAFETY: TS types are weird with decorators. This should work. -setClassicDecorator(action as ExtendedMethodDecorator); +export { action } from './action'; // .......................................................... // OBSERVER HELPER diff --git a/packages/@ember/runloop/index.ts b/packages/@ember/runloop/index.ts index db079ea25b2..0b7fb97aca6 100644 --- a/packages/@ember/runloop/index.ts +++ b/packages/@ember/runloop/index.ts @@ -1,9 +1,16 @@ import { assert } from '@ember/debug'; import { onErrorTarget } from '@ember/-internals/error-handling'; -import { flushAsyncObservers } from '@ember/-internals/metal/lib/observer'; import Backburner, { type Timer, type DeferredActionQueues } from 'backburner.js'; import type { AnyFn } from '@ember/-internals/utility-types'; +let asyncObserverFlush: ((schedule: AsyncObserverScheduler) => void) | null = null; + +type AsyncObserverScheduler = typeof schedule; + +export function registerAsyncObserverFlush(fn: (schedule: AsyncObserverScheduler) => void): void { + asyncObserverFlush = fn; +} + export type { Timer }; // Partial types from https://medium.com/codex/currying-in-typescript-ca5226c85b85 @@ -45,12 +52,16 @@ function onBegin(current: DeferredActionQueues) { function onEnd(_current: DeferredActionQueues, next: DeferredActionQueues) { currentRunLoop = next; - flushAsyncObservers(schedule); + if (asyncObserverFlush !== null) { + asyncObserverFlush(schedule); + } } function flush(queueName: string, next: () => void) { if (queueName === 'render' || queueName === _rsvpErrorQueue) { - flushAsyncObservers(schedule); + if (asyncObserverFlush !== null) { + asyncObserverFlush(schedule); + } } next(); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index ec5b2c206d1..98b020e0131 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -89,7 +89,7 @@ import { ConcreteBounds } from '../../bounds'; import { hasCustomDebugRenderTreeLifecycle } from '../../component/interfaces'; import { resolveComponent } from '../../component/resolve'; import { isCurriedType, isCurriedValue, resolveCurriedValue } from '../../curried-value'; -import { getDebugName } from '../../debug-render-tree'; +import { getDebugName } from '../../get-debug-name'; import { APPEND_OPCODES } from '../../opcodes'; import createClassListRef from '../../references/class-list'; import { EMPTY_ARGS, VMArgumentsImpl } from '../../vm/arguments'; diff --git a/packages/@glimmer/runtime/lib/debug-render-tree-register.ts b/packages/@glimmer/runtime/lib/debug-render-tree-register.ts new file mode 100644 index 00000000000..af8ab37555c --- /dev/null +++ b/packages/@glimmer/runtime/lib/debug-render-tree-register.ts @@ -0,0 +1,4 @@ +import DebugRenderTreeImpl from './debug-render-tree'; +import { registerDebugRenderTreeFactory } from './environment'; + +registerDebugRenderTreeFactory(() => new DebugRenderTreeImpl()); diff --git a/packages/@glimmer/runtime/lib/debug-render-tree.ts b/packages/@glimmer/runtime/lib/debug-render-tree.ts index 8b09ce748d6..25b580c2740 100644 --- a/packages/@glimmer/runtime/lib/debug-render-tree.ts +++ b/packages/@glimmer/runtime/lib/debug-render-tree.ts @@ -2,7 +2,6 @@ import { DEBUG } from '@glimmer/env'; import type { Bounds, CapturedRenderNode, - ComponentDefinition, DebugRenderTree, Nullable, RenderNode, @@ -198,9 +197,4 @@ export default class DebugRenderTreeImpl< } } -export function getDebugName( - definition: ComponentDefinition, - manager = definition.manager -): string { - return definition.resolvedName ?? definition.debugName ?? manager.getDebugName(definition.state); -} +export { getDebugName } from './get-debug-name'; diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index bafbefe2f82..1648aa0b4b7 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -2,6 +2,7 @@ import { DEBUG } from '@glimmer/env'; import type { ClassicResolver, ComponentInstanceWithCreate, + DebugRenderTree, Environment, EnvironmentOptions, GlimmerTreeChanges, @@ -19,10 +20,16 @@ import { ProgramImpl } from '@glimmer/program/lib/program'; import { track } from '@glimmer/validator/lib/tracking'; import { UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; -import DebugRenderTree from './debug-render-tree'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; import { isArgumentError } from './vm/arguments'; +type DebugRenderTreeFactory = () => DebugRenderTree; +let debugRenderTreeFactory: DebugRenderTreeFactory | null = null; + +export function registerDebugRenderTreeFactory(factory: DebugRenderTreeFactory): void { + debugRenderTreeFactory = factory; +} + export const TRANSACTION: TransactionSymbol = Symbol('TRANSACTION') as TransactionSymbol; class TransactionImpl implements Transaction { @@ -107,14 +114,17 @@ export class EnvironmentImpl implements Environment { // eslint-disable-next-line @typescript-eslint/no-explicit-any isArgumentCaptureError: ((error: any) => boolean) | undefined; - debugRenderTree: DebugRenderTree | undefined; + debugRenderTree: DebugRenderTree | undefined; constructor( options: EnvironmentOptions, private delegate: EnvironmentDelegate ) { this.isInteractive = delegate.isInteractive; - this.debugRenderTree = this.delegate.enableDebugTooling ? new DebugRenderTree() : undefined; + this.debugRenderTree = + this.delegate.enableDebugTooling && debugRenderTreeFactory + ? debugRenderTreeFactory() + : undefined; this.isArgumentCaptureError = this.delegate.enableDebugTooling ? isArgumentError : undefined; if (options.appendOperations) { this.appendOperations = options.appendOperations; diff --git a/packages/@glimmer/runtime/lib/get-debug-name.ts b/packages/@glimmer/runtime/lib/get-debug-name.ts new file mode 100644 index 00000000000..4fee4aac3a1 --- /dev/null +++ b/packages/@glimmer/runtime/lib/get-debug-name.ts @@ -0,0 +1,8 @@ +import type { ComponentDefinition } from '@glimmer/interfaces'; + +export function getDebugName( + definition: ComponentDefinition, + manager = definition.manager +): string { + return definition.resolvedName ?? definition.debugName ?? manager.getDebugName(definition.state); +} diff --git a/tests/node-vitest/tree-shakability.test.js b/tests/node-vitest/tree-shakability.test.js index d6220a4bc3d..ad916435d20 100644 --- a/tests/node-vitest/tree-shakability.test.js +++ b/tests/node-vitest/tree-shakability.test.js @@ -117,6 +117,7 @@ it('[dev] has expected tree-shakable entrypoints', async () => { "ember-source/@ember/-internals/runtime/lib/mixins/action_handler.js", "ember-source/@ember/-internals/runtime/lib/mixins/comparable.js", "ember-source/@ember/-internals/runtime/lib/mixins/container_proxy.js", + "ember-source/@ember/-internals/runtime/lib/mixins/content-for.js", "ember-source/@ember/-internals/runtime/lib/mixins/registry_proxy.js", "ember-source/@ember/-internals/runtime/lib/mixins/target_action_support.js", "ember-source/@ember/-internals/utils/index.js", @@ -149,9 +150,11 @@ it('[dev] has expected tree-shakable entrypoints', async () => { "ember-source/@ember/enumerable/mutable.js", "ember-source/@ember/helper/index.js", "ember-source/@ember/instrumentation/index.js", + "ember-source/@ember/instrumentation/lib/internal-instrument.js", "ember-source/@ember/modifier/index.js", "ember-source/@ember/modifier/on.js", "ember-source/@ember/object/-internals.js", + "ember-source/@ember/object/action.js", "ember-source/@ember/object/compat.js", "ember-source/@ember/object/computed.js", "ember-source/@ember/object/core.js", @@ -319,6 +322,7 @@ it('[prod] has expected tree-shakable entrypoints', async () => { "ember-source/@ember/-internals/runtime/lib/mixins/action_handler.js", "ember-source/@ember/-internals/runtime/lib/mixins/comparable.js", "ember-source/@ember/-internals/runtime/lib/mixins/container_proxy.js", + "ember-source/@ember/-internals/runtime/lib/mixins/content-for.js", "ember-source/@ember/-internals/runtime/lib/mixins/registry_proxy.js", "ember-source/@ember/-internals/runtime/lib/mixins/target_action_support.js", "ember-source/@ember/-internals/utils/index.js", @@ -350,9 +354,11 @@ it('[prod] has expected tree-shakable entrypoints', async () => { "ember-source/@ember/enumerable/mutable.js", "ember-source/@ember/helper/index.js", "ember-source/@ember/instrumentation/index.js", + "ember-source/@ember/instrumentation/lib/internal-instrument.js", "ember-source/@ember/modifier/index.js", "ember-source/@ember/modifier/on.js", "ember-source/@ember/object/-internals.js", + "ember-source/@ember/object/action.js", "ember-source/@ember/object/compat.js", "ember-source/@ember/object/computed.js", "ember-source/@ember/object/core.js",