All notable changes to SibuJS will be documented in this file.
This project follows Semantic Versioning.
A router correctness fix. No breaking changes.
- The initial route now resolves its match on load — the router seeds
currentRoutewith an uninitialized placeholder (matched: []) and then navigates to the current location on init. For the root path/— and any path whosepath/params/query/hashcoincided with that placeholder —isSameRoutetreated the first navigation as a duplicate and discarded the real match, soroute().matchedstayed empty forever andOutlet(which renders a nested child only whenmatched.length >= 2) rendered nothing at/.isSameRoutenow treats a placeholder that resolved to no match as never equal to a target that resolved to a real one, so the first navigation commits. The deferred initial navigation is additionally skipped when the app already issued its own navigation before the router finished initializing, so it can no longer clobber a route the app set first.
Reactive islands for HTML-first apps — a third rendering mode that attaches fine-grained reactivity to server-rendered HTML with no build step, no JSX, no compiler, and no dependencies. The whole islands runtime is ~5.2 KB gzipped. Additive; nothing existing changes.
enhance(target, setup)— attach reactivity to existing DOM without re-rendering it (the third mode alongsidemountandhydrate, which replace markup). The setup receives anEnhanceContextto drive server markup in place:text,attr(a11y-correct literal booleans),classed,show(toggles the standardhiddenproperty, so it reveals server-hiddenelements),model(two-way; checkbox / number /<select multiple>),on,ref/refs, andcleanup. Targets resolve via@name→data-ref="name"or a raw selector. Returns a dispose that's also tied to the element. PlusenhanceAll(selector, setup).- Island runtime —
registerIsland(name, setup)+mountIslands(root?). Declare islands in server HTML withdata-sibu-island="name"and choose when each activates viadata-sibu-load:load·idle·visible·interaction·media(withdata-sibu-media).mountIslands()wires the whole page in one call and returns a cleanup. - Lazy island code —
lazyIsland(() => import("./island.js"))fetches an island's module only when it activates, so a page ships ~0 JS for islands that never trigger. - The whole workflow is exported from the package root and the
window.SibuCDN global, so it runs from a single<script>tag. Runnable examples:examples/islands.htmlandexamples/islands-strategies.html(a real-browser smoke test for every strategy); guide:docs/islands.md; size benchmark:bench/islands-size.mjs.
Robustness: enhance skips no-op writes so static content is never re-painted
(no hydration flash when signals are seeded from the server value); re-enhancing
the same element is refused; a failing island's setup is isolated so it can't
take down the rest of the page; and a failed lazy import() is reported rather
than surfacing as an unhandled rejection. Every activation strategy — plus lazy
code-loading over HTTP — is validated in Chromium, Firefox, and WebKit
(Playwright, npm run test:browser) in addition to the jsdom unit tests.
A hardening pass across the non-core subsystems (ui, widgets, components, plugins, data, platform, patterns, performance, devtools): correctness fixes, memory-leak plugs, SSR-safety guards, and accessibility improvements. No breaking changes.
bindFieldreflects an array back to<select multiple>— the field's array value is now written onto each option'sselectedstate on render and on update. (3.3.2 added the read side; this completes the two-way binding.)captureSignalGraph()returns a real node inventory — the devtools hook now implementsgetSignalNodes(), so the snapshot reports live signals/derived/effects (id, name, kind, current value) instead of always being empty. Dependency edges are not yet tracked.prefersReducedMotion()is now exported fromreducedMotion(the one-shot check was previously duplicated privately in the spring/preset helpers).RouterLinkaccepts positional children —RouterLink(props, children)matches the framework's children convention; thenodesprop is kept as a deprecated alias.
- Teardown-tied memory leaks —
Head(), the router's route outlets (Route/KeepAliveRoute/Outlet),transition(), andhover()now release their injected elements / cached subtrees / timers / listeners when their element or subtree is disposed, instead of living for the page's lifetime. - SSR no longer crashes in
persisted(), the priority scheduler,stream()/socket(), the router constructor (createMemoryRouter),scrollRestoration(), andbattery()— each guards the browser global it touches and degrades gracefully instead of throwing aReferenceError. - Devtools trace profiler —
stop()/stopTrace()no longer throw; the hook'son()now returns a working unsubscribe, so listeners are removed instead of leaking. form().isDirtyno longer reports a field as dirty when an array/object value is unchanged (e.g. a multi-select withinitial: []).createAction().submit— a slow earlier submission can no longer overwrite the result of a newer one (run-sequenced state updates).inputMaskcaret restoration — fixed mis-counting of literal characters for*masks and patterns with a leading literal.paginationclamps the current page reactively when the item count shrinks, so indices stay in bounds.infiniteScrollre-checks intersection after loading, so it doesn't stall when appended content doesn't push the sentinel out of view.lazyModulecaches a loader that resolves toundefined/falsy instead of re-invoking it on everyget().normalize— child relations default toidinstead of inheriting the parent's customidKey(which producedString(undefined)ids).- Route matching is specificity-ordered — a broad parameter route can no longer shadow a more specific route registered after it.
- Smaller fixes: the test
mockFetchtolerates non-JSON request bodies; the a11y checker no longer exempts the literal string"undefined"from ARIA value validation.
Loadingexposesrole="status"+aria-live="polite"and a default label, so it is announced to screen readers.datePickergives its grid an accessible name (the displayed month) and moves real focus to follow the roving tabindex during keyboard navigation.- Widget
bind()is reversible —Combobox,Select, anddatePickerrestore therole/aria-*/id/tabindexthey set when torn down (matchingAccordion/Tabs/Popover). Comboboxno longer races a blur-close against an option click (a pointer-down inside the listbox keeps the input focused).contentEditablecan be scoped to an editor element so its formatting commands ignore selections elsewhere on the page.
- Internal coordination state for several modules (the
plugindefault registry, thedevtoolssession, and the WASM / microfrontend module caches) is now shared across duplicate runtime copies, extending the 3.3.1/3.3.2 first-copy-wins sweep. ErrorDisplayreads dev-mode state at render time rather than at module load, so a runtime-configured dev flag is respected.
Continues the duplicate-runtime hardening from 3.3.1, plus two small additive enhancements. No breaking changes.
- Name-based action registry — register reusable actions under a string name with
registerAction(name, fn), look them up withgetAction(name), or apply one directly by name:action(el, "longPress", { duration: 500, callback }). The built-in actions (clickOutside,longPress,copyOnClick,autoResize,trapFocus) are auto-registered under their export names. The registry is shared across duplicate runtime copies (same first-copy-wins mechanism as the reactive core). bindFieldsupports<select multiple>— a change on a multiple-select now sets the bound field to the array of selected option values (viaselectedOptions) instead of just the first. Single selects, text inputs, and checkboxes are unchanged.
createId()no longer collides across a duplicated runtime — the unique-id counter is now shared across copies (the same first-copy-wins mechanism 3.3.1 introduced for the reactive core), so two copies can't both hand outsibu-1. This prevents broken a11y associations (aria-labelledby,for+id) and SSR hydration mismatches when a bundler duplicates the module.- SSR mode is consistent across a duplicated runtime — the SSR flag/context (AsyncLocalStorage + fallback store) is now shared, so
enableSSR()taking effect in one copy is observed byisSSR()in another. Previously a split could let client-only side effects run during a server render, or leak per-request state across copies. - The router survives a duplicated runtime — the global router instance is now shared across copies, so navigation,
Outlet, andLinkhelpers bundled into a second copy of thepluginschunk see the router created bycreateRouter()in the first copy, instead of throwing "Router not initialized."
- Duplicate-runtime dev warning now reports real versions — the warning added in 3.3.1 now prints the actual package version (e.g.
active: 3.3.2, duplicate: …) instead ofdev, making mixed-version duplication easier to diagnose. Dev-only; stripped from production builds.
A robustness release. No breaking changes; no API or behavior change for normal usage.
- Reactivity survives a duplicated runtime — when a bundler materializes the reactive-core module more than once on a page (e.g. dependency pre-bundling in Vite/esbuild, which can serve the same internal chunk twice — once with an
?v=<hash>query and once raw),signal()writes and reactive bindings landed in two independent worlds: a write notified one copy's queue while a binding had tracked itself through the other's. The binding then silently stopped updating, with no error thrown. The runtime now routes every duplicate copy through the first one loaded on the page, so reactivity keeps working regardless of how many copies a bundler emits — and the hot path is unchanged, so single-instance performance is identical. Raw ESM usage was never affected and is unchanged.
- Dev warning for duplicated runtimes — in development, loading a second copy of the reactive runtime now logs a one-time, actionable warning explaining how to de-duplicate (e.g. Vite
optimizeDeps.exclude: ['sibujs']orresolve.dedupe: ['sibujs']). The warning is stripped from production builds.
A security-hardening, correctness, and performance release. No breaking changes.
- Resource-hint URLs are sanitized —
preloadModule,preloadResource, andprefetchnow run theirhrefthrough the protocol allowlist and refuse dangerous schemes (javascript:/data:/blob:), consistent with the rest of the framework. - CSS-selector injection fixed in
preloadModule(CWE-74) — the dedup lookup interpolated the raw URL into aquerySelectorstring; a URL containing"/]could throw or match the wrong element. The value is now escaped (matching the guard already used by the critical-resource preloader). - Testing-helper selectors hardened — the query helpers in
testing/adaptersandtesting/a11yescape interpolated values so labels/ids/roles with special characters can no longer break (or inject into) the selector.
watch/store.subscribe/store.subscribeKeycallbacks run untracked — signals read inside these callbacks are no longer recorded as dependencies, so a callback reading unrelated state can't cause spurious re-fires.- Reactive
srcsetuses per-candidate validation — a reactively-boundsrcsetis now split and each candidate URL validated (matching the static path) instead of being passed through a single-URL sanitizer; the static and reactive write paths share one policy and can no longer drift.
sanitizeCSSValuefast-path — values containing none of the characters that gate a dangerous construct return immediately, skipping the decode + scan (~7× faster on common style values likered/14px/#fff). Affects every static and reactive style write.tagFactoryblocked-tag check precomputed — the security blocklist is resolved once per tag factory instead of per element creation (~4× faster check, one fewer string allocation per element).- No per-notification closures —
watchandstoresubscriptions no longer allocate a closure on every notification.
- Deleted empty deprecation stubs (
memo,memoFn, and thecreateSignal/createMemo/createEffectpattern aliases) that had been no-ops since 1.4.0. Usederived/ the canonical primitives directly.
- 3.2.1 was published wrongly with the same changes than its previous version
- Router —
Route/Outletoutlet wedged after leaving a nested route — navigating from a nested child route (e.g./ui/button) to a top-level route could permanently freeze theRouteoutlet on the old layout, so every later navigation changed the URL but not the rendered page. The asyncRoute.updatestate machine relied on anisUpdating/pendingUpdatepair plus aroute.path === currentPathinsert guard; under a startup/navigation timing race this could leaveisUpdatingstucktrue(every later update short-circuited) and could insert stale route content.RouteandOutletnow use a monotonic per-update sequence — a load that is superseded mid-flight is discarded, the latest navigation always commits ("latest wins"), and rapid bursts of navigations can no longer wedge the outlet. - Router —
Outletkept stale child content and leaked on navigation — when the active route no longer matched a nested child, theOutletreturned early without removing its previous child; it also removed the old child without disposing it, leaking that subtree's reactive bindings and listeners on every nested navigation. TheOutletnow clears (and disposes) stale content when leaving the nested area and skips redundant re-renders of the same child.
A broad security-hardening and bug-fix release. No breaking changes.
RouterLinkoutput is fully sanitized — the resolvedhrefnow goes through the navigation protocol guard (blockingjavascript:/data:click-to-XSS), and spread attributes are checked case-insensitively soHREF/ONCLICK/xlink:hrefcan't bypass it. URL attributes are protocol-checked and inlinestyleis CSS-sanitized.- Case-insensitive URL-attribute sanitization — reactively-bound
HREF/SRC/xlink:HREFare now recognized and sanitized (HTML attribute names are case-insensitive). stripHtmluses a real parser — replaced the naive regex (which left dangerous residue for nested/unclosed tags) with DOM-based text extraction, with a hardened fallback where no DOM exists.- SSR hardening —
srcsetis sanitized as a candidate list (not a single URL); thehttp-equiv="refresh"guard is case-insensitive and now also applies on the router SSR document path. - Open-redirect — navigation targets are normalized (control characters, backslashes, protocol-relative
//host) before the safety check, closing bypasses of the cross-origin guard. - Prototype-pollution / inherited-key hardening —
store,globalStore,createSlots, the state-machine context merge, anddeepClonenow use own-key checks, preventing crashes (store.constructor) and pollution via__proto__. - Centralized security guards — the prototype-pollution key filter, the
on*event-handler check, and the URL control-character strip are now single shared, individually-tested helpers used by every call site (instead of being re-implemented, and occasionally diverging, acrossstore/router/SSR/template paths), so the same hole can't reappear in one path while another stays fixed.
- htm template parser — unquoted attribute values containing
/are preserved (e.g.href=http://x/yno longer truncates tohttp:); a bare<in text (a < b,I <3) renders as text instead of crashing; HTML comments / CDATA / doctype / processing instructions are skipped; multi-root templates keep every sibling node. - Reactive children (
bindChildNode) — node order is preserved across re-renders (multi-node children no longer reverse), and removed nodes are disposed (no leak on() => cond ? X : nulltoggles). - Disposal correctness — a disposed
effector reactive DOM binding can no longer run or re-subscribe if it was already queued when disposed ("dispose" reliably means "stop"). - Reactivity —
derivedapplies a customequalseven when the previous value isundefined;deepEqualdistinguishes objects with different key sets andDataViewcontents. - Router — fixed param interpolation when one param name is a prefix of another, base-path stripping at non-segment boundaries, incomplete
removeRoute(children/aliases/named entries), malformed%-escape decoding, theKeepAlivecache key (query/hash), and component-loader cache cardinality (keyed by route definition, not resolved URL). - Scheduler — fixed a priority inversion (a
USER_BLOCKINGtask no longer waits behind a pending frame) and theprocessInChunkschunk boundary. - Animations / transitions — an interrupted
enter()/leave()promise now settles instead of hanging forever. - Devtools / build —
formatErrorguards cycliccausechains;compileTemplatesno longer drops sibling root nodes; theno-signals-in-conditionalslint rule no longer false-positives after a closed nested callback. - Data —
offlineStorecoalesces pending changes per key and fixes a broken_metaIndexedDB write;infiniteQuerydedups concurrent page fetches and fixes window-edge page-param recomputation. - Memory leaks — directives (
when/match/show),aria,VirtualList, the chunk loader,Suspense's in-flight child, the route-loader abort listener, and the MobX adapter bridges now release their subscriptions/listeners on teardown;debounce/throttle/previousexpose adispose()handle; devtools perf samples and the router error cache are bounded.
- Request-scoped query cache under SSR —
query()no longer shares cached data between concurrent server renders (backed byAsyncLocalStorage). infiniteQuerymaxPages— optional sliding-window cap on retained pages.mutationcancellation —reset()(and a supersedingmutate()) aborts the in-flight retry chain; the mutation function now receives anAbortSignal.
-
Per-run dependency tracking for DOM bindings — a reactive child (
() => value), a reactiveclass/stylegetter, andwatchnow re-track their dependencies on every run, matchingderivedandeffect. Previously these bindings subscribed only to the signals read on their first evaluation: a signal first read on a later run (e.g. a conditional branch that becomes live after a state change) was never subscribed, so updates to it did not re-render. Signals no longer read on the latest run are also pruned (no over-subscription). The fix is centralized intrack()— any eagerly-re-running binding registered without an explicit subscriber now uses a self-re-tracking subscriber.const [total, setTotal] = signal(0); const [bytes, setBytes] = signal(0); const el = div(() => (total() ? `${bytes()} / ${total()}` : "waiting")); mount(() => el, root); setTotal(100); // re-runs; bytes() first read here setBytes(42); // now "42 / 100" — previously stayed "0 / 100"
This also fixes
query()data not reaching a status-branching consumer (if (q.loading()) …; return List(q.data())) wheredata()was only read once not loading.
- Dev warning for a misplaced lone class string —
tag("space-y-6")still renders the string as a text child (unchanged), but development builds now warn when a lone string looks like a CSS class list, hintingtag({ class: "…" })ortag("…", children). Prose strings never trigger the warning.
-
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.