From 95db6cebccc65caa4cacc43c0ad1647a55d7ffef Mon Sep 17 00:00:00 2001 From: hexplus Date: Sat, 28 Mar 2026 15:11:54 -0600 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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 e316ae0b618d8b8ace8679c8156226e8cc6b2561 Mon Sep 17 00:00:00 2001 From: hexplus Date: Fri, 29 May 2026 17:31:50 -0600 Subject: [PATCH 5/6] Missing update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3de3c54..176a247 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sibujs", - "version": "3.0.0", + "version": "3.1.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", From 5e389ee417121e8ee5aa8c398f9fb6827631a147 Mon Sep 17 00:00:00 2001 From: hexplus Date: Fri, 26 Jun 2026 05:46:43 -0600 Subject: [PATCH 6/6] test: cover duplicate-instance detection in track.ts/batch.ts shims --- src/reactivity/track.ts | 3 ++ tests/duplicate-instance-source.test.ts | 68 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/duplicate-instance-source.test.ts diff --git a/src/reactivity/track.ts b/src/reactivity/track.ts index 8199ca7..af49749 100644 --- a/src/reactivity/track.ts +++ b/src/reactivity/track.ts @@ -39,6 +39,9 @@ const _isDev = isDev(); // raw ESM it is undefined, so we fall back to "dev". Only used to enrich the // multi-instance dev warning. declare const __SIBU_VERSION__: string | undefined; +// The `__SIBU_VERSION__` branch only runs when a bundler inlined the define; the +// source test runner always takes the "dev" fallback, so that side is excluded. +/* v8 ignore next */ const _runtimeVersion = typeof __SIBU_VERSION__ !== "undefined" ? __SIBU_VERSION__ : "dev"; interface ReactiveApi { diff --git a/tests/duplicate-instance-source.test.ts b/tests/duplicate-instance-source.test.ts new file mode 100644 index 0000000..db395b1 --- /dev/null +++ b/tests/duplicate-instance-source.test.ts @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +// --------------------------------------------------------------------------- +// Covers the duplicate-instance DETECTION path on the real source modules. +// +// `tests/duplicate-instance.test.ts` proves cross-instance behavior by bundling +// the core and evaluating it twice — but that runs a bundled copy, so coverage +// instrumentation never credits the source module's "a duplicate loaded" branch +// (the in-process module evaluates exactly once, always taking the first-copy +// path). Here we exercise that branch directly: pre-seed the `globalThis` +// registry as if a first copy had already published its API, then freshly +// import the source module so its resolver takes the duplicate path. +// --------------------------------------------------------------------------- + +const REACTIVE_KEY = Symbol.for("sibujs.reactive.v1"); +const BATCH_KEY = Symbol.for("sibujs.reactive.batch.v1"); + +type Registry = Record; + +describe("duplicate-instance detection on the source modules", () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + delete (globalThis as Registry)[REACTIVE_KEY]; + delete (globalThis as Registry)[BATCH_KEY]; + vi.restoreAllMocks(); + }); + + test("track.ts delegates to the first copy and dev-warns once on a duplicate load", async () => { + // Simulate a first copy of the reactive runtime having published its API. + (globalThis as Registry)[REACTIVE_KEY] = { version: "0.0.0-first", __dupWarned: false }; + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await import("../src/reactivity/track"); + + expect(warn).toHaveBeenCalledTimes(1); + expect(String(warn.mock.calls[0][0])).toContain("Multiple instances of the reactive runtime"); + // The registry object is stamped so a third copy stays quiet. + expect((globalThis as Record)[REACTIVE_KEY].__dupWarned).toBe(true); + }); + + test("track.ts does not warn again when the first copy already warned", async () => { + (globalThis as Registry)[REACTIVE_KEY] = { version: "0.0.0-first", __dupWarned: true }; + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await import("../src/reactivity/track"); + + expect(warn).not.toHaveBeenCalled(); + }); + + test("batch.ts re-exports the first copy's functions on a duplicate load", async () => { + const firstBatch = { + batch: (fn: () => T): T => fn(), + enqueueBatchedSignal: () => false, + isBatching: () => false, + }; + (globalThis as Registry)[BATCH_KEY] = firstBatch; + + const mod = await import("../src/reactivity/batch"); + + // The duplicate copy delegates to the first copy's functions verbatim. + expect(mod.batch).toBe(firstBatch.batch); + expect(mod.isBatching).toBe(firstBatch.isBatching); + expect(mod.enqueueBatchedSignal).toBe(firstBatch.enqueueBatchedSignal); + }); +});