From 95db6cebccc65caa4cacc43c0ad1647a55d7ffef Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 15:11:54 -0600 Subject: [PATCH 1/5] Updated CHANGELOG and package.json --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c767a55..64c424d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ This project follows [Semantic Versioning](https://semver.org/). --- +## [1.0.3] — 2026-03-28 + +### Added + +- **Wider `NodeChild` / `NodeChildren` types** — `NodeChild` now accepts `boolean`; `NodeChildren` accepts nested arrays and full reactive functions. Conditional patterns like `condition && element` work without `as any` casts. Boolean values are filtered out in `appendChildren`, `bindChildNode`, `Fragment()`, `htm.ts`, and `resolveChild`. +- **`onCleanup()` lifecycle hook** — `onCleanup(callback, element)` registers teardown logic (closing sockets, clearing timers, removing listeners) tied to an element's disposal. Integrates with the existing `dispose()` system so cleanup runs automatically when `when()`, `match()`, or `each()` swap content. +- **`query()` `select` option** — Optional `select` function that transforms cached data before returning it to consumers. Raw response stays in cache; `select` runs on read, enabling derived views without extra signals. +- **`formatNumber()` and `formatCurrency()`** — `Intl`-based formatting utilities exported from `sibujs/browser`. `formatNumber` wraps `Intl.NumberFormat`; `formatCurrency` is a convenience shorthand that sets `style: "currency"`. + +### Fixed + +- **Boolean values no longer render as text** — `false`, `true` are 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.ts` and formatting issues flagged by Biome. + +--- + ## [1.0.2] — 2026-03-27 ### Fixed diff --git a/package.json b/package.json index 4a30d20..a3cd741 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sibujs", - "version": "1.0.2", + "version": "1.0.3", "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", From 9487727c338809848170d361ea8775a3fa149ad9 Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 22:30:29 -0600 Subject: [PATCH 2/5] ci: use npm install instead of npm ci --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aab4d99..e156d9e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Install dependencies - run: npm ci + run: npm install - name: Run tests run: npm test From 077718418208d14423f9aeddb63876ce57f6454c Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 22:51:26 -0600 Subject: [PATCH 3/5] trusted-publisher --- .github/workflows/publish.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cdf4e5b..f25d1f3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,17 +4,21 @@ on: release: types: [published] +permissions: + id-token: write + contents: read + jobs: publish: runs-on: ubuntu-latest steps: - - name: Checkout código + - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" registry-url: "https://registry.npmjs.org" - name: Install dependencies @@ -28,5 +32,3 @@ jobs: - name: Publish to npm run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From ee7cf487a4e8438c2238b7ed54bc652e48b10b6d Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 11 Apr 2026 09:51:07 -0600 Subject: [PATCH 4/5] Updated main --- README.md | 54 +++++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 633c67a..61c59bc 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,10 @@ import { div, h1, button, signal, mount } from "sibujs"; function Counter() { const [count, setCount] = signal(0); - return div({ - nodes: [ - h1({ nodes: () => `Count: ${count()}` }), - button({ - nodes: "Increment", - on: { click: () => setCount(c => c + 1) } - }) - ] - }); + return div({ class: "counter" }, [ + h1(() => `Count: ${count()}`), + button({ on: { click: () => setCount(c => c + 1) } }, "Increment"), + ]); } mount(Counter, document.getElementById("app")); @@ -43,32 +38,41 @@ mount(Counter, document.getElementById("app")); SibuJS gives you maximum flexibility with three interoperable styles: -#### 1. Tag Factory (Full Props) -Maximum control with an explicit properties object. Perfect for complex elements. +#### 1. Tag Factory +The canonical form: a props object followed by children as a second +positional argument. No `nodes:` key required at any level of the tree — +children can be a string, a number, a single node, an array, or a +reactive getter. ```javascript -import { div, h1, button } from "sibujs"; - -const [count, setCount] = signal(0); - -return div({ - class: "counter", - nodes: [ - h1({ nodes: () => `Count: ${count()}` }), - button({ nodes: "Increment", on: { click: () => setCount(c => c + 1) } }) - ] -}); +import { div, h1, label, input, button } from "sibujs"; + +return div({ class: "counter" }, [ + h1({ class: "title" }, () => `Count: ${count()}`), + label({ for: "amount" }, "Step"), + input({ id: "amount", type: "number", value: 1 }), + button( + { class: "primary", on: { click: () => setCount(c => c + 1) } }, + "Increment", + ), +]); ``` -#### 2. Shorthand API -Concise and readable for common layouts. Class and children passed as positional arguments. +All legacy forms — `tag({ class, nodes })`, `tag("className", children)`, +`tag("text")`, `tag([children])`, `tag(node)`, `tag(() => reactive)` — +continue to work unchanged. When both `props.nodes` and the positional +second argument are present, the positional wins. + +#### 2. Positional Shorthand +The tersest form. Class and children as positional arguments, for +layouts with no event handlers or custom props. ```javascript import { div, h1, button } from "sibujs"; return div("counter", [ h1(() => `Count: ${count()}`), - button({ nodes: "Increment", on: { click: () => setCount(c => c + 1) } }) + button({ on: { click: () => setCount(c => c + 1) } }, "Increment"), ]); ``` From 2f603ee6a943ba66b8087a84d6bfaca4ab194c4c Mon Sep 17 00:00:00 2001 From: hexplus Date: Sun, 19 Apr 2026 08:10:55 -0600 Subject: [PATCH 5/5] Refactored ErrorBoundary --- CHANGELOG.md | 36 +++++ docs/best-practices.md | 21 +-- docs/examples/dashboard-app.md | 24 +-- package.json | 2 +- src/components/ErrorBoundary.ts | 39 +++-- tests/ErrorBoundary.test.ts | 18 ++- tests/errorBoundary-resetKeys.test.ts | 17 +- tests/errorBoundary.nested.test.ts | 10 +- tests/errorBoundary.working.test.ts | 214 ++++++++++++++------------ 9 files changed, 221 insertions(+), 160 deletions(-) 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();