Repository navigation
WEBDEV-9066: Guard custom element registration against duplicate defines - #106
Merged
Merged
Conversation
Each element subpath we serve to non-bundler consumers is its own self-contained bundle, so a component several elements share gets built into every entry point that imports it. Two of those subpaths on one page means two copies of the shared component, and the second customElements.define() for the same tag throws. It throws while the module is still evaluating, so it takes the rest of that bundle down with it and the element the page actually wanted never registers. ia-status-indicator is the worst of these: ia-button, ia-dropdown-search-bar, ia-otp-form, ia-radio-player and its own subpath all carry a copy, so any two of those collide. There are 12 tags in that position. This swaps Lit's @CustomElement for our own, which skips the define when the tag is already claimed. First copy wins, which is fine, the copies come from the same source and nothing narrows a shared element by class identity. An eslint rule keeps the Lit one from creeping back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PoknqYwGpzkxFE3kfcYW81
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #106 +/- ##
=======================================
Coverage 85.79% 85.80%
=======================================
Files 74 75 +1
Lines 2619 2627 +8
Branches 577 579 +2
=======================================
+ Hits 2247 2254 +7
Misses 195 195
- Partials 177 178 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
latonv
approved these changes
Sep 16, 2026
latonv
left a comment
Member
There was a problem hiding this comment.
LGTM, just had one thought about possible logging when we skip a define.
Skipping the define quietly is fine when two subpaths share a component, but it's also how a page ends up on an element it didn't mean to use: load two versions of the package, or let the host page claim one of these tag names, and whatever registered first is what you get. That used to throw, so it was at least loud. Now it needs saying out loud. Nothing here can tell a benign duplicate from a version mismatch, since two builds of the same element differ in nothing observable at this point, so the message names the tag and lays out both readings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged
jbuckner
added a commit
that referenced
this pull request
Sep 24, 2026
…onation-thermometer * origin/main: 1.1.1 (#118) WEBDEV-9132: Fix Safari image-viewer wide-layout breakpoint (#115) 1.1.0 (#116) WEBDEV-9129: Exclude story files from coverage reporting (#113) WEBDEV-9121: Migrate histogram-date-range into elements (#112) WEBDEV-9130: Exclude nested dist and worktree tests from local runs (#114) v1.0.0 (#111) WEBDEV-9025: Add a CLAUDE.md covering the publish flow (#94) WEBDEV-9063: Typecheck in CI (#107) v0.4.1 (#108) WEBDEV-9066: Guard custom element registration against duplicate defines (#106)
jbuckner
added a commit
that referenced
this pull request
Sep 24, 2026
…onation-form-models * origin/main: 1.1.1 (#118) WEBDEV-9132: Fix Safari image-viewer wide-layout breakpoint (#115) 1.1.0 (#116) WEBDEV-9129: Exclude story files from coverage reporting (#113) WEBDEV-9121: Migrate histogram-date-range into elements (#112) WEBDEV-9130: Exclude nested dist and worktree tests from local runs (#114) v1.0.0 (#111) WEBDEV-9025: Add a CLAUDE.md covering the publish flow (#94) WEBDEV-9063: Typecheck in CI (#107) v0.4.1 (#108) WEBDEV-9066: Guard custom element registration against duplicate defines (#106) v0.4.0 (#99) WEBDEV-9022: Migrate the image viewer into elements (#93)
jbuckner
added a commit
that referenced
this pull request
Sep 24, 2026
…opnav * origin/main: 1.1.1 (#118) WEBDEV-9132: Fix Safari image-viewer wide-layout breakpoint (#115) 1.1.0 (#116) WEBDEV-9129: Exclude story files from coverage reporting (#113) WEBDEV-9121: Migrate histogram-date-range into elements (#112) WEBDEV-9130: Exclude nested dist and worktree tests from local runs (#114) v1.0.0 (#111) WEBDEV-9025: Add a CLAUDE.md covering the publish flow (#94) WEBDEV-9063: Typecheck in CI (#107) v0.4.1 (#108) WEBDEV-9066: Guard custom element registration against duplicate defines (#106) v0.4.0 (#99) WEBDEV-9022: Migrate the image viewer into elements (#93)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Importing two element subpaths on the same page throws and one of them never registers. Repros on the published 0.4.0 in headless Chromium:
Each subpath esm.archive.org serves is its own self-contained bundle, so a shared component is built into every entry point that imports it. Two subpaths means two copies and two
customElements.define()calls for the same tag. The second throws mid-evaluation, which aborts the rest of that bundle, so it's partway through: some tags from the failed bundle register and the rest are lost. Bundler consumers like offshoot never see it because Vite dedupes to one instance.12 tags are defined by more than one entry point.
ia-status-indicatoris inia-button,ia-dropdown-search-bar,ia-otp-form,ia-radio-playerand its own subpath, so any two of those collide.So this adds
src/util/custom-element.ts, a@customElementthat skips the define when the tag is already claimed, and points every call site at it. First copy wins. That's safe here: nothing doesinstanceofon a shared element class and the only class-level imports of one areimport type, so the copies are interchangeable. There's an eslint rule to keep Lit's version from coming back.The demo and story files are swapped too. They aren't published so they can't hit this, but leaving them on the Lit decorator just invites someone to copy the wrong import into a new element.
What I didn't do
There's a second, structural fix: a single-URL entry point would give non-bundler consumers one module instance and no duplicates at all. That's a bigger change and it argues with #81, which drops the package root. Skipping it for now since petabox usage is getting deprecated anyway. The guard is worth having on its own for any non-bundler consumer.
Verifying
Unit tests cover the guard, including that registration keeps working after a duplicate is skipped, which is the part that actually breaks.
The real failure mode needs two separately-built bundles, which no unit test can produce, so I checked that by hand: esbuild
dist/.../ia-dropdown-search-baranddist/.../ia-radio-playerinto two self-contained bundles and load both on one page.ia-transcript-viewandia-waveform-progressare lost with it.694 tests pass, lint 0 errors, prettier clean.
https://webarchive.jira.com/browse/WEBDEV-9066
🤖 Generated with Claude Code
https://claude.ai/code/session_01PoknqYwGpzkxFE3kfcYW81