From db258bfec49e5b52892328c4761270de7a54ac09 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Aug 2026 16:44:09 +0800 Subject: [PATCH 1/4] breaking: disable worker and iframe loading by default By default, iframes and workers no longer loaded. Use `--load-resources iframe` and `--load-resources worker` to restore the previous behavior. The disabling makes resource loading more consistent. To further make things more consistent, Config seems the following changes: 1. remove `--timeout` from `serve` which does nothing but has printed a deprecation warning for a long time 2. added .deprecated field to CLI config flags which now logs the specified deprecated warning when used 3. `--log-filter-scopes` is deprecated in favor of `--log-scopes` 4. `--disable_subframes` is deprecated. Iframe loading is disabled by default, use `--load-resources iframe` to enable iframe loading 5. `--disable_workers` is deprecated. Worker loading is disabled by default, use `--load-resources worker` to enable worker loading 6. `--enable_external_stylesheets` is deprecated. Stylesheets remain disabled by default. Use `--load-resources stylesheet` to enable loading external stylesheets CLI log parameters now alter the logger behavior on parse. This helps minimze the window where default log settings are in-play. It also means things like this work: ``` ./lightpanda --log-level fatal --disable_subframes --log-level warn ``` More seriously, there's now an optional `beforeParse` fired once the mode is known. This is used by mcp to set the default log level to logfmt. Previously this was done much later and could easily result in a mix of pretty and logfmt logs. --- .github/workflows/e2e-test.yml | 2 +- src/Config.zig | 119 +++++++++--------- src/browser/Frame.zig | 13 +- src/browser/Session.zig | 27 +--- .../webapi/SharedWorkerGlobalScope.zig | 2 +- src/browser/webapi/Worker.zig | 8 +- src/browser/webapi/element/html/Link.zig | 4 +- src/cdp/domains/lp.zig | 36 +++--- src/cli.zig | 14 ++- src/help.zon | 40 +++--- src/log.zig | 30 ++--- src/main.zig | 10 -- src/testing.zig | 16 ++- 13 files changed, 144 insertions(+), 177 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 10cb9264c..618411698 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" 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 f01195b84..f950ffc19 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -1855,8 +1855,7 @@ 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) { iframe._executed = true; return; } @@ -2269,9 +2268,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 +3793,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..af06aea6c 100644 --- a/src/browser/webapi/SharedWorkerGlobalScope.zig +++ b/src/browser/webapi/SharedWorkerGlobalScope.zig @@ -92,7 +92,7 @@ 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) { + if (session.load_resources.worker == false) { log.debug(.browser, "shared worker disabled", .{ .url = owned_url }); return self; } diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig index 049bb8064..7ca95befa 100644 --- a/src/browser/webapi/Worker.zig +++ b/src/browser/webapi/Worker.zig @@ -91,13 +91,7 @@ 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) { + if (session.load_resources.worker == false) { log.debug(.browser, "worker disabled", .{ .url = resolved_url }); 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