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.3.0 — 2026-07-26

Added

Live server data with data-query #

The headline of this release. A query is a named source declaration in JavaScript and a data-query="name" attribute in markup: declare where the data lives and how fresh it should stay, and the view stays current. wildflower.query('products', { from: '/api/products', key: 'id', refresh: ['focus', 'etag:60'] }) is a complete data layer.

The element's own shape picks the rendering mode. A <template> child renders the result as a keyed list through the same reconciler as data-list, so everything you know about list rendering applies unchanged. An element without a template treats the single result record as its subtree's binding context: a profile card, a detail pane, a row without a list. Query state is bindable anywhere through the $ accessor ($products.count, $products.isLoading, $products.isStale) and readable in JS via wildflower.getQuery(), which tracks automatically inside computed properties the way getStore() does.

The freshness ladder: 'once' (the default), a number for polling seconds, 'etag:N' for conditional GET where a 304 answer costs a body-less round trip, 'focus' and 'reconnect' for the moments staleness actually bites, and 'sse' for server push over EventSource, with an optional dedicated stream: endpoint. The engine owns the failure modes: the last call wins and superseded requests abort, hard load errors (error) are split from background sync errors (syncError) so rows on screen are never wiped by a failed refresh, and queries stand down automatically (timers stopped, streams closed) shortly after their last observer leaves the page.

There is no query language. Refinement is an ordinary computed property over rows, rendered by data-list, exactly as with any other data. Ships in the Full build, alongside SSR. See the new documentation section for the complete surface and live examples.

SSR pages hand off to data queries #

Inside data-ssr="true", server-rendered markup is adopted as a query's first result. The rendered rows or record parse back out of the DOM, so the document itself is the seed: there is no hydration payload re-describing what the HTML already says, no flash of empty content, and no loading state over data the user can already see. The catch-up fetch runs as a background refresh, and the freshness ladder keeps the page live from there.

The two features were designed as one story. Server-side rendering is the first materialization of your data into the page; a data query is every one after it. The server's render is the first result of a standing query that happened to run on the other side of the wire. See SSR with Data Queries.

data-seed carries unrendered fields into hydrated state #

The SSR state parse can only recover what the page displays. Fields your data needs that never appear as text (row ids, API paths, machine-precision values) ride a data-seed attribute: a small JSON object on the element the data belongs to, rendered by the same server loop that renders the element. <tr data-seed='{"id":811}'> gives an adopted row its identity; <span data-bind="count">250+</span> with data-seed='{"count":250}' hydrates as a number while displaying formatted text.

Seed fields merge into the parsed state and win overlaps, since the attribute is the machine value and the text is the display value. It works on SSR component roots, list item roots, and data-query rows and records alike: one convention everywhere the DOM is the seed.

Paged and infinite results for data queries #

Numbered pages need no new surface. refresh({ params: { page } }) swaps the window and the previous rows stay visible with isStale true while the next page is in flight, and a declared key patches any overlapping rows in place. For infinite lists, one option on that same call, append: true, accumulates the page into the existing list instead of replacing it, deduping by key so a re-delivered row updates in place rather than doubling.

Once a query has accumulated, background updates merge rather than truncate. An invalidate(), a focus, reconnect, or poll rung firing, or an SSE message adds new rows at the head and updates existing rows in place, so a live feed the user has scrolled keeps its place. An explicit refresh() without append replaces everything and resets the query to plain: invalidate syncs what you are looking at, refresh starts over.

Deletions ride the rows. A windowed fetch cannot see that a row was removed elsewhere on the server, so absence is never inferred as deletion. Declare a deleted: field and any row arriving with that field truthy is removed by key wherever it appears, whether in a fetch, an append, or an SSE message, which keeps soft-delete sources consistent with no hand-written filter. Sources that hard-delete retain nothing to send; a full refresh() resyncs the window there.

The app keeps ownership of the pagination scheme. It tracks the page or cursor and any hasMore flag in its own state, and replaying an accumulated query after a hard reset is a short app-side loop rather than engine machinery. The engine interprets exactly two row fields: identity (key) and existence (the declared deleted field). Position, recency, and conflict resolution stay in application code. Full build, alongside SSR.

