Skip to content

Fix duplicate rendering and global leakage in the CDN bundle (21.2.0) - #371

Merged
paolo-arge-lf merged 20 commits into
21.xfrom
paolo/no-framework
Oct 1, 2026
Merged

paolo-arge-lf merged 20 commits into
21.xfrom
paolo/no-framework

Conversation

@paolo-arge-lf

Copy link
Copy Markdown
Collaborator

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-container showed two template pickers and two "Add/remove fields" blocks; lf-repository-browser held 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.createComponent or MatDialog.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.js is 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, including Rx, Nx and $. On a page sharing window with 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 like Rx is not a function.

Fix: renameLfCdn wraps the file in a function scope. Measured leakage is now zero.

Nothing here is breaking

  • Registered tag names are unchanged — main.ts still registers all 35, so element hosts see no difference.
  • Public Angular selectors are retained. This was the reason for the dual selector rather than a plain rename: those selectors are public API, used by the docs app itself. The published types confirm both are still advertised:
    LfFieldTemplateContainerComponent, "lfint-field-template-container, lf-field-template-container"
    LfBreadcrumbsComponent,            "lfint-breadcrumbs, lf-breadcrumbs"
    LfSelectionListComponent,          "lfint-selection-list-component, lf-selection-list-component"
    
  • Element properties, inputs and outputs are untouched.

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 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, 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 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 table. The previous demo used lf-tags only — 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

Check Result
Unit tests 426 passed, 75 files
Library / types / CDN / docs builds green
format:check, lint clean
verify-cdn-demo 6 checks × 2 themes, all pass
Leaked globals, fresh build 2,130 → 0

