diff --git a/.github/workflows/agent-regression.yml b/.github/workflows/agent-regression.yml
index 431f88313..a3f689548 100644
--- a/.github/workflows/agent-regression.yml
+++ b/.github/workflows/agent-regression.yml
@@ -65,6 +65,7 @@ jobs:
shell: bash
env:
LPD_PATH: ${{ github.workspace }}/bin/lightpanda
+ LPD_ARGS: --load-resources iframe --load-resources worker
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
LP_MODEL: ${{ github.event.inputs.model }}
# Optional: news.ycombinator.com often blocks datacenter IPs. Reuse
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
index 10cb9264c..02c763b0f 100644
--- a/.github/workflows/e2e-test.yml
+++ b/.github/workflows/e2e-test.yml
@@ -106,7 +106,7 @@ jobs:
- id: args
name: build LP args
run: |
- args="--http-cache-dir /tmp/lp-cache"
+ args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe"
[ "${{ matrix.proxy }}" = "true" ] && args="$args --http-proxy http://127.0.0.1:3000"
[ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots"
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem"
@@ -162,7 +162,7 @@ jobs:
- id: args
name: build LP args
run: |
- args="--http-cache-dir /tmp/lp-cache"
+ args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe"
[ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots"
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem"
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-domain ${{ vars.WBA_DOMAIN }}"
@@ -429,6 +429,7 @@ jobs:
- name: deterministic agent replay
env:
LPD_PATH: ${{ github.workspace }}/bin/lightpanda
+ LPD_ARGS: --load-resources iframe --load-resources worker
run: ./agent/run.sh deterministic
mcp-smoke:
diff --git a/src/Config.zig b/src/Config.zig
index 7dbebfe6f..f23c5127b 100644
--- a/src/Config.zig
+++ b/src/Config.zig
@@ -40,9 +40,11 @@ pub const CDP_TCP_USER_TIMEOUT_MS: c_int = 10_000;
const Config = @This();
-fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
+fn logFilterValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
const str = args.next() orelse return error.InvalidOption;
+ defer log.opts.scope_enabled = log.resolveFilters(list.items);
+
var it = std.mem.splitScalar(u8, str, ',');
while (it.next()) |part| {
if (part.len == 0) continue;
@@ -66,7 +68,7 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterat
}
const v = std.meta.stringToEnum(log.Scope, name) orelse {
- log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter-scopes", .value = part });
+ log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter", .value = part });
return error.InvalidOption;
};
@@ -78,6 +80,7 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l
const str = args.next() orelse return error.MissingArgument;
if (std.mem.eql(u8, str, "error")) {
target.* = .err;
+ log.opts.level = .err;
return;
}
@@ -85,6 +88,24 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l
log.fatal(.app, "invalid option choice", .{ .arg = "--log-level", .value = str });
return error.InvalidArgument;
};
+ log.opts.level = target.*.?;
+}
+
+// The MCP host captures stderr into a log file, where pretty's ANSI
+// escapes and multi-line entries are noise. Runs before any option is
+// read so parse-time lines match; --log-format still overrides.
+fn mcpLogDefaults() void {
+ log.opts.format = .logfmt;
+}
+
+fn logFormatValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?log.Format) !void {
+ const str = args.next() orelse return error.MissingArgument;
+ const format = std.meta.stringToEnum(log.Format, str) orelse {
+ log.fatal(.app, "invalid option choice", .{ .arg = "--log-format", .value = str });
+ return error.InvalidArgument;
+ };
+ target.* = format;
+ log.opts.format = format;
}
fn httpHeaderValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(HttpHeader)) !void {
@@ -211,8 +232,11 @@ fn caPathValidator(
}
}
-pub const LoadResources = packed struct(u1) {
+pub const LoadResources = packed struct(u4) {
image: bool = false,
+ iframe: bool = false,
+ worker: bool = false,
+ stylesheet: bool = false,
};
/// Common CLI args.
@@ -231,8 +255,9 @@ const CommonOptions = .{
.{ .name = "ws_max_concurrent", .type = ?u8 },
.{ .name = "insecure_disable_tls_host_verification", .type = bool },
.{ .name = "log_level", .type = ?log.Level, .validator = logLevelValidator },
- .{ .name = "log_format", .type = ?log.Format },
- .{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterScopesValidator },
+ .{ .name = "log_format", .type = ?log.Format, .validator = logFormatValidator },
+ .{ .name = "log_filter", .type = log.FilterRule, .multiple = true, .validator = logFilterValidator },
+ .{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterValidator, .deprecated = "use --log-filter" },
.{ .name = "user_agent_suffix", .type = ?[]const u8 },
.{ .name = "http_cache_dir", .type = ?[]const u8 },
.{ .name = "http_cache_entry_limit", .type = ?u32, .default = 1000 },
@@ -246,9 +271,9 @@ const CommonOptions = .{
.{ .name = "adblock_lists", .type = ?[]const u8 },
.{ .name = "cookie", .type = ?[]const u8 },
.{ .name = "cookie_jar", .type = ?[]const u8 },
- .{ .name = "disable_subframes", .type = bool },
- .{ .name = "disable_workers", .type = bool },
- .{ .name = "enable_external_stylesheets", .type = bool },
+ .{ .name = "disable_subframes", .type = bool, .deprecated = "subframes are now disabled by default, use \"--load-resources iframe\" to enable" },
+ .{ .name = "disable_workers", .type = bool, .deprecated = "workers are now disabled by default, use \"--load-resources worker\" to enable" },
+ .{ .name = "enable_external_stylesheets", .type = bool, .deprecated = "use \"--load-resources stylesheet\" to enable" },
.{ .name = "load_resources", .type = LoadResources, .default = LoadResources{} },
.{ .name = "v8_flags_unsafe", .type = ?[]const u8 },
.{ .name = "v8_max_heap_mb", .type = ?u32 },
@@ -355,7 +380,6 @@ const Commands = cli.Builder(.{
.{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
.{ .name = "port", .type = u16, .default = 9222 },
.{ .name = "advertise_host", .type = ?[]const u8 },
- .{ .name = "timeout", .type = ?u31 },
.{ .name = "cdp_max_connections", .type = u16, .default = 16 },
.{ .name = "cdp_max_pending_connections", .type = u16, .default = 128 },
.{ .name = "cdp_max_message_size", .type = u32, .default = 1024 * 1024 },
@@ -403,6 +427,7 @@ const Commands = cli.Builder(.{
},
.{
.name = "mcp",
+ .before_parse = mcpLogDefaults,
.options = .{
.{ .name = "port", .type = ?u16 },
.{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
@@ -469,6 +494,18 @@ pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
if (modeNeedsHttp(mode)) {
config.http_headers = try HttpHeaders.init(allocator, &config);
}
+
+ switch (config.mode) {
+ inline else => |*m| {
+ if (@hasField(@TypeOf(m.*), "enable_external_stylesheets")) {
+ if (m.enable_external_stylesheets) {
+ // map deprecated property onto updated one
+ m.load_resources.stylesheet = true;
+ }
+ }
+ },
+ }
+
return config;
}
@@ -503,20 +540,6 @@ pub fn obeyRobots(self: *const Config) bool {
};
}
-pub fn disableSubframes(self: *const Config) bool {
- return switch (self.mode) {
- inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_subframes,
- else => unreachable,
- };
-}
-
-pub fn disableWorkers(self: *const Config) bool {
- return switch (self.mode) {
- inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_workers,
- else => unreachable,
- };
-}
-
pub fn watchdogMs(self: *const Config) ?u32 {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| {
@@ -527,13 +550,6 @@ pub fn watchdogMs(self: *const Config) ?u32 {
};
}
-pub fn enableExternalStylesheets(self: *const Config) bool {
- return switch (self.mode) {
- inline .serve, .fetch, .mcp, .agent => |opts| opts.enable_external_stylesheets,
- else => unreachable,
- };
-}
-
pub fn loadResources(self: *const Config) LoadResources {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.load_resources,
@@ -641,18 +657,6 @@ pub fn wsMaxConcurrent(self: *const Config) u8 {
};
}
-pub fn logLevel(self: *const Config) ?log.Level {
- return switch (self.mode) {
- // Agent mode quiets page-driven `console.error` noise unless verbosity=high.
- .agent => |opts| opts.log_level orelse switch (agentVerbosity(opts)) {
- .low, .medium => .err,
- .high => null,
- },
- inline .serve, .fetch, .mcp => |opts| opts.log_level,
- else => unreachable,
- };
-}
-
/// Resolve --verbosity. Explicit value wins. Else: --task with stderr
/// captured (pipe/file) defaults to .high so benchmark harnesses and
/// other programmatic consumers get the [tool/result] trace; REPL and
@@ -676,23 +680,6 @@ fn stderrIsTty() bool {
return stderr_tty_cached;
}
-pub fn logFormat(self: *const Config) ?log.Format {
- return switch (self.mode) {
- // The MCP host captures stderr into a log file, where pretty's ANSI
- // escapes and multi-line entries are noise.
- .mcp => |opts| opts.log_format orelse .logfmt,
- inline .serve, .fetch, .agent => |opts| opts.log_format,
- else => unreachable,
- };
-}
-
-pub fn logFilterScopes(self: *const Config) std.ArrayList(log.FilterRule) {
- return switch (self.mode) {
- inline .serve, .fetch, .mcp, .agent => |opts| opts.log_filter_scopes,
- else => unreachable,
- };
-}
-
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.user_agent_suffix,
@@ -1075,9 +1062,6 @@ fn printPaged(allocator: Allocator, text: []const u8) void {
pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
const exec_name, var command = try Commands.parse(allocator, proc_args);
- if (command == .serve and command.serve.timeout != null) {
- log.warn(.app, "--timeout is deprecated", .{});
- }
const invoked = std.meta.activeTag(command);
// Rewrite `run` to `.agent` so nothing downstream needs a `.run` case.
if (command == .run) {
@@ -1093,6 +1077,17 @@ pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
}
command = .{ .agent = agent_opts };
}
+
+ // Agent mode quiets page-driven `console.error` noise unless
+ // verbosity=high. Depends on --verbosity/--task, so it can only be
+ // resolved after the options are parsed; an explicit --log-level wins.
+ if (command == .agent) {
+ const opts = command.agent;
+ if (opts.log_level == null and agentVerbosity(opts) != .high) {
+ log.opts.level = .err;
+ }
+ }
+
var config = try Config.init(allocator, exec_name, command);
config.command = invoked;
return config;
diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig
index 8e9469dbd..cef2034f4 100644
--- a/src/browser/Frame.zig
+++ b/src/browser/Frame.zig
@@ -1855,8 +1855,8 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
if (iframe._executed) {
return;
}
- if (!self._session.subframe_loading_enabled) {
- // configured not to load frames
+ if (self._session.load_resources.iframe == false) {
+ log.warnDisabledIFrame();
iframe._executed = true;
return;
}
@@ -2269,9 +2269,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
const session = self._session;
- // this feature is disabled by default, and can be turned on via a command
- // line flag or via an CDP command
- if (session.load_external_stylesheets == false) {
+ if (session.load_resources.stylesheet == false) {
return self.queueLoad(Factory.protoOf(link));
}
@@ -3796,9 +3794,9 @@ test "Frame: iframeAddedCallback does not create a frame when termination is pen
defer testing.test_session.closeAllPages();
const session = frame._session;
- const subframe_loading_enabled = session.subframe_loading_enabled;
- session.subframe_loading_enabled = true;
- defer session.subframe_loading_enabled = subframe_loading_enabled;
+ const subframe_loading_enabled = session.load_resources.iframe;
+ session.load_resources.iframe = true;
+ defer session.load_resources.iframe = subframe_loading_enabled;
const element = try frame.document.createElement("iframe", null, frame);
const iframe = element.as(HtmlElement.IFrame);
diff --git a/src/browser/Session.zig b/src/browser/Session.zig
index f28ce8e29..c9412e64a 100644
--- a/src/browser/Session.zig
+++ b/src/browser/Session.zig
@@ -92,33 +92,14 @@ _tool_frame_override: ?u32 = null,
// connection (see `Browser.frame_id_gen` and issue #2472).
loader_id_gen: u32 = 0,
-// configuration (or CDP command) to disable iframe loading
-subframe_loading_enabled: bool = true,
-
-// configuration (or CDP command) to disable Web Worker loading. When false,
-// `new Worker(url)` returns a Worker object whose script is never fetched
-// and never evaluated. Set from the `--disable-workers` CLI flag at
-// session init; the LP.configureLoading CDP method can flip it per-session.
-worker_loading_enabled: bool = true,
-
// Console.* capture for the `consoleLogs` tool, capped at `max_console_bytes`.
// Opt-in via `enableConsoleCapture`: plain CDP `serve` never drains it, so
// leaving the listener off keeps the buffer at zero bytes.
_console_messages: std.Io.Writer.Allocating,
_console_capture: bool = false,
-// Opt-in fetch of external resources. Defaults to
-// false to preserve the current rendering-free fast path: drivers that
-// don't need accurate visibility checks pay nothing. Set from the
-// `--enable-external-stylesheets` CLI flag at session init; the
-// LP.configureLoading CDP method can flip it per-session. When true,
-// `Link.linkAddedCallback` routes to `Frame.loadExternalStylesheet`
-// (synchronous fetch + parse + register on `document.styleSheets`).
-load_external_stylesheets: bool = false,
-
-// Sub-resources to actually request. Off by default: a driver that only
-// reads the DOM shouldn't pay for bytes it never looks at.
-load_resources: Config.LoadResources = .{},
+// configured external resources (images, stylesheet, worker, iframe) to load
+load_resources: Config.LoadResources,
/// Caller-supplied cancellation probe. `Runner._wait` polls it between
/// ticks; once `check` returns true the wait returns `error.Cancelled`.
@@ -180,11 +161,7 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.browser = browser,
.notification = notification,
.cookie_jar = storage.Cookie.Jar.init(allocator, notification),
- // CLI defaults; LP.configureLoading can flip these per-session.
- .subframe_loading_enabled = !browser.app.config.disableSubframes(),
- .worker_loading_enabled = !browser.app.config.disableWorkers(),
._console_messages = .init(allocator),
- .load_external_stylesheets = browser.app.config.enableExternalStylesheets(),
.load_resources = browser.app.config.loadResources(),
};
errdefer self._console_messages.deinit();
diff --git a/src/browser/webapi/SharedWorkerGlobalScope.zig b/src/browser/webapi/SharedWorkerGlobalScope.zig
index 65ef9f1d2..7bf26cdc6 100644
--- a/src/browser/webapi/SharedWorkerGlobalScope.zig
+++ b/src/browser/webapi/SharedWorkerGlobalScope.zig
@@ -92,8 +92,8 @@ pub fn init(frame: *Frame, url: [:0]const u8, name: []const u8, worker_type: Wor
const proto = self._proto;
errdefer proto.deinit();
- if (!session.worker_loading_enabled) {
- log.debug(.browser, "shared worker disabled", .{ .url = owned_url });
+ if (session.load_resources.worker == false) {
+ log.warnDisabledWorker();
return self;
}
diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig
index 049bb8064..d6013a4b4 100644
--- a/src/browser/webapi/Worker.zig
+++ b/src/browser/webapi/Worker.zig
@@ -91,14 +91,8 @@ pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
self._worker_scope = dedicated_worker;
try frame.trackWorker(self);
- // `--disable-workers` (or `LP.configureLoading { worker: false }`):
- // skip the script fetch and eval. The Worker object is still
- // constructed so JS `new Worker(url)` does not throw, but the
- // worker's eval never runs (postMessage from the page is queued
- // indefinitely with no handler to drain it). Mirrors the
- // `subframe_loading_enabled` pattern for iframes.
- if (!session.worker_loading_enabled) {
- log.debug(.browser, "worker disabled", .{ .url = resolved_url });
+ if (session.load_resources.worker == false) {
+ log.warnDisabledWorker();
return self;
}
diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig
index c1a3b1797..558df7d24 100644
--- a/src/browser/webapi/element/html/Link.zig
+++ b/src/browser/webapi/element/html/Link.zig
@@ -193,7 +193,7 @@ test "WebApi: HTML.Link" {
test "WebApi: HTML.Link external stylesheet" {
testing.silenceLog(&.{.http});
- try testing.htmlRunner("css/external_stylesheet.html", .{ .load_external_stylesheets = true });
+ try testing.htmlRunner("css/external_stylesheet.html", .{ .load_resources = .{ .stylesheet = true } });
}
// Regression: a synchronous external-stylesheet fetch must not strand the
@@ -202,5 +202,5 @@ test "WebApi: HTML.Link external stylesheet" {
// never drains and the document is stuck at readyState "loading".
test "WebApi: HTML.Link deferred script then external stylesheet" {
testing.silenceLog(&.{.http});
- try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_external_stylesheets = true });
+ try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_resources = .{ .stylesheet = true } });
}
diff --git a/src/cdp/domains/lp.zig b/src/cdp/domains/lp.zig
index 12f76efca..e24eba353 100644
--- a/src/cdp/domains/lp.zig
+++ b/src/cdp/domains/lp.zig
@@ -102,10 +102,10 @@ fn configureLoading(cmd: *CDP.Command) !void {
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.NoBrowserContext;
- if (params.subFrame) |v| bc.session.subframe_loading_enabled = v;
- if (params.worker) |v| bc.session.worker_loading_enabled = v;
- if (params.externalStylesheets) |v| bc.session.load_external_stylesheets = v;
if (params.images) |v| bc.session.load_resources.image = v;
+ if (params.worker) |v| bc.session.load_resources.worker = v;
+ if (params.subFrame) |v| bc.session.load_resources.iframe = v;
+ if (params.externalStylesheets) |v| bc.session.load_resources.stylesheet = v;
return cmd.sendResult(null, .{});
}
@@ -771,8 +771,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
_ = try bc.session.createPage();
// Defaults: both loading types enabled.
- try testing.expectEqual(true, bc.session.subframe_loading_enabled);
- try testing.expectEqual(true, bc.session.worker_loading_enabled);
+ try testing.expectEqual(true, bc.session.load_resources.iframe);
+ try testing.expectEqual(true, bc.session.load_resources.worker);
// subFrame-only: leaves worker untouched.
try ctx.processMessage(.{
@@ -781,8 +781,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
.params = .{ .subFrame = false },
});
try ctx.expectSentResult(null, .{ .id = 1 });
- try testing.expectEqual(false, bc.session.subframe_loading_enabled);
- try testing.expectEqual(true, bc.session.worker_loading_enabled);
+ try testing.expectEqual(false, bc.session.load_resources.iframe);
+ try testing.expectEqual(true, bc.session.load_resources.worker);
// worker-only: leaves subFrame untouched.
try ctx.processMessage(.{
@@ -791,8 +791,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
.params = .{ .worker = false },
});
try ctx.expectSentResult(null, .{ .id = 2 });
- try testing.expectEqual(false, bc.session.subframe_loading_enabled);
- try testing.expectEqual(false, bc.session.worker_loading_enabled);
+ try testing.expectEqual(false, bc.session.load_resources.iframe);
+ try testing.expectEqual(false, bc.session.load_resources.worker);
// Both at once.
try ctx.processMessage(.{
@@ -801,8 +801,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
.params = .{ .subFrame = true, .worker = true },
});
try ctx.expectSentResult(null, .{ .id = 3 });
- try testing.expectEqual(true, bc.session.subframe_loading_enabled);
- try testing.expectEqual(true, bc.session.worker_loading_enabled);
+ try testing.expectEqual(true, bc.session.load_resources.iframe);
+ try testing.expectEqual(true, bc.session.load_resources.worker);
}
test "cdp.lp: configureLoading toggles externalStylesheets independently" {
@@ -813,7 +813,7 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
_ = try bc.session.createPage();
// Default is opt-in: off unless the CLI flag or CDP toggle enables it.
- try testing.expectEqual(false, bc.session.load_external_stylesheets);
+ try testing.expectEqual(false, bc.session.load_resources.stylesheet);
// Enable via CDP; the other two loading toggles stay at their defaults.
try ctx.processMessage(.{
@@ -822,9 +822,9 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
.params = .{ .externalStylesheets = true },
});
try ctx.expectSentResult(null, .{ .id = 1 });
- try testing.expectEqual(true, bc.session.load_external_stylesheets);
- try testing.expectEqual(true, bc.session.subframe_loading_enabled);
- try testing.expectEqual(true, bc.session.worker_loading_enabled);
+ try testing.expectEqual(true, bc.session.load_resources.stylesheet);
+ try testing.expectEqual(true, bc.session.load_resources.iframe);
+ try testing.expectEqual(true, bc.session.load_resources.worker);
// Flip back off; partial params must not reset the other fields.
try ctx.processMessage(.{
@@ -833,7 +833,7 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
.params = .{ .externalStylesheets = false },
});
try ctx.expectSentResult(null, .{ .id = 2 });
- try testing.expectEqual(false, bc.session.load_external_stylesheets);
- try testing.expectEqual(true, bc.session.subframe_loading_enabled);
- try testing.expectEqual(true, bc.session.worker_loading_enabled);
+ try testing.expectEqual(false, bc.session.load_resources.stylesheet);
+ try testing.expectEqual(true, bc.session.load_resources.iframe);
+ try testing.expectEqual(true, bc.session.load_resources.worker);
}
diff --git a/src/cli.zig b/src/cli.zig
index 75472eef6..aa788a93d 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -52,6 +52,11 @@ const log = lp.log;
/// that appears in both with the same field name and type is collapsed
/// into one field (the command's own option wins); reusing a name with
/// a different type is a compile error.
+/// - `before_parse: fn () void` (optional) — called once the command is
+/// known (by name or sniffed from a legacy flag), before any option is
+/// read. For mode-level process defaults that must already hold while
+/// the options themselves are parsed, e.g. log settings; an explicit
+/// option parsed later still wins.
/// - `positional: struct` (optional) — a positional argument with `.name`
/// and `.type` that may appear anywhere in argv. By default it holds a
/// single value: `.type` must be an optional pointer-to-u8 slice (e.g.
@@ -79,6 +84,8 @@ const log = lp.log;
/// built-in type switch. See the validator section below.
/// - `variants: tuple` (optional) — alternate flag names that write into
/// the same field. See the variants section below.
+/// - `deprecated: []const u8` (optional) — the option still parses, but
+/// each use logs a warning carrying this note.
///
/// ## Supported types and their defaults
///
@@ -470,7 +477,6 @@ pub fn Builder(comptime commands: anytype) type {
inline for (.{
"--host",
"--port",
- "--timeout",
}) |heuristic| {
if (std.mem.eql(u8, cmd_str, heuristic)) {
return .serve;
@@ -526,6 +532,9 @@ pub fn Builder(comptime commands: anytype) type {
const OptionType = @TypeOf(option);
const is_multiple = @hasField(OptionType, "multiple") and option.multiple;
const has_validator = @hasField(OptionType, "validator");
+ if (@hasField(OptionType, "deprecated")) {
+ log.warn(.app, "deprecated CLI parameter", .{ .name = option.name, .note = option.deprecated });
+ }
// Prefer validator for parsing if provided. The validator writes
// through the field pointer (the list itself for multiples).
@@ -682,6 +691,9 @@ pub fn Builder(comptime commands: anytype) type {
args: *std.process.Args.Iterator,
) !Union {
const Command = @FieldType(Union, command.name);
+ if (@hasField(@TypeOf(command), "before_parse")) {
+ command.before_parse();
+ }
var c = Command{};
const options = blk: {
diff --git a/src/help.zon b/src/help.zon
index 646a8a6b0..49571d17a 100644
--- a/src/help.zon
+++ b/src/help.zon
@@ -351,28 +351,26 @@
\\ --cookie-jar
\\ Path to a JSON file to save cookies to on exit (write-only).
\\ Defaults to no cookie saving.
- \\ --disable-subframes
- \\ Skip loading