State-preserving reorders via Element.moveBefore #

Keyed list and pool reorders now move existing DOM nodes atomically where the browser supports Element.moveBefore, preserving focus, text selection, and running CSS animations through the move. An input inside a row keeps its cursor when the row changes position. Browsers without the API get the previous insert-based behavior, unchanged.

Page-load initialization yields with scheduler.yield #

Large page-load scans yield to the browser with continuation priority where scheduler.yield is available, so input handling stays responsive during heavy initialization while the scan resumes ahead of other queued work. Initialization order is identical everywhere; browsers without the API use the previous scheduling.

$entity.path values in props #

data-prop-* and data-props values resolve external entity paths, so a child component can receive state from any store or query directly: <div data-component="badge" data-prop-total="$cart.total">. Previously the parent had to mirror the external value into its own state to pass it down.

Every warning now carries an error code #

All user-facing development warnings route through a WF- code with an entry in the error-codes reference, which now spans 96 entries including a new Security section. Every diagnostic the framework emits links to a single page explaining what happened, why, and how to fix it; nothing warns anonymously. Warnings remain compiled out of production builds.

The nano build tier #

A new build variant below mini, the smallest tier at roughly 45 KB brotli. It ships the core reactive UI without the data-list render cluster: data-bind, data-show, data-render, data-model, computed properties, external(), props, forms, error boundaries, custom directives and lifecycle hooks, and WildQuery. It is aimed at interactive widgets, embeds, and single-file pages that never render a list; when you need data-list, use mini and up.

Carving out the tier forced a clean separation between the list machinery and the core, and that separation surfaced two real bugs that are fixed in this release for every build: an attribute-sanitization gap and a props-to-computed reactivity gap, both listed under Fixed.

pool.length and pool.size are reactive #

A computed that reads a pool aggregate now re-evaluates when entities are added, removed, or cleared. computed: { count() { return this.pools.enemies.length } } is the natural pattern, and it works, including when the computed first runs before the pool has finished registering. Previously that computed evaluated once against the empty pool and stayed silently stale forever; the documented workaround was mirroring the count into reactive state at every mutation site. That workaround is now obsolete, and the WF-212 development warning that guarded the old trap is retired.

The reactivity is on demand. A pool that is never read from a computed or effect carries zero reactive machinery, plain reads from handlers and tick bodies remain a bare map lookup, and per-frame entity field mutation stays outside reactivity entirely, so the pool's performance contract is unchanged. Our pool benchmark suite measures identical timings with and without the change.

Dev-mode diagnostics for nine silent failure modes #

Development builds now warn, with the fix inline, in the places where the framework previously failed silently. State written at the top level of a component, store, plugin, or pool entity definition (instead of inside state: {}) was ignored without a word; so were methods: and actions: blocks carried over from other frameworks' shapes. A lifecycle hook name wired as a data-action handler ran on the framework's schedule instead of yours (tick fires every animation frame, not on click). A list whose <template> could not resolve rendered nothing with no explanation; the warning now names the cause, including the case where the HTML parser removed the template inside an <svg> subtree before any script ran. A method name shadowing a state field or computed left one of them unreachable. destroyComponent() with the element still in the document quietly resurrects on the next scan. Index-based array methods on pools, direct mutation of pool.items, and preventDefault() inside an action replayed after init round out the set.

Every warning names the entity, states what the framework did, and gives the correction. All of them are compiled out of production builds, which stay byte-lean.

Releases publish with npm trusted publishing and attested provenance #

Starting with this release, the npm package is published by a GitHub workflow using OIDC trusted publishing, with automatic Sigstore-signed SLSA provenance on the published artifact. No npm token exists anywhere in the pipeline, and the CDN inherits the attested artifact because it serves directly from npm. Combined with the framework's no-install build path, the supply-chain surface stays where it has always been: pinned, verified, and small.

Breaking Changes

