Repository: denoland/deno
Pull request: feat: add `--open` to `deno serve` to open server in browser

This PR addresses issue number #25149 

Change:
Supported --open flag with deno serve -> (deno serve --open somescript.ts/js).
The action that takes place is openning the browser on the address that the server is running on.


Changes on code:
- Added the flag --open to the serve flags as "open_site: bool".



Concerns:
The window tab will be opened on deno serve command no matter whether deno serve succeeds or fails.
The added test since it's a serve command then it starts listening for a request and never shuts down, I couldn't figure out how to shut it down same as other serve tests.


Changed files:
filename: cli/args/flags.rs
patch: @@ -356,6 +356,7 @@ pub struct ServeFlags {
   pub port: u16,
   pub host: String,
   pub worker_count: Option<usize>,
+  pub open_site: bool,
 }
 
 impl ServeFlags {
@@ -367,6 +368,7 @@ impl ServeFlags {
       port,
       host: host.to_owned(),
       worker_count: None,
+      open_site: false,
     }
   }
 }
@@ -3086,6 +3088,11 @@ Start a server defined in server.ts, watching for changes and running on port 50
         .long("host")
         .help("The TCP address to serve on, defaulting to 0.0.0.0 (all interfaces)")
         .value_parser(serve_host_validator),
+    ).arg(
+      Arg::new("open")
+      .long("open")
+      .help("Open the browser on the address that the server is running on.")
+      .action(ArgAction::SetTrue)
     )
     .arg(
       parallel_arg("multiple server workers")
@@ -5285,6 +5292,7 @@ fn serve_parse(
   let host = matches
     .remove_one::<String>("host")
     .unwrap_or_else(|| "0.0.0.0".to_owned());
+  let open_site = matches.remove_one::<bool>("open").unwrap_or(false);
 
   let worker_count = parallel_arg_parse(matches).map(|v| v.get());
 
@@ -5331,6 +5339,7 @@ fn serve_parse(
     port,
     host,
     worker_count,
+    open_site,
   });
 
   Ok(())

filename: cli/tools/serve.rs
patch: @@ -1,5 +1,6 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
+use std::str::FromStr;
 use std::sync::Arc;
 
 use deno_core::error::AnyError;
@@ -46,6 +47,12 @@ pub async fn serve(
 
   let worker_factory =
     Arc::new(factory.create_cli_main_worker_factory().await?);
+
+  if serve_flags.open_site {
+    let url = resolve_serve_url(serve_flags.host, serve_flags.port);
+    let _ = open::that_detached(url);
+  }
+
   let hmr = serve_flags
     .watch
     .map(|watch_flags| watch_flags.hmr)
@@ -180,3 +187,40 @@ async fn serve_with_watch(
   .await?;
   Ok(0)
 }
+
+fn resolve_serve_url(host: String, port: u16) -> String {
+  let host = if matches!(host.as_str(), "0.0.0.0" | "::") {
+    "127.0.0.1".to_string()
+  } else if std::net::Ipv6Addr::from_str(&host).is_ok() {
+    format!("[{}]", host)
+  } else {
+    host
+  };
+  if port == 80 {
+    format!("http://{host}/")
+  } else {
+    format!("http://{host}:{port}/")
+  }
+}
+
+#[cfg(test)]
+mod test {
+  use super::*;
+
+  #[test]
+  fn test_resolve_serve_url() {
+    assert_eq!(
+      resolve_serve_url("localhost".to_string(), 80),
+      "http://localhost/"
+    );
+    assert_eq!(
+      resolve_serve_url("0.0.0.0".to_string(), 80),
+      "http://127.0.0.1/"
+    );
+    assert_eq!(resolve_serve_url("::".to_string(), 80), "http://127.0.0.1/");
+    assert_eq!(
+      resolve_serve_url("::".to_string(), 90),
+      "http://127.0.0.1:90/"
+    );
+  }
+}