refactor: remove dead code

This commit is contained in:
ayo 2026-07-24 05:51:27 +02:00 committed by GitHub
parent 9690fa8f68
commit 63daf6e314
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 10 additions and 19 deletions

View file

@ -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.

View file

@ -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, 376377 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. |

View file

@ -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 <input> value, :hover and running transitions survive

View file

@ -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
}
}