this.pool(name) is renamed to this.getPool(name) #

Breaking. The component method that fetches a pool handle by name is now this.getPool(name), matching this.getStore(name) for stores. One rule now covers both subsystems. this.pools.name and this.stores.name are the declared-collection containers; getPool(name) and getStore(name) fetch any instance by name, including a markup-only pool declared without a pools: block. The old this.pool() spelling is removed, not aliased. Prefer the container this.pools.name in most code, and reach for getPool only for a markup-only or imperatively created pool.

Under the hood

One applier engine for list bindings #

List templates now compile to a flat applier program, executed by three engine entry points: initial paint, targeted rebind for a changed field, and positional re-evaluation after reorders. This replaces seven typed executors, three separate re-evaluation sweeps, and a targeted-rebind filter that existed in three divergent copies, a divergence that had already produced a real class of update bugs. Class, style, and attribute paint now has a single owner at every call site, including server-side rendering and slots, which also removed a double class write on initial list render; row-selection updates measure about 10% faster in our krausest rig as a result. The rendered DOM is byte-identical.

Fixed

Emptying a list no longer strands a row that a DOM library moved elsewhere #

Drag libraries such as SortableJS physically move a row into another list before your state catches up. When the state update then emptied the source list, the clear operated on the source container alone, so the moved element survived in its new column right beside the destination list's own render of the same item. A visible duplicate, and only when the drag emptied its column, which made it look intermittent.

The clear now removes every tracked row wherever it currently lives. Cross-list drag needs no manual evt.item cleanup in any state-update style, whether you splice in place or replace the whole array.

Store destroy() and beforeDestroy() lifecycle hooks now run #

Stores accepted teardown hooks in their definition and silently discarded them at registration, so no store teardown ever ran user cleanup. An interval started in init() outlived its store. Both hooks now fire on wildflower.unregister() and every other teardown path, with this bound to store state and methods, matching how components have always behaved.

Binding validation understands configurable-template scope #

The development-mode binding validator checked a data-use-template expansion's bindings against the host component's state rather than the data-with scope they actually resolve in, reporting working bindings as undefined properties. Slot-template subtrees are now excluded from host-scope validation, matching every runtime binding path. Development builds only; production behavior was always correct.

data-bind-attr values are sanitized in every build #

The attribute-value sanitizers lived inside the list cluster, so builds that exclude it (lite, mini) could write unsanitized attribute values. The sanitizers now ship in a core module present in every tier and apply on every binding path.

Props that feed computed properties track reliably #

A reactivity gap in the props-binding update path was surfaced and fixed while extracting that path from the list cluster for the nano tier.

data-bind-class on a list row resolves the item field first #

A class binding whose name was both a list-item field and a same-name component computed or state property resolved to the component value, diverging from text, style, and attribute bindings, which are all item-first per the documented rule. All class paths are now item-field-first. Computed-over-state precedence is preserved for names that have no item field.

A <template> inside <svg> no longer crashes component initialization #

The HTML parser treats a <template> inside an SVG subtree as inert foreign content with no content fragment. The component's list discovery read through it and threw, taking down the entire component's initialization. Discovery now skips the parser's leftover, and development builds explain what the parser did and suggest the working patterns for repeated SVG primitives (a fixed set of bound elements, or a precomputed path).

tick() on a build without the pool module no longer crashes init #

Components, stores, and plugins that define tick() on a tier that excludes the pool frame loop crashed initialization through a missing internal call. Registration is now guarded on every path, and development builds warn that tick will never run on that tier.

A markup portal in a component with no init() teleports on page load #

A component whose portal was declared in its initial markup, in a component with no init() method, never had the portal teleported on the normal page-load path; only components that defined an init() did. The markup-portal pass now runs for every component on that path, so a purely declarative portal teleports regardless.

Array-form subscribe: registers in production builds #

A component declaring subscribe: ['cart'], the array "wait only" form, picked up its store dependency through a development-only path. It re-rendered correctly in dev builds and silently stopped in minified production builds. Both builds now register the dependency the same way, and stores created after the component are covered.

