From 1bc0ca6b8f337d7a9d62c803580b1bf5a0f47198 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Fri, 8 May 2026 09:50:09 +0200 Subject: [PATCH 1/6] add AGENTS.md and CLAUDE.md Cross-vendor agent doc with the project-specific bits not derivable from README or code: test filter env vars, the leak-detection test invariant, the exact zig fmt CI command, and a pointer to mirror neighboring file conventions for @import alias case and struct-init inference. CLAUDE.md is a one-line @AGENTS.md import shim for Claude Code, which doesn't read AGENTS.md natively. --- AGENTS.md | 29 +++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 30 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b5e8fae0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS.md + +## Tests + +```bash +make test # Run all tests +make test F="server" # Filter by substring +TEST_FILTER="WebApi: #selector_all" make test # Filter main + subtest (separator: #) +TEST_VERBOSE=true make test +TEST_FAIL_FIRST=true make test +METRICS=true make test # Capture allocation/duration metrics as JSON +``` + +The custom test runner (`src/test_runner.zig`) detects memory leaks in debug builds. **A test that allocates without freeing fails** — not just lints. + +## Formatting + +```bash +zig fmt --check ./*.zig ./**/*.zig # Exact command CI runs +``` + +`zig build` depends on the fmt step, so a local build catches drift too. + +## Conventions + +Mirror the patterns in neighboring files. In particular: + +- `@import` alias case follows the imported file's basename (`const Frame = @import("Frame.zig")`, `const ast = @import("ast.zig")`). +- Prefer struct-init type inference (`.{ ... }`) where the expected type is known from the function signature or variable annotation. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From 827e696a4ef6144801c89dfa3e868516286da1c3 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Mon, 11 May 2026 20:41:14 +0200 Subject: [PATCH 2/6] link AGENTS.md and CONTRIBUTING.md, add dev section Reciprocal pointers so humans landing in CONTRIBUTING.md discover the agent/operational conventions in AGENTS.md, and agents landing in AGENTS.md discover the CLA gate and pre-PR checks. Adds a short Development section (test + fmt commands) and a Before-opening-a-PR checklist to CONTRIBUTING.md; CLA paragraph preserved verbatim and moved to its own section. --- AGENTS.md | 2 ++ CONTRIBUTING.md | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b5e8fae0..bae474a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # AGENTS.md +See [CONTRIBUTING.md](CONTRIBUTING.md) for how to open a pull request (CLA, dev setup, pre-PR checks). + ## Tests ```bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 351d63e5..f0304578 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,21 @@ Lightpanda accepts pull requests through GitHub. +## Development + +- Run the tests: `make test` +- Check formatting: `zig fmt --check ./*.zig ./**/*.zig` + +See [AGENTS.md](AGENTS.md) for the full set of test, formatting, and code conventions (test filters, the leak-detection invariant, `@import` alias case, struct-init inference). + +## Before opening a PR + +- [ ] Tests pass (`make test`). +- [ ] Formatting is clean (`zig fmt --check ./*.zig ./**/*.zig`). +- [ ] CLA signed (see below). + +## CLA + You have to sign our [CLA](CLA.md) during your first pull request process otherwise we're not able to accept your contributions. From bcd94c4f9d545db06eb053dc648ebef6bc136fd1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 12 May 2026 14:06:17 +0800 Subject: [PATCH 3/6] Update v8 dep https://github.com/lightpanda-io/browser/issues/2407 --- .github/actions/install/action.yml | 2 +- Dockerfile | 2 +- build.zig.zon | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 11e31ff6..e0ab9295 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.4.4' + default: 'v0.4.5' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 5966c66d..33c583b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.4.4 +ARG ZIG_V8=v0.4.5 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index 32898baa..d891d903 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.4.tar.gz", - .hash = "v8-0.0.0-xddH6xiUBACrbPC02HPqL4IOl_1EKAF6zf0IwNKaCILK", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.5.tar.gz", + .hash = "v8-0.0.0-xddH65CXBADLRFlCM_pECcwsoY-9P9mZ7VnYM-6V3mXW", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ From 8bede8684ef0cd17cdc6d18cfb6f059f03b88ece Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 12 May 2026 15:16:40 +0800 Subject: [PATCH 4/6] Revert "Merge pull request #2394 from lightpanda-io/custom_element_reactions" This reverts commit 5c7097b6a0e52495591048ba18be6705925d295d, reversing changes made to 235f1cd9bc0d9b116f36562e0fdc2b295add7a50. --- src/browser/CustomElementReactions.zig | 129 -------------- src/browser/Frame.zig | 61 ++++--- src/browser/ScriptManagerBase.zig | 7 - src/browser/js/Caller.zig | 21 --- src/browser/js/bridge.zig | 12 -- src/browser/parser/Parser.zig | 22 --- .../mutation_during_callback.html | 74 ++++---- src/browser/tests/node/append_child.html | 51 +++--- src/browser/tests/node/insert_before.html | 20 ++- src/browser/webapi/CData.zig | 18 +- src/browser/webapi/CustomElementRegistry.zig | 11 +- src/browser/webapi/DOMParser.zig | 2 +- src/browser/webapi/Document.zig | 14 +- src/browser/webapi/DocumentFragment.zig | 8 +- src/browser/webapi/Element.zig | 67 ++++---- src/browser/webapi/Node.zig | 44 +++-- .../webapi/collections/DOMTokenList.zig | 10 +- src/browser/webapi/element/Attribute.zig | 6 +- src/browser/webapi/element/html/Custom.zig | 161 ++++++++---------- 19 files changed, 291 insertions(+), 447 deletions(-) delete mode 100644 src/browser/CustomElementReactions.zig diff --git a/src/browser/CustomElementReactions.zig b/src/browser/CustomElementReactions.zig deleted file mode 100644 index d7e9b1dc..00000000 --- a/src/browser/CustomElementReactions.zig +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Implements the spec's "custom element reactions" mechanism: callbacks -// (connectedCallback, disconnectedCallback, adoptedCallback, -// attributeChangedCallback) are enqueued during DOM mutation and invoked at -// the outer algorithm boundary, not synchronously mid-mutation. -// -// The "stack of element queues" is collapsed to a single flat ArrayList plus -// per-scope checkpoint indices: push() captures items.len, popAndInvoke() -// drains items[checkpoint..] and truncates. Nested scopes work naturally — -// inside a callback, a new scope captures its own checkpoint past the current -// length, drains its own range, and the outer iteration continues from where -// it left off. - -const std = @import("std"); -const lp = @import("lightpanda"); - -const Frame = @import("Frame.zig"); -const Element = @import("webapi/Element.zig"); -const Document = @import("webapi/Document.zig"); -const Custom = @import("webapi/element/html/Custom.zig"); - -const String = lp.String; -const Allocator = std.mem.Allocator; - -const Self = @This(); - -allocator: Allocator, -queue: std.ArrayList(Reaction) = .empty, -// Number of currently-open scopes (push() that hasn't been pop'd). Every -// enqueue must happen inside a scope — that's the leak-detection invariant. -// Checked in debug at enqueue time so leaks surface where the bug is, not -// later at some unrelated boundary. -active_scopes: u32 = 0, - -/// Open a new reactions scope. Returns a checkpoint to be passed to popAndInvoke. -pub fn push(self: *Self) usize { - self.active_scopes += 1; - return self.queue.items.len; -} - -/// Drain reactions queued at indices >= checkpoint, then truncate. Reactions -/// enqueued within a nested scope drain at that scope's pop, before this loop -/// sees them. -pub fn popAndInvoke(self: *Self, checkpoint: usize, frame: *Frame) void { - for (self.queue.items[checkpoint..]) |reaction| { - Custom.fireReaction(reaction, frame); - } - self.queue.items.len = checkpoint; - self.active_scopes -= 1; -} - -inline fn assertScopeActive(self: *const Self) void { - lp.assert(self.active_scopes > 0, "ce_reactions enqueue without active scope", .{}); -} - -pub fn enqueueConnected(self: *Self, element: *Element) !void { - self.assertScopeActive(); - try self.queue.append(self.allocator, .{ .connected = element }); -} - -pub fn enqueueDisconnected(self: *Self, element: *Element) !void { - self.assertScopeActive(); - try self.queue.append(self.allocator, .{ .disconnected = element }); -} - -pub fn enqueueAdopted(self: *Self, element: *Element, old_document: *Document, new_document: *Document) !void { - self.assertScopeActive(); - try self.queue.append(self.allocator, .{ .adopted = .{ - .element = element, - .old_document = old_document, - .new_document = new_document, - } }); -} - -pub fn enqueueAttributeChanged( - self: *Self, - element: *Element, - name: String, - old_value: ?String, - new_value: ?String, - namespace: ?String, -) !void { - self.assertScopeActive(); - try self.queue.append(self.allocator, .{ .attribute_changed = .{ - .name = name, - .element = element, - .old_value = old_value, - .new_value = new_value, - .namespace = namespace, - } }); -} - -pub const Reaction = union(enum) { - connected: *Element, - disconnected: *Element, - adopted: Adopted, - attribute_changed: AttributeChanged, - - pub const Adopted = struct { - element: *Element, - old_document: *Document, - new_document: *Document, - }; - - pub const AttributeChanged = struct { - element: *Element, - name: String, - old_value: ?String, - new_value: ?String, - namespace: ?String, - }; -}; diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 0f6529e3..c97af909 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -32,8 +32,6 @@ const StyleManager = @import("StyleManager.zig"); const Parser = @import("parser/Parser.zig"); const h5e = @import("parser/html5ever.zig"); -const CustomElementReactions = @import("CustomElementReactions.zig"); - const URL = @import("URL.zig"); const Blob = @import("webapi/Blob.zig"); const Node = @import("webapi/Node.zig"); @@ -190,12 +188,6 @@ _upgrading_element: ?*Node = null, // List of custom elements that were created before their definition was registered _undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{}, -// Pending custom-element reactions (connected/disconnected/adopted/attribute -// changed). Reactions are enqueued during DOM mutation and drained at the -// outer algorithm boundary — set up by the JS bridge for [CEReactions] -// methods and by the parser pump on each yield. -_ce_reactions: CustomElementReactions, - // for heap allocations and managing WebAPI objects _factory: *Factory, @@ -295,7 +287,6 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { ._type = if (parent == null) .root else .frame, ._style_manager = undefined, ._script_manager = undefined, - ._ce_reactions = .{ .allocator = arena }, ._event_manager = EventManager.init(arena, self), }; self._to_load = &self._to_load_1; @@ -1861,7 +1852,7 @@ pub fn adoptNodeTree(self: *Frame, node: *Node, old_owner: *Document, new_owner: // Per spec, adopted steps run on each element after its document is set. if (node.is(Element)) |el| { - Element.Html.Custom.enqueueAdoptedCallbackOnElement(el, old_owner, new_owner, self); + Element.Html.Custom.invokeAdoptedCallbackOnElement(el, old_owner, new_owner, self); } var it = node.childrenIterator(); @@ -2597,7 +2588,7 @@ pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const if (element._attributes) |attributes| { var it = attributes.iterator(); while (it.next()) |attr| { - Element.Html.Custom.enqueueAttributeChangedCallbackOnElement( + Element.Html.Custom.invokeAttributeChangedCallbackOnElement( element, attr._name, null, // old_value is null for initial attributes @@ -2990,7 +2981,7 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt self.removeElementIdWithMaps(id_maps.?, id); } - Element.Html.Custom.enqueueDisconnectedCallbackOnElement(el, self); + Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self); // If a