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..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 @@ -186,6 +186,48 @@ 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; + } + } } );