Release notes

Changelog

All notable changes to WildflowerJS. Per-entry deep links match the one-line summaries in the package CHANGELOG.md. The format follows Keep a Changelog; the project follows Semantic Versioning.

1.1.0 — 2026-05-12

Build & Toolchain

Vendored, npm-free build pipeline #

The framework now builds via a SHA-512-pinned rollup + terser toolchain fetched as 3 frozen tarballs (rollup, terser, acorn). npm run build runs zero npm install; postinstall scripts never execute. Framework users (pre-built bundles from npm/CDN) were already immune to npm supply-chain attacks; this closes the same exposure on the maintainer side. Output bundles byte-near-identical to the previous pipeline (~30 bytes per variant). Removes 5 build-time devDependencies (~50+ transitive packages).

Added

Pool entity model #

Pools now accept an entity: { state, computed, methods } block, bringing the declaration shape into line with components, stores, and plugins. state supplies defaults shallow-merged into every spawned entity; computed defines per-entity derived values bound to each entity's this; methods installs per-entity actions routed by data-action dispatch in preference to component methods. Arrow functions in computed or methods throw at registration with a clear fix suggestion. See entity-model and pool-api.

Pool array-like API #

PoolHandle now exposes JavaScript-native array methods (push, pop, length, at(i), find, filter, map, forEach, some, every, reduce, Symbol.iterator) alongside the existing add/remove/size aliases. Intentionally absent: splice, indexOf, slice. They assume stable indices, which swap-with-last pool storage does not provide. Use remove(key) to delete and at(i) for DOM-ordered positional reads.

mini build variant #

A new smallest tier in the build ladder. Includes everything from lite (core reactive UI, components, stores, lists) except the data-pool renderer. Intended for apps that don't need high-frequency entity rendering: forms, dashboards, tables, navigation, standard CRUD. Registering a component with a pools: {} block against mini throws at registration with a clear message pointing at lite or higher. Build ladder: minilitemin (core) → spafull.

Pool-level props #

Parent components can inject shared data accessible to all pool entities via the props. prefix in expressions (data-show="props.visible", data-bind="props.caption"). Dotted paths are resolved in the binding fallback, not evaluated as expressions.

Browser DevTools integration (__WF_DEVTOOLS_GLOBAL_HOOK__) #

Every WildflowerJS instance now exposes a read-only introspection API on window that external inspectors can drive via chrome.devtools.inspectedWindow.eval() or a drop-in <script>. Methods: getComponents(), getStores(), getPools(), getBindings(), getRoutes() for snapshots; setState() / setStoreState() for live editing from a devtools UI (both guarded against prototype-chain key names). Two companion packages ship separately: @wildflowerjs/devtools (standalone inspector, drop-in script with a floating panel) and a MV3 browser extension for Chrome and Firefox. Bundle cost: approximately +750 bytes brotli across all variants.

jQuery 3.x and 4.x coexistence verified #

WildflowerJS is drop-in safe alongside jQuery on the same page (WordPress / legacy-CMS scenario). 34-test matrix (test-new/jquery-coexistence.test.js) — 17 scenarios × jQuery 4.0.0 + 3.7.1 — covers globals safety, DOM ownership boundaries, co-handled elements, mutation isolation, attribute preservation under reactive updates, AJAX-injected components, $.noConflict(), plus legacy-CMS hardening (detach/append round-trips, zombie listeners, init timing). Live walkthrough at /demos/jquery-integration/.

Item-level computed properties in binding expressions #

Item-level computeds (fn(item) with fn.length > 0) now resolve across every binding type — data-bind, data-bind-class, data-bind-style, data-bind-attr, data-show, data-render — as bare references and inside compound expressions (ternaries, object syntax, string concatenation). Nested lists resolve against the inner item, outer context via _parent. v1.0 silently evaluated such references as undefined. Tests: test-new/list-item-computed-other-binding-gaps.test.js. Docs: live example on /docs/lists.

wildflower.batch(fn) callback wrapper #

Convenience API for batched state mutations. Runs a function inside a batch, applies the batch on success, cancels on exception. Removes the manual try/catch boilerplate around startBatch / applyBatch / cancelBatch and makes batch usage exception-safe by construction. Sync-only; for async work the start/apply/cancel API remains available.