A computed with no reactive sources no longer caches forever #

A computed that ran before the data it reads existed tracked no sources, so nothing could ever wake it, and it returned that first value for the life of the component. Source-less computeds now re-evaluate on each read and correct themselves once the data lands. Computeds that track sources cache exactly as before.

Props passed to a child refresh when a store drives the parent's computed #

When a store update flowed through a parent's computed property into a child's prop, the child kept its initial value while the parent's own bindings updated. The dependents sweep now refreshes child props on that path, so a child sees the new value at the same time the parent does.

Performance

Large-list creation defers per-item reactive proxies #

Bulk list creation (create, replace, append) now reads raw items instead of allocating a reactive proxy per row up front. Consumers that need the proxy resolve it on demand through the entity-state facade: row dependency walks, action-handler item mutation, data-model write-back, and nested-list handoff. Handler items stay identity-stable against your own state reads (=== and indexOf behave exactly as before), and item writes still fire the component update dispatch. Memory is at parity with the previous release, verified by heap census at one thousand and ten thousand rows.

In our krausest rig this cut large-list creation script cost by roughly 10% and left every other operation flat or better, moving data-list creation into the same performance band as the fastest frameworks we test against.

1.2.0 — 2026-07-07

Under the hood

Reactivity rebuilt as a single dependency graph #

State, computeds, effects, and bindings are now nodes in one unified dependency graph, resolved in a single pass. This replaces the previous split between the reactive state manager and a parallel binding-context system. There is no API change and nothing to migrate; the result is a smaller core, faster reads of values that have not changed, leaner effect scheduling and dependency re-tracking, and a single place where dependency tracking is defined, which is where several of this release's reactivity fixes originate.

List rendering rebuilt on targeted updates #

data-list no longer creates a reactive effect per row. Each list now registers its rows' bound fields on one per-list update dispatcher, and an in-place field change (items[3].qty = 5) routes to exactly the bindings that read that field, down to a single direct DOM write for a plainly bound field. There is no API change and nothing to migrate.

The results: roughly 20 to 25% less memory per row on lists whose templates use item-level computed properties, faster targeted updates and removals, a smaller rendering pipeline, and one update path where there used to be four. That last point mattered beyond the numbers: unifying the paths surfaced several latent inconsistencies between them, all found and fixed during this cycle, before they could ever reach a release.

Added

wildflower.unregister(name) unified component/store teardown #

A single teardown entry point that removes a component definition, destroying its live instances and disposing their reactive state, effects, and contexts, and/or disposes a store of that name, so the name is free to register again with a fresh definition. Registration was previously first-write-wins with no way to replace a definition; unregister fills that gap. It is safe to call for an unknown name and handles a name used by both a component and a store. The primary uses are live-preview and hot-module-reload teardown before re-registering the same name, dynamic applications that swap components at runtime, and tests that need a clean registry between cases.

Dev-mode warning (WF-215) for a re-registration with a different definition #

Re-registering a component or store under a name that already exists is silently skipped, keeping the original definition. When the incoming definition actually differs from the stored one, that is almost always an accidental collision (two components sharing a name, a hot reload without teardown), so development builds now warn (WF-215), name the entity, and point at wildflower.unregister. Identical re-registrations, such as the same definition scanned twice, stay quiet. The comparison hashes method source rather than only comparing shape, so two definitions that share method names but differ in a method body are still flagged. Stripped entirely from production builds. See WF-215.

Iteration dependencies are tracked #

A computed or binding that iterates a reactive object's keys, whether through Object.keys, for...in, or spread, now re-runs when a key is added or removed. Previously only reassigning the whole object woke iteration readers, so a computed like Object.keys(this.saved).length went silently stale when a key was added or deleted by direct mutation. Updating an existing key's value still does not re-run keys-only readers, so per-field writes on hot paths stay exactly as cheap as before.

Dev-mode warning (WF-214) for zero-arg computeds reading item properties #

