Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
0fde908
fix: reconciler cached element corruption, boolean attr handling, lin…
hexplus Apr 9, 2026
681c54d
Linter fixes
hexplus Apr 9, 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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ This project follows [Semantic Versioning](https://semver.org/).

---

## [1.2.0] — 2026-04-09

### Added

- **Inline lint disable comments** — The `no-direct-dom-mutation` rule (in both the build-system linter and `sibujs lint` CLI) now supports two inline disable forms:
- `// sibujs-disable-next-line no-direct-dom-mutation` on the line above
- `// sibujs-disable no-direct-dom-mutation` on the same line

### Fixed

- **Cached element DOM corruption in reactive `nodes`** — `bindChildNode` used a naive "remove all, insert all" strategy with no identity tracking. Returning the same `HTMLElement` instance from a reactive function across re-evaluations could cause duplicates or disappearing elements. The reconciler now builds a reuse set, skips removal of reused nodes, and computes the insertion anchor after cleanup to prevent stale references.
- **Boolean `false` silently ignored in tag factory attributes** — Passing `false` for an attribute (e.g., `textarea({ spellcheck: false })`) was silently skipped instead of removing the attribute. Boolean handling now matches the reactive `bindAttribute` behavior: `true` sets an empty attribute, `false` calls `removeAttribute()`, and IDL properties (`checked`, `disabled`, `selected`) are set as DOM properties directly.

---

## [1.1.0] — 2026-04-06

### Added
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.1.0",
"version": "1.2.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
15 changes: 15 additions & 0 deletions src/build/linting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,21 @@ export const lintRules = {
// Skip comments
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;

// Support inline disable: // sibujs-disable-next-line no-direct-dom-mutation
if (lineIdx > 0) {
const prevTrimmed = lines[lineIdx - 1].trim();
if (
prevTrimmed.includes("sibujs-disable-next-line") &&
(prevTrimmed.includes("no-direct-dom-mutation") ||
!prevTrimmed.includes(" ", prevTrimmed.indexOf("sibujs-disable-next-line") + 25))
) {
continue;
}
}

// Support inline disable on same line: // sibujs-disable no-direct-dom-mutation
if (line.includes("sibujs-disable") && !line.includes("sibujs-disable-next-line")) continue;

for (const { pattern, name, suggestion } of mutationPatterns) {
pattern.lastIndex = 0;
const match = pattern.exec(line);
Expand Down
13 changes: 10 additions & 3 deletions src/core/rendering/tagFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,19 @@ export const tagFactory =
continue; // already handled above / below
default: {
const value = props[key];
if (value == null || value === false) continue;
if (value == null) continue;
if (key[0] === "o" && key[1] === "n") continue;
if (typeof value === "function") {
registerDisposer(el, bindAttribute(el as HTMLElement, key, value as () => unknown));
} else if (value === true) {
el.setAttribute(key, "");
} else if (typeof value === "boolean") {
// For IDL properties (checked, disabled, selected), set the DOM property directly
if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
(el as unknown as Record<string, boolean>)[key] = value;
} else if (value) {
el.setAttribute(key, "");
} else {
el.removeAttribute(key);
}
} else {
const str = String(value);
el.setAttribute(key, isUrlAttribute(key) ? sanitizeUrl(str) : str);
Expand Down
64 changes: 47 additions & 17 deletions src/reactivity/bindChildNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ export function bindChildNode(placeholder: Comment, getter: () => NodeChild | No
return;
}

// Remove previously inserted nodes
for (let i = 0; i < lastNodes.length; i++) {
const node = lastNodes[i];
if (node.parentNode) node.parentNode.removeChild(node);
}

if (result == null || typeof result === "boolean") {
// Remove all previously inserted nodes
for (let i = 0; i < lastNodes.length; i++) {
const node = lastNodes[i];
if (node.parentNode) node.parentNode.removeChild(node);
}
lastNodes.length = 0;
return;
}
Expand All @@ -40,27 +39,58 @@ export function bindChildNode(placeholder: Comment, getter: () => NodeChild | No
lastNodes.length = 0;
return;
}
const anchor = placeholder.nextSibling;
let count = 0;

// Build the new node list
let newNodes: Node[];
if (Array.isArray(result)) {
// Reuse lastNodes array if large enough
if (lastNodes.length < result.length) lastNodes = new Array(result.length);
newNodes = [];
for (let i = 0; i < result.length; i++) {
const item = result[i];
if (item == null || typeof item === "boolean") continue;
const node = item instanceof Node ? item : document.createTextNode(String(item));
parent.insertBefore(node, anchor);
lastNodes[count++] = node;
newNodes.push(item instanceof Node ? item : document.createTextNode(String(item)));
}
} else {
if (lastNodes.length < 1) lastNodes = [null as unknown as Node];
const node = result instanceof Node ? result : document.createTextNode(String(result));
parent.insertBefore(node, anchor);
lastNodes[count++] = node;
newNodes = [node];
}

// Build a set of nodes that will be reused (present in both old and new lists)
const reused: Set<Node> | undefined = lastNodes.length > 0 && newNodes.length > 0 ? new Set<Node>() : undefined;
if (reused) {
for (let i = 0; i < newNodes.length; i++) {
for (let j = 0; j < lastNodes.length; j++) {
if (newNodes[i] === lastNodes[j]) {
reused.add(newNodes[i]);
break;
}
}
}
}

// Remove old nodes that are NOT reused
for (let i = 0; i < lastNodes.length; i++) {
const node = lastNodes[i];
if (reused?.has(node)) continue;
if (node.parentNode) node.parentNode.removeChild(node);
}

// Compute anchor AFTER removal so it's not stale
const anchor = placeholder.nextSibling;

// Insert new nodes in order, skipping nodes already in the correct position
for (let i = 0; i < newNodes.length; i++) {
const node = newNodes[i];
if (reused?.has(node) && node.parentNode === parent) {
// Reused node: only move if not already before the anchor
if (node.nextSibling !== anchor) {
parent.insertBefore(node, anchor);
}
} else {
parent.insertBefore(node, anchor);
}
}

lastNodes.length = count;
lastNodes = newNodes;
}

// Initial render and reactive subscription
Expand Down
Loading