wildflower.toRaw(value) for structured-clone boundaries #

Returns a deep plain-JS copy of any reactive value. Required whenever WF state crosses a structured-clone boundary (IndexedDB, postMessage, Web Workers, BroadcastChannel, Cache API, History state), all of which reject reactive proxies with DataCloneError. Supports primitives, plain objects, arrays, Date, RegExp, Map, Set, and cyclic references; skips functions; returns DOM nodes by reference. Don't call from inside a reactive effect or computed — it registers every walked path as a dependency.

await db.put('issues', wildflower.toRaw(pm.issues));
worker.postMessage(wildflower.toRaw(state));

Breaking Changes

Action handlers no longer stop event propagation by default #

Events dispatched through data-action now bubble naturally. Restores clean coexistence with external delegation (jQuery $(document).on(...) was silently being consumed in v1.0). To opt back in on a specific element, add data-event-stop. Internal nested-component double-fire is still prevented via a per-event marker (event._wfHandled). Most apps will see no change; modal/dropdown click-outside guards may need the explicit opt-in.

Removed data-model-debounce attribute #

Debouncing user input now belongs on the action that receives it. Migrate any data-model-debounce="Xms" usage to the corresponding action with a debounce modifier (data-action="input.debounce.Xms:handleInput"). The attribute was experimental and its semantics collided with list re-render timing; routing debounce through the action layer is simpler and avoids the stale-value hazards of capturing state at keydown.

Bare-form item-level computeds removed #

Scope is now declared purely by signature: fn(item, index, info) { ... } is item-level (per row); fn() { ... } is component-level. v1.0's dual interpretation (zero-arg computeds becoming item-level inside list templates, with this.X binding to the current row) is removed because of silent failure modes (name shadowing, scope-dependent semantics). Migration: change fn() { return this.assignee } to fn(item) { return item.assignee }. The new info arg exposes list-context vars (info.first, info.last, info.length). v1.0 had no documented item-level computeds, so user impact is bounded.

Fixed

Item-level computed bindings reactively update on per-row state mutations #

Computed-name bindings (e.g. data-bind-style="assigneeStyle") sometimes stayed stale after the underlying item field mutated, because the targeted-rebind optimization compared binding.path === changedProp and the binding's path was the computed name, never matching the actual changed prop the computed body reads. Fix: per-binding bypass at every targeted-rebind filter site — when the binding name or expression vars match a registered computed, skip the path-equality filter. Tests: test-new/list-binding-targeted-rebind-with-computeds.test.js.

Item-level computeds in class binding expressions #

