All notable changes to SibuJS will be documented in this file.
This project follows Semantic Versioning.
-
ErrorBoundarydrops thenodesoption — the subtree is now passed as the positional second argument, matching the tag-factory shorthand (tag(props, children)). This removes the lastnodes:prop from the public framework surface (tag factories migrated in 1.3.0). Signature:ErrorBoundary(children: () => Element): Element; ErrorBoundary(options: ErrorBoundaryOptions, children: () => Element): Element;
Migration:
// Before ErrorBoundary({ nodes: () => RiskyArea(), fallback: (err, retry) => …, onError, resetKeys, }); // After ErrorBoundary( { fallback: (err, retry) => …, onError, resetKeys }, () => RiskyArea(), ); // Options-free form ErrorBoundary(() => RiskyArea());
ErrorBoundaryPropsis retained as a deprecated alias of the renamedErrorBoundaryOptionsso type imports keep compiling.
Reactivity-core rewrite. Replaces the Set<Subscriber> / Map<Signal, epoch> subscription graph with doubly-linked SubNode edges, a node pool, and an __activeNode back-pointer for O(1) duplicate-dependency detection. Subscription is now O(1) on both add and remove, the hot path has no hash operations, and GC churn on create/destroy workloads drops sharply.
Improvements over 2.1.0 on the reactivity stress-test suite (benchmarks/):
- Wide graph / 10k fan-out: ~73% faster (56.8 ms → 15.4 ms)
- Cascading effects: ~41% faster (2.03 ms → 1.20 ms)
- Memory & cleanup: ~21% faster (51.3 ms → 40.6 ms)
- Component tree propagation: ~10% faster (23.0 ms → 20.6 ms)
- Deep computed chain: ~7% faster (3.87 ms → 3.60 ms)
201/201 test files, 2187/2187 tests passing. No breaking changes to the documented public API — signal, derived, effect, batch, untracked, on, setMaxDrainIterations, setMaxSubscriberRepeats, devtools introspection helpers, all behave identically.
cleanup(subscriber)now exported fromsibujs/reactivity/track. Disposes a subscriber directly without allocating an intermediate closure. Enables custom effect-like primitives to manage their own lifecycle without going throughtrack()'s disposer.getSubscriberCount(signal)— O(1) count of active subscribers, read from the__sccounter maintained on every subscribe/unsubscribe.getSubscriberDeps(subscriber)— returns the signals a subscriber currently depends on, in record order. Replaces the previous_dep/_depsprobe used by devtools.forEachSubscriber(signal, visit)— iterate a signal's subscriber list without exposing the internal linked-list structure to callers.
- Subscription storage migrated from Set + Map to doubly-linked
SubNodeedges. Each(signal, subscriber)pair is one object linked into both the signal's subscriber list and the subscriber's dep list. O(1) subscribe / unsubscribe via pointer splice, no hash operations on the hot path, one allocation per edge instead of two. - Duplicate-dependency detection during tracking is now O(1) via a
signal.__activeNodeback-pointer (Preact Signals' approach). A subscriber with 10 000 deps reading one signal twice no longer pays O(N²) in its inner loop. - Effects now re-run via
retrack()instead oftrack(). Stable-dep effects (the overwhelmingly common case) skip the cleanup-and-rebuild cycle entirely — epoch-based pruning at end of run handles any deps that were dropped this invocation. On the Cascading Effects benchmark this drops per-invocation cost by ~40 ns. - Effect internals consolidated behind an
EffectCtxobject. Per-effect closure count went from six (onCleanup,flushUserCleanups,wrappedFn,drainReruns,subscriber,dispose) to three.runSubscriberandrunBodyare inlined directly into the per-effect closures, eliminating a function frame per invocation. track()is stack-free. The sharedsubscriberStackarray is gone;track()uses a localprev/ restore pattern, andsuspend/resumeTrackingcapturecurrentSubscriberdirectly. ~5–10 ns saved per track call, universal improvement.- Signal state pre-initialises every internal slot (
__v,__sc,subsHead,subsTail,__activeNode,__name) at construction. V8 hidden classes stay monomorphic across all signals; inline caches in the reactivity hot paths don't transition on first subscribe. - Signal setter specialised at creation time — one closure for the default
Object.isequality path, one for customequals, one dev-mode variant carrying the devtools-hook emission. No per-call branching on the hot path. - Cached
track()disposer viasub._dispose ??= …— allocated once per subscriber instead of once pertrack()call. Meaningful for high-churn workloads (large lists, create/destroy cycles). - Node pool (cap 4 096) recycles freed
SubNodeobjects. Shape-stable allocation keeps hidden classes monomorphic; a create/destroy cycle with 25 000 effects reuses edge nodes instead of allocating and freeing them.
signal.__s— the Set-based subscriber cache. Replaced bysubsHead/subsTaillinked-list anchors plus__sc(count). External consumers should read counts viagetSubscriberCount().signal.__f— the single-subscriber fast-path cache. A one-node linked-list walk is inherently as fast as the check it was avoiding.- Internal
subscriberStack— the shared push/pop array used by the oldtrack()/suspend/resumepair. Not observable from user code.
introspect.tsdelegates to the newgetSubscriberCount/getSubscriberDeps/forEachSubscriberhelpers. Public API surface (ReactiveNodeInfo,getSignalName,getDependencies,inspectSignal,walkDependencyGraph) unchanged.devtools.tsreadsnode.ref?.__scinstead ofnode.ref?.__s.size.- A three-color CLEAN/CHECK/DIRTY propagation model was prototyped and reverted after benchmark regression (+122% on Deep Chain). The workloads in the current suite all produce new downstream values on every signal change, so the CHECK state has no work to skip — only overhead to add. A dedicated benchmark suite for stabilisation patterns needs to come first; re-introducing three-color propagation is parked for a future release.
Reactivity-core hardening release. Closes correctness gaps around effect re-entry, derived stale deps, sibling-effect consistency, and cycle detection. 201/201 test files, 2187/2187 tests passing — no behavior changes to user code that was already correct.
-
Effects that write to a signal they subscribe to no longer silently drop the update. Previously the re-entrant invocation was dropped with a dev-only warning, leaving the effect's observed state out of sync with reality. Now the update is flagged as
rerunPendingand the effect re-runs after its current body completes, converging on consistent state. A 100-iteration safety cap breaks legitimate write-reads-self cycles with a loudconsole.errorinstead of hanging. -
derived()no longer accumulates stale dependencies on conditional code paths. A getter like() => flag() ? a() : b()used to keep bothaandbsubscribed forever once both had been read, causing spurious re-evaluations whenever the untaken branch fired. Theretrack()pull path now tags each dependency with a per-evaluation epoch and unsubscribes any edge whose epoch is stale at end of run — bounded memory, no spurious work. -
Sibling effects now converge to consistent state through the outermost notification. Previously two paths of
notifySubscribersdiverged: the pure-effect fast path allowed re-enqueue (effects could run twice, final state consistent), while the mixed-computed slow path forbade it (effects ran once, possibly observing stale downstream state). Both paths now share a single drain with at-most-once enqueue dedup cleared before invoke — sibling effects that cross-write converge rather than one losing to the other. -
Unbounded empty-
__sallocation per signal. Signals whose last subscriber disposed kept an empty subscriberSeton the signal object for the process lifetime. The set is now cleared when size drops to zero. -
subscriberStacknever released memory after a one-off nesting spike. A transient deep-nesting excursion (e.g. a debug-mode traversal) could double the stack and retain it forever. The stack now shrinks lazily at end-of-track()when idle and over-allocated.
-
Cycle detection is now per-subscriber repeat-counted instead of total-iteration-capped. The previous 100 000-iteration cap conflated "infinite cycle" with "legitimate large fan-out" — apps with 100k+ effects in one batch flirted with false positives while real tight cycles could burn the full budget before tripping. The new detector counts per-subscriber firings within a drain and bails when any single subscriber exceeds
maxSubscriberRepeats(default 50) — accurate, cheap, and tolerant of arbitrary legitimate fan-out. The absolute iteration cap is retained as a safety net at 1 000 000. -
setMaxDrainIterations(n)is now the safety-net knob rather than the primary cycle check; semantics unchanged for callers, default raised from 100 000 → 1 000 000.
setMaxSubscriberRepeats(n)— raise/lower the per-subscriber repeat cap used for cycle detection. Returns the previous value.
-
Subscriber dep storage in the reactivity core migrated from
Set<signal>toMap<signal, epoch>to carry per-edge epoch tags forretrack()pruning. Public API unchanged; the single-dep fast path still avoidsMapallocation entirely. -
__f/__sfast-path invariant centralized in asyncFastPath()helper — same performance, simpler to reason about across add/remove sites. -
Devtools
introspect.getDependencies()updated for the newMaplayout; return type unchanged.
Major hardening + features release. Spans reactivity, rendering, SSR, widgets, security, and build tooling. 2187/2187 tests passing, zero lint errors, zero type errors.
-
Adapter method renames —
redux.useSelector→redux.select,zustand.useSelector→zustand.select. Theuse*prefix is no longer used anywhere in the framework.// before const count = redux.useSelector(s => s.count); // after const count = redux.select(s => s.count);
-
useDefaultPluginRegistryrenamed tosetDefaultPluginRegistry— aligns with the verb-based convention used elsewhere. -
loadRemoteModule()now refuses un-allowlisted URLs — previously warned in dev and loaded anyway. Now rejects unless{ allowedOrigins: [...] }or{ unsafelyAllowAnyOrigin: true }is passed (CWE-829 supply-chain hardening). -
loadWasmModule()/preloadWasm()require origin allowlist — same policy asloadRemoteModule. Options bag now disambiguated viaallowedOrigins/unsafelyAllowAnyOriginkeys only. -
compiled.staticTemplate()/precompile()requireTrustedHTML— arbitrary strings no longer accepted to prevent silentinnerHTMLXSS sinks. Mint viatrustHTML(raw)after your own sanitization pass. -
Router refuses protocol-relative redirects —
"//evil.com/path"style redirect targets now throwNavigationFailureErrorinstead of logging a warning (CWE-601 open redirect). -
hydrate()/hydrateIslands/hydrateProgressivelyuse replace strategy — the prior in-place attribute-reconciliation silently orphaned reactive bindings to the discarded client tree, leaving the visible DOM frozen. The client subtree now replaces the server subtree (island markers anddata-sibu-hydratedpreserved) so reactive bindings actually drive the DOM. -
socket()/stream()defaultmaxReconnectsis now 10 — was effectively unbounded. Permanently broken URLs no longer hammer servers forever. Exponential backoff with jitter added. -
optimisticList()deprecated aliases removed —addOptimistic/removeOptimistic/updateOptimisticwere deprecated in 1.5.0 and are now gone. Useadd/remove/update. -
contentEditable().setContentsignature widened — takes either a string (raw HTML, legacy) or{ text, html, sanitize }. The options form is the recommended path.
-
retrack()reactivity primitive for derived pull-path — skips thetrack()cleanup pass; uses save/restore ofcurrentSubscriberinstead of stackTop push/pop. Steady-state chains avoid Set.delete+add churn. -
effect((onCleanup) => { … })— canonical teardown pattern now built in. User cleanups run in reverse registration order before every re-run and on dispose; throwing cleanups are isolated and logged.effect((onCleanup) => { const handler = () => { … }; window.addEventListener("resize", handler); onCleanup(() => window.removeEventListener("resize", handler)); });
-
derived(getter, { equals })— custom equality suppresses notifications when the recomputed value is equivalent to the previous. -
Disposecanonical type exported fromsibu. -
Widget ARIA
bind()layer — every headless widget now ships abind(els)that wires roles, keyboard, and idempotent teardown per WAI-ARIA APG:Tabs— role=tablist, roving tabindex, Arrow/Home/EndAccordion— aria-expanded/controls, Enter/SpaceTooltip— role=tooltip, aria-describedby splice, Escape dismiss, hoverable gracePopover— role=dialog, aria-haspopup, Escape + click-outsideCombobox— Combobox 1.2 pattern, aria-activedescendant, typeaheadSelect— role=listbox, aria-multiselectable, typeahead, disabled-awareFileUpload— labeling, aria-describedby splice, drop-zone keyboarddatePicker— role=grid, arrow/Home/End, PageUp/Down, Shift+PageUp/Down (year)- All
bind()returns are idempotent via WeakMap and restore every touched attribute on dispose.
-
takePendingError()exported — ErrorBoundary now scans mounted subtrees for stashed errors fromlazy()rejections that beat any boundary to mount. Multiple pending errors wrapped inAggregateError. -
trustHTML(html)+TrustedHTMLtype re-exported fromsibu(was only onsibu/ssrandsibu/performance, which minted incompatible brands). -
Test-reset helpers —
__resetQueryCache,__resetDialogStack,__removeRouterPagehideHandler. -
Build/release hardening —
tsup --clean;./cdnsubpath export;publishConfig.access=public+provenance=true;publish.mjspublishes BEFORE git commit/tag (so a publish failure leaves no orphan commit). -
10 new tests —
keepAlive.test.ts,pluginRegistry.test.ts,widgetsAria.test.ts.
derivedpull-path correctness undersuspendTracking— new conditional deps register their markDirty subscription even when the outer caller is inuntracked()context.propagateDirtyis iterative (no recursion) with already-dirty skip — closes O(depth²) walk on deep chains.batch.flushBatchwrapped in try/finally — a throwing subscriber can't strandpendingSignalsfor the next batch.effect()disposer idempotent — double-dispose no longer re-emitseffect:destroyor re-walks subs lists.effect()re-entry detection — a re-entering update now warns in dev and drops (was silent).bindChildNodediff — O(n²) nested scan replaced with O(n+m) Set-based reuse detection; dedupes duplicate node refs in the output array.- Dead
signalSubscribersWeakMap removed (the__sproperty cache is authoritative).
dispose()re-entry safe — snapshot-then-delete + bounded extra-pass drain.Array.from(childNodes)snapshot guards against disposers mutating the tree mid-walk.onUnmountfalse-fires on same-tick re-parent —fireUnmountdefers one microtask and re-checksisConnected.lifecycledescendant walk short-circuited for leaf insertions.keepAlivedisposed-flag prevents post-dispose microtask writes; cached subtrees properly disposed on anchor teardown.eachitemGetter wraps inuntracked()so per-row consumers don't subscribe to the whole-array signal.each,portal,lazy.Suspenseerror propagation — CustomEvent dispatched on the anchor's Element parent (Comment anchors don't bubble); deferred one microtask for pre-mount races.lazy()pending-error stash (PENDING_ERRORmarker) — ErrorBoundary scans descendants on mount so failures before any boundary mounts aren't silently lost.hydrateProgressivelyisland marker preserved on replacement.
workerFnpool crosstalk — per-worker FIFO queue withaddEventListener; terminate-on-error so concurrentrun()calls can't mis-route results.worker()top-level usesaddEventListener+ terminate-on-error.infiniteQueryrun-id generation — stale responses discarded;AbortController.abort()at top of effect.offlineStoreatomic writes —idbPutWithChange/idbDeleteWithChangesingle-transaction acrossitems+_changes; cursor-snapshotted sync; pull skips items with pending local edits (conflict avoidance);idbPutManybatches remote items;closedflag checked between awaits;sync()error now logs viadevWarn(was silent).querydedup capturesentry.promiselocally and re-checks identity after await; sync-throw fromwithRetrycleaned up;onSettledinfinally;dispose()idempotent + gcTimer deduplicated.chunkLoadertrue LRU withlastAccess;invalidate(id)clearspreloaded;this.loadreplaced with closure reference (destructure-safe); preload.delete(id)on failure.serviceWorkerlistener refs tracked; priorstatechangedetached before reassignment; all detached inunregister().incrementalRegeneration,routerSSR,wakeLock,clearQueryCacherefetchers —.catchinstead of silent.mutation.mutate()fire-and-forget rejection now warns (was silentcatch(() => {})).
runInSSRContextuses Node'sAsyncLocalStoragewhen available so concurrent requests don't sharessrMode/suspense counters.serializeStatebyte cap viaTextEncoder; escapes U+2028/9; drops the__SIBU_SSR_STATE_RAW__fallback (defeated escape).deserializeStatedev-warns when novalidateguard is passed.
datePickermonth/year overflow — uses day-1 anchor (no Jan-31→Mar-3 drift).form.wrappedSetclearsmanualErrorson edit (server-side "email taken" errors no longer stick after user edits).Tooltip.bind()teardown splices its id out of the currentaria-describedbyso ids added by other libraries survive.a11y.FocusTrapkeydownremoved on dispose; announce live region checksisConnectedbefore writing.inputMask.bind()returns a dispose function that removes input/focus listeners.customElement._teardownrunsdispose()on rendered subtree before reconnect (reactive bindings no longer leak across reconnects).
router.cleanupNodescallsdispose(node)before detaching — every reactive binding inside a route subtree is torn down on navigation.Route()/KeepAliveRoute()/Outlet()track()teardowns stored inrouteCleanups(was leaking effects).RouterLinkclick listener removed viaregisterDisposer; navigate failures.catch'd.- Router
pagehidelistener lazy-initialized on firstcreateRouter()call (honorssideEffects: false).
URL_ATTRIBUTESexpanded:xlink:href,formtarget,ping,datanow run throughsanitizeUrl()(was bypassed).persist+dragDropJSON.parserevivers block__proto__/constructor/prototype(CWE-1321).eacherror dispatch logs viadevWarnwhen anchor is detached (no silent swallow).
- Spring animation is
dt-aware (REF_DT_MS,MAX_STEP_RATIO=4, NaN-guard) — frame-rate-independent; no runaway on tab-throttle. speech.tssetInterval polls only while actively speaking (was constant 5Hz).socket/streamauto-reconnect — exponential backoff with jitter.
- Error prefix standardized to
[SibuJS](was mix of[Sibu]/[Sibu strict]/[Sibu hydration]). devtools.hmrcallsdisposeNodeon replaced subtrees so HMR reloads don't leak effects/listeners.testing.unmount/unmountAllcalldispose()before clearing DOM (wasinnerHTML = "", leaked every effect/binding).tsconfig.jsondrops"types": ["vitest"]— zerosrc/deps on test-only types.- Unused
biome-ignoresuppressions removed; unused variables cleaned.
Most apps need no changes. If you hit any of these:
redux.useSelector/zustand.useSelector→ rename toselect.useDefaultPluginRegistry→ rename tosetDefaultPluginRegistry.loadRemoteModule(url)without options → pass{ allowedOrigins: [...] }(recommended) or{ unsafelyAllowAnyOrigin: true }for opt-in.loadWasmModule(url)→ same.compiled.staticTemplate(html)→ wrap viatrustHTML(html)after your sanitization.hydrate()consumers relying on preserved server DOM refs → client tree replaces server tree; grab refs after mount.socket({ autoReconnect: true })→ now caps at 10 reconnect attempts; passmaxReconnects: Infinityto restore prior behavior.- Router redirects to
//other-host/path→ now throw; rewrite as relative or absolutehttps://within an allowed origin. optimisticList().addOptimistic/removeOptimistic/updateOptimistic→ rename toadd/remove/update.
Comprehensive bug-fix and hardening release. 30 bugs fixed across 29 files, covering the reactive core, data fetching, state management, routing, rendering, lifecycle, forms, UI utilities, browser composables, and devtools. Full framework audit with 2178/2178 tests passing, zero regressions.
-
optimistic()return shape changed — previously returned a[getter, setter]tuple; now returns a named object{ value, pending, update }. Thependingsignal was created internally but never exposed (Bug: users had no way to show loading indicators). Theupdatemethod now uses a version counter to prevent stale reverts from concurrent operations. Migration:// before const [value, addOptimistic] = optimistic(0); // after const { value, pending, update } = optimistic(0);
-
optimisticList()method names shortened —addOptimistic→add,removeOptimistic→remove,updateOptimistic→update. The old names are kept as deprecated aliases so existing code keeps working.
deepEqualshared-reference false positive — theseenset tracked onlya, not(a, b)pairs. Shared sub-objects compared against different partners were incorrectly treated as equal. Now tracksMap<object, Set<object>>pairs.deepEqualconstructor mismatch —deepEqual(new Date(), {})returnedtruebecause Date has no enumerable keys. Added constructor guard before falling through to key comparison.deepEqualMap/Set not compared —MapandSetcontents were invisible toObject.keys. Added explicit Map (deep value equality) and Set (shallow membership) branches, plus ArrayBuffer and TypedArray support.deepEqualself-referential Map/Set — cycle detection was placed after the Map/Set branches, causing infinite recursion on self-referential containers. Moved cycle detection before all container comparisons.derivedcircular dependency — circular derived chains caused silent stack overflow. Added anevaluatingre-entrance flag that throws a clear"Circular dependency detected"error with the signal's debug name.drainNotificationQueueinfinite loop — an effect writing to a signal it reads could loop forever. Added aMAX_DRAIN_ITERATIONS = 1000cap with a console error diagnostic.deferredValuenever updated — had no reactive subscription on the source getter (noeffect/track). Rewrote to useeffect()for source tracking, scheduling LOW-priority updates via the scheduler.
resource.abort()leftloading()stuck attrue— theAbortErrorcatch returned without resetting the loading signal. Now callssetLoading(false)in the abort path.querysubscriber leak on same-key re-run — effect re-runs with an unchanged key double-countedentry.subscribers, preventing cache GC. Now only increments when the key actually changed or the entry has zero subscribers.mutationconcurrent state clobbering — rapidmutate()calls raced without guard. Added arunIdversion counter; stale responses are silently ignored.withRetryabort listener leak — theabortevent listener onAbortSignalwas never removed when the delay timer resolved normally. AddedremoveEventListenerin the timer resolve path.
optimisticconcurrent stale reverts — each operation now gets a version number; reverts only fire if no newer operation has started. Prevents stale snapshots from overwriting fresher optimistic state.optimisticpendingnever exposed — thependingsignal was created but never returned. Now exposed in the return object for bothoptimisticandoptimisticList.optimisticList.updateOptimisticpredicate failure after patch — the success-path predicate re-ran against the already-mutated item. If the patch changed the matched property, the server result was silently dropped. Now captures patched references during the optimistic phase and matches by identity in the success path.persistedeffect not stopped bydispose()— the persisting effect's return value was discarded, sodispose()only removed the storage listener but left the effect running. Now captured and called indispose().globalStoreshallow initial copy —reset()could fail to fully restore nested objects if they were mutated in-place. Changed toJSON.parse(JSON.stringify(...))for a deep copy of initial state.
- Wildcard route too permissive —
/admin/*incorrectly matched/admin-panelbecause the check usedpath.startsWith(basePath)without a segment boundary. Now requirespath === basePath || path.startsWith(basePath + "/"). - Guard timeout/abort listener leak — when
next()was called asynchronously, the microtask-based cleanup had already run and missed it. MovedclearTimeout+removeEventListenerinto thenext()callback itself. The abort handler now also clears the timeout timer.
dispose()one throwing disposer aborted entire subtree cleanup — wrapped each disposer call in try/catch with a dev-mode warning.onMountcleanup return discarded — the type signature accepted a cleanup return function butsafeCalldiscarded it. Now captured and registered viaregisterDisposer(element, cleanup).onMountMutationObserver leaked — if an element was disposed before ever connecting to the DOM, the observer ondocument.bodyran forever. Now registered for cleanup viaregisterDisposer.onUnmountobserver ran for element's entire lifetime — the MutationObserver ondocument.bodyfired on every DOM mutation globally. Now registered for cleanup viaregisterDisposerand the callback itself is also wired throughregisterDisposeras the primary teardown path.Portalcleanup via MutationObserver only — didn't integrate withdispose()/when()/match()/each(). Replaced withregisterDisposer(anchor, ...)so portal content is properly disposed and removed through the standard dispose system.lazystale load — if the container was removed before the dynamic import resolved, the rendered component leaked subscriptions. Added adisposedguard that silently drops stale.then()/.catch()callbacks. Removed dead_status/_errorsignals that were created but never read.
bindFieldmerge order —{...fieldOn, ...extraOn}let extras clobber field handlers (input/change/blur). Contradicted the 1.0.4 fix intent. Flipped to{...extraOn, ...fieldOn}so field handlers always win.form.handleSubmitdouble-submit — no guard against concurrent async submissions. Added asubmittingsignal;handleSubmitchecks it before calling the callback and resets on resolve/reject. Exposed asform.submitting()onFormReturn.inputMaskcursor jump — no cursor position restoration after mask application; cursor jumped to end on every keystroke. Added cursor tracking that counts raw chars before the old cursor position and places the cursor after that many filled slots in the masked output.inputMaskstrip regex too aggressive —/[^a-zA-Z0-9]/gstripped all special characters, making*mask slots unable to accept non-alphanumeric input. Now builds a pattern-aware strip regex: patterns with*only strip literal mask characters.transitionrapid enter/leave — stalesetTimeoutcallbacks from a previous enter/leave fired during the opposite animation, corrupting class state. AddedactiveTimertracking withcancelPending()at the start of each enter/leave.scopedStylepseudo-element scoping — scope attribute was appended after::before/::afterpseudo-elements, producing invalid CSS selectors. Now splits at::and inserts[attr]before the pseudo-element.VirtualListscroll listener leak — the scroll event listener was never cleaned up. AddedregisterDisposerwithremoveEventListener.dialogno dispose — the global keydown listener leaked if the dialog was open when the component was destroyed. Addeddispose()method that detaches the listener and resets state.FocusTrapobserver scope — MutationObserver watched only the direct parent; ancestor removal leaked the observer and missed focus restore. Changed todocument.bodywithsubtree: true. AddedregisterDisposerintegration for SPA cleanup. Zero-focusable-elements case now callse.preventDefault()to prevent Tab from escaping the trap.
urlStatemissinghashchangelistener — anchor clicks andlocation.hashassignments don't firepopstate, sohash()went stale. Addedhashchangelistener alongsidepopstate. Added deduplication guard to avoid unnecessary signal notifications.setHash("#")now clears the hash instead of keeping a bare#.scrollnon-reactive target — the scroll target element was resolved once at creation and never re-evaluated. Rewrote to useeffect()for reactive target tracking, re-attaching the listener when the element changes (same pattern asresize/dragDrop).socket.close()auto-reconnected — theonclosehandler couldn't distinguish manual close from unexpected disconnect. Added amanuallyClosedflag set inclose()and checked inoncloseto suppress auto-reconnect.
createTraceProfilersubscribed to non-existent events — listened foreffect:start/effect:end/signal:setbut the core emitseffect:create/effect:destroy/signal:update. Fixed event names and changed to instant ("I") events since the core doesn't emit begin/end pairs.
optimistic()returns a named object —{ value, pending, update }instead of[getter, setter]. See Breaking section.optimisticList()shorter method names —add/remove/updatewith deprecatedaddOptimistic/removeOptimistic/updateOptimisticaliases.deepSignalreturn type — now infers fromsignal()directly, preserving theAccessor<T>brand on the getter.hotkeyglobaloption removed — was declared but never used (dead code).contextJSDoc updated — accurately describes global reactive store semantics instead of falsely promising subtree-scoped DI.- JSDoc examples across 17 source files — ~35 code examples converted from legacy
{ nodes: }form to canonical positional shorthand. - README — updated to canonical shorthand authoring style;
$(pattern matching)$typo fixed.
deepSignal.test.ts— expanded from 4 → 52 tests covering Map, Set, TypedArray, shared refs, cycles, constructor mismatch.urlState.test.ts— expanded from 6 → 20 tests covering hashchange, dedup, edge cases, SSR.optimistic.test.ts— expanded from 5 → 17 tests covering pending, concurrent guards, predicate-after-mutation.- Full suite: 2178 / 2178 passing (up from 2105 in 1.4.0). Zero regressions.
Cleanup release. Removes six public aliases that contradicted the SibuJS philosophy — plain verbs, no framework ceremony, no redundant synonyms for the same primitive. All of the removed APIs were either one-line forwards to an existing primitive or identity wrappers; every existing example can be rewritten by deleting the wrapper and calling the underlying primitive directly.
createSignal— wasreturn signal(value). Usesignal()directly.createMemo— wasreturn derived(fn). Usederived()directly.createEffect— wasreturn effect(fn). Useeffect()directly.memo— wasreturn derived(factory). Usederived()directly.memoFn— wasreturn derived(callback). Usederived()directly.composable— wasreturn setup(identity function). Plain functions are already composables in SibuJS; just write one and call it.
The three removed files (src/patterns/primitives.ts, src/core/signals/memo.ts, src/core/signals/memoFn.ts) are currently empty stubs exporting nothing — they can be deleted from disk in a follow-up commit without further code changes.
// before
import { createSignal, createMemo, createEffect, memo, memoFn, composable } from "sibujs";
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
const sorted = memo(() => items().slice().sort());
const handler = memoFn(() => (e: Event) => process(e));
createEffect(() => console.log(count()));
const useCounter = composable(() => { /* … */ });
// after
import { signal, derived, effect } from "sibujs";
const [count, setCount] = signal(0);
const doubled = derived(() => count() * 2);
const sorted = derived(() => items().slice().sort());
const handler = derived(() => (e: Event) => process(e));
effect(() => console.log(count()));
function useCounter() { /* … */ }generateComponentMetadata,generateTypeStubs, and the Vite/Webpack pure-annotation factory list insibujs/buildno longer mention the removed names.- Lint rule
no-signals-in-conditionalsno longer checksmemo/memoFn(they don't exist). SignalNodeSnapshot.kindcomment updated to drop the"memo"tag.- Test suite:
tests/primitives.test.ts,tests/memo.test.ts,tests/memoFn.test.tsreduced to placeholder stubs;tests/types.test.tsandtests/ide.test.tsupdated to assert the aliases are gone. Suite: 2105/2105 passing (down from 2113 by exactly the 8 deleted alias-specific tests).
Large minor release. Adds 27 new reactive/DOM primitives, a full SSR + OWASP security hardening pass (A01, A02, A03, A10 + CWE-1321 prototype pollution), 10 ergonomic features that stay inside the SibuJS philosophy (No VDOM, No JSX, No compilation, Zero dependencies, fine-grained reactivity), typed tag factory overloads for common elements, and a new tag(props, children) positional shorthand that removes the need for the nodes: key at every level of the tree. Test suite grew from 1875 → 2113 passing tests (+238, 0 regressions).
visibility()— Page Visibility API wrapper. Pause polling / animations while the tab is hidden.network()— Network Information API reactive getters (effectiveType,downlink,rtt,saveData). Adapt image quality and prefetching to the real connection, not just online/offline.mouse({ target?, touch? })— reactive pointer position with optional touch unification.swipe(target, { threshold?, onSwipe? })— touch swipe detection with configurable threshold and direction callback.windowSize()— reactive viewport dimensions via theresizeevent (complements the element-scopedresize()).urlState()— reactive URL search params + hash withsetParams/setHashbacked byhistory.pushState/replaceStateandpopstatesync. Independent ofcreateRouter().broadcast(channelName)— BroadcastChannel wrapper exposing a reactivelastsignal and apost(message)sender.fullscreen()— Fullscreen API with reactiveisFullscreen/elementplusenter/exit/toggle.wakeLock()— Screen Wake Lock API with auto re-acquire onvisibilitychange.animationFrame({ fpsLimit?, immediate? })— reactivedelta/elapseddriven byrequestAnimationFrame, withpause/resume/disposeand optional FPS limit.mutationObserver(target, options)— reactive DOM MutationObserver wrapper. Escape hatch for reacting to DOM changes outside the reactive system.bounds(target)— reactivegetBoundingClientRect(). Updates on resize (ResizeObserver) AND on window scroll (capture-phase passive listener), so absolute top/left stay accurate for overlays.keyboard({ target?, keys? })— reactive set of currently-pressed keys with optional filter. Clears onwindow.blurto avoid stuck modifiers.speech()— Web Speech Synthesis wrapper with reactivespeaking/pausedandspeak(text, options)supporting rate / pitch / volume / voice / lang.gamepad()— Gamepad API as reactive snapshots. Auto-polls viarequestAnimationFrameonly when at least one pad is connected, and emits updates only when button or axis state actually changes (deep equality short-circuit).pointerLock()— Pointer Lock API with reactivelockedsignal andrequest(el)/exit().vibrate(pattern)— thin Vibration API wrapper; returnsfalseon unsupported platforms.favicon(url)/svgFavicon(svg)— runtime favicon updater. Creates the<link rel="icon">if missing;svgFaviconencodes inline SVG to a data URI for notification-count badges.textSelection()— reactive text-selection tracker (text,rect,hasSelection,clear) for building selection toolbars and citation tools. Syncs viaselectionchange(mouse drag, Shift+arrow, touch select).imageLoader(src)— reactive image-load status ("pending"|"loaded"|"error") plus intrinsicwidth/height. Prevents CLS in lazy galleries. Gracefully aborts in-flight loads ondispose().
defer(getter)— deferred mirror of a reactive getter. Converges to the source on a microtask +requestAnimationFrameso expensive derived views lag behind fast input.transition()—{ pending, start }handle that schedules work onrequestIdleCallback(with rAF / setTimeout fallback).pending()stays reactive for both sync and async bodies; exceptions reset the state cleanly.nextTick()— await for DOM flush. Resolves on microtask + rAF so imperative code can read post-render state.asyncDerived(factory, initial)— async counterpart ofderived(). Reactivevalue/loading/errortriple with stale-response cancellation and arefresh()trigger.createId(prefix?)— stable unique id generator for a11y pairing (aria-labelledby,for+id). Exports__resetIdCounter()for deterministic tests and SSR.strict(fn)/strictEffect(fn)— dev-only double-invocation helpers that surface cleanup bugs (missing disposers, duplicate listeners). No-op in production.escapeScriptJson(json)— exported helper used internally byserializeState/serializeRouteState/setStructuredData. Escapes<,>,&,U+2028,U+2029.
interval(fn, ms)— declarativesetIntervalhandle withstop/pause/resume/isRunning.timeout(fn, ms)— declarativesetTimeouthandle withcancel/isPending.hover(target)— reactive hover tracker usingpointerenter/pointerleave(touch-friendly).scrollLock()— stacked body scroll lock that compensates for scrollbar width. Multiple concurrent overlays each own a handle; only the lastunlock()restores the original styles.formAction(fn)— async form-action wrapper: reactivepending/error/result/reset/onSubmit.onSubmitis a ready-to-attach<form>handler that builds aFormDataand invokes the action. Stale-response guard drops older in-flight calls on re-submit.createFocusManager(container, options?)— headless focus walker (focusFirst/focusLast/focusNext/focusPrev) with optional loop wrap-around.createListbox(container, options?)— full ARIA listbox wiring:role="listbox",aria-activedescendant, Arrow / Home / End / Enter / Space keyboard navigation, click-to-select, multi-select. Stamps stable ids on every option viacreateId().createDialogAria(element, options?)— returns stabletitleId/descriptionId, setsrole="dialog"(or"alertdialog"),aria-modal,aria-labelledby/aria-describedby,tabindex="-1". Intentionally decoupled from focus trap and Escape-to-close.
LazyRouteshorthand —{ path: "/page", lazy: () => import("./Page") }is now accepted as a route definition.createRouter()andsetRoutes()normalize the route tree recursively, so nested children get the shorthand too.
hydrate(component, container, { diagnostics, onMismatch })— dev-mode tree walker that reports the first tag / attribute / child-count / missing-child mismatch. Internal markers (data-sibu-ssr,data-sibu-hydrated,data-sibu-island) are excluded. Stops after five findings to prevent log spam on a broken tree.HydrateOptionsandHydrationMismatchtypes exported fromsibujs/ssr.renderToSuspenseStream(element, pending, { nonce? })— newnonceoption propagated to the swap scripts for strict-CSP compatibility.serializeState(state, nonce?)/serializeRouteState(state, nonce?)— optionalnonceargument for strict-CSP.
ErrorDisplay(props)— shared rich error UI with copy-to-clipboard (full message + stack + cause + metadata + env), colored severity header (error/warning/info), colored error-code badge (fromerror.codeorerror.name), parsed stack frames (Chrome/V8 + Firefox/Safari formats),Error.causechain walked recursively, metadata + environment sections (URL, UA, ISO timestamp), optional retry + reload buttons. Dev/prod split — stack and metadata hidden in prod unlessalwaysShowDetails: true.ErrorBoundary— newresetKeys: Array<() => unknown>prop. When any listed reactive getter changes after an error has been caught, the boundary auto-resets and re-renders the subtree.
captureSignalGraph()— synchronous snapshot of every observed signal node (id, kind, value preview, subscribers, dependencies, eval count). Empty snapshot when devtools are not enabled so tests and production code can call it unconditionally.diffSignalGraphs(before, after)— classifies nodes intoadded/removed/reevaluated. Useful for regression assertions like "navigating to /page X must not add more than N new signals".createTraceProfiler()— subscribes toeffect:start/effect:end/signal:setevents and emits a Chrome tracing JSON blob viastopTrace(). Drop the output intochrome://tracingorui.perfetto.devfor a flamegraph. Distinct from the existingcreateProfiler()incomponentProfiler.ts, which tracks per-component render counts.
queryByText/queryByTestId/queryByRole/queryByLabel— non-throwing finders.findByText/findByTestId/findByRole— async finders that poll untiltimeout.waitForSignal(getter, predicate, { timeout })— signal-aware wait. Subscribes to the getter and resolves immediately when the predicate matches, instead of polling.type(element, text)— dispatches oneInputEventper character + a finalchangeevent for realistic keyboard simulation.
-
tag(props, children)positional shorthand — every tag factory now accepts the children as an optional second argument. This removes the last reason to writenodes:in nested trees:div({ class: "page" }, [ h1({ class: "title" }, "Welcome"), div({ class: "row" }, [ label({ for: "email" }, "Email"), input({ id: "email", type: "email" }), button({ class: "primary", type: "submit" }, "Submit"), ]), ])
All legacy forms (
tag({...props}),tag("className", children),tag("text"),tag([...]),tag(node),tag(() => child)) continue to work unchanged. When bothprops.nodesand the positional second-arg are present, the positional wins. -
Per-element typed prop overloads —
a,input,img,button,form,select,textarea,label,option,video,audionow have element-specific prop interfaces (AnchorProps,InputProps,ButtonProps,FormProps,SelectProps,TextareaProps,LabelProps,OptionProps,ImgProps,VideoProps,AudioProps,MediaProps,InputType) with full IDE autocomplete and typo detection. Runtime unchanged; the stronger typing is a zero-costTypedTagFunction<Props, El>cast insidehtml.ts. The[attr: string]: unknownescape hatch is preserved for custom attributes. -
TypedTagFunction<Props, El>type exported for building custom typed factories.
persisted(key, initial, options)— newsyncTabsoption (defaulttruefor localStorage). Listens to thestorageevent so changes in one tab propagate to others. Reentry-guarded against bounce-back.nullnewValue from another tab resets toinitial.- The returned setter now carries a non-enumerable
dispose()method that removes the cross-tab listener — previously there was no way to clean it up.
- Tag factory dispatch rewritten — strings / numbers / arrays / nodes / functions each own an explicit branch, and the props-object path resolves children as
second ?? props.nodes. Unblocks thetag(props, children)shorthand at every level of the tree. No hot-path regression — the fast paths fortag(),tag("text"), andtag([...])still short-circuit. ErrorBoundary's default fallback is now rendered byErrorDisplay. The legacy inline renderer and its local stack parser were removed. AnyErrorBoundarywithout a customfallbackprop gets the richer UI automatically.withSSR(fn)is nesting-safe — saves the prior SSR flag intowasSSRand only callsdisableSSR()on exit when the outer scope was not already in SSR mode. A nestedwithSSR(...)call that throws no longer flips the outer scope's SSR flag back tofalse.routerSSR.renderRouteToDocumentdelegates meta/link/bodyAttrs validation to the shared hardened helper fromplatform/ssr.ts— the hand-rolled duplicate escaping functions are removed.tsconfig.jsonadds"lib": ["ES2022", "DOM", "DOM.Iterable"]soObject.hasOwnresolves while keepingtarget: ES2020.
ErrorBoundaryresetKeysedge-cases — a key-getter that throws is treated as a valid reactive dependency and does not crash the effect.bindAttributerefuseson*event-handler attribute bindings with a dev-mode warning that suggests the safeon: { click: fn }prop instead. Previously,bindAttribute(el, "onclick", () => "alert(1)")would callsetAttribute("onclick", ...)and turn the string into inline JS.machine(...)context merge — replaced{ ...ctx, ...patch }with a filtered loop that drops__proto__/constructor/prototypekeys. Prevents prototype pollution from action-returned patches parsed out of JSON.scopedStyle()— CSS sanitizer now decodes CSS hex escapes (\75 rl(→url() before the dangerous-pattern scan, closing the obfuscation bypass forurl()/expression()/@import/-moz-binding/behavior.persisted()— the cross-tabstoragelistener can now be cleaned up via a non-enumerabledispose()method on the returned setter.routerSSR.parseURL— wrapsdecodeURIComponentin a try/catch so malformed percent-sequences no longer crash SSR (DoS vector).paramsandquerynow useObject.create(null)and filter forbidden keys.
A complete OWASP audit beyond the top 10 was performed, with three review passes and 74 dedicated security tests.
A01 Broken Access Control
- Router
navigate()— refusesjavascript:,data:,vbscript:, andblob:URIs at every entry: the top-levelnavigate()call,beforeEachguard redirects,beforeEnterguard redirects,route.redirect, andbeforeResolveguard redirects. Previously these could land inhistory.stateand be reflected into anchor hrefs.
A02 Cryptographic Failures
persisted()JSDoc no longer references a "simple XOR cipher for illustration" — the example now clearly states that XOR andbtoa()/atob()are NOT encryption and points to AES-GCM via the Web Crypto API.persisted()cross-tab listener now cleanable (see Fixed).
A03 Injection (XSS / prototype pollution / CSS injection)
renderToString/renderToStream— attribute names validated against^[A-Za-z_:][-A-Za-z0-9_.:]*$;on*event-handler attributes dropped; URL-bearing attributes (href,src,action,formaction,cite,poster,background,srcset,ping,manifest,data,xlink:href) routed throughsanitizeUrl; attribute values escaped against both"and';<script>and<style>elements stripped from the serialized output; comment-terminator forms (-->,--!>,<!--, trailing--) escaped inside comment bodies.renderToDocument— meta / link / bodyAttrs attribute names validated viabuildAttrString;on*keys dropped; URL attributes pass throughsanitizeUrl;<meta http-equiv="refresh" content="0;url=javascript:…">detected and refused viaisDangerousMetaRefresh; the pagetitleis HTML-escaped; scriptsrcentries go throughsanitizeUrl.serializeState/serializeRouteState/setStructuredData— JSON payloads escaped against<,>,&,U+2028,U+2029so nothing inside a string literal can close the<script>tag or break out of JS string context on pre-ES2019 engines.suspenseSwapScript(id)— ids validated against^[A-Za-z0-9_-]+$and rejected otherwise. Previously a crafted id could inject context-breakers into the CSS selector or the JS string literal.bindAttribute— refuseson*event handlers (defense-in-depth — the tag factory already filters them, butbindAttributeis exported and could be called directly).machine(...)— filtered prototype-pollution keys from action-returned context patches.scopedStyle— CSS escape-sequence obfuscation bypass fixed (see Fixed).
A10 Server-Side Request Forgery (client-side analogue)
socket()—validateWsUrl()restricts WebSocket URLs tows:///wss://and strips control characters that would bypass a naïvestartsWithcheck.stream()—validateSseUrl()routes EventSource URLs throughsanitizeUrl()to blockjavascript:/data:/blob:.
CWE-1321 Prototype pollution
routerSSR.parseURL—paramsandquerycreated withObject.create(null);__proto__/constructor/prototypefiltered from both query-string parsing and pattern-captured route params.hydrateIslands/hydrateProgressively— island lookups go throughObject.hasOwninstead of direct indexing. Adata-sibu-island="__proto__"marker cannot resolve toObject.prototype.
Head tag hardening
Head— meta / link / script attribute names validated;on*keys rejected;base.hrefrouted throughsanitizeUrl(an attacker-controlled base href could otherwise rewrite every relative URL on the page into ajavascript:URI);setStructuredDataescapes JSON via the sharedescapeScriptJson;<meta http-equiv="refresh">with a dangerous URL dropped entirely.
- +238 tests, 0 regressions. Full suite: 2113 / 2113 passing (baseline was 1875).
- 74 dedicated security tests across
ssr-security.test.ts(38),head-security.test.ts(11),ssr-context.test.ts(4), andowasp-security.test.ts(21). - 10 new feature-test files covering concurrent primitives,
formAction,strict,ErrorBoundary resetKeys, routerlazyshorthand, hydration diagnostics, a11y primitives, testing queries,ErrorDisplay, and the devtools signal graph. - New
shorthand-nested.test.ts(10 tests) locks in thetag(props, children)dispatch including deep nesting, string/array/node/function second-args, positional-override-of-nodes, and legacy form compatibility.
- Inline lint disable comments — The
no-direct-dom-mutationrule (in both the build-system linter andsibujs lintCLI) now supports two inline disable forms:// sibujs-disable-next-line no-direct-dom-mutationon the line above// sibujs-disable no-direct-dom-mutationon the same line
- Cached element DOM corruption in reactive
nodes—bindChildNodeused a naive "remove all, insert all" strategy with no identity tracking. Returning the sameHTMLElementinstance from a reactive function across re-evaluations could cause duplicates or disappearing elements. The reconciler now builds a reuse set, skips removal of reused nodes, and computes the insertion anchor after cleanup to prevent stale references. - Boolean
falsesilently ignored in tag factory attributes — Passingfalsefor an attribute (e.g.,textarea({ spellcheck: false })) was silently skipped instead of removing the attribute. Boolean handling now matches the reactivebindAttributebehavior:truesets an empty attribute,falsecallsremoveAttribute(), and IDL properties (checked,disabled,selected) are set as DOM properties directly.
Accessor<T>brand type — All reactive getters returned bysignal(),derived(),memo(),memoFn(),writable(),array(), andreactiveArray()are now typed asAccessor<T>instead of the plain() => T. The brand is purely a compile-time phantom (zero runtime cost) and makes signal getters clearly distinguishable from regular functions in IDE hover tooltips and type signatures.NodeChildrenandNodeChildhave been updated to explicitly listAccessor<NodeChild>alongside the plain arrow-function form.
isDev()unsafe default — The fallback when neitherglobalThis.__SIBU_DEV__nor the compile-time__SIBU_DEV__constant is set now evaluatesprocess.env.NODE_ENV !== "production"instead of hard-codingtrue. In a browser environment without a Vite build (whereprocessis undefined), this resolves tofalse, preventing DevTools from being silently active in production.- Prototype pollution in
globalStore— Thedispatch()function now strips__proto__,constructor, andprototypekeys from the action patch before spreading it into state. Previously a malicious or malformed action could polluteObject.prototypevia{ "__proto__": { isAdmin: true } }. workerFn/worker()CSP documentation — Added a prominent JSDoc warning documenting that the inline worker pattern serializes functions via.toString()into ablob:URL (equivalent toeval()), is incompatible with strictworker-src 'self'CSP directives, and must never receive user-controlled or dynamically constructed function arguments.
when()condition type widened to genericT— The runtime already uses===identity comparison to decide re-renders, supporting non-boolean values (e.g. string IDs, object references). The TypeScript signature now reflects this:when<T>(condition: () => T, ...)instead ofwhen(condition: () => boolean, ...). Removes the need foras unknown as () => booleancasts.
- Enforce LF line endings — Added
.gitattributeswith* text=auto eol=lfto prevent CRLF formatting drift on Windows.
each()render callback receives reactive getters (BREAKING) — The render function signature changed from(item: T, index: number)to(item: () => T, index: () => number). When a keyed item's data changes but its key stays the same, the DOM is reused without re-calling render — so the old plain-value parameter was a stale snapshot. The new getters are backed by akeyIndexMapupdated on every reconciliation pass, ensuring they always return fresh data from the current array. Migration: add()after the item/index parameter wherever it is accessed inside the render callback.
hotkey()string combo syntax — Supportshotkey("ctrl+shift+z", handler)in addition to the existing explicit-flags style. Recognized modifiers:ctrl/control,shift,alt,meta/cmd/command.hotkey()preventDefaultoption —hotkey("ctrl+s", handler, { preventDefault: true })callse.preventDefault()automatically before invoking the handler.
- Nested Route Protection —
beforeEnterguards now evaluate for every segment in the matched route chain. Previously, only the leaf route's guard was checked. This ensures that parent layout protection (e.g.,/dashboard) is respected regardless of which nested child is accessed. - Direct Access Protection — The router now executes guard checks on initial page load and
popstateevents. Navigating directly to a protected URL will now trigger redirects before the component renders.
- Documentation Overhaul — The
README.mdhas been streamlined and now points to the official sibujs.dev website. - Authoring Guide — Added a clear comparison of the three supported component authoring styles (Tag Factory, Shorthand, and HTML Templates).
RouterLinkpreserves userclassprop — Theclassprop was being discarded because the reactive effect overwroteclassNamewith only the active/exact classes. Now the base class is captured from props and always prepended, so user classes persist and active classes are appended on top. When inactive, the element retains its original class instead of becoming an empty string.
bindField()helper (sibujs/ui) — One-liner to wire aFormFieldto any input, select, or checkbox. Handlesvalue,input,change, andblurevents automatically. Accepts extra props (placeholder, class, etc.) as a second argument.- Toast severity shortcuts —
toast()now returns.info(),.success(),.error(), and.warning()convenience methods alongside the existing.show(). KeepAliveRoute()component (sibujs/plugins) — Route outlet that caches rendered components using LRU eviction, preserving signals, form state, and scroll position across navigations. Configurable viaRouterOptions.keepAlive(boolean, string[], or number) or per-outlet options.RouterOptions.keepAlive— New router option to enable route-level KeepAlive caching. Acceptstrue(cache all), a string array of route names, or a number (max cache size).copyOnClickaction — Copies element text (or custom getter value) to clipboard on click. Usage:action(el, copyOnClick).autoResizeaction — Auto-grows a textarea to fit its content on input. Usage:action(el, autoResize).
show()acceptsElement— Signature widened fromshow(condition, element: HTMLElement): HTMLElementtoshow<T extends Element>(condition, element: T): T. Eliminates theas HTMLElementcast required on every call since tag factories returnElement.contentEditableuses modern Selection/Range API — Replaced deprecateddocument.execCommand()withrange.surroundContents()for bold/italic/underline. Supports toggle (unwrap) when already formatted. TheexecCommandmethod has been removed from the public API.renderToDocument()headExtrarequiresTrustedHTML— Now accepts a brandedTrustedHTMLtype instead of plainstring. UsetrustHTML()to wrap developer-controlled HTML. Prevents accidental injection of unsanitized user input at compile time. Same change applied torouterSSR.
scopedStyle()CSS sanitization — Stripsurl(),@import,expression(),-moz-binding, andbehaviorfrom CSS before injection. Prevents data exfiltration via attribute selectors and network requests.persisted()encryption docs — Removed misleadingbtoa()/atob()example (Base64 is encoding, not encryption). Updated guidance to recommendcrypto.subtle/ AES-GCM.TrustedHTMLbranded type — NewTrustedHTMLtype andtrustHTML()factory exported fromsibujs/ssr. Enforces type-level safety for raw HTML injection points.
bindField()extras no longer clobber event handlers — Passing{ on: { click: handler } }as extras now merges with the field'sinput/change/blurhandlers instead of replacing them. Extrasvalueis also ignored to prevent overriding the field getter.KeepAliveRoutememory leak — Evicted nodes are now properlydispose()d. Non-cached routes are disposed when navigating away. Cleanup function disposes all cached nodes.contentEditableselection restore — After unwrap, selection now targets the actual unwrapped content range instead of the parent container. After wrap, selection targets the wrapper's contents instead ofdocument.body.sanitizeCSSurl()bypass — Regex now handles quoted strings (url("..."),url('...')) as opaque tokens, preventing bypass via closing paren inside quotes.
- Wider
NodeChild/NodeChildrentypes —NodeChildnow acceptsboolean;NodeChildrenaccepts nested arrays and full reactive functions. Conditional patterns likecondition && elementwork withoutas anycasts. Boolean values are filtered out inappendChildren,bindChildNode,Fragment(),htm.ts, andresolveChild. onCleanup()lifecycle hook —onCleanup(callback, element)registers teardown logic (closing sockets, clearing timers, removing listeners) tied to an element's disposal. Integrates with the existingdispose()system so cleanup runs automatically whenwhen(),match(), oreach()swap content.query()selectoption — Optionalselectfunction that transforms cached data before returning it to consumers. Raw response stays in cache;selectruns on read, enabling derived views without extra signals.formatNumber()andformatCurrency()—Intl-based formatting utilities exported fromsibujs/browser.formatNumberwrapsIntl.NumberFormat;formatCurrencyis a convenience shorthand that setsstyle: "currency".
- Boolean values no longer render as text —
false,trueare filtered in all rendering paths (tagFactory,bindChildNode,Fragment,htm.ts,resolveChild) preventing visible"false"text nodes. - Lint fixes — Resolved unused variable in
router.basic.test.tsand formatting issues flagged by Biome.
clearQueryCache()now resets active queries — Active subscribers get their signals reset (data,error,isFetching) and automatically refetch, instead of silently going stale.query()cache entry recovery —doFetch()recreates the cache entry if it was evicted mid-flight, preventing silent fetch failures.onCacheUpdatehandles missing entries — Gracefully resets signals when a cache entry is cleared instead of bailing out silently.setDatapropagatesundefined—onCacheUpdatenow correctly syncsundefineddata from cleared cache entries instead of skipping the update.
- CI workflow (
ci.yml,on: [pull_request]) — GitHub Actions pipeline on pull requests: lint, test, and build (Node 20).
- DevTools disabled by default in production —
initDevTools()now defaults toenabled: isDev(). Production builds get a no-op API unless explicitly opted in, preventing signal/state exposure viawindow.__SIBU_DEVTOOLS__. - SSR error comments no longer leak internals — Production renders
<!--SSR error-->without the error message. Dev mode retains full details for debugging. - ErrorBoundary hides error details in production — Default fallback shows a generic message instead of
err.message, preventing exposure of file paths, DB strings, or stack traces. - CSP nonce support for SSR inline scripts —
suspenseSwapScript(id, nonce?)andserializeState(state, nonce?)accept an optional nonce for strict Content Security Policy compliance. - CSS injection guard — New
sanitizeCSSValue()blocksurl(),expression(),javascript:, and-moz-bindingin style property values. Applied automatically intagFactorystyle bindings. persisted()encryption support — Newencrypt/decryptoptions for data-at-rest protection in localStorage/sessionStorage.- SSR state deserialization validation —
deserializeState(validate?)accepts an optional type guard to reject tampered payloads.
KeepAlive— Caches component DOM subtrees by key, preserving reactive bindings when switching views. Supports LRU eviction via{ max }option. Unlikewhen()/match(), toggling does NOT dispose the previous branch — scroll position, form state, and signal subscriptions survive.action()— Reusable element-level behaviors with automatic disposal. Built-in actions:clickOutside(close on outside click),longPress(sustained press detection),trapFocus(keyboard focus trapping for a11y). Custom actions return a cleanup function.writable()— Computed with setter. Combines aderived()getter with a user-provided setter for two-way computed state. Setter is automatically batched.springSignal()— Reactive spring-animated value with physics simulation (stiffness, damping, precision). Animates toward target viarequestAnimationFrame. Respectsprefers-reduced-motion(snaps instantly). Returns[get, set, dispose]tuple. Import fromsibujs/motion.on()— Explicit dependency specification for effects. Only the deps getter is tracked; the handler runs untracked. Provides(value, prev)callback signature.untracked()— Execute a function without tracking signal reads as dependencies. Wraps the internalsuspendTracking()/resumeTracking()pair.signal()equalsoption — Custom equality function viasignal(value, { equals: (a, b) => boolean }). Defaults toObject.is().deepSignalrefactored to delegate tosignal()withequals: deepEqual, eliminating code duplication.effect()onErroroption — Optional error handler viaeffect(fn, { onError: (err) => ... }). Zero overhead when not provided (no wrapper closure).
batch()returns the callback's value — Signature changed from(fn: () => void): voidto<T>(fn: () => T): T. Existing code is unaffected (void return still works).deepSignalrefactored — Now delegates tosignal()withequals: deepEqual. Gains devtools support for free.deepEqual()is now exported for reuse.
- Notification queue isolation — One failing subscriber no longer crashes remaining subscribers. All subscriber invocation points in
track.tsare wrapped insafeInvoke()with dev-mode warnings. - Dev-mode warnings in silent binding catches —
bindAttributeandbindChildNodenow logdevWarn()instead of silently swallowing errors. Zero cost in production (tree-shaken). - Lifecycle error protection —
onMount/onUnmountcallbacks wrapped insafeCall()— throwing callbacks no longer crash the microtask queue or MutationObserver. - Per-item error isolation in
each()— A throwing render function for one item no longer kills the entire list. Failed items render as comment node placeholders; other items render normally. - SSR error handling —
renderToString,renderToStream, andrenderToDocumentnow catch errors per child node, rendering<!--SSR error: ...-->comments instead of crashing the server. Error messages are HTML-escaped for security.
- derived() re-tracks dependencies on re-evaluation —
computedGetternow usestrack()instead ofsuspendTracking()when re-evaluating, so derived-of-derived chains propagate correctly. Formula cells like=SUM(F2:F4)where F2 is itself=SUM(B2:E2)now update automatically. - propagateDirty simplified — removed eager evaluation path; dirty flags propagate through the chain and lazy pull via
computedGetter+track()handles re-evaluation with correct dependency registration.
lazyEffect()—import { lazyEffect } from "sibujs/ui"— creates effects that only activate when the target element is visible (via IntersectionObserver). When the element leaves the viewport, the effect is disposed. Ideal for large grids with thousands of cells.- Spreadsheet showcase demo upgraded: safe math parser (CSP-safe, no
eval/new Function), circular reference detection (#CIRC),lazyEffectfor scalable cell rendering
- ref() is now reactive — reading
.currenttracks dependencies, writing.currentnotifies subscribers. Works directly withresize(),draggable(),dropZone(), and other APIs that accept reactive getters - Browser APIs accept ref or getter —
resize(),draggable(),dropZone()now acceptRef<HTMLElement> | (() => HTMLElement | null) - debugValue() is now reactive — uses
effect()internally to track signal changes; returns a dispose function - Router lazy() uses symbol marker —
isAsyncComponentnow checksSymbol.for("sibujs:lazy")instead of relying onAsyncFunctionconstructor name heuristic - Widget reactive accessor methods —
tabs().isActive(id),accordion().isExpanded(id),datePicker().isSelected(date)— safe to use insideeach()render callbacks
onElementprop in tag factories —input({ onElement: (el) => mask.bind(el) })— called after element creation for imperative bindings- 93+ interactive examples in sibujs-test covering every module
- 10-tab examples page in sibujs-web (Showcase, Core, Data, Browser, Patterns, Motion, UI & Widgets, Plugins, DevTools, Performance)
- Spreadsheet showcase demo (reactive formulas, SUM, keyboard navigation, cell editing)
- Comprehensive framework review: fix 23 bugs, clean up module structure
- Update documentation and module exports
- Correct subpackage import paths in README and documentation
- Update package references across all entry points
- Handle array expressions in
htmltagged template engine - Documentation updates
- Optimize reactivity core,
tagFactory, andhtmltemplate engine for performance - General improvements and cleanup
- Update all references to match current
sibujsAPI (renamed from oldsibunaming)
Initial public beta release.