Item-level computed properties receive the list item as their first argument. A computed declared without parameters evaluates at component scope, so a this read of an item field inside it silently resolves undefined. Development builds now warn (WF-214) when such a read misses on the component while the current list item has a property of that name, naming the computed and property and suggesting the (item) signature. Zero-arg computeds that read only component state remain legitimate inside rows and never trigger the warning. Fires once per component and computed; stripped entirely from production builds. See WF-214.

The data-csp-safe script attribute #

On pages served with a strict Content Security Policy, add data-csp-safe to the framework script tag and WildflowerJS starts directly in CSP-safe mode, never attempting dynamic code evaluation at all. The page produces zero CSP violations and zero report-uri reports. Without the attribute the framework still auto-detects the policy and falls back to its CSP-safe expression parser, at the cost of one benign violation report from the startup capability probe. See the Expressions documentation for CSP mode details and a live example.

Custom directives and lifecycle hooks in every build #

The declarative data-* custom-directive API and the component lifecycle hooks were previously bundled only with the plugin system, which left them out of the lite and mini builds. They now ship in all five build variants. The heavier plugin() registration and dependency-injection APIs stay gated to the full, spa, and standard builds. This change also fixes a latent crash in lite and mini, where the directive scan read .size on a registry that was never initialized in those variants.

DevTools timeline observability #

New introspection on the global DevTools hook, available in development builds only: a per-frame timeline records microtask drains, effect runs, and render-sweep duration. It is tree-shaken out of production builds, where only the hook's schemaVersion, version, and dev fields remain for capability detection. The data surfaces in the companion DevTools extension.

Dev-mode warning when a computed reads pool.length / pool.size #

pool.length and pool.size are plain, non-reactive getters. A computed property that reads one therefore caches a single value on its first evaluation and never re-runs, so any UI bound to it goes silently stale as the pool grows or shrinks. Development builds now emit a warning, at most once per pool, that names the offending computed and points to the fix: mirror the count into reactive state updated from tick(). The check lives entirely in a development-only path and adds nothing to production builds.

Breaking Changes

A non-boolean attribute bound to false now renders ="false" instead of being removed #

The component binding path previously removed a non-boolean attribute when its bound value was false; it now writes the literal string, matching the list path and the documented contract that non-boolean attributes hold their literal value. Boolean attributes are unchanged: false still removes them, since for a boolean attribute presence is the value. For most bindings this is more correct, and ARIA states such as aria-expanded now emit ="false" (the accessible form) instead of vanishing. The one place it can bite is a CSS attribute-presence selector used as a flag: an element bound { 'data-active': isActive } with isActive === false now matches [data-active], because it renders data-active="false", where before the attribute was absent. Migration: if you relied on a falsey bind removing such an attribute, bind null or undefined (both still remove it) instead of false, or select on the value ([data-active="true"]) rather than on presence.

Fixed

wildflower.config({ forceCSPMode: true }) now takes effect at runtime #

The documented call for forcing CSP-safe expression evaluation updated the configuration option without switching the live evaluator, so it was silently ineffective; and had the switch occurred, the first expression compiled afterward would have failed on an internal cache that only construction-time CSP mode initialized. Both are fixed: expressions compiled after the call now evaluate through the CSP-safe parser. For zero-violation strict-CSP pages, prefer the new data-csp-safe attribute, which takes effect before the framework's startup probe runs.

data-action on a component's own root element now binds #

<div data-component="x" data-action="click:save"> never wired up. The action scan used querySelectorAll, which by specification excludes the element it is called on, so an action declared on a component's own root was silently skipped. The scan now also checks the root element itself.

data-bind-html expressions re-apply on a targeted single-prop update #

When a single item property changed, the targeted-rebind path checked only each binding's path, not the variables its expression actually read. A data-bind-html expression that referenced the changed property resolved the new value but skipped the innerHTML write, leaving stale markup on screen. The targeted-rebind filter now accounts for an html expression's variables.

A nested item prop read only through an expression binding now reacts #

