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
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-06-17 18:17:06 -07:00
1 parent 6fcb496fd5
commit 3bf8f09c65
10 files changed
+291 -207

No files matched your search

+5 -2
View File
@@ -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
View File
@@ -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())
}
+58 -117
View File
@@ -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,88 +92,29 @@ 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 = 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 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 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
-75
View File
@@ -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
View File
@@ -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?;