Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
60ca861
fix: audit hardening, Accessor<T> brand type, eslint-plugin
hexplus Apr 7, 2026
e2195e3
Updated version, added changelog input
hexplus Apr 7, 2026
d160a78
Updated types
hexplus Apr 7, 2026
afa2b27
Fixed linter issues
hexplus Apr 7, 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ This project follows [Semantic Versioning](https://semver.org/).

---

## [1.1.0] — 2026-04-06

### Added

- **`Accessor<T>` brand type** — All reactive getters returned by `signal()`, `derived()`, `memo()`, `memoFn()`, `writable()`, `array()`, and `reactiveArray()` are now typed as `Accessor<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. `NodeChildren` and `NodeChild` have been updated to explicitly list `Accessor<NodeChild>` alongside the plain arrow-function form.

### Fixed

- **`isDev()` unsafe default** — The fallback when neither `globalThis.__SIBU_DEV__` nor the compile-time `__SIBU_DEV__` constant is set now evaluates `process.env.NODE_ENV !== "production"` instead of hard-coding `true`. In a browser environment without a Vite build (where `process` is undefined), this resolves to `false`, preventing DevTools from being silently active in production.
- **Prototype pollution in `globalStore`** — The `dispatch()` function now strips `__proto__`, `constructor`, and `prototype` keys from the action patch before spreading it into state. Previously a malicious or malformed action could pollute `Object.prototype` via `{ "__proto__": { isAdmin: true } }`.
- **`workerFn` / `worker()` CSP documentation** — Added a prominent JSDoc warning documenting that the inline worker pattern serializes functions via `.toString()` into a `blob:` URL (equivalent to `eval()`), is incompatible with strict `worker-src 'self'` CSP directives, and must never receive user-controlled or dynamically constructed function arguments.

---

## [1.0.9] — 2026-04-03

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sibujs",
"version": "1.0.9",
"version": "1.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",
Expand Down
2 changes: 1 addition & 1 deletion src/core/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function isDev(): boolean {
? !!(globalThis as any).__SIBU_DEV__
: typeof __SIBU_DEV__ !== "undefined"
? __SIBU_DEV__
: true; // default to dev mode when no flag is set
: typeof process !== "undefined" && process.env?.NODE_ENV !== "production"; // safe default: off in browser, on in test/dev Node
}

// Cache dev mode at module load — avoids 3 typeof checks per call
Expand Down
2 changes: 2 additions & 0 deletions src/core/rendering/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type NodeChild =
| string
| number
| boolean
// Reactive: pass an Accessor<NodeChild> directly or wrap in an arrow function.
// Accessor<T> extends () => T so both forms are covered by this union member.
| (() => NodeChild)
| null
| undefined;
Expand Down
5 changes: 3 additions & 2 deletions src/core/signals/array.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { enqueueBatchedSignal } from "../../reactivity/batch";
import type { ReactiveSignal } from "../../reactivity/signal";
import { notifySubscribers, recordDependency } from "../../reactivity/track";
import type { Accessor } from "./signal";
import { signal } from "./signal";

/**
Expand Down Expand Up @@ -51,7 +52,7 @@ export interface ArrayActions<T> {
clear(): void;
}

export function array<T>(initial: T[] = []): [() => T[], ArrayActions<T>] {
export function array<T>(initial: T[] = []): [Accessor<T[]>, ArrayActions<T>] {
const [arr, setArr] = signal<T[]>([...initial]);

const actions: ArrayActions<T> = {
Expand Down Expand Up @@ -163,7 +164,7 @@ export function array<T>(initial: T[] = []): [() => T[], ArrayActions<T>] {
* clear(); // []
* ```
*/
export function reactiveArray<T>(initial: T[] = []): [() => readonly T[], ArrayActions<T>] {
export function reactiveArray<T>(initial: T[] = []): [Accessor<readonly T[]>, ArrayActions<T>] {
// Mutable internal storage — never exposed directly
let data: T[] = [...initial];

Expand Down
5 changes: 3 additions & 2 deletions src/core/signals/derived.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactiveSignal } from "../../reactivity/signal";
import { recordDependency, track, trackingSuspended } from "../../reactivity/track";
import { devAssert } from "../dev";
import type { Accessor } from "./signal";

/**
* derived creates a derived reactive signal whose value updates when dependencies change.
Expand All @@ -12,7 +13,7 @@ import { devAssert } from "../dev";
* - On re-evaluation, dependencies are re-tracked via track() so that
* derived-of-derived chains propagate correctly.
*/
export function derived<T>(getter: () => T, options?: { name?: string }): () => T {
export function derived<T>(getter: () => T, options?: { name?: string }): Accessor<T> {
devAssert(typeof getter === "function", "derived: argument must be a getter function.");
const debugName = options?.name;
const cs: any = {};
Expand Down Expand Up @@ -78,5 +79,5 @@ export function derived<T>(getter: () => T, options?: { name?: string }): () =>

if (hook) hook.emit("computed:create", { signal: cs, name: debugName, getter: computedGetter });

return computedGetter;
return computedGetter as Accessor<T>;
}
3 changes: 2 additions & 1 deletion src/core/signals/memo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { derived } from "./derived";
import type { Accessor } from "./signal";

/**
* memo returns a memoized value that only recomputes when its
Expand All @@ -10,6 +11,6 @@ import { derived } from "./derived";
* @param factory Function that computes the memoized value
* @returns Getter function that returns the memoized value
*/
export function memo<T>(factory: () => T): () => T {
export function memo<T>(factory: () => T): Accessor<T> {
return derived(factory);
}
3 changes: 2 additions & 1 deletion src/core/signals/memoFn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { derived } from "./derived";
import type { Accessor } from "./signal";

/**
* memoFn returns a memoized callback function that only updates
Expand All @@ -8,6 +9,6 @@ import { derived } from "./derived";
* @param callback The callback function to memoize
* @returns Getter that returns the current memoized callback
*/
export function memoFn<T extends (...args: any[]) => any>(callback: () => T): () => T {
export function memoFn<T extends (...args: any[]) => any>(callback: () => T): Accessor<T> {
return derived(callback);
}
21 changes: 19 additions & 2 deletions src/core/signals/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,25 @@ import type { ReactiveSignal } from "../../reactivity/signal";
import { notifySubscribers, recordDependency } from "../../reactivity/track";
import { isDev } from "../dev";

// Phantom brand symbol — exists only in the type system, never at runtime.
declare const __accessor: unique symbol;

/**
* A reactive signal getter returned by signal(), derived(), memo(), and similar primitives.
*
* Pass an Accessor directly into reactive prop positions — never call it there:
* ```ts
* const [count, setCount] = signal(0);
*
* div({ nodes: count }) // ✓ reactive — Accessor passed directly
* div({ nodes: () => count() }) // ✓ reactive — explicit arrow wrapper
* div({ nodes: count() }) // ✗ static — evaluated once, not reactive
* ```
*/
export type Accessor<T> = (() => T) & { readonly [__accessor]?: never };

type SetState<T> = (next: T | ((prev: T) => T)) => void;
type StateTuple<T> = [() => T, SetState<T>];
type StateTuple<T> = [Accessor<T>, SetState<T>];

/** Options for signal */
export interface SignalOptions<T = unknown> {
Expand Down Expand Up @@ -70,5 +87,5 @@ export function signal<T>(initial: T, options?: SignalOptions<T>): StateTuple<T>
if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial });
}

return [get, set];
return [get as Accessor<T>, set];
}
3 changes: 2 additions & 1 deletion src/core/signals/writable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { batch } from "../../reactivity/batch";
import { derived } from "./derived";
import type { Accessor } from "./signal";

/**
* Creates a writable computed value — a derived getter paired with
Expand Down Expand Up @@ -38,7 +39,7 @@ export function writable<T>(
get: () => T,
set: (value: T) => void,
options?: { name?: string },
): [() => T, (value: T) => void] {
): [Accessor<T>, (value: T) => void] {
const getter = derived(get, options);

const setter = (value: T): void => {
Expand Down
9 changes: 8 additions & 1 deletion src/patterns/globalStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ export function globalStore<

const execute = () => {
const current = getState();
const patch = actionFn(current, payload);
const rawPatch = actionFn(current, payload);
// Strip prototype-pollution keys before merging to prevent __proto__ / constructor attacks
const patch: Partial<S> = {};
for (const key of Object.keys(rawPatch)) {
if (key !== "__proto__" && key !== "constructor" && key !== "prototype") {
(patch as Record<string, unknown>)[key] = (rawPatch as Record<string, unknown>)[key];
}
}
setState({ ...current, ...patch } as S);
// Notify listeners
const newState = getState();
Expand Down
9 changes: 9 additions & 0 deletions src/platform/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export interface UseWorkerReturn<TInput, TOutput> {
* and should call `postMessage` to send results back. It is serialized
* into a Blob URL, so it must be self-contained (no closures).
*
* **CSP Warning:** This function serializes the provided function via `.toString()`
* and executes it inside a `blob:` URL Worker. This is equivalent to `eval()` and
* is incompatible with strict Content Security Policies that restrict
* `worker-src 'self'` or block `blob:` URLs. Additionally:
* - Minifiers may break captured variable references (closures silently fail).
* - Module-level imports are NOT accessible inside the worker.
* - Never pass user-controlled or dynamically constructed functions — this
* would be equivalent to `eval()` on untrusted input.
*
* @param workerFn The function body to run inside the worker.
* It receives `self` as the worker global scope.
* @returns An object with post, result, error, loading, and terminate.
Expand Down
Loading