Reviewer notes

  • Four specs needed updating: three mock selectors and one querySelector('lf-field-multivalue-component') that became lfint-. That last one is itself confirmation the dynamic host element renamed as intended.
  • lfint was added to the @angular-eslint/component-selector prefix list.
  • The source-map handling in wrapInFunctionScope is deliberate — the opening brace stays on line 1 and sourceMappingURL moves past the closing brace, so the shipped 7.1 MB map stays line-aligned.
  • Downstream consumers wrapping the bundle themselves as a build step can drop that workaround once this ships.

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-container declares providers: [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

Paolo Argentieri and others added 4 commits September 21, 2026 17:58
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>
Paolo Argentieri and others added 5 commits September 22, 2026 10:16
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Coverage Report for Unit Test Coverage

Status Category Percentage Covered / Total
🔵 Lines 70.49% 3090 / 4383
🔵 Statements 70.67% 3183 / 4504
🔵 Functions 72.77% 711 / 977
🔵 Branches 56.67% 1468 / 2590
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
projects/ui-components/internal-shared/general-dialog-layout/general-dialog-layout.component.ts 100% 100% 100% 100%
projects/ui-components/internal-shared/lf-loader/lf-loader.component.ts 100% 100% 100% 100%
projects/ui-components/internal-shared/lf-modals/lf-popup-modal/lf-popup-modal.component.ts 70% 100% 40% 66.66% 25-35
projects/ui-components/internal-shared/lf-toast-message/lf-toast-message.component.ts 77.77% 70.83% 88.88% 75.75% 39, 68-72, 79-82, 87
projects/ui-components/lf-checklist/items/items.component.ts 61.29% 54.16% 60% 60.65% 73-85, 89-98, 116-124, 161
projects/ui-components/lf-checklist/options/options.component.ts 80% 100% 50% 75% 24
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/date-field/date-field.component.ts 93.75% 63.63% 100% 93.33% 88
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/date-time-field/date-time-field.component.ts 91.66% 64.7% 100% 91.3% 99-102
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/dynamic-field/dynamic-field.component.ts 85.71% 100% 66.66% 83.33% 37
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/lf-field-base/lf-field-base.component.ts 100% 100% 100% 100%
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/list-field/list-field.component.ts 55.55% 50% 50% 50% 27-31, 47-48
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/number-field/number-field.component.ts 79.76% 71.42% 75% 81.48% 58-69, 110, 114, 143-148, 155, 156, 164-165, 182, 212
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/text-field/text-field.component.ts 38.46% 28.57% 33.33% 36.84% 36, 45, 71-112
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-field-base/time-field/time-field.component.ts 61.29% 50% 75% 60% 85, 90, 102-117
projects/ui-components/lf-metadata/field-components/field-base-parts/lf-token-picker/lf-token-picker.component.ts 75% 0% 66.66% 71.42% 30-31
projects/ui-components/lf-metadata/field-components/lf-field/lf-field.component.ts 85.41% 80% 83.33% 85.1% 92, 94, 144-146, 156-157
projects/ui-components/lf-metadata/field-components/lf-field-group/lf-field-group.component.ts 76.33% 52.5% 78.12% 76.21% 74, 100-114, 143, 146, 148, 168, 198-205, 224, 233-249, 296
projects/ui-components/lf-metadata/field-components/lf-field-multivalue/lf-field-multivalue.component.ts 85.86% 76% 89.28% 86.36% 94, 96, 127-128, 132-133, 238-241, 257-258
projects/ui-components/lf-metadata/lf-date-time-picker/uni-date-time.component.ts 56.31% 45.9% 64.58% 56.47% 182-206, 217, 243-249, 290-299, 364, 368, 375-389, 395-396, 399-401, 404-406, 409-438, 443-459, 468-474, 524-532, 538-539, 631-651, 689-703, 711-720, 799-802, 834, 905-906, 910-911, 914-915, 918-919, 928, 931, 939-961, 966, 969, 976-997, 1002, 1005, 1016-1043, 1052-1062, 1074-1076, 1125-1130, 1134-1139
projects/ui-components/lf-metadata/lf-field-adhoc-container/lf-field-adhoc-container.component.ts 94.21% 75% 93.93% 94.11% 109-112, 297, 306-307
projects/ui-components/lf-metadata/lf-field-adhoc-container/lf-field-add-remove/lf-field-add-remove.component.ts 84.16% 60.34% 88.23% 83.89% 133-134, 119-121, 164-167, 176-177, 203-206, 216, 272, 279, 294
projects/ui-components/lf-metadata/lf-field-template-container/lf-field-template-container.component.ts 79.27% 69.29% 82.14% 79.39% 130-133, 139, 149-152, 100, 165, 205-206, 243, 255, 288-292, 328-352, 418-425, 443-444, 475-481, 541-547, 555, 579-580, 595-596, 628, 660
projects/ui-components/lf-selection-list/lf-selection-list.component.ts 81.52% 59.04% 76.36% 81.63% 82-86, 124-131, 143, 241, 253-257, 266, 275, 292, 305-306, 337-345, 361, 374-378, 384, 393, 421, 470-491, 501, 508, 512, 524, 557-558, 563
projects/ui-components/lf-user-feedback/feedback-image-upload/feedback-image-upload.component.ts 84.37% 48.64% 81.81% 84.21% 59-60, 75, 105, 140-156, 177-178, 188
projects/ui-components/lf-user-feedback/feedback-submission/feedback-submission.component.ts 100% 100% 100% 100%
projects/ui-components/lf-user-feedback/feedback-suggestion-selection/feedback-suggestion-selection.component.ts 75% 100% 33.33% 71.42% 37-41
projects/ui-components/lf-user-feedback/user-feedback-dialog/user-feedback-dialog.component.ts 77.19% 66.66% 76.19% 76.78% 86, 109, 132, 142-155, 164, 168
projects/ui-components/shared/lf-breadcrumbs/lf-breadcrumbs.component.ts 27.77% 0% 25% 26.66% 31-48
Generated in workflow #1850 for commit e7052ce by the Vitest Coverage Report Action

paolo-arge-lf and others added 7 commits September 29, 2026 17:10
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
jonathanliu-lf previously approved these changes Sep 30, 2026
Comment thread CHANGELOG.md Outdated
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>
Comment thread CHANGELOG.md
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>
@paolo-arge-lf paolo-arge-lf changed the title Fix duplicate rendering and global leakage in the CDN bundle (21.1.3) Fix duplicate rendering and global leakage in the CDN bundle (21.2.0) Oct 1, 2026
@paolo-arge-lf
paolo-arge-lf merged commit d5aa090 into 21.x Oct 1, 2026
7 checks passed
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.

3 participants