Fix duplicate rendering and global leakage in the CDN bundle (21.2.0) - #371
Merged
Merged
Conversation
Two defects that only affect the custom-element build. The Angular
component build was never affected, which is why the docs site looked
correct and no demo caught either one.
Components rendered twice
-------------------------
Every component the library instantiated inside another component
rendered twice in the CDN build: once from the parent's Angular view,
once from the browser upgrading the same host element, because the
registered custom element tag was also the component's Angular selector.
lf-field-container showed two template pickers and two "Add/remove
fields" blocks; lf-repository-browser held a second, empty breadcrumbs
bar. An audit found 28 of the 35 registered tags affected - 24 reached
through a template and 4 through ViewContainerRef.createComponent or
MatDialog.open, which a template scan alone would miss.
Library components now declare an internal selector first
('lfint-x, lf-x'), so Angular builds <lfint-x> host elements while the
public selector stays available to Angular consumers and the registered
tag names are unchanged. Angular uses the first selector for
dynamically created hosts, so the four dynamic sites needed no change.
Bundle leaked its declarations as globals
-----------------------------------------
lf-ui-components.js is loaded as a classic script, so its 2,130
minified top-level declarations became globals - 1,852 of them two
characters or fewer, including Rx, Nx and $. On a page sharing window
with other bundles, anything that later assigned one of those names
replaced the library's own binding and the next component creation
failed with errors like "Rx is not a function". renameLfCdn now wraps
the file in a function scope; measured leakage is zero. The opening
brace stays on line 1 and the sourceMappingURL comment is moved past
the closing brace, so the shipped source map stays aligned.
Guarding both
-------------
- registered-components.spec.ts asserts no registered tag appears in a
library template, locks the set of tags that double as Angular host
tags, and renders the composite components to assert one view each.
It fails with "expected 2 to be 1" against the unfixed build.
- scripts/verify-cdn-demo.mjs loads the demo page against the built
bundle in Chromium, once per shipped theme, and fails on any count
other than one. Wired into CI ahead of the step that moves the bundle
out of dist/lf-cdn/browser.
- The framework-agnostic demo and guide are rebuilt around the
composite components, with service mocks, both documented lifecycle
orders, and an on-page self-check. The previous demo used lf-tags
only, the one component shape that cannot exhibit this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Analog Vite plugin defaults test.pool to vmThreads, and Vitest disables isolation for VM pools, so every spec file ran in one shared iframe. The custom elements registered by registered-components.spec.ts then upgraded <lf-user-feedback> in later documentation specs, replacing the Angular instance and failing the new hosting_context tests depending on file order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jonathanliu-lf
previously approved these changes
Sep 30, 2026
Start the entry with "Fixed" and drop the sentence about internal lfint- selectors, which describes the implementation; point to the Changed entry, which already covers the client-visible renamed inner elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21.1.3 was never released (only previews), so the entry is renamed rather than added. Updates the CHANGELOG heading, NPM_VERSION, and the note on when the old framework-agnostic guide URL went away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shiyuanzhao-lf
approved these changes
Oct 1, 2026
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.
Two defects in the CDN custom-element build, plus the tests that would have caught them. The Angular component build was never affected by either, which is why the docs site looked correct and no demo caught them.
1. Components rendered twice
Every component the library instantiated inside another component rendered twice in the CDN build: once from the parent's Angular view, once from the browser upgrading the same host element — because the registered custom element tag was also the component's Angular selector.
lf-field-containershowed two template pickers and two "Add/remove fields" blocks;lf-repository-browserheld a second, empty breadcrumbs bar that nobody reported because it renders invisibly.An audit of all 35 registered tags found 28 affected: 24 reached through a template, and 4 through
ViewContainerRef.createComponentorMatDialog.open. That second group matters — Angular builds host elements from the selector for dynamically created components too, so a template-only scan would have reported a false green.Fix: library components now declare an internal selector first,
selector: 'lfint-x, lf-x'. Angular builds<lfint-x>host elements, so no registered tag is ever a host Angular creates.2. The bundle leaked its declarations as globals
lf-ui-components.jsis loaded as a classic script, so its top-level declarations became globals. Measured on 21.1.2: 2,130 globals, 1,852 of them two characters or fewer, includingRx,Nxand$. On a page sharingwindowwith other bundles (SharePoint being the obvious case), anything that later assigned one of those names replaced the library's own binding and the next component creation failed with errors likeRx is not a function.Fix:
renameLfCdnwraps the file in a function scope. Measured leakage is now zero.Nothing here is breaking
main.tsstill registers all 35, so element hosts see no difference.Narrowing the registered surface to the 11 documented entry points is the natural follow-up, but it is breaking, so it's deliberately left for a major version.
Guards
projects/lf-cdn/src/app/registered-components.spec.ts— asserts no registered tag appears in a library template, locks the set of tags that double as Angular host tags, and renders the composite components asserting one view each. It fails withexpected 2 to be 1against the unfixed build.scripts/verify-cdn-demo.mjs— loads the demo page against the built bundle in Chromium, once per shipped theme, failing on any count other than 1. Component unit tests structurally cannot catch this class of defect: they never register a custom element. Wired into CI ahead of the step that moves the bundle out ofdist/lf-cdn/browser.lf-tagsonly — the one component shape that cannot exhibit this bug.I verified the gate actually fails: reverting a single template usage and rebuilding produced
FAIL template picker in lf-field-container: expected 1, got 2, exit code 1.Verification
format:check,lintverify-cdn-demoReviewer notes
querySelector('lf-field-multivalue-component')that becamelfint-. That last one is itself confirmation the dynamic host element renamed as intended.lfintwas added to the@angular-eslint/component-selectorprefix list.wrapInFunctionScopeis deliberate — the opening brace stays on line 1 andsourceMappingURLmoves past the closing brace, so the shipped 7.1 MB map stays line-aligned.Not included
Re-measuring
getAllFieldDefinitionsAsync(118.7s against a production repository) needs a real repository with ~3,100 field definitions, which I could not access. Worth noting for whoever picks it up:lf-field-adhoc-containerdeclaresproviders: [AdhocFieldConnectorService]at component level, so before this fix the duplicate instance held its own connector and fetched independently — some of that time may simply disappear. Re-measure against 21.1.3 before designing paging.🤖 Generated with Claude Code