Переглянути джерело

Fix `--args` and handle merging in `@server` and `@client` (#4212)

* Pass args after -- to the executable during run and serve

* fix clippy

* switch to accepting a string argument for subcommand compatibility

* Handle merging with `@server` and `@client`

* fix clippy

* fix formatting

* always build into the client package

* fix clippy

* more clippy fixes
Evan Almloff 1 тиждень тому
батько
коміт
3bf8f09c65

+ 5 - 2
packages/cli/src/build/builder.rs

@@ -417,6 +417,7 @@ impl AppBuilder {
         }
     }
 
+    #[allow(clippy::too_many_arguments)]
     pub(crate) async fn open(
         &mut self,
         devserver_ip: SocketAddr,
@@ -425,6 +426,7 @@ impl AppBuilder {
         open_browser: bool,
         always_on_top: bool,
         build_id: BuildId,
+        args: &[String],
     ) -> Result<()> {
         let krate = &self.build;
 
@@ -501,7 +503,7 @@ impl AppBuilder {
             | Platform::MacOS
             | Platform::Windows
             | Platform::Linux
-            | Platform::Liveview => self.open_with_main_exe(envs)?,
+            | Platform::Liveview => self.open_with_main_exe(envs, args)?,
         };
 
         self.builds_opened += 1;
@@ -732,12 +734,13 @@ impl AppBuilder {
     /// paths right now, but they will when we start to enable things like swift integration.
     ///
     /// Server/liveview/desktop are all basically the same, though
-    fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<()> {
+    fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>, args: &[String]) -> Result<()> {
         let main_exe = self.app_exe();
 
         tracing::debug!("Opening app with main exe: {main_exe:?}");
 
         let mut child = Command::new(main_exe)
+            .args(args)
             .envs(envs)
             .env_remove("CARGO_MANIFEST_DIR") // running under `dx` shouldn't expose cargo-only :
             .stderr(Stdio::piped())

+ 13 - 3
packages/cli/src/build/request.rs

@@ -374,6 +374,7 @@ pub(crate) struct BuildRequest {
     pub(crate) triple: Triple,
     pub(crate) device: bool,
     pub(crate) package: String,
+    pub(crate) main_target: String,
     pub(crate) features: Vec<String>,
     pub(crate) extra_cargo_args: Vec<String>,
     pub(crate) extra_rustc_args: Vec<String>,
@@ -463,7 +464,11 @@ impl BuildRequest {
     ///
     /// Note: Build requests are typically created only when the CLI is invoked or when significant
     /// changes are detected in the `Cargo.toml` (e.g., features added or removed).
-    pub(crate) async fn new(args: &TargetArgs, workspace: Arc<Workspace>) -> Result<Self> {
+    pub(crate) async fn new(
+        args: &TargetArgs,
+        main_target: Option<String>,
+        workspace: Arc<Workspace>,
+    ) -> Result<Self> {
         let crate_package = workspace.find_main_package(args.package.clone())?;
 
         let config = workspace
@@ -506,6 +511,10 @@ impl BuildRequest {
             })
             .unwrap_or(workspace.krates[crate_package].name.clone());
 
+        // Use the main_target for the client + server build if it is set, otherwise use the target name for this
+        // specific build
+        let main_target = main_target.unwrap_or(target_name.clone());
+
         let crate_target = main_package
             .targets
             .iter()
@@ -727,6 +736,7 @@ impl BuildRequest {
             extra_cargo_args,
             release,
             package,
+            main_target,
             skip_assets: args.skip_assets,
             base_path: args.base_path.clone(),
             wasm_split: args.wasm_split,
@@ -2737,7 +2747,7 @@ impl BuildRequest {
     /// target/dx/build/app/web/server.exe
     pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf {
         self.internal_out_dir()
-            .join(self.executable_name())
+            .join(&self.main_target)
             .join(if release { "release" } else { "debug" })
             .join(platform.build_folder_name())
     }
@@ -2748,7 +2758,7 @@ impl BuildRequest {
     /// target/dx/bundle/app/public/
     pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf {
         self.internal_out_dir()
-            .join(self.executable_name())
+            .join(&self.main_target)
             .join("bundle")
             .join(platform.build_folder_name())
     }

+ 57 - 116
packages/cli/src/cli/build.rs

@@ -1,8 +1,7 @@
-use crate::{cli::*, AppBuilder, BuildRequest, Workspace, PROFILE_SERVER};
+use crate::{cli::*, AppBuilder, BuildRequest, Workspace};
 use crate::{BuildMode, Platform};
-use target_lexicon::Triple;
 
-use super::target::{TargetArgs, TargetCmd};
+use super::target::TargetArgs;
 
 /// Build the Rust Dioxus app and all of its assets.
 ///
@@ -16,42 +15,9 @@ pub struct BuildArgs {
     #[clap(long)]
     pub(crate) fullstack: Option<bool>,
 
-    /// The feature to use for the client in a fullstack app [default: "web"]
-    #[clap(long)]
-    pub(crate) client_features: Vec<String>,
-
-    /// The feature to use for the server in a fullstack app [default: "server"]
-    #[clap(long)]
-    pub(crate) server_features: Vec<String>,
-
-    /// Build with custom profile for the fullstack server
-    #[clap(long, default_value_t = PROFILE_SERVER.to_string())]
-    pub(crate) server_profile: String,
-
-    /// The target to build for the server.
-    ///
-    /// This can be different than the host allowing cross-compilation of the server. This is useful for
-    /// platforms like Cloudflare Workers where the server is compiled to wasm and then uploaded to the edge.
-    #[clap(long)]
-    pub(crate) server_target: Option<Triple>,
-
     /// Arguments for the build itself
     #[clap(flatten)]
     pub(crate) build_arguments: TargetArgs,
-
-    /// A list of additional targets to build.
-    ///
-    /// Server and Client are special targets that integrate with `dx serve`, while `crate` is a generic.
-    ///
-    /// ```sh
-    /// dx serve \
-    ///     client --target aarch64-apple-darwin \
-    ///     server --target wasm32-unknown-unknown \
-    ///     crate --target aarch64-unknown-linux-gnu --package foo \
-    ///     crate --target x86_64-unknown-linux-gnu --package bar
-    /// ```
-    #[command(subcommand)]
-    pub(crate) targets: Option<TargetCmd>,
 }
 
 pub struct BuildTargets {
@@ -60,6 +26,40 @@ pub struct BuildTargets {
 }
 
 impl BuildArgs {
+    fn default_client(&self) -> &TargetArgs {
+        &self.build_arguments
+    }
+
+    fn default_server(&self, client: &BuildRequest) -> Option<&TargetArgs> {
+        // Now resolve the builds that we need to.
+        // These come from the args, but we'd like them to come from the `TargetCmd` chained object
+        //
+        // The process here is as follows:
+        //
+        // - Create the BuildRequest for the primary target
+        // - If that BuildRequest is "fullstack", then add the client features
+        // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server
+        //   with the server features
+        //
+        // This involves modifying the BuildRequest to add the client features and server features
+        // only if we can properly detect that it's a fullstack build. Careful with this, since
+        // we didn't build BuildRequest to be generally mutable.
+        let default_server = client.enabled_platforms.contains(&Platform::Server);
+
+        // Make sure we set the fullstack platform so we actually build the fullstack variant
+        // Users need to enable "fullstack" in their default feature set.
+        // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled
+        //
+        // Now we need to resolve the client features
+        let fullstack = ((default_server || client.fullstack_feature_enabled())
+            || self.fullstack.unwrap_or(false))
+            && self.fullstack != Some(false);
+
+        fullstack.then_some(&self.build_arguments)
+    }
+}
+
+impl CommandWithPlatformOverrides<BuildArgs> {
     pub async fn build(self) -> Result<StructuredOutput> {
         tracing::info!("Building project...");
 
@@ -92,89 +92,30 @@ impl BuildArgs {
         // do some logging to ensure dx matches the dioxus version since we're not always API compatible
         workspace.check_dioxus_version_against_cli();
 
-        let mut server = None;
+        let client_args = match &self.client {
+            Some(client) => &client.build_arguments,
+            None => self.shared.default_client(),
+        };
+        let client = BuildRequest::new(client_args, None, workspace.clone()).await?;
 
-        let client = match self.targets {
-            // A simple `dx serve` command with no explicit targets
-            None => {
-                // Now resolve the builds that we need to.
-                // These come from the args, but we'd like them to come from the `TargetCmd` chained object
-                //
-                // The process here is as follows:
-                //
-                // - Create the BuildRequest for the primary target
-                // - If that BuildRequest is "fullstack", then add the client features
-                // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server
-                //   with the server features
-                //
-                // This involves modifying the BuildRequest to add the client features and server features
-                // only if we can properly detect that it's a fullstack build. Careful with this, since
-                // we didn't build BuildRequest to be generally mutable.
-                let client = BuildRequest::new(&self.build_arguments, workspace.clone()).await?;
-                let default_server = client
-                    .enabled_platforms
-                    .iter()
-                    .any(|p| *p == Platform::Server);
-
-                // Make sure we set the fullstack platform so we actually build the fullstack variant
-                // Users need to enable "fullstack" in their default feature set.
-                // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled
-                //
-                // Now we need to resolve the client features
-                let fullstack = ((default_server || client.fullstack_feature_enabled())
-                    || self.fullstack.unwrap_or(false))
-                    && self.fullstack != Some(false);
-
-                if fullstack {
-                    let mut build_args = self.build_arguments.clone();
-                    build_args.platform = Some(Platform::Server);
-
-                    let _server = BuildRequest::new(&build_args, workspace.clone()).await?;
-
-                    // ... todo: add the server features to the server build
-                    // ... todo: add the client features to the client build
-                    // // Make sure we have a server feature if we're building a fullstack app
-                    if self.fullstack.unwrap_or_default() && self.server_features.is_empty() {
-                        return Err(anyhow::anyhow!("Fullstack builds require a server feature on the target crate. Add a `server` feature to the crate and try again.").into());
-                    }
-
-                    server = Some(_server);
-                }
-
-                client
-            }
-
-            // A command in the form of:
-            // ```
-            // dx serve \
-            //     client --package frontend \
-            //     server --package backend
-            // ```
-            Some(cmd) => {
-                let mut client_args_ = None;
-                let mut server_args_ = None;
-                let mut cmd_outer = Some(Box::new(cmd));
-                while let Some(cmd) = cmd_outer.take() {
-                    match *cmd {
-                        TargetCmd::Client(cmd_) => {
-                            client_args_ = Some(cmd_.inner);
-                            cmd_outer = cmd_.next;
-                        }
-                        TargetCmd::Server(cmd) => {
-                            server_args_ = Some(cmd.inner);
-                            cmd_outer = cmd.next;
-                        }
-                    }
-                }
-
-                if let Some(server_args) = server_args_ {
-                    server = Some(BuildRequest::new(&server_args, workspace.clone()).await?);
-                }
-
-                BuildRequest::new(&client_args_.unwrap(), workspace.clone()).await?
-            }
+        let server_args = match &self.server {
+            Some(server) => Some(&server.build_arguments),
+            None => self.shared.default_server(&client),
         };
 
+        let mut server = None;
+        // If there is a server, make sure we output in the same directory as the client build so we use the server
+        // to serve the web client
+        if let Some(server_args) = server_args {
+            // Copy the main target from the client to the server
+            let main_target = client.main_target.clone();
+            let mut server_args = server_args.clone();
+            // The platform in the server build is always set to Server
+            server_args.platform = Some(Platform::Server);
+            server =
+                Some(BuildRequest::new(&server_args, Some(main_target), workspace.clone()).await?);
+        }
+
         Ok(BuildTargets { client, server })
     }
 }

+ 1 - 1
packages/cli/src/cli/bundle.rs

@@ -41,7 +41,7 @@ pub struct Bundle {
 
     /// The arguments for the dioxus build
     #[clap(flatten)]
-    pub(crate) args: BuildArgs,
+    pub(crate) args: CommandWithPlatformOverrides<BuildArgs>,
 }
 
 impl Bundle {

+ 1 - 1
packages/cli/src/cli/check.rs

@@ -19,7 +19,7 @@ pub(crate) struct Check {
 
     /// Information about the target to check
     #[clap(flatten)]
-    pub(crate) build_args: BuildArgs,
+    pub(crate) build_args: CommandWithPlatformOverrides<BuildArgs>,
 }
 
 impl Check {

+ 3 - 1
packages/cli/src/cli/mod.rs

@@ -8,6 +8,7 @@ pub(crate) mod config;
 pub(crate) mod create;
 pub(crate) mod init;
 pub(crate) mod link;
+pub(crate) mod platform_override;
 pub(crate) mod run;
 pub(crate) mod serve;
 pub(crate) mod target;
@@ -20,6 +21,7 @@ pub(crate) use serve::*;
 pub(crate) use target::*;
 pub(crate) use verbosity::*;
 
+use crate::platform_override::CommandWithPlatformOverrides;
 use crate::{error::Result, Error, StructuredOutput};
 use clap::builder::styling::{AnsiColor, Effects, Style, Styles};
 use clap::{Parser, Subcommand};
@@ -62,7 +64,7 @@ pub(crate) enum Commands {
 
     /// Build the Dioxus project and all of its assets.
     #[clap(name = "build")]
-    Build(build::BuildArgs),
+    Build(CommandWithPlatformOverrides<build::BuildArgs>),
 
     /// Run the project without any hotreloading.
     #[clap(name = "run")]

+ 174 - 0
packages/cli/src/cli/platform_override.rs

@@ -0,0 +1,174 @@
+#![allow(dead_code)]
+use clap::parser::ValueSource;
+use clap::{ArgMatches, Args, CommandFactory, FromArgMatches, Parser, Subcommand};
+
+/// Wraps a component with the subcommands `@server` and `@client` which will let you override the
+/// base arguments for the client and server instances.
+#[derive(Debug, Clone, Default)]
+pub struct CommandWithPlatformOverrides<T> {
+    /// The arguments that are shared between the client and server
+    pub shared: T,
+    /// The merged arguments for the server
+    pub server: Option<T>,
+    /// The merged arguments for the client
+    pub client: Option<T>,
+}
+
+impl<T> CommandWithPlatformOverrides<T> {
+    pub(crate) fn with_client_or_shared<'a, O>(&'a self, f: impl FnOnce(&'a T) -> O) -> O {
+        match &self.client {
+            Some(client) => f(client),
+            None => f(&self.shared),
+        }
+    }
+
+    pub(crate) fn with_server_or_shared<'a, O>(&'a self, f: impl FnOnce(&'a T) -> O) -> O {
+        match &self.server {
+            Some(server) => f(server),
+            None => f(&self.shared),
+        }
+    }
+}
+
+impl<T: CommandFactory + Args> Parser for CommandWithPlatformOverrides<T> {}
+
+impl<T: CommandFactory + Args> CommandFactory for CommandWithPlatformOverrides<T> {
+    fn command() -> clap::Command {
+        T::command()
+    }
+
+    fn command_for_update() -> clap::Command {
+        T::command_for_update()
+    }
+}
+
+impl<T> Args for CommandWithPlatformOverrides<T>
+where
+    T: Args,
+{
+    fn augment_args(cmd: clap::Command) -> clap::Command {
+        T::augment_args(cmd).defer(|cmd| {
+            PlatformOverrides::<Self>::augment_subcommands(cmd.disable_help_subcommand(true))
+        })
+    }
+
+    fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
+        unimplemented!()
+    }
+}
+
+fn merge_matches<T: Args>(base: &ArgMatches, platform: &ArgMatches) -> Result<T, clap::Error> {
+    let mut base = T::from_arg_matches(base)?;
+
+    let mut platform = platform.clone();
+    let original_ids: Vec<_> = platform.ids().cloned().collect();
+    for arg_id in original_ids {
+        let arg_name = arg_id.as_str();
+        // Remove any default values from the platform matches
+        if platform.value_source(arg_name) == Some(ValueSource::DefaultValue) {
+            _ = platform.try_clear_id(arg_name);
+        }
+    }
+
+    // Then merge the stripped platform matches into the base matches
+    base.update_from_arg_matches(&platform)?;
+
+    Ok(base)
+}
+
+impl<T> FromArgMatches for CommandWithPlatformOverrides<T>
+where
+    T: Args,
+{
+    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
+        let mut client = None;
+        let mut server = None;
+        let mut subcommand = matches.subcommand();
+        while let Some((name, sub_matches)) = subcommand {
+            match name {
+                "@client" => {
+                    client = Some(sub_matches);
+                }
+                "@server" => {
+                    server = Some(sub_matches);
+                }
+                _ => {}
+            }
+            subcommand = sub_matches.subcommand();
+        }
+
+        let shared = T::from_arg_matches(matches)?;
+        let client = client
+            .map(|client| merge_matches::<T>(matches, client))
+            .transpose()?;
+        let server = server
+            .map(|server| merge_matches::<T>(matches, server))
+            .transpose()?;
+
+        Ok(Self {
+            shared,
+            server,
+            client,
+        })
+    }
+
+    fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
+        unimplemented!()
+    }
+}
+
+/// Chain together multiple target commands
+#[derive(Debug, Subcommand, Clone)]
+#[command(subcommand_precedence_over_arg = true)]
+pub(crate) enum PlatformOverrides<T: Args> {
+    /// Specify the arguments for the client build
+    #[clap(name = "@client")]
+    Client(ChainedCommand<T, PlatformOverrides<T>>),
+
+    /// Specify the arguments for the server build
+    #[clap(name = "@server")]
+    Server(ChainedCommand<T, PlatformOverrides<T>>),
+}
+
+// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894
+//
+//
+/// `[Args]` wrapper to match `T` variants recursively in `U`.
+#[derive(Debug, Clone)]
+pub struct ChainedCommand<T, U> {
+    /// Specific Variant.
+    pub inner: T,
+
+    /// Enum containing `Self<T>` variants, in other words possible follow-up commands.
+    pub next: Option<Box<U>>,
+}
+
+impl<T, U> Args for ChainedCommand<T, U>
+where
+    T: Args,
+    U: Subcommand,
+{
+    fn augment_args(cmd: clap::Command) -> clap::Command {
+        // We use the special `defer` method which lets us recursively call `augment_args` on the inner command
+        // and thus `from_arg_matches`
+        T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true)))
+    }
+
+    fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
+        unimplemented!()
+    }
+}
+
+impl<T, U> FromArgMatches for ChainedCommand<T, U>
+where
+    T: Args,
+    U: Subcommand,
+{
+    fn from_arg_matches(_: &ArgMatches) -> Result<Self, clap::Error> {
+        unimplemented!()
+    }
+
+    fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
+        unimplemented!()
+    }
+}

+ 11 - 4
packages/cli/src/cli/serve.rs

@@ -50,10 +50,6 @@ pub(crate) struct ServeArgs {
     #[clap(long)]
     pub(crate) cross_origin_policy: bool,
 
-    /// Additional arguments to pass to the executable
-    #[clap(long)]
-    pub(crate) args: Vec<String>,
-
     /// Sets the interval in seconds that the CLI will poll for file changes on WSL.
     #[clap(long, default_missing_value = "2")]
     pub(crate) wsl_file_poll_interval: Option<u16>,
@@ -76,8 +72,19 @@ pub(crate) struct ServeArgs {
     #[clap(long)]
     pub(crate) force_sequential: bool,
 
+    /// Platform-specific arguments for the build
+    #[clap(flatten)]
+    pub(crate) platform_args: CommandWithPlatformOverrides<PlatformServeArgs>,
+}
+
+#[derive(Clone, Debug, Default, Parser)]
+pub(crate) struct PlatformServeArgs {
     #[clap(flatten)]
     pub(crate) targets: BuildArgs,
+
+    /// Additional arguments to pass to the executable
+    #[clap(long, default_value = "")]
+    pub(crate) args: String,
 }
 
 impl ServeArgs {

+ 0 - 75
packages/cli/src/cli/target.rs

@@ -1,6 +1,5 @@
 use crate::cli::*;
 use crate::Platform;
-use clap::{ArgMatches, Args, FromArgMatches, Subcommand};
 use target_lexicon::Triple;
 
 /// A single target to build for
@@ -96,77 +95,3 @@ pub(crate) struct TargetArgs {
     #[clap(long)]
     pub(crate) base_path: Option<String>,
 }
-
-/// Chain together multiple target commands
-#[derive(Debug, Subcommand, Clone)]
-#[command(subcommand_precedence_over_arg = true)]
-pub(crate) enum TargetCmd {
-    /// Specify the arguments for the client build
-    #[clap(name = "@client")]
-    Client(ChainedCommand<TargetArgs, TargetCmd>),
-
-    /// Specify the arguments for the server build
-    #[clap(name = "@server")]
-    Server(ChainedCommand<TargetArgs, TargetCmd>),
-}
-
-// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894
-//
-//
-/// `[Args]` wrapper to match `T` variants recursively in `U`.
-#[derive(Debug, Clone)]
-pub struct ChainedCommand<T, U> {
-    /// Specific Variant.
-    pub inner: T,
-
-    /// Enum containing `Self<T>` variants, in other words possible follow-up commands.
-    pub next: Option<Box<U>>,
-}
-
-impl<T, U> Args for ChainedCommand<T, U>
-where
-    T: Args,
-    U: Subcommand,
-{
-    fn augment_args(cmd: clap::Command) -> clap::Command {
-        // We use the special `defer` method which lets us recursively call `augment_args` on the inner command
-        // and thus `from_arg_matches`
-        T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true)))
-    }
-
-    fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
-        unimplemented!()
-    }
-}
-
-impl<T, U> FromArgMatches for ChainedCommand<T, U>
-where
-    T: Args,
-    U: Subcommand,
-{
-    fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
-        // Parse the first command before we try to parse the next one.
-        let inner = T::from_arg_matches(matches)?;
-
-        // Try to parse the remainder of the command as a subcommand.
-        let next = match matches.subcommand() {
-            // Subcommand skips into the matched .subcommand, hence we need to pass *outer* matches, ignoring the inner matches
-            // (which in the average case should only match enumerated T)
-            //
-            // Here, we might want to eventually enable arbitrary names of subcommands if they're prefixed
-            // with a prefix like "@" ie `dx serve @dog-app/backend --args @dog-app/frontend --args`
-            //
-            // we are done, since sub-sub commands are matched in U::
-            Some(_) => Some(Box::new(U::from_arg_matches(matches)?)),
-
-            // no subcommand matched, we are done
-            None => None,
-        };
-
-        Ok(Self { inner, next })
-    }
-
-    fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
-        unimplemented!()
-    }
-}

+ 25 - 3
packages/cli/src/serve/runner.rs

@@ -1,7 +1,8 @@
 use super::{AppBuilder, ServeUpdate, WebServer};
 use crate::{
-    BuildArtifacts, BuildId, BuildMode, BuildTargets, Error, HotpatchModuleCache, Platform, Result,
-    ServeArgs, TailwindCli, TraceSrc, Workspace,
+    platform_override::CommandWithPlatformOverrides, BuildArtifacts, BuildId, BuildMode,
+    BuildTargets, Error, HotpatchModuleCache, Platform, Result, ServeArgs, TailwindCli, TraceSrc,
+    Workspace,
 };
 use anyhow::Context;
 use dioxus_core::internal::{
@@ -73,6 +74,11 @@ pub(crate) struct AppServer {
     pub(crate) proxied_port: Option<u16>,
     pub(crate) cross_origin_policy: bool,
 
+    // The arguments that should be forwarded to the client app when it is opened
+    pub(crate) client_args: Vec<String>,
+    // The arguments that should be forwarded to the server app when it is opened
+    pub(crate) server_args: Vec<String>,
+
     // Additional plugin-type tools
     pub(crate) tw_watcher: tokio::task::JoinHandle<Result<()>>,
 }
@@ -93,6 +99,13 @@ impl AppServer {
         let force_sequential = args.force_sequential;
         let cross_origin_policy = args.cross_origin_policy;
 
+        // Find the launch args for the client and server
+        let split_args = |args: &str| args.split(' ').map(|s| s.to_string()).collect::<Vec<_>>();
+        let server_args = args.platform_args.with_server_or_shared(|c| &c.args);
+        let server_args = split_args(server_args);
+        let client_args = args.platform_args.with_client_or_shared(|c| &c.args);
+        let client_args = split_args(client_args);
+
         // These come from the args but also might come from the workspace settings
         // We opt to use the manually specified args over the workspace settings
         let hot_reload = args
@@ -125,7 +138,12 @@ impl AppServer {
         let (watcher_tx, watcher_rx) = futures_channel::mpsc::unbounded();
         let watcher = create_notify_watcher(watcher_tx.clone(), wsl_file_poll_interval as u64);
 
-        let BuildTargets { client, server } = args.targets.into_targets().await?;
+        let target_args = CommandWithPlatformOverrides {
+            shared: args.platform_args.shared.targets,
+            server: args.platform_args.server.map(|s| s.targets),
+            client: args.platform_args.client.map(|c| c.targets),
+        };
+        let BuildTargets { client, server } = target_args.into_targets().await?;
 
         // All servers will end up behind us (the devserver) but on a different port
         // This is so we can serve a loading screen as well as devtools without anything particularly fancy
@@ -187,6 +205,8 @@ impl AppServer {
             cross_origin_policy,
             fullstack,
             tw_watcher,
+            server_args,
+            client_args,
         };
 
         // Only register the hot-reload stuff if we're watching the filesystem
@@ -546,6 +566,7 @@ impl AppServer {
                     false,
                     false,
                     BuildId::SERVER,
+                    &self.server_args,
                 )
                 .await?;
         }
@@ -560,6 +581,7 @@ impl AppServer {
                 open_browser,
                 self.always_on_top,
                 BuildId::CLIENT,
+                &self.client_args,
             )
             .await?;