diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ac325e..07492d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,42 @@ This project follows [Semantic Versioning](https://semver.org/). --- +## [3.0.0] — 2026-04-19 + +### Breaking + +- **`ErrorBoundary` drops the `nodes` option** — the subtree is now passed as the positional second argument, matching the tag-factory shorthand (`tag(props, children)`). This removes the last `nodes:` prop from the public framework surface (tag factories migrated in 1.3.0). Signature: + + ```ts + ErrorBoundary(children: () => Element): Element; + ErrorBoundary(options: ErrorBoundaryOptions, children: () => Element): Element; + ``` + + **Migration:** + + ```ts + // Before + ErrorBoundary({ + nodes: () => RiskyArea(), + fallback: (err, retry) => …, + onError, + resetKeys, + }); + + // After + ErrorBoundary( + { fallback: (err, retry) => …, onError, resetKeys }, + () => RiskyArea(), + ); + + // Options-free form + ErrorBoundary(() => RiskyArea()); + ``` + + `ErrorBoundaryProps` is retained as a deprecated alias of the renamed `ErrorBoundaryOptions` so type imports keep compiling. + +--- + ## [2.2.0] — 2026-04-18 Reactivity-core rewrite. Replaces the `Set` / `Map` 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. diff --git a/docs/best-practices.md b/docs/best-practices.md index 7cf037d..a675361 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -268,18 +268,19 @@ function LiveFeed(): HTMLElement { ### Wrap components with ErrorBoundary ```ts -import { ErrorBoundary } from "sibujs"; +import { ErrorBoundary, div, p, button } from "sibujs"; function App(): HTMLElement { - return ErrorBoundary({ - nodes: () => MainContent(), - fallback: (err, retry) => div({ - nodes: [ - p({ nodes: `Error: ${err.message}` }), - button({ nodes: "Retry", on: { click: retry } }), - ], - }) as HTMLElement, - }); + return ErrorBoundary( + { + fallback: (err, retry) => + div([ + p(`Error: ${err.message}`), + button({ on: { click: retry } }, "Retry"), + ]) as HTMLElement, + }, + () => MainContent(), + ); } ``` diff --git a/docs/examples/dashboard-app.md b/docs/examples/dashboard-app.md index d4aebac..f094824 100644 --- a/docs/examples/dashboard-app.md +++ b/docs/examples/dashboard-app.md @@ -214,22 +214,24 @@ function App(): HTMLElement { div({ class: "content", nodes: [ - ErrorBoundary({ - nodes: () => + ErrorBoundary( + { + fallback: (err, retry) => + div({ + class: "error-panel", + nodes: [ + p({ nodes: `Error: ${err.message}` }), + button({ nodes: "Retry", on: { click: retry } }), + ], + }) as HTMLElement, + }, + () => Suspense({ fallback: () => div({ class: "loading", nodes: "Loading..." }) as HTMLElement, nodes: () => Outlet(), }), - fallback: (err, retry) => - div({ - class: "error-panel", - nodes: [ - p({ nodes: `Error: ${err.message}` }), - button({ nodes: "Retry", on: { click: retry } }), - ], - }) as HTMLElement, - }), + ), ], }), ], diff --git a/package.json b/package.json index ca083a3..3de3c54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sibujs", - "version": "2.2.0", + "version": "3.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", diff --git a/src/components/ErrorBoundary.ts b/src/components/ErrorBoundary.ts index 9ebbc5d..d21e21d 100644 --- a/src/components/ErrorBoundary.ts +++ b/src/components/ErrorBoundary.ts @@ -6,11 +6,7 @@ import { effect } from "../core/signals/effect"; import { signal } from "../core/signals/signal"; import { ErrorDisplay } from "./ErrorDisplay"; -export interface ErrorBoundaryProps { - /** - * Function that renders child content or throws. - */ - nodes: () => Element; +export interface ErrorBoundaryOptions { /** * Fallback renderer given an Error and retry callback. * Memoized internally — only re-created when the error changes. @@ -30,15 +26,18 @@ export interface ErrorBoundaryProps { * @example * ```ts * const [route, setRoute] = signal("/"); - * ErrorBoundary({ - * resetKeys: [route], - * nodes: () => div(riskyPageFor(route())), - * }); + * ErrorBoundary( + * { resetKeys: [route] }, + * () => div(riskyPageFor(route())), + * ); * ``` */ resetKeys?: Array<() => unknown>; } +/** @deprecated Renamed to `ErrorBoundaryOptions`; kept for typing compatibility. */ +export type ErrorBoundaryProps = ErrorBoundaryOptions; + // CSS styles for ErrorBoundary const errorBoundaryStyles = ` .sibu-error-boundary { @@ -250,15 +249,25 @@ function getMemoizedFallback( * ErrorBoundary component using SibuJS reactive pattern. * * Features: - * - Catches sync errors thrown by nodes - * - Catches async errors (Promise rejections) from nodes + * - Catches sync errors thrown by children + * - Catches async errors (Promise rejections) from children * - Supports nested ErrorBoundaries (inner catches first, outer catches propagation) - * - Retry functionality to clear error and re-render nodes + * - Retry functionality to clear error and re-render children * - Memoized fallback to avoid re-creating fallback UI on every render * - onError callback for logging/telemetry * - Improved CSS styling */ -export function ErrorBoundary({ nodes, fallback, onError, resetKeys }: ErrorBoundaryProps): Element { +export function ErrorBoundary(children: () => Element): Element; +export function ErrorBoundary(options: ErrorBoundaryOptions, children: () => Element): Element; +export function ErrorBoundary( + optionsOrChildren: ErrorBoundaryOptions | (() => Element), + maybeChildren?: () => Element, +): Element { + const children: () => Element = + typeof optionsOrChildren === "function" ? optionsOrChildren : (maybeChildren as () => Element); + const options: ErrorBoundaryOptions = typeof optionsOrChildren === "function" ? {} : optionsOrChildren; + const { fallback, onError, resetKeys } = options; + injectStyles(); const [error, setError] = signal(null); @@ -360,9 +369,9 @@ export function ErrorBoundary({ nodes, fallback, onError, resetKeys }: ErrorBoun } try { - const result = nodes(); + const result = children(); - // Handle async nodes (Promise-returning components) + // Handle async children (Promise-returning components) if (result && typeof (result as unknown as Promise).then === "function") { const asyncContainer = div({ class: "sibu-error-async" }) as Element; asyncContainer.appendChild(span({ class: "sibu-lazy-loading", nodes: "Loading..." })); diff --git a/tests/ErrorBoundary.test.ts b/tests/ErrorBoundary.test.ts index cfbe6c4..46bf4d5 100644 --- a/tests/ErrorBoundary.test.ts +++ b/tests/ErrorBoundary.test.ts @@ -7,19 +7,21 @@ describe("ErrorBoundary", () => { const [toggle, setToggle] = signal(false); const parent = document.createElement("div"); - const boundary = ErrorBoundary({ - nodes: () => { + const boundary = ErrorBoundary( + { + fallback: (err) => { + const fallbackEl = document.createElement("div"); + fallbackEl.textContent = `Fallback: ${err.message}`; + return fallbackEl; + }, + }, + () => { if (toggle()) throw new Error("Oops"); const el = document.createElement("span"); el.textContent = "OK"; return el; }, - fallback: (err) => { - const fallbackEl = document.createElement("div"); - fallbackEl.textContent = `Fallback: ${err.message}`; - return fallbackEl; - }, - }); + ); parent.appendChild(boundary); diff --git a/tests/errorBoundary-resetKeys.test.ts b/tests/errorBoundary-resetKeys.test.ts index 3401909..3a8769f 100644 --- a/tests/errorBoundary-resetKeys.test.ts +++ b/tests/errorBoundary-resetKeys.test.ts @@ -7,16 +7,13 @@ describe("ErrorBoundary / resetKeys", () => { const [route, setRoute] = signal("/a"); let throwIt = true; - const boundary = ErrorBoundary({ - resetKeys: [route], - nodes: () => { - if (throwIt) { - throw new Error("first render failed"); - } - const d = document.createElement("div"); - d.textContent = "ok"; - return d; - }, + const boundary = ErrorBoundary({ resetKeys: [route] }, () => { + if (throwIt) { + throw new Error("first render failed"); + } + const d = document.createElement("div"); + d.textContent = "ok"; + return d; }); document.body.appendChild(boundary); diff --git a/tests/errorBoundary.nested.test.ts b/tests/errorBoundary.nested.test.ts index 1c97ecd..3b29f57 100644 --- a/tests/errorBoundary.nested.test.ts +++ b/tests/errorBoundary.nested.test.ts @@ -30,10 +30,7 @@ describe("ErrorBoundary nested behavior", () => { // Compose nested boundaries const tree = () => { - return ErrorBoundary({ - nodes: () => ErrorBoundary({ nodes: BadChild, fallback: innerFallback }), - fallback: outerFallback, - }); + return ErrorBoundary({ fallback: outerFallback }, () => ErrorBoundary({ fallback: innerFallback }, BadChild)); }; const { container } = mountComponent(tree); @@ -61,10 +58,7 @@ describe("ErrorBoundary nested behavior", () => { }; const tree = () => { - return ErrorBoundary({ - nodes: () => ErrorBoundary({ nodes: BadChild, fallback: BadFallback }), - fallback: outerFallback, - }); + return ErrorBoundary({ fallback: outerFallback }, () => ErrorBoundary({ fallback: BadFallback }, BadChild)); }; const { container } = mountComponent(tree); diff --git a/tests/errorBoundary.working.test.ts b/tests/errorBoundary.working.test.ts index 1689898..f5d5ee4 100644 --- a/tests/errorBoundary.working.test.ts +++ b/tests/errorBoundary.working.test.ts @@ -39,14 +39,16 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: TestComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Error caught: ${error.message}`; - return div; + ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Error caught: ${error.message}`; + return div; + }, }, - }); + TestComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -73,23 +75,25 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: TestComponent, - fallback: (error, retry) => { - const div = document.createElement("div"); - div.textContent = `Error: ${error.message}`; - - const button = document.createElement("button"); - button.textContent = "Retry"; - button.onclick = () => { - setShouldThrow(false); - retry?.(); - }; - - div.appendChild(button); - return div; + ErrorBoundary( + { + fallback: (error, retry) => { + const div = document.createElement("div"); + div.textContent = `Error: ${error.message}`; + + const button = document.createElement("button"); + button.textContent = "Retry"; + button.onclick = () => { + setShouldThrow(false); + retry?.(); + }; + + div.appendChild(button); + return div; + }, }, - }); + TestComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -113,14 +117,16 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: FailingComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Init error: ${error.message}`; - return div; + ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Init error: ${error.message}`; + return div; + }, }, - }); + FailingComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -142,25 +148,29 @@ describe("ErrorBoundary Working Features", () => { } function MiddleComponent() { - return ErrorBoundary({ - nodes: InnerComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Inner fallback: ${error.message}`; - return div; + return ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Inner fallback: ${error.message}`; + return div; + }, }, - }); + InnerComponent, + ); } const tree = () => - ErrorBoundary({ - nodes: MiddleComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Outer fallback: ${error.message}`; - return div; + ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Outer fallback: ${error.message}`; + return div; + }, }, - }); + MiddleComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -197,23 +207,25 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: TestComponent, - fallback: (error, retry) => { - const div = document.createElement("div"); - div.textContent = `Error state: ${error.message}`; - - const button = document.createElement("button"); - button.textContent = "Recover"; - button.onclick = () => { - setErrorState("recovered"); - retry?.(); - }; - - div.appendChild(button); - return div; + ErrorBoundary( + { + fallback: (error, retry) => { + const div = document.createElement("div"); + div.textContent = `Error state: ${error.message}`; + + const button = document.createElement("button"); + button.textContent = "Recover"; + button.onclick = () => { + setErrorState("recovered"); + retry?.(); + }; + + div.appendChild(button); + return div; + }, }, - }); + TestComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -241,14 +253,16 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: StringErrorComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Caught: ${error.message}`; - return div; + ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Caught: ${error.message}`; + return div; + }, }, - }); + StringErrorComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -261,14 +275,16 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: ErrorObjectComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Caught: ${error.message}`; - return div; + ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Caught: ${error.message}`; + return div; + }, }, - }); + ErrorObjectComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -295,14 +311,16 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: TestComponent, - fallback: (error) => { - const div = document.createElement("div"); - div.textContent = `Error: ${error.message}`; - return div; + ErrorBoundary( + { + fallback: (error) => { + const div = document.createElement("div"); + div.textContent = `Error: ${error.message}`; + return div; + }, }, - }); + TestComponent, + ); const { container } = mountComponent(tree); await waitForAsync(); @@ -340,23 +358,25 @@ describe("ErrorBoundary Working Features", () => { } const tree = () => - ErrorBoundary({ - nodes: CyclingComponent, - fallback: (error, retry) => { - const div = document.createElement("div"); - div.textContent = error.message; - - const button = document.createElement("button"); - button.textContent = "Next Cycle"; - button.onclick = () => { - setCycle((c) => c + 1); - retry?.(); - }; - - div.appendChild(button); - return div; + ErrorBoundary( + { + fallback: (error, retry) => { + const div = document.createElement("div"); + div.textContent = error.message; + + const button = document.createElement("button"); + button.textContent = "Next Cycle"; + button.onclick = () => { + setCycle((c) => c + 1); + retry?.(); + }; + + div.appendChild(button); + return div; + }, }, - }); + CyclingComponent, + ); const { container } = mountComponent(tree); await waitForAsync();