Class bindings like data-bind-class="isOn ? 'active' : 'inactive'" silently evaluated to undefined. Two evaluator branches in ListExpressionEval.js (_resolveListExprArgs and _applyCompiledClassBinding's var-resolver closure) gated computed lookup on itemComputeds[name].length > 0, but the wrapped accessor at itemComputeds[name] always has length === 0 so the branch never fired for any computed. Replaced both call sites with _originalComputedFunctions lookup routed through _evaluateComputedInListContext. Tests: test-new/item-level-computed-form-capabilities.test.js.

Nested data-list source resolves item-level computeds #

A nested-list path (e.g., inner <ul data-list="reactionChips">) now falls back to evaluating an item-level computed when the path isn't a raw field on the parent item, mirroring how data-bind already resolved them. Previously item-level computeds only worked as data-bind values, not as nested-list array sources, so users had to pre-decorate the parent rows. Patched in both ListNestedManager (initial setup) and ListRenderer (the per-frame arrayFn accessor). Test: test-new/nested-list-item-computed-source.test.js.

Multi-component scan init race that left nested data-list inner items unrendered #

The render effect fired synchronously but _listRelationships wasn't populated until later (_setupListContexts), so renders landing in the scan's sprint window saw an empty map and skipped nested-list integration — outer list rendered, inner data-list stayed a bare <template>. Symptom: section headers with no rows beneath them, intermittent based on idle-callback scheduling. Fix: walk the scan root once before features run and pre-populate _listRelationships from every template. Test: tests/nested-list-prepopulate-init-race.test.js.

Pool entity binding and dispatch issues #

Boolean-prop sync, data-bind on form inputs, and dotted-path bindings (props.X) in data-show/data-bind fallback paths now resolve correctly. Mini-build error messages include a copy-pasteable fix.

Bindings on data-list root elements #

data-bind-style, data-bind-class, data-bind-attr, and data-model placed on an element that also has data-list are now correctly collected and applied. Previously they were silently skipped because _isOwnedBindingElement treated the list root itself as "inside a list" and filtered it out. Fix: check ancestors only (not self). Unblocks the common carousel pattern of animating a list container's transform while the list renders its children.

data-cloak retained on dynamically-added list items #

List items added after the initial DOM scan, or moved between sibling data-lists, inherited data-cloak from the cached template and stayed hidden by [data-cloak]{display:none} forever, defeating data-show on inner elements. The strip is now applied along every row-creation path: the cached template attribute list in TemplateSystem, the rendered innerHTML parts, the cloneNode fallback used when the cached template was bypassed (root element and all descendants), and the data-render conditional template clones. Belt-and-suspenders so no row-creation path can leave data-cloak alive on a newly-added item.

Hover events on data-list row templates #

data-action declarations for mouseenter, mouseleave, mouseover, and mouseout inside list-row templates were silently dropped — the delegated event registry only attached listeners for a fixed whitelist that excluded them. Fix: add mouseover / mouseout to the whitelist (both bubble); synthesize mouseenter / mouseleave on top via the standard event.relatedTarget containment check.

Multiple actions on a single list-row element #

A row-template element with multiple actions (e.g. data-action="click:open mouseenter:hover mouseleave:unhover") only wired up the first one — the per-row context kept a single action context per element and skipped subsequent defs. Fix: accumulate every declared (eventType → handler) pair on the row's action context; the dispatcher routes by event.type through that map.

data-event-outside on data-list row children #

data-event-outside inside a row template was a silent no-op — neither the list-row context-creation path nor _setupActions wired up the document-level outside-click handler. Fix: TemplateSystem records a hasEventOutside flag on action metadata; ListItemBinding and ListRenderer's innerHTML fast path register the handler eagerly per row. Companion: PropsSystem._setupOutsideClickHandler rebuilt around a single document listener + per-element registry keyed by (element, methodName). Test: test-new/actions.test.js.

data-event-outside row-child handlers receive a details object #

Row-child outside-click handlers got only (event, el), unlike regular row actions which receive (event, el, details) with details.item. Now the row context is captured at registration and the outside-click registry builds the same { item, index, list, length, first, last, context } shape on dispatch. Non-list handlers unchanged. Test: test-new/actions.test.js.

Idempotent attribute writes in list and effect paths #

setAttribute is now skipped when the target attribute already holds the same value. Harmless for most attributes, but <video> fires emptied/loadstart on any write to src (even identical), which caused visible reload flashes and lost playback state during list reconciliation.

Debounce writeback regression #

Stale state from an in-flight debounced writeback no longer overwrites user input typed after the debounce window opened.

Binding validator false positives #

The dev-mode validator no longer flags property accesses of state variables (user.name when user is defined) as unknown paths, and now delegates expression-containing attributes to the expression validator instead of re-parsing them as binding paths.

Pool sub-array remove() O(n²) on bulk clear #

The internal _staticArray/_dynamicArray tracking used Array.prototype.indexOf for removal despite the pool's own main array using O(1) swap-with-last. Now uses a stored subIdx for constant-time removal, eliminating the quadratic cleanup cost at 800+ entities.

ListRenderer fingerprint collisions on arrays between 100 and 1000 items #

The change-detection fingerprint sampled only 3 positions for arrays over 100 items, causing interior mutations to be missed when only interior items changed. Full-item hashing is now used up to 1000; 7-position sampling beyond that.

SSR state parser for <input> / <textarea> / <select> #

Hydration now reads element.value for these tags instead of falling back to textContent, so server-rendered default values survive client activation.

Portaled event listener leaks on component destroy #

Listeners registered on portaled elements are now explicitly removed before the portaled content is detached, releasing handler closures (which captured the component instance) immediately rather than on the next GC cycle.

Reactivity correctness in expression cache and sync-effect reentrancy #

Four state-layer fixes including a snapshot-before-iterate guard in _notifyEffectDependents to prevent sync effect reentry from corrupting the outer loop, and _reusableEffectSet promoted to per-instance state.

Computeds that delegate to branching helper functions now re-track dependencies on every evaluation #

The optimizer used to seal the dep set from the first call, missing state read only on later branches — so a helper like pickName(state) { if (state.locale === 'en') return state.englishName; return state.spanishName; } would never see spanishName change after a locale flip. Function calls inside computed bodies now block that optimizer promotion. Companion fix: cached value is updated synchronously when a computed transitions out of the optimized fast path.

Action handlers fired before init() completes are queued and replayed #

Pre-init events (e.g. clicks landing while init awaits a slow subscribe) used to throw or be silently dropped. They're now queued and replayed in order after init() returns; replay errors route through onError. Caveats: lifecycle names (init, beforeInit, destroy, etc.) must not be reused for action handlers; replayed handlers see the original event but event.preventDefault() is a no-op by replay time — use data-event-prevent on forms instead.

Composed computed properties no longer drop dependencies in nested evaluations #

When one optimized computed read another optimized computed inside its evaluator, the inner evaluation could clobber the outer's dependency-tracking buffer, leaving the outer with an incomplete dep set. The buffer is now saved and restored across nested evaluations, and dep comparison reads from a local variable to prevent the bug from being reintroduced by future cleanup.

Effect cleanup on component destroy walks all three places effects can live #

The destroy sweep walked instance._effects and instance.context._effects but missed instance.stateManager._effects. Framework-internal effects scoped to the RSM's pre-instance stub landed there and survived destroyComponent, including every list's mapArray structural effect and per-item effects — which kept firing against external store mutations on already-removed DOM. Fix: walk the RSM's _effects too (_disposeEffect is idempotent). Test: test-new/effect-cleanup-on-destroy.test.js.

data-bind-style and data-bind-attr clear keys that drop out of the bound result #

When a style/attr computed shrank between renders (e.g. {background: color}{} when unassigned), the framework applied the new keys but never cleared the dropped ones — visible as an avatar retaining its old background after the assignee was set back to null. Three apply paths now diff against per-element tracking sets and clear dropped keys before applying new ones. Test: test-new/binding-drop-out-clearing.test.js.

data-bind-class shape mismatch no longer crashes deep in the framework (WF-505) #

A data-bind-class binding whose computed returns a non-string (object, array, number) used to throw TypeError: t.split is not a function inside the rendering core, leaving the page blank. The element-level path now coerces the value (truthy keys joined to a class string for objects, String(value) for primitives) so the page keeps rendering, with a one-time __DEV__ warning per binding context pointing at the root cause. The Effect-based path already handled object form via {className: bool} syntax; this aligns the slower SET-trap path with that shape. Documented at /docs/error-codes?code=WF-505.

List click delegation no longer drops row clicks when an ancestor element carries data-action #

Click delegation tries closest('[data-action]') first then falls back to compiled metadata. But row data-action is stripped from the DOM (innerHTML fast path), so closest() walked past the row and returned an outer ancestor's data-action (e.g. a data-event-outside wrapper). The handler then saw the scope mismatch and bailed without trying the metadata fallback. Fix: when closest() returns an out-of-scope action, retry the metadata fallback and accept only rows whose parentElement is this list (nested-list safety). Tests: test-new/event-modifiers.test.js.

Item-level computeds in list rows re-evaluate on external store/plugin mutations #

Per-item effects only tracked the row's own item proxy and the component's local state, so an item-level computed that read from another store (e.g. a row's rollupBadge counting subtasks in a separate store) went silently stale on cross-store mutations. Per-item effects are now collected in a per-RSM _listItemEffects registry and woken from _handleEntityStateChange regardless of mutation shape. Test: test-new/list-item-computed-store-array-reassign.test.js covers six shapes (array reassign, push/splice, row property writes, reorder, keyed lookup, control).

Dev-mode warning for cross-subtree state proxy aliasing #

When the framework reuses an existing state proxy under a different parent path that doesn't share the original's first segment, dev builds now surface a warning. Catches a class of subtle aliasing bugs where the same nested object is reachable from two unrelated state subtrees and dependency tracking can get confused about which path the change occurred on.

Subscribe-only components now receive onStoreUpdate notifications #

The store's _hasNotifyTargets fast-exit cache was computed lazily on the first state change (the synthetic _internal.ready = true write at end of construction) and locked at false when no component had registered yet. Subsequent subscribePath calls populated the path-subscriber set but didn't invalidate the cache, so dispatch short-circuited and onStoreUpdate never fired. Components that also read the store via computed / data-bind were unaffected (tracking proxy refreshed the cache as a side effect); subscribe-only components silently received nothing. Fix: subscribePath now sets _hasNotifyTargets = true alongside _hasPathSubscribers. Test: test-new/notification-shape-matrix.test.js Shape 5.

Subscribe-block components are now registered as entity dependents of their store #

A subscribe: { store: ['path'] } contract previously wired the component only as a path subscriber, not as an entity dependent. The entity-dependent dispatch loop is what dirties dependent computeds — so data-bind / data-show backed by computeds reading the subscribed path could stay on stale values. The gap was usually masked by the tracking proxy registering the dep as a side effect on the first computed read, but unreliable: early-return computeds + cache-hit fast path could permanently skip registration. Surfaced as a Chrome-only blank-detail-pane in the PM demo after soft reload. Fix: subscribePath now also calls _registerEntityDependent; path-scoped invalidation still gates on declared paths. Tests: test-new/subscribe-block-registers-entity-dep.test.js.

LEAN re-eval path now sets _computedTrackingContext #

The LEAN re-evaluation path for cross-store computeds assumed external deps were stable after first eval, and skipped tracking-context setup. That breaks when the first eval early-returns before a cross-store read: the dep is never tracked, and subsequent lean re-evals also skip tracking, so the computed stays permanently disconnected from a store it actually reads on the non-early-return path. Fix: set _computedTrackingContext around node.fn() in the lean path too; per-call dedup keeps the cost minimal; finally restores prior context. Test: test-new/computed-lean-path-tracks-cross-store-deps.test.js.

_resolvePendingStoreDependencies resets _externalEvalCount on dependents #

When a late-arriving store resolves, the resolver cleared computedCache but left each computed node's _externalEvalCount intact, so subsequent re-evals stayed on the LEAN path and couldn't re-establish the cross-store dep graph. Fix: reset _externalEvalCount to zero across _computedNodes after the cache clear, forcing the next eval through the full tracking path. Combined with the LEAN tracking-context fix, this closes the late-store-resolution failure end to end. Test: test-new/computed-lean-path-tracks-cross-store-deps.test.js.

Component-level computeds referenced inside list templates no longer get falsely flagged item-level #

_evaluateComputedInListContext was adding every computed touched in list-row evaluation to _itemLevelComputedProperties, regardless of arity. A zero-arg component computed referenced inside a list-row binding got tagged item-level, then the component-level cascade skipped it on the (now wrong) assumption that per-row effects would drive re-eval — leaving the binding silently stuck on its first cached value. Fix: scope the marking to fn.length > 0 via stateManager._originalComputedFunctions. Test: test-new/lifecycle-invariants.test.js.

_setupStoreSubscriptions hoisted ahead of computed setup in the scanner #

The async scanner could yield (requestIdleCallback) between computed-setup and feature-setup. An async store init resolving in that window mutated state before the component was registered as an entity-dep, so the cascade missed it and the component stuck on its empty cached value. Surfaced as Firefox-only blank-detail-panes in the PM demo after soft reload. Fix: run _setupStoreSubscriptions as a synchronous pre-pass in both orchestrators, before any computed-eval enqueue. Tests: test-new/scanner-subscribe-before-computed.test.js plus parameterised coverage in test-new/race-harness.test.js.

List-row click delegation now bounds closest() to the list element #

Scope leakage in event delegation: when the canUseInnerHTML fast path stripped a row's data-action, event.target.closest('[data-action]') walked past the empty row and latched onto an unrelated outer ancestor (e.g. <form data-action="submit"> wrapping a modal). The handler then bailed on the form tagname and never reached the compiled-metadata fallback — silent dead-click. Fix: reject any actionEl outside the owning listElement, plus a defensive fallback in _handleDelegatedActionWithListItem that recovers the action name from listItem._compiledMetadata when the DOM attribute is missing. The compile-time strip stays for krausest-scale perf. Tests: test-new/list-row-action-attribute-preserved.test.js (3 scenarios including the form-ancestor case).

Per-row field precedence honoured by data-bind-style and data-bind-class #

Two compounding bugs caused visual leakage between unrelated entities sharing a binding name. (1) In list templates, style/class bindings resolved simple-name expressions against the component computed registry first, shadowing any same-name per-row field — opposite of the documented item-first contract. (2) RenderingCore registered a component-level effect for every in-list bind-style/bind-class element, creating a second writer that raced the list-row update path; the text-bind path had a listBoundElements guard but style/class did not. PM team page symptom: project chips followed the parent team's color ~7/10 reloads in Chrome. Fix: item-first lookup in ListExpressionEval; !inList gate on the effect-meta push in RenderingCore. Tests in test-new/data-bind-style.test.js include a 30-iteration stress loop to surface the race.

wildflower.createRouter() staged-init pattern no longer emits spurious warnings #

createRouter always auto-initialized inside the factory, so the documented staged pattern (createRouter({ mode }).onRoute(...) → manual .init()) ran against an empty routeTree and emitted three warnings per page load. Fix: auto-initialize only when options.routes is a non-empty array. Declarative form unchanged; staged form now skips auto-init so the caller controls timing.

router.navigate(path, { replace: true }) updates the address bar #

{ replace: true } was a no-op against the address bar: the route handler ran but no history.replaceState call was made, so the URL never updated. Replace navigation now performs replaceState (history mode) or hash update + replaceState (hash mode), via an explicit _replace flag so initial-load and popstate paths still leave the URL alone. Test: test-new/router-gaps.test.js "Replace navigation".

data-cloak strip for components registered after framework init #

Closes a Chrome-only "appear then hide" flash on default-hidden elements (welcome modals, routed sections, popovers) inside components whose wildflower.component(...) call lands after the initial cloak-strip rAF. The strip was unconditionally removing data-cloak on elements whose component ancestor hadn't initialized yet, exposing the element briefly until the late render effect wrote display:none. Fix: cloak-strip rAF defers when the closest [data-component] has no data-component-id; _initializeComponentElement then strips remaining cloaks after the first render effect runs. Both passes share _evaluateCloakShowVerdict so the strip commits the right inline display first. Tests: test-new/data-cloak.test.js.

Nested-list and refresh-effect cleanup on list re-render #

Two compounding leaks in _renderList: (1) the per-list refresh effect was only cleaned up on component destroy, so mid-life re-renders left stale effects firing on every state mutation; (2) row removal disposed the row's own item effect but didn't walk its subtree for nested [data-list] elements, orphaning every popover sub-list's mapArray + effects. PM issue list (26 rows × 4 nested popovers) leaked ~870 effects per priority change. Fix: element._disposeMapArray recursively disposes nested mapArrays and the owning list's refresh effect; mid-life and destroy paths share the wrapped dispose. 0 leaked effects per change post-fix.

Performance

Cross-store computed cache-hit fast path #

Reads of an already-cached computed property that depends on another store's state now skip the full re-evaluation path when the source stores have not changed. Measured speedups (1M-read microbenchmark): 8.7x on Firefox (533 ns → 61 ns per read), 4x on Chrome (450 ns → 115 ns). Read-heavy cross-store rendering patterns (1000:1 read:write ratio) speed up 2.7-6x end-to-end. Write-heavy patterns are unchanged.

Portal binding lookup #

_renderPortalBindings replaced an O(all-bindings) linear scan with a per-component context index, removing a per-teleport hotspot in apps with many active bindings.

Reactivity batch change-detection rebuilt around the proxy #

wildflower.batch(fn) (and startBatch / applyBatch) no longer serialize every component's state on entry and re-diff on exit. The proxy already records per-batch mutations via its set trap; the new path consumes that directly. startBatch is constant-time per batch instead of scaling with total state size — the dominant cost in small-batch / many-component apps. Krausest data-pool variant: swap1k ~25% faster, remove-one-1k ~20% faster. Real-world Lighthouse held within ±2 across 18 demos. ~600 lines of legacy code removed; startBatch shrank from ~25 lines to 6.

Portal visibility update skipped for portal-free components #

_updatePortalVisibility ran a descendant querySelectorAll on every entity state change for every component before early-returning on zero matches. For portal-free apps that's an O(descendants) DOM walk per mutation per component (PM demo: 38% of main-thread time during a select/deselect cycle). Fix: cache instance._hasPortals at init (and on late-list-item portal discovery); _scheduleComponentRender skips the call entirely when false. Apps without portals (most apps) pay zero descendant-walk cost on reactivity updates.

Class-binding eager item-computed eval gated on merged-context need #

_applyClassBindingsToRow eagerly evaluated every item-level computed on the component before applying any class binding, regardless of whether any evaluator on the row actually needed the merged context. For simple-property class bindings (the common case), that allocated 2 Proxies per computed per row per update and threw the result away. Fix: pre-scan the row's class evaluators; skip the eager loop entirely when none set _usesMergedContext. Reactivity and per-item resolution unchanged.

Path-scoped entity invalidation #

Store-state changes previously re-dirtied every dependent's computeds and re-ran every per-item effect, even when the changed path was nothing the dependent ever reads. The changed path is now matched (prefix-aware in both directions) against the component's declared subscribe paths and its runtime-tracked deps — non-matching dependents are skipped entirely. Conservative by construction: only narrows for explicit subscribe: {} contracts; falls back to full invalidation for computed-path notifications, store-computed readers, and missing metadata. Test: test-new/entity-path-scoped-invalidation.test.js.

Security

xlink:href sanitizer coverage #

Added xlink:href to the URL-attribute allow-list in ListExpressionEval._sanitizeAttrValue and PoolRenderer's _POOL_URL_ATTRS. Previously an attacker-controlled value bound to xlink:href on an SVG <a> or <use> could carry a javascript: URI through to the DOM, where Chrome and Firefox will execute it on activation. Now blocked with the same policy used for href/src/formaction/action/poster. Addressed the single exploitable finding from the 2026-04-15 security audit. Includes a 16-test regression suite (test-new/security-audit.test.js) exercising each audited finding through realistic ingress paths.

Narrowed data:image/ allowlist to raster formats only #

The previous regex permitted data:image/svg+xml, which could embed inline scripted SVG in URL-bearing attributes. Now restricted to png, jpe?g, gif, webp, avif, bmp, ico, tiff?, and x-icon. Other data:image/* subtypes are blocked.

1.0.0 — 2026-04-10

Added

  • Core reactive framework with component system
  • Reactive state management with computed properties and dependency tracking
  • Store system for cross-component state sharing
  • List rendering with automatic keyed reconciliation
  • Conditional rendering (data-show, data-render)
  • Event handling with modifiers (throttle, debounce, self, outside, once, passive, capture)
  • Two-way data binding (data-model) with modifiers (trim, number, debounce, lazy)
  • Attribute, style, and class binding (data-bind-attr, data-bind-style, data-bind-class)
  • Client-side routing with history and hash modes
  • Server-side rendering with hydration
  • Plugin system architecture
  • Portal, modal, and transition systems
  • Entity pools (data-pool) for high-frequency DOM rendering
  • Anti-FOUC data-cloak system
  • wildflower.whenSettled() API for deterministic async waits
  • 4 build variants (core, lite, spa, full)
  • Comprehensive test suite (3,646 tests in real Chromium)

Security

  • Expression evaluator blocklist for unsafe patterns (eval, Function, globalThis, window)
  • Pool renderer attribute blocklist and URL protocol sanitization
  • HTML sanitizer routing for data-bind-html and router outlet
  • data: URI blocking (except data:image/) in URL-bearing attributes