A binding such as data-bind-class="user.active ? 'on' : 'off'", and the equivalent style, attr, show, and render expressions, failed to update when user.active mutated in the case where no other binding on the element read that nested leaf. Only the root identifier (user) was being registered as a dependency, not the full dotted path. The whole path is now registered.

Expression and component-root bindings track dependencies consistently #

Dependency registration for expression bindings (data-bind-html, data-show) and for bindings on a component's root element is now driven by a single dependency descriptor that every consumer reads from. This closes a class of drift where one pass handled plain path bindings correctly while a parallel pass missed html or show expressions, or root-element bindings, leaving them under-tracked.

Removed an orphaned profiling timer in the data-render-in-list re-run path #

The re-run path for a data-render inside a list referenced a profiling timer that was never initialized, throwing a TypeError on every re-run. The surrounding effect's try/catch swallowed the error, but it still spammed the console and left two stray performance.now() calls running in production, since timing calls are not stripped by minification. The dead timer has been removed.

Reactivity gaps closed in item-level-computed and data-render list paths #

Three related gaps. A per-item computed that reads component-level state behind a short-circuit (for example openField === 'status' && openId === item.id) now wakes when that state changes; previously the unread branch never registered as a dependency and the binding stayed visually stuck, which is the popover-style toggle that surfaced this. A per-item data-render insert is no longer immediately undone by a stale cached false, because the conditional cache is now cleared before the data is resolved. And a data-render placeholder comment node is guarded against, so it no longer triggers a swallowed error while the renderer checks for a custom-element tag name.

data-pool binding errors surface in dev instead of failing silently #

A data-bind, -class, -style, or -attr expression in a data-pool template that threw was caught and skipped on every flush with nothing logged, so a typo failed invisibly, and in animation mode it failed on every single frame. Development builds now warn once per offending binding, naming the pool and the error. Production builds are unaffected and carry no added cost.

Passive data-pools filled one item at a time no longer wake the animation loop #

A passive pool, one that applies updates synchronously and skips the per-frame flush, still started the shared requestAnimationFrame loop when it was populated one item at a time through single add() calls. That was an idle wakeup with no work to do. Single-add now respects the same passive guard the bulk-add path already had, so a passive pool never spins the animation loop.

data-show toggles the .wf-show class on every path #

The documented anti-flash rule [data-show]:not(.wf-show) { display: none } relies on the .wf-show class being present once an element is visible, but data-show inside components and list rows toggled display without ever adding the class, so the CSS guard kept those elements hidden. The class is now written wherever a data-show verdict is applied, so the anti-flash contract holds in components and lists, not only context-bound elements.

data-bind-attr clears keys dropped from a bound object on the component path #

A key removed from a bound attribute object now removes its attribute from the element instead of leaving the previous value behind. The list path already did this; the component effect path did not, so a stale attribute could linger after the bound object stopped including it. (The related change to how a non-boolean false renders is listed under Breaking Changes.)

data-bind-style applies !important and CSS custom properties correctly on every path #

Style values were assigned through element.style[prop], which silently drops !important priority and no-ops for CSS custom properties (--x). Both the component update path and the list-row writers now route through setProperty, so !important survives reactive updates and custom properties apply. A property removed from the bound style object on a later update is also cleared instead of left in place.

subscribe store-wait timeout is bounded by elapsed time, not poll count #

The timeout for a component waiting on a subscribed store counted polling iterations rather than wall-clock time, so on a busy page the effective wait could stretch past the configured subscribeTimeout. It now fires at the configured millisecond bound regardless of how often the poll runs.

Memory leaks on list clear and row removal closed #

Clearing a list left scope-captured references to the old array alive, and a retired row's update-dispatch entry was not released when the row was removed. Both are now freed, so repeated build-and-clear cycles no longer accumulate memory.

Server-rendered nested lists hydrate into their parent item's state #

A nested data-list inside a server-rendered row was parsed into a flattened top-level shape instead of the parent item's nested array. It now reads from the correct nested state.

$this / $item primitive lists now render #

A data-list over an array of primitives (strings or numbers) referenced with $this or $item threw and rendered nothing. These lists now render their values.

Nested data-lists update when their parent item's identity changes #

