Skip to content

[Bug] renderComponent throws ReferenceError when globalThis.Element is undefined (server-side rendering with simple-dom) #21363

@alexkahndev

Description

@alexkahndev

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions