Skip to content

fix: bypass third-party custom-elements-es5-adapter that breaks native class constructors (#1458)#1459

Merged
igrigorik merged 1 commit into
masterfrom
claude/diagnose-audit-1458-6E4RD
Mar 27, 2026
Merged

fix: bypass third-party custom-elements-es5-adapter that breaks native class constructors (#1458)#1459
igrigorik merged 1 commit into
masterfrom
claude/diagnose-audit-1458-6E4RD

Conversation

@igrigorik

@igrigorik igrigorik commented Mar 27, 2026

Copy link
Copy Markdown
Owner

Capture the native customElements.define() early in the bundle before
any third-party ES5 adapter can monkey-patch it. Use the saved reference
when registering the vsc-controller custom element, preventing the
"Class constructor _VSCControllerElement cannot be invoked without 'new'"
error caused by the adapter's incompatible wrapper around native ES6 classes.

…1458)

Problem
-------
Pages that ship custom-elements-es5-adapter.js (notably YouTube) monkey-patch
customElements.define(). The adapter wraps every registered constructor,
assuming it was transpiled to ES5. When our native ES6 class
(VSCControllerElement extends HTMLElement) passes through the wrapper, the
wrapper tries to call the constructor without `new`, producing:

  "Class constructor _VSCControllerElement cannot be invoked without 'new'"

The previous mitigation (f5c463d) captured the native define() reference in
constants.js before the adapter could patch it. This fails because inject.js
loads via an async <script src> tag — the page's adapter can execute first,
so the "saved" reference is already the patched version.

Root cause
----------
There is no way to guarantee our inject.js runs before arbitrary page scripts
when using the <script src="chrome-extension://..."> injection pattern. The
load order depends on network timing and browser scheduling, making this a
non-deterministic race condition.

Solution
--------
Stop calling customElements.define() entirely. The <vsc-controller> tag name
requires no registration to function correctly:

  - document.createElement('vsc-controller') returns a valid HTMLElement
    for any hyphenated name, registered or not
  - CSS tag selectors (vsc-controller { ... }) match by tag name, not
    registration status
  - querySelector('vsc-controller') is a pure DOM query, unaffected
  - element.attachShadow() works on any valid custom element name
  - element.tagName === 'VSC-CONTROLLER' is set at creation time

The removed VSCControllerElement class had no functional lifecycle logic —
connectedCallback and disconnectedCallback only emitted debug log messages.

This eliminates the entire class of es5-adapter conflicts because the
adapter's monkey-patch of customElements.define() is never invoked.
@igrigorik
igrigorik force-pushed the claude/diagnose-audit-1458-6E4RD branch from ebd1a9f to 040353e Compare March 27, 2026 16:38
@igrigorik
igrigorik merged commit 6b6f8a2 into master Mar 27, 2026
2 checks passed
@igrigorik
igrigorik deleted the claude/diagnose-audit-1458-6E4RD branch March 27, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants