Commit Graph

6304 Commits

Author SHA1 Message Date
Karl Seguin
8cfdb7e13c try to use direct ws 2026-05-18 12:07:28 +08:00
Karl Seguin
ba97c1e43c Re-organization CDP connection
network/WsConnection.zig was poorly named. It didn't represent a generic WS
connection, but rather a CDP-specific connection. This splits the generic WS
logic into network/WS.zig and the CDP-specific details in cdp/Connection.zig.

Some of the connection management in the Server has also been simplified.
2026-05-18 08:18:59 +08:00
Karl Seguin
63490b4000 Implement websocket read + cleanup
@panic("OOM") on various allocation fails which significantly simplifies a
bunch of code (e.g. not having to rollback things).
2026-05-17 20:17:44 +08:00
Karl Seguin
9ffc0c3a26 Shared libcurl WIP.
This takes the existing shared libcurl initiative and changes a few critical
design decisions.

1. Move to a OTP-style mailbox at both ends. Unifies all communication.
    a. Network add/remove/op are all just different message types
    b. Worker http/cdp/websocket are all just different message types

2. Move Network.Handle logic directly into HttpClient
    a. In theory, the composition is nice, in practice, there's only 1 composer.

3. Use `curl_multi_wakeup` to wake up poll (rather than a separate `pipe`)

This is currently a WIP, and the big thing still missing is the websocket
integration.
2026-05-17 16:49:36 +08:00
Pierre Tachoire
4e80db6cf0 Merge pull request #2483 from navidemad/dockerfile-pipefail-hygiene
Dockerfile: fix curl|sh pipefail; trim builder stage
2026-05-16 19:21:30 +02:00
Pierre Tachoire
a3944a3b40 Merge pull request #2484 from lightpanda-io/e2e_kill_between_steps
Force kill lightpanda between steps to prevent "port already in-use" …
2026-05-16 18:51:36 +02:00
Karl Seguin
ab63cfbf39 Merge pull request #2478 from navidemad/fix-c10-inline-media-evaluation
css: evaluate @media and matchMedia against viewport
2026-05-16 21:42:56 +08:00
Karl Seguin
d870972ceb Small tweaks to @media
- Depth counter when recursing
- Better comment support
- Small perf tweak (e.g. lowercase once into stack buffer before multiple
  compares)
- Few more test cases
2026-05-16 20:52:11 +08:00
Karl Seguin
21e74b46ea Merge pull request #2486 from willmafh/typo-fix
typo fix
2026-05-16 20:39:36 +08:00
willmafh
c52356b6d7 chore: lowercase demo word 2026-05-16 20:07:32 +08:00
willmafh
c1e64232e5 chore: typo fix 2026-05-16 20:05:52 +08:00
Karl Seguin
7f8cb145e6 Merge pull request #2485 from lightpanda-io/nikneym/timers-hash
`Timers`: prefer integer-optimized hashing
2026-05-16 16:52:53 +08:00
Halil Durak
33d594be43 Timers: prefer integer-optimized hashing 2026-05-16 10:19:33 +03:00
Karl Seguin
d926291241 Merge pull request #2467 from lightpanda-io/http_transfer
Cleanup HttpClient.Transfer
2026-05-16 08:52:12 +08:00
Karl Seguin
0b358fd410 Merge pull request #2474 from staylor/fix/2472-frame-id-reset
Fix #2472: scope frame ID generator to Browser, not Session
2026-05-16 08:46:27 +08:00
Karl Seguin
94e8b06583 Merge pull request #2482 from navidemad/make-v8-path
make: forward optional V8_PATH to zig build
2026-05-16 08:41:05 +08:00
Karl Seguin
a5c1068b85 Force kill lightpanda between steps to prevent "port already in-use" error in CI 2026-05-16 08:39:53 +08:00
Navid EMAD
54e09a5ace make: rename V8_PATH to generic ZIGFLAGS
Per review feedback, generalise the optional pass-through so any
`-D...` build option can be forwarded, not just the prebuilt V8 path.
2026-05-16 02:27:52 +02:00
Karl Seguin
5550b61d2d Merge pull request #2480 from navidemad/make-clean
make: add clean target
2026-05-16 07:35:09 +08:00
Karl Seguin
732e19c7b6 add cargo clean to html5ever 2026-05-16 07:34:35 +08:00
Karl Seguin
d3f3e7f335 Merge pull request #2475 from navidemad/fix-a41-json-undefined
js: emit `null` when JSON-stringifying unserializable values
2026-05-16 07:24:14 +08:00
Karl Seguin
2163a2fd5a Merge pull request #2463 from lightpanda-io/nikneym/nav-accept-header
Send `Accept` header when navigating
2026-05-16 06:39:40 +08:00
Navid EMAD
fd0700a572 dockerfile: fix curl|sh pipefail; trim builder stage
- Download rustup to a file then execute, so a failed curl is not
  masked by sh's exit code under /bin/sh (no pipefail).
- Add --no-install-recommends and apt-list cleanup to both apt stages
  (stage 0 drops from 156 to 116 packages, 1144 MB to 605 MB).
- Add --retry 3 --retry-delay 2 to all 4 external downloads.
- Use git clone --depth 1 (28 MB to 9.6 MB working tree).
- Drop -v from tar for minisign and zig extractions (log noise only).

Final shipped image is unchanged; the wins live in the builder stage
and build-cache footprint.
2026-05-15 23:45:06 +02:00
Navid EMAD
d1a0203d88 make: forward optional V8_PATH to zig build
Without -Dprebuilt_v8_path, the build/test targets rebuild V8 from
source (10+ minutes per invocation). Contributors who already have a
cached archive can now short-circuit by exporting V8_PATH:

    V8_PATH=v8/libc_v8.a mise exec -- make test

When V8_PATH is empty (default), behavior is unchanged.
2026-05-15 22:53:00 +02:00
Navid EMAD
ee1cbf1bb3 make: add clean target 2026-05-15 22:51:54 +02:00
Navid EMAD
dd5e335262 css: harden media-query evaluator and @media boundary
Address review feedback on PR #2478:

- MediaQuery.zig: strip CSS `/* ... */` comments before tokenization so
  `screen and /*x*/ (min-width: 1px)` evaluates the same as without the
  comment.
- MediaQuery.zig: bound-check `em` / `rem` multiplication via
  `std.math.mul` so a u32-overflowing length (e.g. `268435456em`) fails
  closed instead of panicking in debug or wrapping in release.
- StyleManager.zig: prelude brace search skips `/* ... */` comments, so
  `@media /* { fake */ screen { ... }` splits at the real opening brace
  rather than the one inside the comment.
- Tests: unit tests for stripped comments, em/rem overflow, and
  unimplemented units (cm/mm/pt/in/vw). HTML fixtures cover commented
  preludes/queries and the `replaceSync` cascade path.
2026-05-15 20:49:45 +02:00
Navid EMAD
68bd1441af css: evaluate @media and matchMedia against viewport
Inline `@media` rules were parsed but never applied to the cascade, and
`window.matchMedia(q).matches` always returned false. Add a Media Queries
Level 4 subset evaluator (`width`/`height`/`orientation`, lengths in
`px`/`em`/`rem`, comma OR, `and`, `not`, `only`) wired into both surfaces.
External `<link rel="stylesheet">` fetch remains out of scope; the
evaluator reads the same 1920x1080 viewport already exposed by
`Window.innerWidth` / `innerHeight`.

Closes #2477
2026-05-15 20:30:01 +02:00
Navid EMAD
353be6382d js: emit null when JSON-stringifying unserializable values
V8's `JSON::Stringify` finishes by calling `Object::ToString` on whatever
`i::JsonStringify` returns. For values that `JSON.stringify` treats as
non-serializable at the top level (`undefined`, functions, symbols),
`i::JsonStringify` yields the undefined sentinel and `ToString` coerces
it to the JS string `"undefined"`. `Value.jsonStringify` then wrote those
9 bytes raw via `writer.writeAll`, embedding a bare `undefined` token in
the JSON stream — invalid per RFC 8259 and rejected by any strict-JSON
CDP client. Detect the sentinel and emit JSON `null` instead, matching
what `JSON.stringify` produces when the same value sits in an array slot
(`JSON.stringify([undefined])` → `"[null]"`).

Closes #2473
2026-05-15 19:21:57 +02:00
Scott Taylor
4bd2edb596 Fix #2472: scope frame ID generator to Browser, not Session
CDP target IDs (`FID-{d:0>10}`) must stay unique for the lifetime of
the CDP connection -- Playwright's `CRBrowser._onAttachedToTarget`
asserts on duplicates and the assertion is fatal (the connection is
unusable afterwards).

Before this fix, `Session.frame_id_gen` reset to 0 in two places:

  1. `tearDownActivePage` explicitly reset to 0 after every page
     teardown (likely intended to mimic pre-pending-page numbering
     within a single Session, but invisible there because the
     immediately-following `installNewActivePage` typically reuses
     the old frame's explicit `frame_id`, see `replaceRootImmediate`).

  2. Fresh Sessions started from the field default of 0. Each
     `Target.createBrowserContext` calls `Browser.newSession`, which
     deinits the old Session and constructs a new one -- so even
     without (1), the next BrowserContext's first page would still
     get `FID-0000000001`.

(2) is what trips Playwright on the second `browser.newContext()`
on a connection: the second context's first frame re-issues
`FID-0000000001`, identical to the first context's frame, and
Playwright's `CRBrowser._onAttachedToTarget` raises
`Duplicate target FID-0000000001`.

Move `frame_id_gen` (and `nextFrameId`) from `Session` to `Browser`,
which is per-CDP-connection. Existing callers (`Session.createPage`,
`Frame.zig:1327`, `Frame.zig:1437`, `Worker.zig:74`) still go through
`Session.nextFrameId` -- it's now a thin pass-through to
`browser.nextFrameId()` -- so no call sites change. Removed the
explicit reset in `tearDownActivePage`; it was redundant within a
Session (root navigation reuses the old frame_id) and harmful across
Sessions.

`loader_id_gen` stays on Session: Loader IDs (`LID-...`) are scoped
per-frame in CDP and Playwright doesn't track them in the target
registry, so the per-Session reset is correct there.

Repro (`playwright-core@1.58.2`):

  for (let i = 1; i <= 3; i++) {
    const ctx = await browser.newContext();
    await ctx.newPage();
    await ctx.close();
  }

Before: cycle 2 throws `Duplicate target FID-0000000001`.
After: 5/5 cycles complete cleanly.

Tests: 653/653 pass. Added regression coverage in
`cdp.target: createTarget assigns unique IDs across BrowserContexts
(issue #2472)` -- verified to fail against the original source
(reverted Browser.zig and Session.zig, kept the test, ran zig build
test: only the new test fails).
2026-05-15 13:17:49 -04:00
Halil Durak
9c40cd9fb2 send Accept header when navigating 2026-05-15 18:30:18 +03:00
Pierre Tachoire
4f33d64c5c Merge pull request #2433 from lightpanda-io/webmcp
Implement webMCP API and webMCP cdp domain
2026-05-15 16:13:12 +02:00
Pierre Tachoire
60e3d48dbd webmcp: update comments 2026-05-15 15:12:27 +02:00
Pierre Tachoire
f00c0ab276 webmcp: implement abortSignal with _dependent 2026-05-15 13:11:59 +02:00
Pierre Tachoire
3803a1f8c6 webmcp: use value.jsonStringify for JSON write 2026-05-15 11:17:53 +02:00
Pierre Tachoire
dbb9b31061 webmcp: fix invoke callback with correct ModelContextClient param 2026-05-15 11:00:56 +02:00
Karl Seguin
64c5843e9e Merge pull request #2466 from lightpanda-io/pending_queue_pump
Clear pending destroy on createPage (a known safepoint).
2026-05-15 16:42:15 +08:00
Pierre Tachoire
7c5a3b211f cdp: cancel inflight webmcp invocation on bc deinit 2026-05-15 08:50:48 +02:00
Pierre Tachoire
19fd9a6e35 cdp: adjust inv_id address usage 2026-05-15 08:50:47 +02:00
Pierre Tachoire
5e0901aaf7 cdp: fix invalid arena usage in webmcp 2026-05-15 08:50:47 +02:00
Pierre Tachoire
3ef6e57d58 cdp: adjust invocation id usage for webmcp 2026-05-15 08:50:47 +02:00
Pierre Tachoire
c23d0f4f35 cdp: implement webMCP domain 2026-05-15 08:50:46 +02:00
Pierre Tachoire
0023bd7d19 Add WebMCP navigator.modelContext
Implements the page-side surface of the W3C WebMCP spec
(https://webmachinelearning.github.io/webmcp/): exposes
`navigator.modelContext.registerTool(...)` for declaring MCP tools to a
browser agent, with full name/description validation, AbortSignal-based
unregistration, and a `ModelContextClient` whose `requestUserInteraction`
invokes its callback directly (closest faithful behavior in a headless
browser).
2026-05-15 08:50:38 +02:00
Karl Seguin
a5162bea8f Cleanup HttpClient.Transfer
This is just moving fields around. The end result is that there's a
`transfer.req` and a `transfer.res`.

On the Request side, we use to have a nested `params: RequestParam` resulting
in a lot of `transfer.req.params.url`. This is now `transfer.req.url`. On the
Response side, we had the exact opposite: response fields splattered directly
in the transfer, `transfer.response_header`. This is now `transfer.res.header`.

There is now an HttpClient.Response, which is the actual final response (which
could be for a transfer or something else, e.g the cache). And an
HttpClient.Transfer.Response which captures the inflight response data (and is
one of the polymorphic variants of the HttpClient.Response). Probably still not
ideal, but I'm not sure how to make it cleaner, and even if this is just an
intermediary step, I consider it an small win.
2026-05-15 12:55:47 +08:00
Karl Seguin
4205cd905b Clear pending destroy on createPage (a known safepoint).
This allows pending destroys that have been accumulated to be cleaned up. In
normal operations, this likely isn't going to happen. But we see a some unit
tests create _many_ pages that never have the change to be cleaned up. The
result is that the next "normal" unit test, which actually runs enough through
Runner to trigger the cleanup, pays a huge cleanup price.

Arguably, for a test-only solution, we could create a session per test, or
have explicit cleanup in the test. But having 1 long-lasting session is useful
as it can show us these potential pitfalls AND, it isn't impossible that a
real-world case runs into similar issues.
2026-05-15 11:31:53 +08:00
Karl Seguin
cb8c2bc4d8 Merge pull request #2456 from lightpanda-io/cdp-proper-cache-disable
properly disable cache on `Network.setCacheDisabled`
2026-05-15 09:11:12 +08:00
Karl Seguin
632f3ea7d6 Merge pull request #2457 from lightpanda-io/fetch_dump_navigate_fix
Dump using the latest Frame to prevent segfault during on frame change
2026-05-15 07:38:19 +08:00
Karl Seguin
94f0d94192 Merge pull request #2461 from staylor/fix/2459-surface-at-rules-via-insertrule
Surface at-rules through insertRule and replaceSync (fixes #2459 partial)
2026-05-15 07:35:59 +08:00
Karl Seguin
b7a0ca2bca fallback unknown rule to new unknown type 2026-05-15 06:59:21 +08:00
Scott Taylor
6d1740b40f Surface at-rules through insertRule and replaceSync (fixes #2459)
CSSStyleSheet.insertRule previously detected at-rules in the parser
(has_skipped_at_rule) and returned the requested index without inserting
anything. The original change (PR #1972) did this to keep at-rule input
from killing module evaluation in apps like Expo Web -- correct, but the
silent-success contract has a second-order effect: CSS-in-JS libraries
(emotion, styled-components, Stitches, Mantine, Linaria) that round-trip
through cssRules to deduplicate their stylesheets see the rule as missing
after insertion, conclude the stylesheet is empty, and fall back to direct
<style> element injection per render. The result is unbounded <style>
accumulation on long-lived sessions doing repeated DOM interaction. See
issue #2459 for measurements (Allbirds-style PDPs accumulating thousands
of <style> elements over a render loop).

Changes:

* Parser.RulesIterator now returns a Rule union of {.style, .at_rule}
  instead of skipping at-rules and setting has_skipped_at_rule. The
  at-rule variant carries the keyword (without `@`) and the full source
  span so callers can construct an opaque placeholder rule.

* CSSRule gains a `_text` field and an `initAtRule` constructor for
  storing the at-rule source. CSSRule.getCssText returns the stored
  text (CSSStyleRule's overridden getCssText still wins for `.style`
  rules via the bridge dispatch on the most-derived class).

* CSSStyleSheet.insertRule and replaceSync handle both Rule variants:
  regular rules become CSSStyleRule as before, at-rules become opaque
  CSSRule placeholders with the matching CSSRule.Type variant
  (vendor-prefixed keyframes are normalized; unrecognized at-rule
  keywords fall back to .media). The CSS engine still doesn't apply
  these rules -- that's intentional and outside the scope of this
  change -- but they now surface via cssRules so library dedup paths
  work correctly.

* StyleManager.addRawRules switches on the Rule kind and skips
  at-rules (it only filters on display/visibility/opacity at the top
  level, no semantic change).

* The CSSRule spec constants (STYLE_RULE, KEYFRAMES_RULE, MEDIA_RULE,
  ...) were declared as plain Zig consts inside JsApi but never wrapped
  with bridge.property, so they came back as `undefined` from JS. Fixed
  while writing the regression tests since the tests need to compare
  rule.type against the spec constants.

Tests:

* Parser unit tests: cover statement at-rules (`@import url(...);`),
  block at-rules (`@media`), and vendor-prefixed at-rules
  (`@-webkit-keyframes`).

* HTML runner tests: cover insertRule for @keyframes, @media,
  @supports, @font-face, vendor-prefixed at-rules, mixed style + at-rule
  insertion, replaceSync at-rule preservation, and the dedup-via-cssRules
  pattern that's the actual library code path the bug breaks.

A/B verification with the synthetic CSS-in-JS dedup pattern (50 calls to
inject the same @keyframes through a library that falls back to <style>
element injection when insertRule appears empty):

  baseline (1.0.0-nightly.6240): styles=51 rules=0
  patched (this branch):         styles=1  rules=50

The leak collapses: instead of 50 <style> fallbacks stacking up, the
single persistent stylesheet receives all 50 insertions and dedup works.

Note on partial coverage of #2459: the original Allbirds reproducer
involves a Vue.js + Yotpo widget that injects <style> elements via
direct document.head.appendChild rather than insertRule. That code
path is unaffected by this change; it appears to be a separate
mechanism (possibly related to Vue's vue-style-loader closure-based
dedup or web-component lifecycle on Lightpanda) and is worth filing
separately. This PR fixes the specific insertRule contract issue
described in #2459 and unblocks the major CSS-in-JS libraries.
2026-05-14 16:19:36 -04:00
Muki Kiboigo
940976b6a7 properly disable cache on Network.setCacheDisabled 2026-05-14 09:03:51 -07:00