Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
95db6ce
Updated CHANGELOG and package.json
hexplus Mar 28, 2026
56080d8
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 28, 2026
7eeec49
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 28, 2026
14a9cd4
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
9487727
ci: use npm install instead of npm ci
hexplus Mar 29, 2026
6b4bd83
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0b9a0cc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0777184
trusted-publisher
hexplus Mar 29, 2026
4d46e82
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
bea9788
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
825a8dc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
55c4436
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0d2c7e0
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
8da81e8
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
325ce5d
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Mar 29, 2026
0cad329
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 1, 2026
aea6787
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 4, 2026
00e5e88
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 7, 2026
b10a2c5
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 7, 2026
639eae0
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 9, 2026
405e4fe
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 11, 2026
ee7cf48
Updated main
hexplus Apr 11, 2026
8c77fca
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 11, 2026
da6d752
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 11, 2026
c047837
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 12, 2026
a52fffc
Merge branch 'main' of https://github.com/hexplus/SibuJS
hexplus Apr 12, 2026
8540ede
chore: harden reactivity + rendering; fix SSR hydration, worker pool …
hexplus Apr 14, 2026
447a489
Updated version
hexplus Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,167 @@ This project follows [Semantic Versioning](https://semver.org/).

---

## [2.0.0] — 2026-04-14

Major hardening + features release. Spans reactivity, rendering, SSR, widgets, security, and build tooling. **2187/2187 tests passing, zero lint errors, zero type errors.**

### Breaking

- **Adapter method renames** — `redux.useSelector` → `redux.select`, `zustand.useSelector` → `zustand.select`. The `use*` prefix is no longer used anywhere in the framework.
```ts
// before
const count = redux.useSelector(s => s.count);
// after
const count = redux.select(s => s.count);
```

- **`useDefaultPluginRegistry` renamed to `setDefaultPluginRegistry`** — 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 as `loadRemoteModule`. Options bag now disambiguated via `allowedOrigins`/`unsafelyAllowAnyOrigin` keys only.

- **`compiled.staticTemplate()` / `precompile()` require `TrustedHTML`** — arbitrary strings no longer accepted to prevent silent `innerHTML` XSS sinks. Mint via `trustHTML(raw)` after your own sanitization pass.

- **Router refuses protocol-relative redirects** — `"//evil.com/path"` style redirect targets now throw `NavigationFailureError` instead of logging a warning (CWE-601 open redirect).

- **`hydrate()` / `hydrateIslands` / `hydrateProgressively` use 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 and `data-sibu-hydrated` preserved) so reactive bindings actually drive the DOM.

- **`socket()` / `stream()` default `maxReconnects` is now 10** — was effectively unbounded. Permanently broken URLs no longer hammer servers forever. Exponential backoff with jitter added.

- **`optimisticList()` deprecated aliases removed** — `addOptimistic`/`removeOptimistic`/`updateOptimistic` were deprecated in 1.5.0 and are now gone. Use `add`/`remove`/`update`.

- **`contentEditable().setContent` signature widened** — takes either a string (raw HTML, legacy) or `{ text, html, sanitize }`. The options form is the recommended path.

### Added

