What version of Ember.js is running?
ember-source@6.12.0 (current LTS, also reproduces on main)
What environment?
Plain Node 22 / Bun 1.3 (no FastBoot, no jsdom, no DOM globals patched onto globalThis).
Steps to reproduce
import { renderComponent } from 'ember-source/dist/packages/@ember/renderer/index.js';
import Document from 'ember-source/dist/packages/@simple-dom/document/index.js';
import Component from '@glimmer/component';
import { template } from 'ember-source/dist/packages/@ember/template-compiler/index.js';
class Hello extends Component {
static {
template('<h1>hi</h1>', { component: this, eval() { return eval(arguments[0]); } });
}
}
const doc = new Document();
const root = doc.createElement('div');
renderComponent(Hello, {
owner: {},
env: { document: doc, hasDOM: true, isInteractive: false },
into: root,
args: {},
});
Expected behavior
Renders the component into the simple-dom node and returns a RenderResult. Anyone serializing root afterwards (via @simple-dom/serializer or similar) gets back <h1>hi</h1>.
Actual behavior
ReferenceError: Element is not defined
at renderComponent (packages/@ember/-internals/glimmer/lib/renderer.ts:674)
renderComponent reads globalThis.Element via into instanceof Element. Browsers and FastBoot have Element defined; a plain Node/Bun host that's been handed a simple-dom Document via env.document does not.
Why this is worth fixing
renderComponent's public contract (introduced in 6.8) accepts IntoTarget = Cursor | Element | SimpleElement. The body even has an intoTarget() helper that already discriminates the union without touching any global:
function intoTarget(into: IntoTarget): Cursor {
if ('element' in into) {
return into;
} else {
return { element: into as SimpleElement, nextSibling: null };
}
}
…but two other call sites fall back to into instanceof Element, which crashes when Element isn't a global. Aligning the two outliers with the existing helper takes the renderer from "browser/FastBoot only" to "any host that supplies an IntoTarget-shaped object".
This matters as the codebase moves further away from the FastBoot environment-setup story (see #21349 for the FastBoot-codepath removal direction). Anyone wanting to call renderComponent from a Bun server, Cloudflare Worker, Deno deployment, or a plain Node script with simple-dom currently hits this — even though every other piece of the public renderComponent API is host-agnostic.
Proposed fix
PR linked below. Two-hunk diff:
- The first-render
innerHTML clearing branch becomes a structural check — !('element' in into) && 'innerHTML' in into — fires on a real DOM Element, skips SimpleElement (no innerHTML setter) and Cursor.
- The re-render
parentElement extraction calls the existing intoTarget() helper instead of duplicating the discriminator. (Drive-by fix: the previous else-branch incorrectly tried (simpleElement as Cursor).element on a SimpleElement input — that path was already buggy for the rare SimpleElement-passed-on-rerender case.)
Net: no dependency on globalThis.Element, no behavior change in the browser, and the SimpleElement path now works correctly on rerender too.
Includes a regression test that calls renderComponent with globalThis.Element set to undefined.
PR: #21364
Notes
Happy to iterate on review feedback. The diff is intentionally minimal — same shape as the existing intoTarget() helper, no new exports, no behavior change for in-browser callers.
What version of Ember.js is running?
ember-source@6.12.0(current LTS, also reproduces onmain)What environment?
Plain Node 22 / Bun 1.3 (no FastBoot, no jsdom, no DOM globals patched onto
globalThis).Steps to reproduce
Expected behavior
Renders the component into the simple-dom node and returns a
RenderResult. Anyone serializingrootafterwards (via@simple-dom/serializeror similar) gets back<h1>hi</h1>.Actual behavior
renderComponentreadsglobalThis.Elementviainto instanceof Element. Browsers and FastBoot haveElementdefined; a plain Node/Bun host that's been handed a simple-dom Document viaenv.documentdoes not.Why this is worth fixing
renderComponent's public contract (introduced in 6.8) acceptsIntoTarget = Cursor | Element | SimpleElement. The body even has anintoTarget()helper that already discriminates the union without touching any global:…but two other call sites fall back to
into instanceof Element, which crashes whenElementisn't a global. Aligning the two outliers with the existing helper takes the renderer from "browser/FastBoot only" to "any host that supplies anIntoTarget-shaped object".This matters as the codebase moves further away from the FastBoot environment-setup story (see #21349 for the FastBoot-codepath removal direction). Anyone wanting to call
renderComponentfrom a Bun server, Cloudflare Worker, Deno deployment, or a plain Node script with simple-dom currently hits this — even though every other piece of the publicrenderComponentAPI is host-agnostic.Proposed fix
PR linked below. Two-hunk diff:
innerHTMLclearing branch becomes a structural check —!('element' in into) && 'innerHTML' in into— fires on a real DOMElement, skipsSimpleElement(noinnerHTMLsetter) andCursor.parentElementextraction calls the existingintoTarget()helper instead of duplicating the discriminator. (Drive-by fix: the previous else-branch incorrectly tried(simpleElement as Cursor).elementon aSimpleElementinput — that path was already buggy for the rare SimpleElement-passed-on-rerender case.)Net: no dependency on
globalThis.Element, no behavior change in the browser, and the SimpleElement path now works correctly on rerender too.Includes a regression test that calls
renderComponentwithglobalThis.Elementset toundefined.PR: #21364
Notes
Happy to iterate on review feedback. The diff is intentionally minimal — same shape as the existing
intoTarget()helper, no new exports, no behavior change for in-browser callers.