diff --git a/packages/@glimmer-workspace/benchmark-env/lib/benchmark/on-modifier.ts b/packages/@glimmer-workspace/benchmark-env/lib/benchmark/on-modifier.ts index e55f509e25..ea1470b20e 100644 --- a/packages/@glimmer-workspace/benchmark-env/lib/benchmark/on-modifier.ts +++ b/packages/@glimmer-workspace/benchmark-env/lib/benchmark/on-modifier.ts @@ -29,7 +29,11 @@ class OnModifierManager implements InternalModifierManager<OnModifierState, obje } getDebugName() { - return 'on-modifier'; + return 'on'; + } + + getDebugInstance() { + return null; } install(state: OnModifierState) { diff --git a/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts b/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts index e11edd319b..eb0e26717c 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts @@ -44,8 +44,12 @@ export class TestModifierManager return tag; } - getDebugName() { - return '<unknown>'; + getDebugName({ Klass }: TestModifierDefinitionState) { + return Klass?.name || '<unknown>'; + } + + getDebugInstance({ instance }: TestModifier) { + return instance; } install({ element, args, instance }: TestModifier) { diff --git a/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts b/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts index 7e63ee93f5..485f7539f9 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts @@ -63,6 +63,19 @@ export async function setupQunit() { qunit.moduleDone(pause); } + // @ts-expect-error missing in types, does exist: https://api.qunitjs.com/callbacks/QUnit.on/#the-testend-event + QUnit.on('testEnd', (testEnd) => { + if (testEnd.status === 'failed') { + testEnd.errors.forEach((assertion: any) => { + console.error(assertion.stack); + // message: speedometer + // actual: 75 + // expected: 88 + // stack: at dmc.test.js:12 + }); + } + }); + qunit.done(({ failed }) => { if (failed > 0) { console.log('[HARNESS] fail'); diff --git a/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts b/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts index f1acdc5d49..6c8dbf24a0 100644 --- a/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts @@ -9,7 +9,7 @@ import type { SimpleNode, } from '@glimmer/interfaces'; import type { TemplateOnlyComponent } from '@glimmer/runtime'; -import { setComponentTemplate } from '@glimmer/manager'; +import { modifierCapabilities, setComponentTemplate, setModifierManager } from '@glimmer/manager'; import { EMPTY_ARGS, templateOnlyComponent, TemplateOnlyComponentManager } from '@glimmer/runtime'; import { assign, expect } from '@glimmer/util'; @@ -331,6 +331,188 @@ class DebugRenderTreeTest extends RenderTest { ]); } + @test modifiers() { + this.registerComponent('Glimmer', 'HelloWorld', 'Hello World'); + const didInsert = () => null; + + class DidInsertModifier { + element?: SimpleElement; + didInsertElement() {} + didUpdate() {} + willDestroyElement() {} + } + + this.registerModifier('did-insert', DidInsertModifier); + + class MyCustomModifier {} + + setModifierManager( + () => ({ + capabilities: modifierCapabilities('3.22'), + createModifier() { + return new MyCustomModifier(); + }, + installModifier() {}, + updateModifier() {}, + destroyModifier() {}, + }), + MyCustomModifier + ); + + const foo = Symbol('foo'); + const bar = Symbol('bar'); + + this.render( + `<div {{on 'click' this.didInsert}} {{did-insert this.foo bar=this.bar}} {{this.modifier this.bar foo=this.foo}} + ><HelloWorld /> + {{~#if this.more~}} + <div {{on 'click' this.didInsert passive=true}}></div> + {{~/if~}} + </div>`, + { + didInsert: didInsert, + modifier: MyCustomModifier, + foo, + bar, + more: false, + } + ); + + this.assertRenderTree([ + { + type: 'modifier', + name: 'on', + args: { positional: ['click', didInsert], named: {} }, + instance: null, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'DidInsertModifier', + args: { positional: [foo], named: { bar } }, + instance: (instance: unknown) => instance instanceof DidInsertModifier, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'MyCustomModifier', + args: { positional: [bar], named: { foo } }, + instance: (instance: unknown) => instance instanceof MyCustomModifier, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'component', + name: 'HelloWorld', + args: { positional: [], named: {} }, + instance: (instance: any) => instance !== undefined, + template: '(unknown template module)', + bounds: this.nodeBounds(this.element.firstChild!.firstChild), + children: [], + }, + ]); + + this.rerender({ + more: true, + }); + + this.assertRenderTree([ + { + type: 'modifier', + name: 'on', + args: { positional: ['click', didInsert], named: {} }, + instance: null, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'DidInsertModifier', + args: { positional: [foo], named: { bar } }, + instance: (instance: unknown) => instance instanceof DidInsertModifier, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'MyCustomModifier', + args: { positional: [bar], named: { foo } }, + instance: (instance: unknown) => instance instanceof MyCustomModifier, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'component', + name: 'HelloWorld', + args: { positional: [], named: {} }, + instance: (instance: any) => instance !== undefined, + template: '(unknown template module)', + bounds: this.nodeBounds(this.element.firstChild!.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'on', + args: { positional: ['click', didInsert], named: { passive: true } }, + instance: null, + template: null, + bounds: this.nodeBounds(this.element.firstChild!.lastChild), + children: [], + }, + ]); + + this.rerender({ + more: false, + }); + + this.assertRenderTree([ + { + type: 'modifier', + name: 'on', + args: { positional: ['click', didInsert], named: {} }, + instance: null, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'DidInsertModifier', + args: { positional: [foo], named: { bar } }, + instance: (instance: unknown) => instance instanceof DidInsertModifier, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'modifier', + name: 'MyCustomModifier', + args: { positional: [bar], named: { foo } }, + instance: (instance: unknown) => instance instanceof MyCustomModifier, + template: null, + bounds: this.nodeBounds(this.element.firstChild), + children: [], + }, + { + type: 'component', + name: 'HelloWorld', + args: { positional: [], named: {} }, + instance: (instance: any) => instance !== undefined, + template: '(unknown template module)', + bounds: this.nodeBounds(this.element.firstChild!.firstChild), + children: [], + }, + ]); + } + @test 'getDebugCustomRenderTree works'() { let bucket1 = {}; let instance1 = {}; diff --git a/packages/@glimmer/interfaces/lib/managers/internal/modifier.d.ts b/packages/@glimmer/interfaces/lib/managers/internal/modifier.d.ts index 4907d9048f..b717072d23 100644 --- a/packages/@glimmer/interfaces/lib/managers/internal/modifier.d.ts +++ b/packages/@glimmer/interfaces/lib/managers/internal/modifier.d.ts @@ -23,6 +23,7 @@ export interface InternalModifierManager< getTag(modifier: TModifierInstanceState): UpdatableTag | null; getDebugName(Modifier: TModifierDefinitionState): string; + getDebugInstance(Modifier: TModifierInstanceState): unknown; // At initial render, the modifier gets a chance to install itself on the // element it is managing. It can also return a bucket of state that diff --git a/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts b/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts index a2edcedce5..e3a7aafd75 100644 --- a/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts @@ -3,7 +3,13 @@ import type { SimpleElement, SimpleNode } from '@simple-dom/interface'; import type { Bounds } from '../dom/bounds.js'; import type { Arguments, CapturedArguments } from './arguments.js'; -export type RenderNodeType = 'outlet' | 'engine' | 'route-template' | 'component' | 'keyword'; +export type RenderNodeType = + | 'outlet' + | 'engine' + | 'route-template' + | 'component' + | 'modifier' + | 'keyword'; export interface RenderNode { type: RenderNodeType; diff --git a/packages/@glimmer/manager/lib/public/modifier.ts b/packages/@glimmer/manager/lib/public/modifier.ts index 7e15217cf3..ad598d31cc 100644 --- a/packages/@glimmer/manager/lib/public/modifier.ts +++ b/packages/@glimmer/manager/lib/public/modifier.ts @@ -112,17 +112,21 @@ export class CustomModifierManager<O extends Owner, ModifierInstance> modifier: instance, }; - if (import.meta.env.DEV) { - state.debugName = typeof definition === 'function' ? definition.name : definition.toString(); - } - registerDestructor(state, () => delegate.destroyModifier(instance, args)); return state; } - getDebugName({ debugName }: CustomModifierState<ModifierInstance>) { - return debugName!; + getDebugName(definition: object) { + if (typeof definition === 'function') { + return definition.name || definition.toString(); + } else { + return '<unknown>'; + } + } + + getDebugInstance({ modifier }: CustomModifierState<ModifierInstance>) { + return modifier; } getTag({ tag }: CustomModifierState<ModifierInstance>) { diff --git a/packages/@glimmer/manager/test/managers-test.ts b/packages/@glimmer/manager/test/managers-test.ts index c1f723ad0f..70e2018927 100644 --- a/packages/@glimmer/manager/test/managers-test.ts +++ b/packages/@glimmer/manager/test/managers-test.ts @@ -289,6 +289,10 @@ module('Managers', () => { return 'internal'; } + getDebugInstance() { + return null; + } + getDestroyable() { return null; } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index 36749bf03c..655c4e52f6 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -56,6 +56,7 @@ import type { UpdatingVM } from '../../vm'; import type { InternalVM } from '../../vm/append'; import type { BlockArgumentsImpl } from '../../vm/arguments'; +import { ConcreteBounds } from '../../bounds'; import { hasCustomDebugRenderTreeLifecycle } from '../../component/interfaces'; import { resolveComponent } from '../../component/resolve'; import { isCurriedType, isCurriedValue, resolveCurriedValue } from '../../curried-value'; @@ -489,8 +490,46 @@ export class ComponentElementOperations implements ElementOperations { this.attributes[name] = deferred; } - addModifier(modifier: ModifierInstance): void { + addModifier(vm: InternalVM, modifier: ModifierInstance, capturedArgs: CapturedArguments): void { this.modifiers.push(modifier); + + if (vm.env.debugRenderTree !== undefined) { + const { manager, definition, state } = modifier; + + // TODO: we need a stable object for the debugRenderTree as the key, add support for + // the case where the state is a primitive, or if in practice we always have/require + // an object, then change the internal types to reflect that + if (state === null || (typeof state !== 'object' && typeof state !== 'function')) { + return; + } + + let { element, constructing } = vm.elements(); + let name = manager.getDebugName(definition.state); + let instance = manager.getDebugInstance(state); + + assert(constructing, `Expected a constructing element in addModifier`); + + let bounds = new ConcreteBounds(element, constructing, constructing); + + vm.env.debugRenderTree.create(state, { + type: 'modifier', + name, + args: capturedArgs, + instance, + }); + + vm.env.debugRenderTree.didRender(state, bounds); + + // For tearing down the debugRenderTree + vm.associateDestroyable(state); + + vm.updateWith(new DebugRenderTreeUpdateOpcode(state)); + vm.updateWith(new DebugRenderTreeDidRenderOpcode(state, bounds)); + + registerDestructor(state, () => { + vm.env.debugRenderTree?.willDestroy(state); + }); + } } flush(vm: InternalVM): ModifierInstance[] { @@ -645,8 +684,6 @@ APPEND_OPCODES.add(Op.GetComponentSelf, (vm, { op1: _state, op2: _names }) => { instance: valueForRef(selfRef), }); - vm.associateDestroyable(instance); - registerDestructor(instance, () => { vm.env.debugRenderTree?.willDestroy(instance); }); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 94542550d0..f575240eb5 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -144,11 +144,12 @@ APPEND_OPCODES.add(Op.Modifier, (vm, { op1: handle }) => { 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, - args.capture() + capturedArgs ); let instance: ModifierInstance = { @@ -162,7 +163,7 @@ APPEND_OPCODES.add(Op.Modifier, (vm, { op1: handle }) => { 'BUG: ElementModifier could not find operations to append to' ); - operations.addModifier(instance); + operations.addModifier(vm, instance, capturedArgs); let tag = manager.getTag(state); @@ -263,7 +264,7 @@ APPEND_OPCODES.add(Op.DynamicModifier, (vm) => { 'BUG: ElementModifier could not find operations to append to' ); - operations.addModifier(instance); + operations.addModifier(vm, instance, args); tag = instance.manager.getTag(instance.state); diff --git a/packages/@glimmer/runtime/lib/modifiers/on.ts b/packages/@glimmer/runtime/lib/modifiers/on.ts index a1df7e51f9..bc79fa7713 100644 --- a/packages/@glimmer/runtime/lib/modifiers/on.ts +++ b/packages/@glimmer/runtime/lib/modifiers/on.ts @@ -312,6 +312,10 @@ class OnModifierManager implements InternalModifierManager<OnModifierState, obje return 'on'; } + getDebugInstance(): unknown { + return null; + } + get counters(): { adds: number; removes: number } { return { adds, removes }; }