- **`retrack()`** reactivity primitive for derived pull-path — skips the `track()` cleanup pass; uses save/restore of `currentSubscriber` instead 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.
```ts
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.

- **`Dispose` canonical type** exported from `sibu`.

- **Widget ARIA `bind()` layer** — every headless widget now ships a `bind(els)` that wires roles, keyboard, and idempotent teardown per WAI-ARIA APG:
- `Tabs` — role=tablist, roving tabindex, Arrow/Home/End
- `Accordion` — aria-expanded/controls, Enter/Space
- `Tooltip` — role=tooltip, aria-describedby splice, Escape dismiss, hoverable grace
- `Popover` — role=dialog, aria-haspopup, Escape + click-outside
- `Combobox` — Combobox 1.2 pattern, aria-activedescendant, typeahead
- `Select` — role=listbox, aria-multiselectable, typeahead, disabled-aware
- `FileUpload` — labeling, aria-describedby splice, drop-zone keyboard
- `datePicker` — 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 from `lazy()` rejections that beat any boundary to mount. Multiple pending errors wrapped in `AggregateError`.

- **`trustHTML(html)` + `TrustedHTML` type** re-exported from `sibu` (was only on `sibu/ssr` and `sibu/performance`, which minted incompatible brands).

- **Test-reset helpers** — `__resetQueryCache`, `__resetDialogStack`, `__removeRouterPagehideHandler`.

- **Build/release hardening** — `tsup --clean`; `./cdn` subpath export; `publishConfig.access=public` + `provenance=true`; `publish.mjs` publishes BEFORE git commit/tag (so a publish failure leaves no orphan commit).

- **10 new tests** — `keepAlive.test.ts`, `pluginRegistry.test.ts`, `widgetsAria.test.ts`.

### Fixed — Reactivity

- **`derived` pull-path correctness under `suspendTracking`** — new conditional deps register their markDirty subscription even when the outer caller is in `untracked()` context.
- **`propagateDirty`** is iterative (no recursion) with already-dirty skip — closes O(depth²) walk on deep chains.
- **`batch.flushBatch`** wrapped in try/finally — a throwing subscriber can't strand `pendingSignals` for the next batch.
- **`effect()` disposer idempotent** — double-dispose no longer re-emits `effect:destroy` or re-walks subs lists.
- **`effect()` re-entry detection** — a re-entering update now warns in dev and drops (was silent).
- **`bindChildNode` diff** — O(n²) nested scan replaced with O(n+m) Set-based reuse detection; dedupes duplicate node refs in the output array.
- **Dead `signalSubscribers` WeakMap** removed (the `__s` property cache is authoritative).

### Fixed — Rendering & Lifecycle

- **`dispose()` re-entry safe** — snapshot-then-delete + bounded extra-pass drain. `Array.from(childNodes)` snapshot guards against disposers mutating the tree mid-walk.
- **`onUnmount` false-fires on same-tick re-parent** — `fireUnmount` defers one microtask and re-checks `isConnected`.
- **`lifecycle` descendant walk** short-circuited for leaf insertions.
- **`keepAlive` disposed-flag** prevents post-dispose microtask writes; cached subtrees properly disposed on anchor teardown.
- **`each` itemGetter** wraps in `untracked()` so per-row consumers don't subscribe to the whole-array signal.
- **`each`, `portal`, `lazy.Suspense` error 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_ERROR` marker) — ErrorBoundary scans descendants on mount so failures before any boundary mounts aren't silently lost.
- **`hydrateProgressively` island marker preserved** on replacement.

### Fixed — Data & Platform

- **`workerFn` pool crosstalk** — per-worker FIFO queue with `addEventListener`; terminate-on-error so concurrent `run()` calls can't mis-route results.
- **`worker()` top-level** uses `addEventListener` + terminate-on-error.
- **`infiniteQuery` run-id generation** — stale responses discarded; `AbortController.abort()` at top of effect.
- **`offlineStore` atomic writes** — `idbPutWithChange` / `idbDeleteWithChange` single-transaction across `items`+`_changes`; cursor-snapshotted sync; pull skips items with pending local edits (conflict avoidance); `idbPutMany` batches remote items; `closed` flag checked between awaits; `sync()` error now logs via `devWarn` (was silent).
- **`query` dedup** captures `entry.promise` locally and re-checks identity after await; sync-throw from `withRetry` cleaned up; `onSettled` in `finally`; `dispose()` idempotent + gcTimer deduplicated.
- **`chunkLoader`** true LRU with `lastAccess`; `invalidate(id)` clears `preloaded`; `this.load` replaced with closure reference (destructure-safe); preload `.delete(id)` on failure.
- **`serviceWorker`** listener refs tracked; prior `statechange` detached before reassignment; all detached in `unregister()`.
- **`incrementalRegeneration`, `routerSSR`, `wakeLock`, `clearQueryCache` refetchers** — `.catch` instead of silent.
- **`mutation.mutate()`** fire-and-forget rejection now warns (was silent `catch(() => {})`).

### Fixed — SSR

