From 63daf6e314b2cdce454381cad5d7e5d97481fe62 Mon Sep 17 00:00:00 2001 From: ayo Date: Fri, 24 Jul 2026 05:51:27 +0200 Subject: [PATCH] refactor: remove dead code --- docs/src/content/docs/guides/library-size.md | 2 +- size-change-log.md | 1 + src/WebComponent.js | 19 +++++++------------ src/utils/create-element.mjs | 7 +------ 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/docs/src/content/docs/guides/library-size.md b/docs/src/content/docs/guides/library-size.md index bc92557..d2f6005 100644 --- a/docs/src/content/docs/guides/library-size.md +++ b/docs/src/content/docs/guides/library-size.md @@ -5,6 +5,6 @@ slug: library-size All the functions and the base class in the library are minimalist by design and only contains what is needed for their purpose. -The `WebComponent` base class is **2.01 kB** (min + brotli) according to [size-limit](http://github.com/ai/size-limit). +The `WebComponent` base class is **1.98 kB** (min + brotli) according to [size-limit](http://github.com/ai/size-limit). Every change that moves this number is recorded in [`size-change-log.md`](https://github.com/ayo-run/wcb/blob/main/size-change-log.md), with the reason the bytes were spent and the benefit they bought. diff --git a/size-change-log.md b/size-change-log.md index 66db927..ff75f9a 100644 --- a/size-change-log.md +++ b/size-change-log.md @@ -14,3 +14,4 @@ A running record of how each correctness/feature change affects the `WebComponen | Reflect boolean props as bare attributes (WCB-REQ-07, **breaking**) | 1.93 kB (+0.12 kB) | 1.85 → 1.95 kB | **`toggleAttribute()` and `[attr]` CSS selectors work again.** Booleans reflected as the strings `flag="true"`/`flag="false"`, so the attribute always existed: `el.toggleAttribute('flag', true)` was a silent no-op (it only adds/removes, never rewrites a value — no `attributeChangedCallback`, no re-render), and presence selectors like `:host([flag])` matched the stamped `flag="false"` too, painting "on" styles onto elements whose prop was `false`. Boolean props now follow HTML in both directions: reflecting `true` sets the **bare** attribute, `false` **removes** it, and attribute removal means `false` rather than the declared default. Reads are strict — **any present value is `true`**, including the literal `flag="false"`, exactly like native `disabled="false"`. **Breaking:** `setAttribute(name, String(bool))` now always means `true`; migrate those call sites to `toggleAttribute(name, bool)`. Two dev warnings carry most of the cost and make the migration safe: one when a boolean attribute is written as a literal `"true"`/`"false"` string (the silent inversion), and one once-per-class when a boolean prop declares a `true` default (HTML has no true-default boolean attribute — absence must mean both "false" and "default"). `applyProp` also stops stamping `"false"` for boolean vnode props with no matching DOM property, which a nested `WebComponent` would otherwise read back as `true`. Limit raised to fit the warnings. | | Overrideable `toAttribute` / `fromAttribute` converters (#56) | 1.96 kB (+0.03 kB) | 1.95 → 2 kB | **An escape hatch for the edge cases strict boolean semantics create.** The reflection and parsing rules were hardcoded, so a prop needing custom serialization (a `Date`, a delimited list, an enumerated `"true"`/`"false"` attribute) had no seam to hook into. Two overrideable methods now own both directions — `toAttribute(name, value)` returns the attribute value, where **`null` removes the attribute**, and `fromAttribute(name, value)` parses a present attribute back into a prop value. Both take the camelCase prop key, matching `static props` and `onChanges`'s `property`. The existing behavior became the default implementations (pure code movement — boolean presence/absence is now simply `toAttribute` returning `''` or `null`), so subclasses customize one prop and delegate the rest to `super`. Cheaper than a per-prop converter map, which would cost a lookup on every prop even for the vast majority of components that never need one. Limit raised for the two non-manglable public method names. | | Don't reconcile a nested component's own light DOM | 2.01 kB (+0.05 kB) | 2 → 2.05 kB | **Nested components stop erasing each other.** The in-place reconciler recursed into every same-tag element it reused, including nested custom elements. A parent's vnode never describes what a nested light-DOM component rendered *for itself*, so the trailing-node trim in `patchChildren` deleted that content on every ancestor re-render — and since the child only re-renders when its own props change, it stayed blank. Any `WebComponent` that renders another `WebComponent` (without shadow DOM) lost the inner subtree the first time the outer one re-rendered, even for an unrelated prop. `patchNode` now treats a custom element that has no open shadow root as opaque: it still patches the element's props (that is how data flows down) but leaves the children to the child. Shadow-DOM components are exempt — their own content lives in the shadow root, invisible to the parent, so any *light* children are genuine parent-authored slot content that must still be reconciled. Limit raised 50 B to fit the one-line guard. | +| Remove unreachable branches from `render()` and `createElement` | 1.98 kB (−0.03 kB) | 2.05 kB | **Dead-code cleanup, no behavior change.** Three guards could never take their other branch: `render()`'s first-render path tested `Array.isArray(el)` and truthiness on the `createElement` return, but `createElement` only ever returns a `DocumentFragment`, a text node or an `Element` — a multi-root template already arrives as a fragment, which `replaceChildren` splices in, so one unguarded call covers both shapes; `#initializeProps` guarded the proxy creation with `if (!this.#props)` although it runs only from the constructor, where the private field is always `undefined`; and `createElement` filtered its own recursive return through `instanceof Node`, which always holds. V8 coverage had flagged all three (`WebComponent.js` lines 307, 376–377 and `create-element.mjs` line 29); with them gone both files report 100 % statement coverage and `create-element.mjs` reaches 100 % branch coverage. Limit left at 2.05 kB. | diff --git a/src/WebComponent.js b/src/WebComponent.js index 08c8347..5a2ead5 100644 --- a/src/WebComponent.js +++ b/src/WebComponent.js @@ -304,12 +304,10 @@ export class WebComponent extends HTMLElement { Object.keys(initialProps).forEach((camelCase) => { this.#typeMap[camelCase] = typeof initialProps[camelCase] }) - if (!this.#props) { - this.#props = new Proxy( - initialProps, - this.#handler((key, value) => this.#reflect(key, value), this) - ) - } + this.#props = new Proxy( + initialProps, + this.#handler((key, value) => this.#reflect(key, value), this) + ) } /** @@ -371,12 +369,9 @@ export class WebComponent extends HTMLElement { * - resolve prop values * - attach event listeners */ - const el = createElement(template) - - if (el) { - if (Array.isArray(el)) this.#host.replaceChildren(...el) - else this.#host.replaceChildren(el) - } + // a multi-root template comes back as a DocumentFragment, which + // replaceChildren splices in — so one call covers both shapes + this.#host.replaceChildren(createElement(template)) } else { // re-render: reconcile in place so focus, caret/selection, an // uncommitted value, :hover and running transitions survive diff --git a/src/utils/create-element.mjs b/src/utils/create-element.mjs index 1188241..d8aaabf 100644 --- a/src/utils/create-element.mjs +++ b/src/utils/create-element.mjs @@ -24,12 +24,7 @@ export function createElement(tree) { /** * handle children */ - tree.children?.forEach((child) => { - const childEl = createElement(child) - if (childEl instanceof Node) { - el.appendChild(childEl) - } - }) + tree.children?.forEach((child) => el.appendChild(createElement(child))) return el } }