From e2d2bf01e14371e7a76f77e98bf621b9e0ecb35f Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 17 Mar 2026 15:17:26 -0400
Subject: [PATCH 01/10] Failing test for better debugRenderTree names
---
.../test/debug-render-tree-test.ts | 67 +++++++++++++++++++
1 file changed, 67 insertions(+)
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 5cecd7d0852..9a7cb929557 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
@@ -110,6 +110,73 @@ class DebugRenderTreeTest extends RenderTest {
]);
}
+ @test 'strict-mode components without debug symbols preserve names from scope'() {
+ const state = trackedObj({ showSecond: false });
+
+ const HelloWorld = defComponent('{{@arg}}');
+ const Root = defComponent(
+ `{{#if state.showSecond}}{{/if}}`,
+ { scope: { HelloWorld, state }, emit: { moduleName: 'root.hbs', debugSymbols: false } }
+ );
+
+ this.renderComponent(Root);
+
+ this.assertRenderTree([
+ {
+ type: 'component',
+ name: '{ROOT}',
+ args: { positional: [], named: {} },
+ instance: null,
+ template: 'root.hbs',
+ bounds: this.elementBounds(this.delegate.getInitialElement()),
+ children: [
+ {
+ type: 'component',
+ name: 'HelloWorld',
+ args: { positional: [], named: { arg: 'first' } },
+ instance: null,
+ template: '(unknown template module)',
+ bounds: this.nodeBounds(this.delegate.getInitialElement().firstChild),
+ children: [],
+ },
+ ],
+ },
+ ]);
+
+ state['showSecond'] = true;
+
+ this.assertRenderTree([
+ {
+ type: 'component',
+ name: '{ROOT}',
+ args: { positional: [], named: {} },
+ instance: null,
+ template: 'root.hbs',
+ bounds: this.elementBounds(this.delegate.getInitialElement()),
+ children: [
+ {
+ type: 'component',
+ name: 'HelloWorld',
+ args: { positional: [], named: { arg: 'first' } },
+ instance: null,
+ template: '(unknown template module)',
+ bounds: this.nodeBounds(this.delegate.getInitialElement().firstChild),
+ children: [],
+ },
+ {
+ type: 'component',
+ name: 'HelloWorld',
+ args: { positional: [], named: { arg: 'second' } },
+ instance: null,
+ template: '(unknown template module)',
+ bounds: this.nodeBounds(this.delegate.getInitialElement().lastChild),
+ children: [],
+ },
+ ],
+ },
+ ]);
+ }
+
@test 'strict-mode modifiers'() {
const state = trackedObj({ showSecond: false });
From 9498833de2410ee3ccb6d51549ccdbbdc10e4b35 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 17 Mar 2026 16:38:09 -0400
Subject: [PATCH 02/10] glhf
---
.../integration-tests/lib/compile.ts | 7 +++-
.../test/compiler/compile-options-test.ts | 6 +--
.../test/debug-render-tree-test.ts | 39 +------------------
packages/@glimmer/compiler/lib/compiler.ts | 9 ++++-
.../lib/compile/wire-format/api.d.ts | 2 +-
.../@glimmer/interfaces/lib/template.d.ts | 2 +-
.../lib/opcode-builder/helpers/shared.ts | 5 ++-
packages/internal-test-helpers/lib/compile.ts | 7 +++-
8 files changed, 28 insertions(+), 49 deletions(-)
diff --git a/packages/@glimmer-workspace/integration-tests/lib/compile.ts b/packages/@glimmer-workspace/integration-tests/lib/compile.ts
index 012278e8765..ace69285e2c 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/compile.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/compile.ts
@@ -24,7 +24,10 @@ export function createTemplate(
): TemplateFactory {
options.locals = options.locals ?? Object.keys(scopeValues ?? {});
let [block, usedLocals] = precompileJSON(templateSource, options);
- let reifiedScopeValues = usedLocals.map((key) => scopeValues[key]);
+ let reifiedScope: Record = {};
+ for (let key of usedLocals) {
+ reifiedScope[key] = scopeValues[key];
+ }
if ('emit' in options && options.emit?.debugSymbols) {
block.push(usedLocals);
@@ -34,7 +37,7 @@ export function createTemplate(
id: String(templateId++),
block: JSON.stringify(block),
moduleName: options.meta?.moduleName ?? '(unknown template module)',
- scope: reifiedScopeValues.length > 0 ? () => reifiedScopeValues : null,
+ scope: usedLocals.length > 0 ? () => reifiedScope : null,
isStrictMode: options.strictMode ?? false,
};
diff --git a/packages/@glimmer-workspace/integration-tests/test/compiler/compile-options-test.ts b/packages/@glimmer-workspace/integration-tests/test/compiler/compile-options-test.ts
index c166e06c1d0..7d2c5d2505e 100644
--- a/packages/@glimmer-workspace/integration-tests/test/compiler/compile-options-test.ts
+++ b/packages/@glimmer-workspace/integration-tests/test/compiler/compile-options-test.ts
@@ -57,7 +57,7 @@ module('[glimmer-compiler] precompile', ({ test }) => {
...WireFormat.Statement[],
];
- assert.deepEqual(wire.scope?.(), [hello]);
+ assert.deepEqual(wire.scope?.(), { hello });
assert.deepEqual(
componentNameExpr,
@@ -84,7 +84,7 @@ module('[glimmer-compiler] precompile', ({ test }) => {
...WireFormat.Statement[],
];
- assert.deepEqual(wire.scope?.(), [f]);
+ assert.deepEqual(wire.scope?.(), { f });
assert.deepEqual(
componentNameExpr,
[SexpOpcodes.GetLexicalSymbol, 0, ['hello']],
@@ -218,7 +218,7 @@ module('[glimmer-compiler] precompile', ({ test }) => {
_wire = compile(`{{this.message}}`, ['this'], (source) => eval(source));
}).call(target);
let wire = _wire!;
- assert.deepEqual(wire.scope?.(), [target]);
+ assert.deepEqual(wire.scope?.(), { this: target });
assert.deepEqual(wire.block[0], [
[SexpOpcodes.Append, [SexpOpcodes.GetLexicalSymbol, 0, ['message']]],
]);
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 9a7cb929557..1d2cd56fb28 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
@@ -111,12 +111,10 @@ class DebugRenderTreeTest extends RenderTest {
}
@test 'strict-mode components without debug symbols preserve names from scope'() {
- const state = trackedObj({ showSecond: false });
-
const HelloWorld = defComponent('{{@arg}}');
const Root = defComponent(
- `{{#if state.showSecond}}{{/if}}`,
- { scope: { HelloWorld, state }, emit: { moduleName: 'root.hbs', debugSymbols: false } }
+ ``,
+ { scope: { HelloWorld }, emit: { moduleName: 'root.hbs', debugSymbols: false } }
);
this.renderComponent(Root);
@@ -142,39 +140,6 @@ class DebugRenderTreeTest extends RenderTest {
],
},
]);
-
- state['showSecond'] = true;
-
- this.assertRenderTree([
- {
- type: 'component',
- name: '{ROOT}',
- args: { positional: [], named: {} },
- instance: null,
- template: 'root.hbs',
- bounds: this.elementBounds(this.delegate.getInitialElement()),
- children: [
- {
- type: 'component',
- name: 'HelloWorld',
- args: { positional: [], named: { arg: 'first' } },
- instance: null,
- template: '(unknown template module)',
- bounds: this.nodeBounds(this.delegate.getInitialElement().firstChild),
- children: [],
- },
- {
- type: 'component',
- name: 'HelloWorld',
- args: { positional: [], named: { arg: 'second' } },
- instance: null,
- template: '(unknown template module)',
- bounds: this.nodeBounds(this.delegate.getInitialElement().lastChild),
- children: [],
- },
- ],
- },
- ]);
}
@test 'strict-mode modifiers'() {
diff --git a/packages/@glimmer/compiler/lib/compiler.ts b/packages/@glimmer/compiler/lib/compiler.ts
index 089608f65f5..a712196610a 100644
--- a/packages/@glimmer/compiler/lib/compiler.ts
+++ b/packages/@glimmer/compiler/lib/compiler.ts
@@ -148,7 +148,14 @@ export function precompile(
let stringified = JSON.stringify(templateJSONObject);
if (usedLocals.length > 0) {
- const scopeFn = `()=>[${usedLocals.join(',')}]`;
+ const scopeEntries = usedLocals.map((name) => {
+ // Reserved words like "this" can't use shorthand property syntax
+ if (name === 'this') {
+ return `"this":this`;
+ }
+ return name;
+ });
+ const scopeFn = `()=>({${scopeEntries.join(',')}})`;
stringified = stringified.replace(`"${SCOPE_PLACEHOLDER}"`, scopeFn);
}
diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts
index 6db2138eb87..9032916b8d1 100644
--- a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts
+++ b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts
@@ -397,7 +397,7 @@ export interface SerializedTemplateWithLazyBlock {
id?: Nullable;
block: SerializedTemplateBlockJSON;
moduleName: string;
- scope?: (() => unknown[]) | undefined | null;
+ scope?: (() => Record) | undefined | null;
isStrictMode: boolean;
}
diff --git a/packages/@glimmer/interfaces/lib/template.d.ts b/packages/@glimmer/interfaces/lib/template.d.ts
index 5e008c8039c..2990176844a 100644
--- a/packages/@glimmer/interfaces/lib/template.d.ts
+++ b/packages/@glimmer/interfaces/lib/template.d.ts
@@ -18,7 +18,7 @@ export interface LayoutWithContext {
readonly block: SerializedTemplateBlock;
readonly moduleName: string;
readonly owner: Owner | null;
- readonly scope: (() => unknown[]) | undefined | null;
+ readonly scope: (() => Record) | undefined | null;
readonly isStrictMode: boolean;
}
diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts
index 640b63494b7..634fd603510 100644
--- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts
+++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts
@@ -107,14 +107,15 @@ export function CompilePositional(
export function meta(layout: LayoutWithContext): BlockMetadata {
let [, locals, upvars, lexicalSymbols] = layout.block;
+ let scopeRecord = layout.scope?.() ?? null;
return {
symbols: {
locals,
upvars,
- lexical: lexicalSymbols,
+ lexical: scopeRecord ? Object.keys(scopeRecord) : lexicalSymbols,
},
- scopeValues: layout.scope?.() ?? null,
+ scopeValues: scopeRecord ? Object.values(scopeRecord) : null,
isStrictMode: layout.isStrictMode,
moduleName: layout.moduleName,
owner: layout.owner,
diff --git a/packages/internal-test-helpers/lib/compile.ts b/packages/internal-test-helpers/lib/compile.ts
index 7fd317a7fe8..2266b47457e 100644
--- a/packages/internal-test-helpers/lib/compile.ts
+++ b/packages/internal-test-helpers/lib/compile.ts
@@ -24,12 +24,15 @@ export default function compile(
): TemplateFactory {
options.locals = options.locals ?? Object.keys(scopeValues ?? {});
let [block, usedLocals] = precompileJSON(templateSource, compileOptions(options));
- let reifiedScopeValues = usedLocals.map((key) => scopeValues[key]);
+ let reifiedScope: Record = {};
+ for (let key of usedLocals) {
+ reifiedScope[key] = scopeValues[key];
+ }
let templateBlock: SerializedTemplateWithLazyBlock = {
block: JSON.stringify(block),
moduleName: options.moduleName ?? options.meta?.moduleName ?? '(unknown template module)',
- scope: reifiedScopeValues.length > 0 ? () => reifiedScopeValues : null,
+ scope: usedLocals.length > 0 ? () => reifiedScope : null,
isStrictMode: options.strictMode ?? false,
};
From 427b3926c3629069ba265392c86f81a3ba2cc1f4 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 17 Mar 2026 17:33:35 -0400
Subject: [PATCH 03/10] style: fix prettier formatting in
debug-render-tree-test
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../integration-tests/test/debug-render-tree-test.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
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 1d2cd56fb28..794877eb5cc 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
@@ -112,10 +112,10 @@ class DebugRenderTreeTest extends RenderTest {
@test 'strict-mode components without debug symbols preserve names from scope'() {
const HelloWorld = defComponent('{{@arg}}');
- const Root = defComponent(
- ``,
- { scope: { HelloWorld }, emit: { moduleName: 'root.hbs', debugSymbols: false } }
- );
+ const Root = defComponent(``, {
+ scope: { HelloWorld },
+ emit: { moduleName: 'root.hbs', debugSymbols: false },
+ });
this.renderComponent(Root);
From 5e7eeda04c8154337d4afec70f8c70003eb6c354 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 17 Mar 2026 18:07:37 -0400
Subject: [PATCH 04/10] Add a smoke test so we can test the full e2e
---
smoke-tests/scenarios/basic-test.ts | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts
index 1e575338e0e..6d49a80bf27 100644
--- a/smoke-tests/scenarios/basic-test.ts
+++ b/smoke-tests/scenarios/basic-test.ts
@@ -223,6 +223,29 @@ function basicTest(scenarios: Scenarios, appName: string) {
});
`,
+ 'debug-render-tree-test.gjs': `
+ import { module, test } from 'qunit';
+ import { setupRenderingTest } from 'ember-qunit';
+ import { render } from '@ember/test-helpers';
+ import { captureRenderTree } from '@ember/debug';
+ import Component from '@glimmer/component';
+
+ class HelloWorld extends Component {
+ {{@arg}}
+ }
+
+ module('Integration | captureRenderTree', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('scope-based components have correct names in debugRenderTree', async function (assert) {
+ await render();
+
+ let tree = captureRenderTree(this.owner);
+ let names = tree.filter(n => n.type === 'component').map(n => n.name);
+ assert.true(names.includes('HelloWorld'), 'HelloWorld component name is preserved in the render tree (found: ' + names.join(', ') + ')');
+ });
+ });
+ `,
},
},
});
From 497f868f6520baec33a4854f794090702c048059 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 17 Mar 2026 18:26:03 -0400
Subject: [PATCH 05/10] fix: flatten render tree when checking component names
in smoke test
The captureRenderTree API returns a nested tree structure. Component
nodes are children of other nodes, so we need to recursively flatten
the tree before searching for component names.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
smoke-tests/scenarios/basic-test.ts | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts
index 6d49a80bf27..b58d060f79a 100644
--- a/smoke-tests/scenarios/basic-test.ts
+++ b/smoke-tests/scenarios/basic-test.ts
@@ -230,6 +230,17 @@ function basicTest(scenarios: Scenarios, appName: string) {
import { captureRenderTree } from '@ember/debug';
import Component from '@glimmer/component';
+ function flattenTree(nodes) {
+ let result = [];
+ for (let node of nodes) {
+ result.push(node);
+ if (node.children) {
+ result.push(...flattenTree(node.children));
+ }
+ }
+ return result;
+ }
+
class HelloWorld extends Component {
{{@arg}}
}
@@ -241,7 +252,8 @@ function basicTest(scenarios: Scenarios, appName: string) {
await render();
let tree = captureRenderTree(this.owner);
- let names = tree.filter(n => n.type === 'component').map(n => n.name);
+ let allNodes = flattenTree(tree);
+ let names = allNodes.filter(n => n.type === 'component').map(n => n.name);
assert.true(names.includes('HelloWorld'), 'HelloWorld component name is preserved in the render tree (found: ' + names.join(', ') + ')');
});
});
From 87d55e6e91132ed7f319f9aacf73f24d5d0ef790 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Wed, 18 Mar 2026 10:42:15 -0400
Subject: [PATCH 06/10] test: add failing tests for dynamic component names in
debugRenderTree
Add tests for:
- invocations
- <@argComponent> invocations
Both currently produce '(unknown template-only component)' instead of
the expected component name because dynamic resolution at runtime
loses the invocation-site name information.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../test/debug-render-tree-test.ts | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
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 794877eb5cc..7692b57b8a2 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
@@ -142,6 +142,56 @@ class DebugRenderTreeTest extends RenderTest {
]);
}
+ @test 'dynamic component via '() {
+ const HelloWorld = defComponent('{{@arg}}');
+
+ class Root extends GlimmerishComponent {
+ HelloWorld = HelloWorld;
+ }
+
+ const RootDef = defComponent(``, {
+ component: Root,
+ emit: { moduleName: 'root.hbs' },
+ });
+
+ this.renderComponent(RootDef);
+
+ const rootChildren = this.delegate.getCapturedRenderTree()[0]?.children ?? [];
+ const componentNode = rootChildren.find(
+ (n: CapturedRenderNode) => n.type === 'component' && n.name !== '{ROOT}'
+ );
+
+ this.assert.ok(componentNode, 'found a component child node');
+
+ this.assert.strictEqual(
+ componentNode?.name,
+ 'HelloWorld',
+ `dynamic component name (got "${componentNode?.name}")`
+ );
+ }
+
+ @test 'dynamic component via <@argComponent>'() {
+ const HelloWorld = defComponent('{{@arg}}');
+ const Root = defComponent(`<@Greeting @arg="first"/>`, {
+ emit: { moduleName: 'root.hbs' },
+ });
+
+ this.renderComponent(Root, { Greeting: HelloWorld });
+
+ const rootChildren = this.delegate.getCapturedRenderTree()[0]?.children ?? [];
+ const componentNode = rootChildren.find(
+ (n: CapturedRenderNode) => n.type === 'component' && n.name !== '{ROOT}'
+ );
+
+ this.assert.ok(componentNode, 'found a component child node');
+
+ this.assert.strictEqual(
+ componentNode?.name,
+ 'HelloWorld',
+ `dynamic <@X> component name (got "${componentNode?.name}")`
+ );
+ }
+
@test 'strict-mode modifiers'() {
const state = trackedObj({ showSecond: false });
From f4dc41a637cdfa126f4527272d92b81069e03160 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Wed, 18 Mar 2026 10:51:24 -0400
Subject: [PATCH 07/10] feat: propagate invocation-site names for dynamic
components in debugRenderTree
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
For dynamic component invocations like `` and `<@Greeting>`,
the debug render tree now shows the invocation-site name instead of
'(unknown template-only component)'.
This works by extracting the Reference's debugLabel in
VM_RESOLVE_DYNAMIC_COMPONENT_OP and VM_RESOLVE_CURRIED_COMPONENT_OP,
and setting it as the ComponentDefinition's debugName when no name
is already present.
- `` → name: "Foo"
- `<@Greeting>` → name: "Greeting"
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../test/debug-render-tree-test.ts | 3 ++-
.../runtime/lib/compiled/opcodes/component.ts | 23 ++++++++++++++++++-
2 files changed, 24 insertions(+), 2 deletions(-)
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 7692b57b8a2..6e3b30001a0 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
@@ -185,9 +185,10 @@ class DebugRenderTreeTest extends RenderTest {
this.assert.ok(componentNode, 'found a component child node');
+ // For <@Greeting>, the invocation-site name "Greeting" is used
this.assert.strictEqual(
componentNode?.name,
- 'HelloWorld',
+ 'Greeting',
`dynamic <@X> component name (got "${componentNode?.name}")`
);
}
diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
index a8091130ec7..5fc922269cf 100644
--- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
+++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
@@ -154,8 +154,9 @@ APPEND_OPCODES.add(VM_PUSH_COMPONENT_DEFINITION_OP, (vm, { op1: handle }) => {
APPEND_OPCODES.add(VM_RESOLVE_DYNAMIC_COMPONENT_OP, (vm, { op1: _isStrict }) => {
let stack = vm.stack;
+ let ref = check(stack.pop(), CheckReference);
let component = check(
- valueForRef(check(stack.pop(), CheckReference)),
+ valueForRef(ref),
CheckOr(CheckString, CheckCurriedComponentDefinition)
);
let constants = vm.constants;
@@ -182,6 +183,14 @@ APPEND_OPCODES.add(VM_RESOLVE_DYNAMIC_COMPONENT_OP, (vm, { op1: _isStrict }) =>
definition = constants.component(component, owner);
}
+ if (DEBUG && !isCurriedValue(definition) && !definition.resolvedName && !definition.debugName) {
+ let debugLabel = ref.debugLabel;
+ if (debugLabel) {
+ // Extract the last segment of the path (e.g. "this.Foo" → "Foo", "Foo" → "Foo")
+ definition.debugName = debugLabel.split('.').pop();
+ }
+ }
+
stack.push(definition);
});
@@ -217,6 +226,18 @@ APPEND_OPCODES.add(VM_RESOLVE_CURRIED_COMPONENT_OP, (vm) => {
}
}
+ if (DEBUG && definition && !isCurriedValue(definition) && !definition.resolvedName && !definition.debugName) {
+ let debugLabel = ref.debugLabel;
+ if (debugLabel) {
+ // Extract the component name from the arg path (e.g. "@Greeting" → "Greeting")
+ let name = debugLabel.split('.').pop()!;
+ if (name.startsWith('@')) {
+ name = name.slice(1);
+ }
+ definition.debugName = name;
+ }
+ }
+
stack.push(definition);
});
From df2068a81f2b8ee9fc3b4bd7a3d4f152a6f947cb Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Wed, 18 Mar 2026 10:56:21 -0400
Subject: [PATCH 08/10] fix: use full invocation path for dynamic component
debugRenderTree names
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Use the full debugLabel as the component name so users see exactly
how the component was invoked:
- `` → name: "this.Foo"
- `<@Greeting>` → name: "@Greeting"
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../integration-tests/test/debug-render-tree-test.ts | 5 ++---
.../@glimmer/runtime/lib/compiled/opcodes/component.ts | 10 ++--------
2 files changed, 4 insertions(+), 11 deletions(-)
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 6e3b30001a0..df4e06003a1 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
@@ -165,7 +165,7 @@ class DebugRenderTreeTest extends RenderTest {
this.assert.strictEqual(
componentNode?.name,
- 'HelloWorld',
+ 'this.HelloWorld',
`dynamic component name (got "${componentNode?.name}")`
);
}
@@ -185,10 +185,9 @@ class DebugRenderTreeTest extends RenderTest {
this.assert.ok(componentNode, 'found a component child node');
- // For <@Greeting>, the invocation-site name "Greeting" is used
this.assert.strictEqual(
componentNode?.name,
- 'Greeting',
+ '@Greeting',
`dynamic <@X> component name (got "${componentNode?.name}")`
);
}
diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
index 5fc922269cf..28a38c3a1de 100644
--- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
+++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
@@ -186,8 +186,7 @@ APPEND_OPCODES.add(VM_RESOLVE_DYNAMIC_COMPONENT_OP, (vm, { op1: _isStrict }) =>
if (DEBUG && !isCurriedValue(definition) && !definition.resolvedName && !definition.debugName) {
let debugLabel = ref.debugLabel;
if (debugLabel) {
- // Extract the last segment of the path (e.g. "this.Foo" → "Foo", "Foo" → "Foo")
- definition.debugName = debugLabel.split('.').pop();
+ definition.debugName = debugLabel;
}
}
@@ -229,12 +228,7 @@ APPEND_OPCODES.add(VM_RESOLVE_CURRIED_COMPONENT_OP, (vm) => {
if (DEBUG && definition && !isCurriedValue(definition) && !definition.resolvedName && !definition.debugName) {
let debugLabel = ref.debugLabel;
if (debugLabel) {
- // Extract the component name from the arg path (e.g. "@Greeting" → "Greeting")
- let name = debugLabel.split('.').pop()!;
- if (name.startsWith('@')) {
- name = name.slice(1);
- }
- definition.debugName = name;
+ definition.debugName = debugLabel;
}
}
From 5d8a4215650225dc1dce5e4214b3e6a0c5777733 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:38:18 -0400
Subject: [PATCH 09/10] style: fix prettier formatting in component opcodes
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../runtime/lib/compiled/opcodes/component.ts | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
index 28a38c3a1de..55baed8453a 100644
--- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
+++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
@@ -155,10 +155,7 @@ APPEND_OPCODES.add(VM_PUSH_COMPONENT_DEFINITION_OP, (vm, { op1: handle }) => {
APPEND_OPCODES.add(VM_RESOLVE_DYNAMIC_COMPONENT_OP, (vm, { op1: _isStrict }) => {
let stack = vm.stack;
let ref = check(stack.pop(), CheckReference);
- let component = check(
- valueForRef(ref),
- CheckOr(CheckString, CheckCurriedComponentDefinition)
- );
+ let component = check(valueForRef(ref), CheckOr(CheckString, CheckCurriedComponentDefinition));
let constants = vm.constants;
let owner = vm.getOwner();
let isStrict = constants.getValue(_isStrict);
@@ -225,7 +222,13 @@ APPEND_OPCODES.add(VM_RESOLVE_CURRIED_COMPONENT_OP, (vm) => {
}
}
- if (DEBUG && definition && !isCurriedValue(definition) && !definition.resolvedName && !definition.debugName) {
+ if (
+ DEBUG &&
+ definition &&
+ !isCurriedValue(definition) &&
+ !definition.resolvedName &&
+ !definition.debugName
+ ) {
let debugLabel = ref.debugLabel;
if (debugLabel) {
definition.debugName = debugLabel;
From 88bb0d137d4ee2b774ca73a992949d0362dab1f2 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:58:34 -0400
Subject: [PATCH 10/10] fix: skip dynamic component name tests in production
builds
The invocation-site name propagation relies on ref.debugLabel which
is only available in DEBUG mode. Skip these tests in production builds.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../integration-tests/test/debug-render-tree-test.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
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 df4e06003a1..81a594e7d49 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
@@ -11,6 +11,7 @@ import type {
import type { TemplateOnlyComponent } from '@glimmer/runtime';
import type { EmberishCurlyComponent } from '@glimmer-workspace/integration-tests';
import { expect } from '@glimmer/debug-util';
+import { DEBUG } from '@glimmer/env';
import { modifierCapabilities, setComponentTemplate, setModifierManager } from '@glimmer/manager';
import { EMPTY_ARGS, templateOnlyComponent, TemplateOnlyComponentManager } from '@glimmer/runtime';
import { assign } from '@glimmer/util';
@@ -142,7 +143,7 @@ class DebugRenderTreeTest extends RenderTest {
]);
}
- @test 'dynamic component via '() {
+ @test({ skip: !DEBUG }) 'dynamic component via '() {
const HelloWorld = defComponent('{{@arg}}');
class Root extends GlimmerishComponent {
@@ -170,7 +171,7 @@ class DebugRenderTreeTest extends RenderTest {
);
}
- @test 'dynamic component via <@argComponent>'() {
+ @test({ skip: !DEBUG }) 'dynamic component via <@argComponent>'() {
const HelloWorld = defComponent('{{@arg}}');
const Root = defComponent(`<@Greeting @arg="first"/>`, {
emit: { moduleName: 'root.hbs' },