- **`runInSSRContext`** uses Node's `AsyncLocalStorage` when available so concurrent requests don't share `ssrMode`/suspense counters.
- **`serializeState`** byte cap via `TextEncoder`; escapes U+2028/9; drops the `__SIBU_SSR_STATE_RAW__` fallback (defeated escape).
- **`deserializeState`** dev-warns when no `validate` guard is passed.

### Fixed — Widgets & UI

- **`datePicker` month/year overflow** — uses day-1 anchor (no Jan-31→Mar-3 drift).
- **`form.wrappedSet`** clears `manualErrors` on edit (server-side "email taken" errors no longer stick after user edits).
- **`Tooltip.bind()` teardown** splices its id out of the current `aria-describedby` so ids added by other libraries survive.
- **`a11y.FocusTrap`** `keydown` removed on dispose; announce live region checks `isConnected` before writing.
- **`inputMask.bind()`** returns a dispose function that removes input/focus listeners.
- **`customElement._teardown`** runs `dispose()` on rendered subtree before reconnect (reactive bindings no longer leak across reconnects).

### Fixed — Plugins & Router

- **`router.cleanupNodes`** calls `dispose(node)` before detaching — every reactive binding inside a route subtree is torn down on navigation.
- **`Route()` / `KeepAliveRoute()` / `Outlet()`** `track()` teardowns stored in `routeCleanups` (was leaking effects).
- **`RouterLink` click listener** removed via `registerDisposer`; navigate failures `.catch`'d.
- **Router `pagehide` listener lazy-initialized** on first `createRouter()` call (honors `sideEffects: false`).

### Fixed — Security

- **`URL_ATTRIBUTES`** expanded: `xlink:href`, `formtarget`, `ping`, `data` now run through `sanitizeUrl()` (was bypassed).
- **`persist` + `dragDrop` `JSON.parse`** revivers block `__proto__`/`constructor`/`prototype` (CWE-1321).
- **`each` error dispatch** logs via `devWarn` when anchor is detached (no silent swallow).

### Fixed — Performance

- **Spring animation** is `dt`-aware (`REF_DT_MS`, `MAX_STEP_RATIO=4`, NaN-guard) — frame-rate-independent; no runaway on tab-throttle.
- **`speech.ts` setInterval** polls only while actively speaking (was constant 5Hz).
- **`socket` / `stream` auto-reconnect** — exponential backoff with jitter.

### Fixed — DX

- **Error prefix standardized to `[SibuJS]`** (was mix of `[Sibu]` / `[Sibu strict]` / `[Sibu hydration]`).
- **`devtools.hmr`** calls `disposeNode` on replaced subtrees so HMR reloads don't leak effects/listeners.
- **`testing.unmount` / `unmountAll`** call `dispose()` before clearing DOM (was `innerHTML = ""`, leaked every effect/binding).
- **`tsconfig.json`** drops `"types": ["vitest"]` — zero `src/` deps on test-only types.
- **Unused `biome-ignore` suppressions** removed; unused variables cleaned.

### Migration

Most apps need no changes. If you hit any of these:

- **`redux.useSelector` / `zustand.useSelector`** → rename to `select`.
- **`useDefaultPluginRegistry`** → rename to `setDefaultPluginRegistry`.
- **`loadRemoteModule(url)` without options** → pass `{ allowedOrigins: [...] }` (recommended) or `{ unsafelyAllowAnyOrigin: true }` for opt-in.
- **`loadWasmModule(url)`** → same.
- **`compiled.staticTemplate(html)`** → wrap via `trustHTML(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; pass `maxReconnects: Infinity` to restore prior behavior.
- **Router redirects to `//other-host/path`** → now throw; rewrite as relative or absolute `https://` within an allowed origin.
- **`optimisticList().addOptimistic/removeOptimistic/updateOptimistic`** → rename to `add`/`remove`/`update`.

---

## [1.5.0] — 2026-04-11

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.
Expand Down
35 changes: 29 additions & 6 deletions bench.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,37 @@
import { JSDOM } from "jsdom";

