From 3e39c8252715fd7915884650dcce3bf96b014dc4 Mon Sep 17 00:00:00 2001 From: Alex Kahn Date: Sun, 3 May 2026 09:48:27 -0400 Subject: [PATCH 1/2] [BUGFIX] renderComponent: don't read globalThis.Element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `renderComponent` does `into instanceof Element` in two places to discriminate `Cursor` from `Element` / `SimpleElement`. That works in a browser (where `Element` is a built-in) and inside FastBoot (which patches a simulated `Element` onto `globalThis`), but throws `ReferenceError: Element is not defined` on a plain Node / Bun host that calls `renderComponent` with a fresh `@simple-dom/document` node as `into`. The function already has an `intoTarget()` helper a few lines up that classifies the same union without touching any global: function intoTarget(into: IntoTarget): Cursor { if ('element' in into) { return into; } else { return { element: into as SimpleElement, nextSibling: null }; } } This commit aligns the two `into instanceof Element` outliers with that helper: * The first-render `innerHTML` clearing branch becomes a structural check — `!('element' in into) && 'innerHTML' in into` — so it fires only on a real DOM `Element` and skips on `SimpleElement` (which has no `innerHTML` setter) and `Cursor`. * The re-render `parentElement` extraction calls `intoTarget(into)` directly instead of duplicating the discriminator. Drive-by fix: the previous `(into as Cursor).element` cast on the else branch was incorrect for `SimpleElement` input — `SimpleElement` has no `.element` property — though that path is rare in practice. Net effect: `renderComponent` works with any of the three `IntoTarget` shapes in any host environment, with no dependency on `globalThis.Element`. Adds a regression test that calls `renderComponent` with `globalThis.Element` set to `undefined` and asserts the render completes. --- .../@ember/-internals/glimmer/lib/renderer.ts | 21 ++++++-- .../components/render-component-test.ts | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index cfc74e0e423..1f56b911e19 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -670,9 +670,19 @@ export function renderComponent( * We can only replace the inner HTML the first time. * Because destruction is async, it won't be safe to * do this again, and we'll have to rely on the above destroy. + * + * Use a structural check instead of `into instanceof Element` so the + * renderer doesn't depend on the `Element` constructor being a + * global. Browsers always have it; Node / Bun servers running with a + * bare simple-dom Document do not, and would otherwise hit + * `ReferenceError: Element is not defined` here. The `'element' in + * into` test matches the existing `intoTarget()` helper above: + * `Cursor` has `element`; `Element` / `SimpleElement` do not. The + * `'innerHTML' in into` follow-up keeps the clearing scoped to the + * real-Element case (`SimpleElement` has no `innerHTML` setter). */ - if (!existing && into instanceof Element) { - into.innerHTML = ''; + if (!existing && !('element' in into) && 'innerHTML' in into) { + (into as Element).innerHTML = ''; } /** @@ -689,8 +699,11 @@ export function renderComponent( */ let renderTarget: IntoTarget = into; if (existing?.glimmerResult) { - let parentElement = - into instanceof Element ? (into as unknown as SimpleElement) : (into as Cursor).element; + // Reuse the same `intoTarget()` shape used by the lower-level + // `BaseRenderer#render` path so all code that needs a parent + // Element from an `IntoTarget` agrees on the discriminator. As a + // bonus, this avoids the `globalThis.Element` dependency above. + let parentElement = intoTarget(into).element; let firstNode = existing.glimmerResult.firstNode(); renderTarget = { element: parentElement, nextSibling: firstNode }; } diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts index 85319efad4c..201dcbe1a40 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts @@ -186,6 +186,56 @@ moduleFor( assertHTML(''); this.assertStableRerender(); } + + '@test renderComponent does not depend on a global Element constructor'( + assert: Assert + ) { + // Stash and unset `globalThis.Element` so the previous + // `into instanceof Element` check would crash with + // `ReferenceError: Element is not defined`. After the structural- + // check fix, `renderComponent` should still resolve the parent + // element and render without consulting any global. This matters + // for non-browser hosts (Node / Bun servers, edge workers) that + // don't ship a DOM Element constructor by default — only the + // simple-dom node types they were given via `env.document`. + let saved = (globalThis as { Element?: unknown }).Element; + (globalThis as { Element?: unknown }).Element = undefined; + + try { + let Foo = setComponentTemplate( + precompileTemplate('Hello, world!'), + templateOnly() + ); + + let owner = buildOwner({}); + let manualDestroy: () => void; + + run(() => { + let result = renderComponent(Foo, { + owner, + into: this.element, + }); + manualDestroy = result.destroy; + this.component = { + ...result, + rerender() { + // unused, but asserted against + }, + }; + }); + + assertHTML('Hello, world!'); + assert.ok( + true, + 'renderComponent ran to completion with `globalThis.Element` undefined' + ); + + run(() => manualDestroy()); + run(() => destroy(owner)); + } finally { + (globalThis as { Element?: unknown }).Element = saved; + } + } } ); From 248e3617353ee015a88e1f4000c9f0d6b29901ab Mon Sep 17 00:00:00 2001 From: Alex Kahn Date: Sun, 3 May 2026 10:25:09 -0400 Subject: [PATCH 2/2] lint: prettier --write on render-component-test.ts --- .../components/render-component-test.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts index 201dcbe1a40..a24fcd1cf9c 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts @@ -187,9 +187,7 @@ moduleFor( this.assertStableRerender(); } - '@test renderComponent does not depend on a global Element constructor'( - assert: Assert - ) { + '@test renderComponent does not depend on a global Element constructor'(assert: Assert) { // Stash and unset `globalThis.Element` so the previous // `into instanceof Element` check would crash with // `ReferenceError: Element is not defined`. After the structural- @@ -202,10 +200,7 @@ moduleFor( (globalThis as { Element?: unknown }).Element = undefined; try { - let Foo = setComponentTemplate( - precompileTemplate('Hello, world!'), - templateOnly() - ); + let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly()); let owner = buildOwner({}); let manualDestroy: () => void; @@ -225,10 +220,7 @@ moduleFor( }); assertHTML('Hello, world!'); - assert.ok( - true, - 'renderComponent ran to completion with `globalThis.Element` undefined' - ); + assert.ok(true, 'renderComponent ran to completion with `globalThis.Element` undefined'); run(() => manualDestroy()); run(() => destroy(owner));