Reassigning a parent row to a new object now reconciles its nested lists against the new parent instead of leaving them stale.

Portal bindings re-evaluate on store changes #

A portal driven by store state now re-evaluates when that store changes, and no longer over-invalidates unrelated portals.

The class-shape dev warning (WF-505) fires only for computeds #

It no longer flags inline data-bind-class expressions, where the shape guidance does not apply. Like every WF-NNN warning, it is stripped from production.

Performance

data-list / data-pool create path rebuilt (clone + setter) #

The create path has been rebuilt. Rows are now produced by cloning a cached row prototype and writing each text binding straight to textContent, with the whole batch assembled in a single DocumentFragment. This replaces the previous approach of serializing every row to an HTML string and reparsing it through innerHTML, which dominated the cost of building a list. Row creation is substantially faster, most visibly on large lists.

Second pass on the data-list / data-pool create path #

Three further refinements to row creation, most visible when building or replacing large lists. Each row reads its values straight from the underlying raw array rather than through the reactive proxy, since create-time reads do not need dependency tracking, which skips a per-row proxy and its access traps. Each row resolves its bound child elements by walking element node pointers instead of indexing a live element collection. And class setup copies only the item properties its class expressions actually reference, rather than the whole item. Builds compiled without server-side rendering also drop an unused legacy list path, making them slightly smaller with no change in behavior.

Single-text-binding update fast-path #

When a single item property changes and it is bound to exactly one plain text node, the framework now writes textContent directly and skips the generic per-item bind dispatch. Once such a field has been identified, later writes to it update the text node directly at assignment time, bypassing the update-batching step entirely. Anything more involved on the row (multiple bindings, attributes, or classes) falls back to the normal path.

Nested-path targeted rebind #

A change to a deep item property (for example rows[i].user.name) now rebinds only the bindings that actually read that path and leaves the rest of the row untouched, instead of rebinding the entire row. Shallow, flat item-property updates keep their existing behavior.

Leaner data-list update path #

Two redundancies removed from the per-update list path. Class bindings on a row are no longer re-evaluated when the changed property is not referenced by any class binding, and the per-update DOM re-scan that looked for nested [data-list] elements, and found none on a flat list, is gone. On flat lists this brings the per-update querySelectorAll count to zero.

Faster item insertion and removal on reactive lists #

Inserting items with push, unshift, or splice, and removing them, now operate on the underlying raw array, skipping a layer of reactive-proxy traversal on each operation. Single-item removal also drops a redundant proxy lookup while it re-indexes the rows that remain. Lists that add or remove rows frequently do less work per change.

Faster cross-store computed reads and writes on shallow chains #

Three stacked changes make cross-store computed reads and writes faster on shallow dependency chains. The proxy set traps now use direct property assignment instead of the receiver form of Reflect; a cross-store read resolves through a single proxy instead of hopping through two; and a lean re-evaluation of a computed whose cross-store dependencies are already static skips re-tracking them. Deep dependency chains are unaffected.

Targeted updates extended beyond text to class, style, and attribute bindings #

Eligible list rows now retire their per-item update effect and apply changes through a direct per-binding writer, so a class, style, or attribute change updates its single target without re-running the row's bindings. The single-text fast path is the special case of this.

Replacing a list's array with new objects updates each row once #

Reassigning a keyed list a fresh array whose items carry the same keys, the common pattern of swapping in a new page of results, now applies each reused row's bindings a single time instead of twice.

Targeted structural updates for swap, move, and single removal #

Swapping two rows, moving a row, or removing one now applies a precise minimal DOM update classified from the exact array operation, instead of re-diffing the whole list.

Reactive updates flush on the microtask #

Pending effects drain on the microtask after a state change rather than waiting for the next animation frame, removing up to a frame of latency before the DOM reflects an update.

Lower per-row memory and allocation on large lists #

Per-object reactive bookkeeping moved off the row objects into a side table, repeated per-row metadata was de-duplicated, and the per-row text writer is now shared, so building and holding large lists allocates less.

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