// ── Bootstrap jsdom ──────────────────────────────────────────────────────────
//
// This script is standalone (run via `node bench.mjs`) so mutating globalThis
// here is safe. The installBenchGlobals/restoreBenchGlobals pair is exported
// shape for anyone importing this module from a test runner that cares about
// global cleanup. Note: queueMicrotask is already available natively in Node.

const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
globalThis.document = dom.window.document;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.Element = dom.window.Element;
globalThis.Node = dom.window.Node;
globalThis.Comment = dom.window.Comment;
// Note: queueMicrotask is already available natively in Node.js

const BENCH_GLOBAL_KEYS = ["document", "HTMLElement", "Element", "Node", "Comment"];
const _savedGlobals = {};

export function installBenchGlobals() {
for (const key of BENCH_GLOBAL_KEYS) {
_savedGlobals[key] = Object.prototype.hasOwnProperty.call(globalThis, key)
? globalThis[key]
: undefined;
globalThis[key] = dom.window[key];
}
}

export function restoreBenchGlobals() {
for (const key of BENCH_GLOBAL_KEYS) {
if (_savedGlobals[key] === undefined) {
delete globalThis[key];
} else {
globalThis[key] = _savedGlobals[key];
}
}
}

installBenchGlobals();

// ── Import Sibu (from source via tsup output) ────────────────────────────────

Expand Down
8 changes: 6 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type {
export { html } from "./src/core/rendering/htm";

// Rendering types
export type { NodeChild, NodeChildren } from "./src/core/rendering/types";
export type { Dispose, NodeChild, NodeChildren } from "./src/core/rendering/types";

// Mounting & rendering
export * from "./src/core/rendering/mount";
Expand Down Expand Up @@ -78,9 +78,13 @@ export { untracked } from "./src/reactivity/track";
export { bindDynamic } from "./src/reactivity/bindAttribute";

// Lazy loading & Suspense
export { lazy, Suspense } from "./src/core/rendering/lazy";
export { lazy, Suspense, takePendingError } from "./src/core/rendering/lazy";
export type { SuspenseProps } from "./src/core/rendering/lazy";

// Trusted HTML brand for opt-in unsafe-HTML APIs (compiled.staticTemplate, ssr.headExtra)
export { trustHTML } from "./src/platform/ssr";
export type { TrustedHTML } from "./src/platform/ssr";

// Components
export * from "./src/components/ErrorBoundary";
export * from "./src/components/ErrorDisplay";
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sibujs",
"version": "1.5.0",
"version": "2.0.0",
"description": "A lightweight, function-based frontend framework that combines the best of React, Svelte, and Vue — with zero VDOM and maximum simplicity. Designed for developers who want fine-grained reactivity and full control without compilation or magic.",
"keywords": [
"frontend",
Expand Down Expand Up @@ -33,7 +33,7 @@
"format": "biome format --write src/ tests/",
"test": "vitest",
"test:ui": "vitest --ui",
"build": "tsup index.ts data.ts browser.ts patterns.ts motion.ts ui.ts widgets.ts ssr.ts devtools.ts performance.ts ecosystem.ts plugins.ts build.ts testing.ts extras.ts --dts --format esm,cjs --out-dir dist && tsup cdn.ts --format iife --globalName Sibu --out-dir dist --no-dts --minify",
"build": "tsup index.ts data.ts browser.ts patterns.ts motion.ts ui.ts widgets.ts ssr.ts devtools.ts performance.ts ecosystem.ts plugins.ts build.ts testing.ts extras.ts --dts --format esm,cjs --out-dir dist --clean && tsup cdn.ts --format iife --globalName Sibu --out-dir dist --no-dts --minify",
"bench": "node bench.mjs",
"bench:save": "node bench.mjs --save",
"bench:check": "node bench.mjs --compare",
Expand Down Expand Up @@ -116,8 +116,15 @@
"types": "./dist/extras.d.ts",
"import": "./dist/extras.js",
"require": "./dist/extras.cjs"
},
"./cdn": {
"default": "./dist/cdn.global.js"
}
},
"publishConfig": {
"access": "public",
"provenance": true
},
"browserslist": [
"Chrome >= 80",
"Firefox >= 78",
Expand Down
Loading
Loading