codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+4 -1
View File
@@ -320,7 +320,10 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
{
tracing::debug!("Found asset at offset {offset}: {:?}", bundled_asset);
tracing::debug!(
"Found asset at offset {offset}: {:?}",
bundled_asset.absolute_source_path()
);
assets.push(bundled_asset);
} else {
tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
+3 -2
View File
@@ -715,7 +715,7 @@ impl AppBuilder {
let changed_file = changed_files.first().unwrap();
tracing::info!(
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#} ({})",
changed_file
.display()
.to_string()
@@ -723,7 +723,8 @@ impl AppBuilder {
SystemTime::now()
.duration_since(res.time_start)
.unwrap()
.as_millis()
.as_millis(),
triple
);
self.patches.push(jump_table.clone());
+10 -6
View File
@@ -1191,6 +1191,8 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
})
.collect::<HashMap<_, _>>();
let mut exported = HashSet::new();
// Wasm-bindgen will synthesize imports to satisfy its external calls. This facilitates things
// like inline-js, snippets, and literally the `#[wasm_bindgen]` macro. All calls to JS are
// just `extern "wbg"` blocks!
@@ -1227,9 +1229,10 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
let new_func_id = module.funcs.add_local(builder.local_func(locals));
module
.exports
.add(&format!("__saved_wbg_{}", import.name), new_func_id);
let saved_name = format!("__saved_wbg_{}", import.name);
if exported.insert(saved_name.clone()) {
module.exports.add(&saved_name, new_func_id);
}
make_indirect.push(new_func_id);
}
@@ -1254,9 +1257,10 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
//
// https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1505
if name.starts_with("__wbindgen") {
module
.exports
.add(&format!("__saved_wbg_{name}"), func.id());
let saved_name = format!("__saved_wbg_{}", name);
if exported.insert(saved_name.clone()) {
module.exports.add(&saved_name, func.id());
}
}
// This is basically `--export-all` but designed to work around wasm-bindgen not properly gc-ing
+297 -107
View File
@@ -321,7 +321,7 @@
use crate::{
AndroidTools, BuildContext, BundleFormat, DioxusConfig, Error, LinkAction, LinkerFlavor,
Renderer, Result, RustcArgs, TargetAlias, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
};
use anyhow::{bail, Context};
@@ -348,7 +348,7 @@ use std::{
},
time::{SystemTime, UNIX_EPOCH},
};
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
use target_lexicon::{Architecture, OperatingSystem, Triple};
use tempfile::TempDir;
use tokio::{io::AsyncBufReadExt, process::Command};
use uuid::Uuid;
@@ -549,105 +549,257 @@ impl BuildRequest {
})?
.clone();
// The crate might enable multiple platforms or no platforms at
// We collect all the platforms it enables first and then select based on the --platform arg
let enabled_renderers =
Self::enabled_cargo_toml_renderers(main_package, args.no_default_features);
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
let mut features = args.features.clone();
let mut no_default_features = args.no_default_features;
// Apply the platform alias if present
let mut target_alias = args.target_alias;
let mut renderer = args.renderer;
let mut bundle = args.bundle;
if let Some(platform) = args.platform {
let (default_target, default_renderer, default_bundle) = platform.into_triple();
target_alias = target_alias.or(default_target);
renderer.renderer = Some(renderer.renderer.unwrap_or(default_renderer));
bundle = Some(bundle.unwrap_or(default_bundle));
}
let mut renderer: Option<Renderer> = match renderer.into() {
Some(renderer) => match enabled_renderers.len() {
0 => Some(renderer),
// The user passed --platform XYZ but already has `default = ["ABC"]` in their Cargo.toml or dioxus = { features = ["abc"] }
// We want to strip out the default platform and use the one they passed, setting no-default-features
_ => {
features.extend(Self::rendererless_features(main_package));
no_default_features = true;
Some(renderer)
}
},
None if !using_dioxus_explicitly => None,
None => match enabled_renderers.as_slice() {
[] => None, // Wait until we resolve everything else first then we will resolve it from the triple
[(renderer, feature)] => {
let targeting_mobile = match (&args.target, target_alias) {
(_, TargetAlias::Android | TargetAlias::Ios) => true,
(Some(target), _) => {
matches!(
(target.environment, target.operating_system),
(Environment::Android, OperatingSystem::IOS(_))
)
}
_ => false,
};
// The renderer usually maps directly to a feature flag, but the mobile crate is an exception.
// If we see the `desktop` renderer in the default features, but the user is targeting mobile,
// we should remove the default `desktop` feature, but still target webview. The feature selection
// logic below will handle adding back in the `mobile` feature flag
if targeting_mobile && feature == "desktop" {
features.extend(Self::rendererless_features(main_package));
no_default_features = true;
}
Some(*renderer)
}
_ => {
bail!(
"Multiple platforms enabled in Cargo.toml. Please specify a platform with `--web`, `--webview`, or `--native` or set a default platform in Cargo.toml"
);
}
},
};
// We usually use the simulator unless --device is passed *or* a device is detected by probing.
// For now, though, since we don't have probing, it just defaults to false
// Tools like xcrun/adb can detect devices
let device = args.device.clone();
// Resolve the target alias and renderer into a concrete target alias.
// If the user didn't pass a platform, but we have a renderer, get the default platform for that renderer
if let (TargetAlias::Unknown, Some(renderer)) = (target_alias, renderer) {
target_alias = renderer.default_platform();
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
/*
Determine which features, triple, profile, etc to pass to the build.
Most of the time, users should use `dx serve --<platform>` where the platform name directly
corresponds to the feature in their cargo.toml. So,
- `dx serve --web` will enable the `web` feature
- `dx serve --mobile` will enable the `mobile` feature
- `dx serve --desktop` will enable the `desktop` feature
In this case, we set default-features to false and then add back the default features that
aren't renderers, and then add the feature for the given renderer (ie web/desktop/mobile).
We call this "no-default-features-stripped."
There are a few cases where the user doesn't need to pass a platform.
- they selected one via `dioxus = { features = ["web"] }`
- they have a single platform in their default features `default = ["web"]`
- there is only a single non-server renderer as a feature `web = ["dioxus/web"], server = ["dioxus/server"]`
- they compose the super triple via triple + bundleformat + features
Note that we only use the names of the features to correspond with the platform.
Platforms are "super triples", meaning they contain information about
- bundle format
- target triple
- how to serve
- enabled features
By default, the --platform presets correspond to:
- web: bundle(web), triple(wasm32), serve(http-serve), features("web")
- desktop: alias to mac/win/linux
- mac: bundle(mac), triple(host), serve(appbundle-open), features("desktop")
- windows: bundle(exefolder), triple(host), serve(run-exe), features("desktop")
- linux: bundle(appimage), triple(host), serve(run-exe), features("desktop")
- ios: bundle(ios), triple(arm64-apple-ios), serve(ios-simulator/xcrun), features("mobile")
- android: bundle(android), triple(arm64-apple-ios), serve(android-emulator/adb), features("mobile")
- server: bundle(server), triple(host), serve(run-exe), features("server") (and disables the client)
- liveview: bundle(liveview), triple(host), serve(run-exe), features("liveview")
- unknown: <auto or default to desktop>
Fullstack usage is inferred from the presence of the fullstack feature or --fullstack.
*/
let mut features = args.features.clone();
let mut no_default_features = args.no_default_features;
let mut triple = args.target.clone();
let mut renderer = args.renderer;
let mut bundle_format = args.bundle;
let mut platform = args.platform;
// the crate might be selecting renderers but the user also passes a renderer. this is weird
// ie dioxus = { features = ["web"] } but also --platform desktop
// anyways, we collect it here in the event we need it if platform is not specified.
let dioxus_direct_renderer = Self::renderer_enabled_by_dioxus_dependency(main_package);
let known_features_as_renderers = Self::features_that_enable_renderers(main_package);
// The crate might enable multiple platforms or no platforms at
// We collect all the platforms it enables first and then select based on the --platform arg
let enabled_renderers = if no_default_features {
vec![]
} else {
Self::enabled_cargo_toml_default_features_renderers(main_package)
};
// We want a real triple to build with, so we'll autodetect it if it's not provided
// The triple ends up being a source of truth for us later hence all this work to figure it out
let triple = match (args.target.clone(), target_alias) {
// If there is an explicit target, use it
(Some(target), _) => target,
// If there is a platform, use it to determine the target triple
(None, platform) => platform.into_target(device.is_some(), &workspace).await?,
};
// Try the easy autodetects.
// - if the user has `dioxus = { features = ["web"] }`
// - if the `default =["web"]` or `default = ["dioxus/web"]`
// - if there's only one non-server platform ie `web = ["dioxus/web"], server = ["dioxus/server"]`
// Only do this if we're explicitly using dioxus
if matches!(platform, Platform::Unknown) && using_dioxus_explicitly {
let auto = dioxus_direct_renderer
.or_else(|| {
if enabled_renderers.len() == 1 {
Some(enabled_renderers[0].clone())
} else {
None
}
})
.or_else(|| {
let non_server_features = known_features_as_renderers
.iter()
.filter(|f| f.1.as_str() != "server")
.collect::<Vec<_>>();
if non_server_features.len() == 1 {
Some(non_server_features[0].clone())
} else {
None
}
});
// If the user didn't pass a renderer, and we didn't find a good default renderer, but they are using dioxus explicitly,
// then we can try to guess the renderer based on the target triple.
if renderer.is_none() && using_dioxus_explicitly {
renderer = Some(Renderer::from_target(&triple));
if let Some((direct, feature)) = auto {
match direct {
_ if feature == "mobile" || feature == "dioxus/mobile" => {
bail!(
"Could not autodetect mobile platform. Use --ios or --android instead."
);
}
Renderer::Webview | Renderer::Native => {
if cfg!(target_os = "macos") {
platform = Platform::MacOS;
} else if cfg!(target_os = "linux") {
platform = Platform::Linux;
} else if cfg!(target_os = "windows") {
platform = Platform::Windows;
}
}
Renderer::Server => platform = Platform::Server,
Renderer::Liveview => platform = Platform::Liveview,
Renderer::Web => platform = Platform::Web,
}
renderer = renderer.or(Some(direct));
}
}
// Resolve the bundle format based on the combination of the target triple and renderer
let bundle = match bundle {
// If there is an explicit bundle format, use it
Some(bundle) => bundle,
// Otherwise guess a bundle format based on the target triple and renderer
None => BundleFormat::from_target(&triple, renderer)?,
// Set the super triple from the platform if it's provided.
// Otherwise, we attempt to guess it from the rest of their inputs.
match platform {
Platform::Unknown => {}
Platform::Web => {
if main_package.features.contains_key("web") && renderer.is_none() {
features.push("web".into());
}
renderer = renderer.or(Some(Renderer::Web));
bundle_format = bundle_format.or(Some(BundleFormat::Web));
triple = triple.or(Some("wasm32-unknown-unknown".parse()?));
no_default_features = true;
}
Platform::MacOS => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::MacOS));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Windows => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Windows));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Linux => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Linux));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Ios => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Ios));
no_default_features = true;
match device.is_some() {
// If targeting device, we want to build for the device which is always aarch64
true => triple = triple.or(Some("aarch64-apple-ios".parse()?)),
// If the host is aarch64, we assume the user wants to build for iOS simulator
false if matches!(Triple::host().architecture, Architecture::Aarch64(_)) => {
triple = triple.or(Some("aarch64-apple-ios-sim".parse()?))
}
// Otherwise, it's the x86_64 simulator, which is just x86_64-apple-ios
_ => triple = triple.or(Some("x86_64-apple-ios".parse()?)),
}
}
Platform::Android => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Android));
no_default_features = true;
// maybe probe adb?
if let Some(_device_name) = device.as_ref() {
if triple.is_none() {
triple = Some(
crate::get_android_tools()
.context("Failed to get android tools")?
.autodetect_android_device_triple()
.await,
);
}
} else {
triple = triple.or(Some({
match Triple::host().architecture {
Architecture::X86_32(_) => "i686-linux-android".parse()?,
Architecture::X86_64 => "x86_64-linux-android".parse()?,
Architecture::Aarch64(_) => "aarch64-linux-android".parse()?,
_ => "aarch64-linux-android".parse()?,
}
}));
}
}
Platform::Server => {
if main_package.features.contains_key("server") && renderer.is_none() {
features.push("server".into());
}
renderer = renderer.or(Some(Renderer::Server));
bundle_format = bundle_format.or(Some(BundleFormat::Server));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
Platform::Liveview => {
if main_package.features.contains_key("liveview") && renderer.is_none() {
features.push("liveview".into());
}
renderer = renderer.or(Some(Renderer::Liveview));
bundle_format = bundle_format.or(Some(BundleFormat::Server));
triple = triple.or(Some(Triple::host()));
no_default_features = true;
}
}
// If no default features are enabled, we need to add the rendererless features
if no_default_features {
features.extend(Self::rendererless_features(main_package));
features.dedup();
features.sort();
}
// The triple will be the triple passed or the host if using dioxus.
let triple = if using_dioxus_explicitly {
triple.context("Could not automatically detect target triple")?
} else {
triple.unwrap_or(Triple::host())
};
// The bundle format will be the bundle format passed or the host.
let bundle = if using_dioxus_explicitly {
bundle_format.context("Could not automatically detect bundle format")?
} else {
bundle_format.unwrap_or(BundleFormat::host())
};
// Add any features required to turn on the client
@@ -657,6 +809,7 @@ impl BuildRequest {
&triple,
renderer,
));
features.dedup();
}
// Set the profile of the build if it's not already set
@@ -714,6 +867,14 @@ impl BuildRequest {
]);
}
// automatically set the getrandom backend for web builds if the user requested it
if matches!(bundle, BundleFormat::Web) && args.wasm_js_cfg {
rustflags.flags.extend(
cargo_config2::Flags::from_space_separated(r#"--cfg getrandom_backend="wasm_js""#)
.flags,
);
}
// Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET_<triple>_RUSTFLAGS
for env in [
"RUSTFLAGS".to_string(),
@@ -1032,7 +1193,11 @@ impl BuildRequest {
// since that is a really bad user experience.
Message::BuildFinished(finished) => {
if !finished.success {
bail!("cargo build finished with errors.")
bail!(
"cargo build finished with errors for target: {} [{}]",
self.main_target,
self.triple
);
}
}
_ => {}
@@ -1190,7 +1355,6 @@ impl BuildRequest {
async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
let framework_dir = self.frameworks_folder();
_ = std::fs::create_dir_all(&framework_dir);
// We have some prebuilt stuff that needs to be copied into the framework dir
let openssl_dir = AndroidTools::openssl_lib_dir(&self.triple);
@@ -1208,6 +1372,8 @@ impl BuildRequest {
tracing::debug!("Copying framework from {from:?} to {to:?}");
_ = std::fs::create_dir_all(&framework_dir);
// in dev and on normal oses, we want to symlink the file
// otherwise, just copy it (since in release you want to distribute the framework)
if cfg!(any(windows, unix)) && !self.release {
@@ -1225,6 +1391,11 @@ impl BuildRequest {
}
}
// Always create the framework dir for android
if self.bundle == BundleFormat::Android {
_ = std::fs::create_dir_all(&framework_dir);
}
// On android, the c++_shared flag means we need to copy the libc++_shared.so precompiled
// library to the jniLibs folder
if self.bundle == BundleFormat::Android && arg.contains("-lc++_shared") {
@@ -3352,13 +3523,9 @@ impl BuildRequest {
}
}
/// Return the platforms that are enabled for the package
///
/// Ideally only one platform is enabled but we need to be able to
pub(crate) fn enabled_cargo_toml_renderers(
pub(crate) fn renderer_enabled_by_dioxus_dependency(
package: &krates::cm::Package,
no_default_features: bool,
) -> Vec<(Renderer, String)> {
) -> Option<(Renderer, String)> {
let mut renderers = vec![];
// Attempt to discover the platform directly from the dioxus dependency
@@ -3367,13 +3534,40 @@ impl BuildRequest {
// dioxus = { features = ["web"] }
//
if let Some(dxs) = package.dependencies.iter().find(|dep| dep.name == "dioxus") {
for f in dxs.features.iter() {
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(f) {
renderers.push((renderer, f.clone()));
for feature in dxs.features.iter() {
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(feature) {
renderers.push((renderer, format!("dioxus/{}", feature)));
}
}
}
if renderers.len() != 1 {
return None;
}
Some(renderers[0].clone())
}
pub(crate) fn features_that_enable_renderers(
package: &krates::cm::Package,
) -> Vec<(Renderer, String)> {
package
.features
.keys()
.filter_map(|key| {
Renderer::autodetect_from_cargo_feature(key).map(|v| (v, key.to_string()))
})
.collect()
}
/// Return the platforms that are enabled for the package only from the default features
///
/// Ideally only one platform is enabled but we need to be able to
pub(crate) fn enabled_cargo_toml_default_features_renderers(
package: &krates::cm::Package,
) -> Vec<(Renderer, String)> {
let mut renderers = vec![];
// Start searching through the default features
//
// [features]
@@ -3384,10 +3578,6 @@ impl BuildRequest {
// [features]
// default = ["web"]
// web = ["dioxus/web"]
if no_default_features {
return renderers;
}
let Some(default) = package.features.get("default") else {
return renderers;
};
@@ -4396,7 +4586,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
///
/// This might stop working if/when cargo stabilizes contents-based fingerprinting.
fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
if matches!(ctx.mode, BuildMode::Fat | BuildMode::Base { run: true }) {
if matches!(ctx.mode, BuildMode::Fat) {
// `dx` compiles everything with `--target` which ends up with a structure like:
// target/<triple>/<profile>/.fingerprint/<package_name>-<hash>
//
+1 -5
View File
@@ -3,8 +3,7 @@ use anyhow::Context;
use itertools::Itertools;
use std::{path::PathBuf, sync::Arc};
use target_lexicon::{
Aarch64Architecture, Architecture, ArmArchitecture, Environment, OperatingSystem, Triple,
X86_32Architecture,
Aarch64Architecture, Architecture, ArmArchitecture, Triple, X86_32Architecture,
};
use tokio::process::Command;
@@ -236,9 +235,6 @@ impl AndroidTools {
// - We try to match the architecture unless otherwise specified. This is because
// emulators that match the host arch are usually faster.
let mut triple = "aarch64-linux-android".parse::<Triple>().unwrap();
triple.operating_system = OperatingSystem::Linux;
triple.environment = Environment::Android;
triple.architecture = target_lexicon::HOST.architecture;
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
let output = Command::new(&self.adb)
+12 -3
View File
@@ -1,7 +1,8 @@
use dioxus_dx_wire_format::StructuredBuildArtifacts;
use crate::{
cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, TargetArgs, Workspace,
cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, BundleFormat,
TargetArgs, Workspace,
};
/// Build the Rust Dioxus app and all of its assets.
@@ -116,14 +117,22 @@ impl CommandWithPlatformOverrides<BuildArgs> {
Some(server_args) => {
// Make sure we set the client target here so @server knows to place its output into the @client target directory.
server_args.build_arguments.client_target = Some(client.main_target.clone());
// We don't override anything except the bundle format since @server usually implies a server output
server_args.build_arguments.bundle = server_args
.build_arguments
.bundle
.or(Some(BundleFormat::Server));
server = Some(
BuildRequest::new(&server_args.build_arguments, workspace.clone()).await?,
);
}
None => {
let mut args = self.shared.build_arguments.clone();
args.platform = Some(crate::Platform::Server);
args.renderer.renderer = Some(crate::Renderer::Server);
args.platform = crate::Platform::Server;
args.renderer = Some(crate::Renderer::Server);
args.bundle = Some(crate::BundleFormat::Server);
args.target = Some(target_lexicon::Triple::host());
server = Some(BuildRequest::new(&args, workspace.clone()).await?);
}
+4 -4
View File
@@ -190,8 +190,8 @@ impl LinkAction {
target_lexicon::BinaryFormat::Macho => object::BinaryFormat::MachO,
target_lexicon::BinaryFormat::Wasm => object::BinaryFormat::Wasm,
target_lexicon::BinaryFormat::Xcoff => object::BinaryFormat::Xcoff,
target_lexicon::BinaryFormat::Unknown => todo!(),
_ => todo!("Binary format not supported"),
target_lexicon::BinaryFormat::Unknown => unimplemented!(),
_ => unimplemented!("Binary format not supported"),
};
let arch = match triple.architecture {
@@ -202,13 +202,13 @@ impl LinkAction {
target_lexicon::Architecture::Aarch64(_) => object::Architecture::Aarch64,
target_lexicon::Architecture::LoongArch64 => object::Architecture::LoongArch64,
target_lexicon::Architecture::Unknown => object::Architecture::Unknown,
_ => todo!("Architecture not supported"),
_ => unimplemented!("Architecture not supported"),
};
let endian = match triple.endianness() {
Ok(target_lexicon::Endianness::Little) => object::Endianness::Little,
Ok(target_lexicon::Endianness::Big) => object::Endianness::Big,
Err(_) => todo!("Endianness not supported"),
Err(_) => unimplemented!("Endianness not supported"),
};
let bytes = object::write::Object::new(format, arch, endian)
+3 -1
View File
@@ -102,7 +102,9 @@ impl RunArgs {
path,
} => tracing::info!(
"[{bundle_format}] Copying asset {} ({current}/{total})",
path.display(),
path.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default(),
),
BuildStage::Bundling => {
tracing::info!("[{bundle_format}] Bundling app")
+1 -5
View File
@@ -61,7 +61,7 @@ pub(crate) struct ServeArgs {
pub(crate) hot_patch: bool,
/// Watch the filesystem for changes and trigger a rebuild [default: true]
#[clap(long, default_missing_value = "true")]
#[clap(long, default_missing_value = "true", num_args=0..=1)]
pub(crate) watch: Option<bool>,
/// Exit the CLI after running into an error. This is mainly used to test hot patching internally
@@ -92,10 +92,6 @@ impl ServeArgs {
///
/// We also set up proper panic handling since the TUI has a tendency to corrupt the terminal.
pub(crate) async fn serve(self, tracer: &TraceController) -> Result<StructuredOutput> {
if std::env::var("RUST_BACKTRACE").is_err() {
std::env::set_var("RUST_BACKTRACE", "1");
}
// Redirect all logging the cli logger - if there's any pending after a panic, we flush it
let is_interactive_tty = self.is_interactive_tty();
if is_interactive_tty {
+17 -18
View File
@@ -1,8 +1,8 @@
use crate::cli::*;
use crate::BundleFormat;
use crate::Platform;
use crate::RendererArg;
use crate::TargetAlias;
use crate::{cli::*, Renderer};
// use crate::RendererArg;
// use crate::PlatformAlias;
use target_lexicon::Triple;
const HELP_HEADING: &str = "Target Options";
@@ -10,18 +10,6 @@ const HELP_HEADING: &str = "Target Options";
/// A single target to build for
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub(crate) struct TargetArgs {
/// The target alias to use for this build. Supports wasm, macos, windows, linux, ios, android, and host [default: "host"]
#[clap(flatten)]
pub(crate) target_alias: TargetAlias,
/// Build renderer: supports web, webview, native, server, and liveview
#[clap(flatten)]
pub(crate) renderer: RendererArg,
/// The bundle format to target for the build: supports web, macos, windows, linux, ios, android, and server
#[clap(long, value_enum, help_heading = HELP_HEADING)]
pub(crate) bundle: Option<BundleFormat>,
/// Build platform: supports Web, MacOS, Windows, Linux, iOS, Android, and Server
///
/// The platform implies a combination of the target alias, renderer, and bundle format flags.
@@ -29,8 +17,16 @@ pub(crate) struct TargetArgs {
/// You should generally prefer to use the `--web`, `--webview`, or `--native` flags to set the renderer
/// or the `--wasm`, `--macos`, `--windows`, `--linux`, `--ios`, or `--android` flags to set the target alias
/// instead of this flag. The renderer, target alias, and bundle format will be inferred if you only pass one.
#[clap(flatten)]
pub(crate) platform: Platform,
/// Which renderer to use? By default, this is usually inferred from the platform.
#[clap(long, value_enum, help_heading = HELP_HEADING)]
pub(crate) platform: Option<Platform>,
pub(crate) renderer: Option<Renderer>,
/// The bundle format to target for the build: supports web, macos, windows, linux, ios, android, and server
#[clap(long, value_enum, help_heading = HELP_HEADING)]
pub(crate) bundle: Option<BundleFormat>,
/// Build in release mode [default: false]
#[clap(long, short, help_heading = HELP_HEADING)]
@@ -112,7 +108,7 @@ pub(crate) struct TargetArgs {
/// simulator. If the device name is passed, we will upload to that device instead.
///
/// This performs a search among devices, and fuzzy matches might be found.
#[arg(long, default_missing_value=None, num_args=0..=1, help_heading = HELP_HEADING)]
#[arg(long, default_missing_value=Some("".into()), num_args=0..=1)]
pub(crate) device: Option<String>,
/// The base path the build will fetch assets relative to. This will override the
@@ -153,12 +149,15 @@ pub(crate) struct TargetArgs {
/// when merging `@client and @server` targets together.
#[clap(long, help_heading = HELP_HEADING)]
pub(crate) client_target: Option<String>,
/// Automatically pass `--features=js_cfg` when building for wasm targets. This is enabled by default.
#[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)]
pub(crate) wasm_js_cfg: bool,
}
impl Anonymized for TargetArgs {
fn anonymized(&self) -> Value {
json! {{
"target_alias": self.target_alias,
"renderer": self.renderer,
"bundle": self.bundle,
"platform": self.platform,
+1
View File
@@ -18,6 +18,7 @@ mod rustcwrapper;
mod serve;
mod settings;
mod tailwind;
mod test_harnesses;
mod wasm_bindgen;
mod wasm_opt;
mod workspace;
+119 -334
View File
@@ -1,82 +1,106 @@
use anyhow::{Context, Result};
use anyhow::Result;
use clap::{arg, ArgMatches, Args, FromArgMatches};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
use crate::Workspace;
use target_lexicon::{Environment, OperatingSystem, Triple};
#[derive(
Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug, Default,
)]
#[non_exhaustive]
pub(crate) enum TargetAlias {
/// Targeting the WASM architecture
Wasm,
pub(crate) enum Platform {
/// Alias for `--target wasm32-unknown-unknown --renderer websys --bundle-format web`
#[serde(rename = "web")]
Web,
/// Targeting macos desktop
/// Alias for `--target <host> --renderer webview --bundle-format macos`
#[serde(rename = "macos")]
MacOS,
/// Targeting windows desktop
/// Alias for `--target <host> --renderer webview --bundle-format windows`
#[serde(rename = "windows")]
Windows,
/// Targeting linux desktop
/// Alias for `--target <host> --renderer webview --bundle-format linux`
#[serde(rename = "linux")]
Linux,
/// Targeting the ios platform
///
/// Can't work properly if you're not building from an Apple device.
/// Alias for `--target <aarch64-apple-ios/sim> --renderer webview --bundle-format ios`
#[serde(rename = "ios")]
Ios,
/// Targeting the android platform
/// Alias for `--target <device-triple> --renderer webview --bundle-format android`
#[serde(rename = "android")]
Android,
/// Targeting the current target
Host,
/// Alias for `--target <host> --renderer ssr --bundle-format server`
#[serde(rename = "server")]
Server,
/// An unknown target platform
/// Alias for `--target <host> --renderer liveview --bundle-format server`
#[serde(rename = "liveview")]
Liveview,
/// No platform was specified, so the CLI is free to choose the best one.
#[default]
Unknown,
}
impl Args for TargetAlias {
impl Args for Platform {
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
fn augment_args(cmd: clap::Command) -> clap::Command {
const HELP_HEADING: &str = "Target Alias";
cmd.arg(arg!(--wasm "Target the wasm triple").help_heading(HELP_HEADING))
.arg(arg!(--macos "Target the macos triple").help_heading(HELP_HEADING))
.arg(arg!(--windows "Target the windows triple").help_heading(HELP_HEADING))
.arg(arg!(--linux "Target the linux triple").help_heading(HELP_HEADING))
.arg(arg!(--ios "Target the ios triple").help_heading(HELP_HEADING))
.arg(arg!(--android "Target the android triple").help_heading(HELP_HEADING))
.arg(arg!(--host "Target the host triple").help_heading(HELP_HEADING))
.arg(arg!(--desktop "Target the host triple").help_heading(HELP_HEADING))
const HELP_HEADING: &str = "Platform";
cmd.arg(arg!(--web "Target a web app").help_heading(HELP_HEADING))
.arg(arg!(--desktop "Target a desktop app").help_heading(HELP_HEADING))
.arg(arg!(--macos "Target a macos desktop app").help_heading(HELP_HEADING))
.arg(arg!(--windows "Target a windows desktop app").help_heading(HELP_HEADING))
.arg(arg!(--linux "Target a linux desktop app").help_heading(HELP_HEADING))
.arg(arg!(--ios "Target an ios app").help_heading(HELP_HEADING))
.arg(arg!(--android "Target an android app").help_heading(HELP_HEADING))
.arg(arg!(--server "Target a server build").help_heading(HELP_HEADING))
.arg(arg!(--liveview "Target a liveview build").help_heading(HELP_HEADING))
.group(
clap::ArgGroup::new("target_alias")
.args([
"wasm", "macos", "windows", "linux", "ios", "android", "host", "host",
"web", "desktop", "macos", "windows", "linux", "ios", "android", "server",
"liveview",
])
.multiple(false)
.required(false),
)
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}
impl FromArgMatches for TargetAlias {
impl FromArgMatches for Platform {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
if let Some(platform) = matches.get_one::<clap::Id>("target_alias") {
match platform.as_str() {
"wasm" => Ok(Self::Wasm),
"web" => Ok(Self::Web),
"desktop" => {
if cfg!(target_os = "macos") {
Ok(Self::MacOS)
} else if cfg!(target_os = "windows") {
Ok(Self::Windows)
} else if cfg!(unix) {
Ok(Self::Linux)
} else {
Err(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
"Desktop alias is not supported on this platform",
))
}
}
"macos" => Ok(Self::MacOS),
"windows" => Ok(Self::Windows),
"linux" => Ok(Self::Linux),
"ios" => Ok(Self::Ios),
"android" => Ok(Self::Android),
"host" => Ok(Self::Host),
"liveview" => Ok(Self::Liveview),
"server" => Ok(Self::Server),
_ => Err(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
format!("Unknown target alias: {platform}"),
@@ -86,134 +110,26 @@ impl FromArgMatches for TargetAlias {
Ok(Self::Unknown)
}
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
*self = Self::from_arg_matches(matches)?;
Ok(())
}
}
impl TargetAlias {
#[cfg(target_os = "macos")]
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::MacOS);
#[cfg(target_os = "windows")]
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Windows);
#[cfg(target_os = "linux")]
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Linux);
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
pub(crate) const TARGET_PLATFORM: Option<Self> = None;
pub(crate) async fn into_target(self, device: bool, workspace: &Workspace) -> Result<Triple> {
match self {
// Generally just use the host's triple for native executables unless specified otherwise
Self::MacOS | Self::Windows | Self::Linux | Self::Host | Self::Unknown => {
Ok(Triple::host())
}
// We currently assume unknown-unknown for web, but we might want to eventually
// support emscripten
Self::Wasm => Ok("wasm32-unknown-unknown".parse()?),
// For iOS we should prefer the actual architecture for the simulator, but in lieu of actually
// figuring that out, we'll assume aarch64 on m-series and x86_64 otherwise
Self::Ios => {
// use the host's architecture and sim if --device is passed
use target_lexicon::{Architecture, HOST};
let triple_str = match HOST.architecture {
Architecture::Aarch64(_) if device => "aarch64-apple-ios",
Architecture::Aarch64(_) => "aarch64-apple-ios-sim",
_ if device => "x86_64-apple-ios",
_ => "x86_64-apple-ios",
};
Ok(triple_str.parse()?)
}
// Same idea with android but we figure out the connected device using adb
Self::Android => Ok(workspace
.android_tools()?
.autodetect_android_device_triple()
.await),
}
}
pub(crate) fn or(self, other: Self) -> Self {
if self == Self::Unknown {
other
} else {
self
}
}
}
#[derive(
Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug, Default,
Copy,
Clone,
Hash,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Debug,
clap::ValueEnum,
)]
pub(crate) struct RendererArg {
pub(crate) renderer: Option<Renderer>,
}
impl Args for RendererArg {
fn augment_args(cmd: clap::Command) -> clap::Command {
const HELP_HEADING: &str = "Renderer";
cmd.arg(arg!(--web "Enable the dioxus web renderer").help_heading(HELP_HEADING))
.arg(arg!(--webview "Enable the dioxus webview renderer").help_heading(HELP_HEADING))
.arg(arg!(--native "Enable the dioxus native renderer").help_heading(HELP_HEADING))
.arg(arg!(--server "Enable the dioxus server renderer").help_heading(HELP_HEADING))
.arg(arg!(--liveview "Enable the dioxus liveview renderer").help_heading(HELP_HEADING))
.group(
clap::ArgGroup::new("renderer")
.args(["web", "webview", "native", "server", "liveview"])
.multiple(false)
.required(false),
)
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}
impl FromArgMatches for RendererArg {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
if let Some(renderer) = matches.get_one::<clap::Id>("renderer") {
match renderer.as_str() {
"web" => Ok(Self {
renderer: Some(Renderer::Web),
}),
"webview" => Ok(Self {
renderer: Some(Renderer::Webview),
}),
"native" => Ok(Self {
renderer: Some(Renderer::Native),
}),
"server" => Ok(Self {
renderer: Some(Renderer::Server),
}),
"liveview" => Ok(Self {
renderer: Some(Renderer::Liveview),
}),
_ => Err(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
format!("Unknown platform: {renderer}"),
)),
}
} else {
Ok(Self { renderer: None })
}
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
*self = Self::from_arg_matches(matches)?;
Ok(())
}
}
impl From<RendererArg> for Option<Renderer> {
fn from(val: RendererArg) -> Self {
val.renderer
}
}
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
#[non_exhaustive]
pub(crate) enum Renderer {
/// Targeting webview renderer
@@ -231,7 +147,7 @@ pub(crate) enum Renderer {
/// Targeting the static generation platform using SSR and Dioxus-Fullstack
Liveview,
/// Targeting the web renderer
/// Targeting the web-sys renderer
Web,
}
@@ -260,24 +176,6 @@ impl Renderer {
_ => None,
}
}
pub(crate) fn default_platform(&self) -> TargetAlias {
match self {
Renderer::Webview | Renderer::Native | Renderer::Server | Renderer::Liveview => {
TargetAlias::TARGET_PLATFORM.unwrap()
}
Renderer::Web => TargetAlias::Wasm,
}
}
pub(crate) fn from_target(triple: &Triple) -> Self {
match triple.architecture {
// Assume any wasm32 or wasm64 target is a web target
Architecture::Wasm32 | Architecture::Wasm64 => Self::Web,
// Otherwise, assume webview for native targets
_ => Self::Webview,
}
}
}
#[derive(Debug)]
@@ -357,57 +255,19 @@ pub(crate) enum BundleFormat {
Android,
}
#[derive(Debug)]
pub(crate) struct UnknownBundleFormatError;
impl std::error::Error for UnknownBundleFormatError {}
impl std::fmt::Display for UnknownBundleFormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Unknown bundle format")
}
}
impl FromStr for BundleFormat {
type Err = UnknownBundleFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"web" => Ok(Self::Web),
"macos" => Ok(Self::MacOS),
"windows" => Ok(Self::Windows),
"linux" => Ok(Self::Linux),
"server" => Ok(Self::Server),
"ios" => Ok(Self::Ios),
"android" => Ok(Self::Android),
_ => Err(UnknownBundleFormatError),
impl BundleFormat {
/// The native "desktop" host app format.
pub(crate) fn host() -> Self {
if cfg!(target_os = "macos") {
Self::MacOS
} else if cfg!(target_os = "windows") {
Self::Windows
} else if cfg!(target_os = "linux") {
Self::Linux
} else {
Self::Web
}
}
}
impl Display for BundleFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
BundleFormat::Web => "web",
BundleFormat::MacOS => "macos",
BundleFormat::Windows => "windows",
BundleFormat::Linux => "linux",
BundleFormat::Server => "server",
BundleFormat::Ios => "ios",
BundleFormat::Android => "android",
})
}
}
impl BundleFormat {
#[cfg(target_os = "macos")]
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::MacOS);
#[cfg(target_os = "windows")]
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Windows);
#[cfg(target_os = "linux")]
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Linux);
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
pub(crate) const TARGET_PLATFORM: Option<Self> = None;
/// Get the name of the folder we need to generate for this platform
///
@@ -449,121 +309,46 @@ impl BundleFormat {
Self::Server => "Server",
}
}
}
pub(crate) fn from_target(target: &Triple, renderer: Option<Renderer>) -> Result<BundleFormat> {
match (
renderer,
target.architecture,
target.environment,
target.operating_system,
) {
// The server always uses the server bundle format
(Some(Renderer::Server), _, _, _) => Ok(BundleFormat::Server),
// The web renderer always uses the web bundle format
(Some(Renderer::Web), _, _, _) => Ok(BundleFormat::Web),
// Otherwise, guess it based on the target
// Assume any wasm32 or wasm64 target is a web target
(_, Architecture::Wasm32 | Architecture::Wasm64, _, _) => Ok(BundleFormat::Web),
// For native targets, we need to determine the bundle format based on the OS
(_, _, Environment::Android, _) => Ok(BundleFormat::Android),
(_, _, _, OperatingSystem::IOS(_)) => Ok(BundleFormat::Ios),
(_, _, _, OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_)) => {
Ok(BundleFormat::MacOS)
}
(_, _, _, OperatingSystem::Linux) => Ok(BundleFormat::Linux),
(_, _, _, OperatingSystem::Windows) => Ok(BundleFormat::Windows),
// If we don't recognize the target, default to desktop
_ => BundleFormat::TARGET_PLATFORM.context(
"failed to determine bundle format. Try setting the `--bundle` flag manually",
),
#[derive(Debug)]
pub(crate) struct UnknownBundleFormatError;
impl std::error::Error for UnknownBundleFormatError {}
impl std::fmt::Display for UnknownBundleFormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Unknown bundle format")
}
}
impl FromStr for BundleFormat {
type Err = UnknownBundleFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"web" => Ok(Self::Web),
"macos" => Ok(Self::MacOS),
"windows" => Ok(Self::Windows),
"linux" => Ok(Self::Linux),
"server" => Ok(Self::Server),
"ios" => Ok(Self::Ios),
"android" => Ok(Self::Android),
_ => Err(UnknownBundleFormatError),
}
}
}
#[derive(
Copy,
Clone,
Hash,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Debug,
Default,
clap::ValueEnum,
)]
#[non_exhaustive]
pub(crate) enum Platform {
/// Alias for `--wasm --web --bundle-format web`
#[clap(name = "web")]
#[serde(rename = "web")]
#[default]
Web,
/// Alias for `--macos --webview --bundle-format macos`
#[cfg_attr(target_os = "macos", clap(alias = "desktop"))]
#[clap(name = "macos")]
#[serde(rename = "macos")]
MacOS,
/// Alias for `--windows --webview --bundle-format windows`
#[cfg_attr(target_os = "windows", clap(alias = "desktop"))]
#[clap(name = "windows")]
#[serde(rename = "windows")]
Windows,
/// Alias for `--linux --webview --bundle-format linux`
#[cfg_attr(target_os = "linux", clap(alias = "desktop"))]
#[clap(name = "linux")]
#[serde(rename = "linux")]
Linux,
/// Alias for `--ios --webview --bundle-format ios`
#[clap(name = "ios")]
#[serde(rename = "ios")]
Ios,
/// Alias for `--android --webview --bundle-format android`
#[clap(name = "android")]
#[serde(rename = "android")]
Android,
/// Alias for `--host --server --bundle-format server`
#[clap(name = "server")]
#[serde(rename = "server")]
Server,
/// Alias for `--host --liveview --bundle-format host`
#[clap(name = "liveview")]
#[serde(rename = "liveview")]
Liveview,
}
impl Platform {
pub(crate) fn into_triple(self) -> (TargetAlias, Renderer, BundleFormat) {
match self {
Platform::Web => (TargetAlias::Wasm, Renderer::Web, BundleFormat::Web),
Platform::MacOS => (TargetAlias::MacOS, Renderer::Webview, BundleFormat::MacOS),
Platform::Windows => (
TargetAlias::Windows,
Renderer::Webview,
BundleFormat::Windows,
),
Platform::Linux => (TargetAlias::Linux, Renderer::Webview, BundleFormat::Linux),
Platform::Ios => (TargetAlias::Ios, Renderer::Webview, BundleFormat::Ios),
Platform::Android => (
TargetAlias::Android,
Renderer::Webview,
BundleFormat::Android,
),
Platform::Server => (TargetAlias::Host, Renderer::Server, BundleFormat::Server),
Platform::Liveview => (
TargetAlias::Host,
Renderer::Liveview,
BundleFormat::TARGET_PLATFORM.unwrap(),
),
}
impl Display for BundleFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
BundleFormat::Web => "web",
BundleFormat::MacOS => "macos",
BundleFormat::Windows => "windows",
BundleFormat::Linux => "linux",
BundleFormat::Server => "server",
BundleFormat::Ios => "ios",
BundleFormat::Android => "android",
})
}
}
+3 -3
View File
@@ -21,13 +21,13 @@ pub(crate) async fn proxy_websocket(
.await
.map_err(IntoResponse::into_response)?;
tracing::info!(dx_src = ?TraceSrc::Dev, "Proxying websocket connection {req:?}");
tracing::trace!(dx_src = ?TraceSrc::Dev, "Proxying websocket connection {req:?}");
let proxied_request = into_proxied_request(req, backend_url).map_err(handle_proxy_error)?;
tracing::info!(dx_src = ?TraceSrc::Dev, "Connection proxied to {proxied_uri}", proxied_uri = proxied_request.uri());
tracing::trace!(dx_src = ?TraceSrc::Dev, "Connection proxied to {proxied_uri}", proxied_uri = proxied_request.uri());
Ok(ws.on_upgrade(move |client_ws| async move {
match handle_ws_connection(client_ws, proxied_request).await {
Ok(()) => tracing::info!(dx_src = ?TraceSrc::Dev, "Websocket connection closed"),
Ok(()) => tracing::trace!(dx_src = ?TraceSrc::Dev, "Websocket connection closed"),
Err(e) => {
tracing::error!(dx_src = ?TraceSrc::Dev, "Error proxying websocket connection: {e}")
}
-4
View File
@@ -337,10 +337,6 @@ impl WebServer {
/// Tells all clients to reload if possible for new changes.
pub(crate) async fn send_reload_command(&mut self) {
tracing::trace!("Sending reload to toast");
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
self.build_status.set(Status::Ready);
self.send_build_status().await;
self.send_devserver_message_to_all(DevserverMsg::FullReloadCommand)
+445
View File
@@ -0,0 +1,445 @@
use crate::{BuildTargets, BundleFormat, Cli, Commands, Workspace};
use anyhow::Result;
use clap::Parser;
use futures_util::{stream::FuturesUnordered, StreamExt};
use std::{
collections::HashSet,
fmt::Write,
path::{Path, PathBuf},
pin::Pin,
prelude::rust_2024::Future,
};
use target_lexicon::Triple;
use tracing_subscriber::{prelude::*, util::SubscriberInitExt, EnvFilter, Layer};
#[tokio::test]
async fn run_harness() {
test_harnesses().await;
}
#[allow(dead_code)]
async fn test_harnesses() {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().with_filter(EnvFilter::new("error,dx=debug,dioxus_cli=debug,manganis_cli_support=debug,wasm_split_cli=debug,subsecond_cli_support=debug",)))
.init();
TestHarnessBuilder::run(vec![
TestHarnessBuilder::new("harness-simple-web")
.deps(r#"dioxus = { workspace = true, features = ["web"] }"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
assert_eq!(t.client.triple, "wasm32-unknown-unknown".parse().unwrap());
assert!(t.server.is_none());
}),
TestHarnessBuilder::new("harness-simple-desktop")
.deps(r#"dioxus = { workspace = true, features = ["desktop"] }"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
assert_eq!(t.client.triple, Triple::host());
assert!(t.server.is_none());
}),
TestHarnessBuilder::new("harness-simple-mobile")
.deps(r#"dioxus = { workspace = true, features = ["mobile"] }"#)
.asrt(
"dx build",
|targets| async move { assert!(targets.is_err()) },
),
TestHarnessBuilder::new("harness-simple-fullstack")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"web=["dioxus/web"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-simple-fullstack-with-default")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"default=["web", "server"]"#)
.fetr(r#"web=["dioxus/web"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-simple-fullstack-native-with-default")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"default=["native", "server"]"#)
.fetr(r#"native=["dioxus/native"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
assert_eq!(t.client.features.len(), 1);
assert_eq!(t.client.features[0], "native");
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-fullstack-multi-target")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"default=["web", "desktop", "mobile", "server"]"#)
.fetr(r#"web=["dioxus/web"]"#)
.fetr(r#"desktop=["dioxus/desktop"]"#)
.fetr(r#"mobile=["dioxus/mobile"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |t| async move { assert!(t.is_err()) })
.asrt(r#"dx build --web"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
})
.asrt(r#"dx build --desktop"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
})
.asrt(r#"dx build --ios"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Ios);
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
})
.asrt(r#"dx build --ios --device"#, |targets| async move {
let targets = targets.unwrap();
assert_eq!(targets.client.bundle, BundleFormat::Ios);
assert_eq!(targets.client.triple, "aarch64-apple-ios".parse().unwrap());
})
.asrt(r#"dx build --android --device"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Android);
assert_eq!(t.client.triple, "aarch64-linux-android".parse().unwrap());
}),
TestHarnessBuilder::new("harness-fullstack-multi-target-no-default")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"web=["dioxus/web"]"#)
.fetr(r#"desktop=["dioxus/desktop"]"#)
.fetr(r#"mobile=["dioxus/mobile"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
assert!(targets.is_err())
})
.asrt(r#"dx build --desktop"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
})
.asrt(r#"dx build --ios"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Ios);
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-fullstack-desktop")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"desktop=["dioxus/desktop"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-fullstack-desktop-with-features")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.deps(r#"anyhow = { workspace = true, optional = true }"#)
.fetr(r#"desktop=["dioxus/desktop", "has_anyhow"]"#)
.fetr(r#"has_anyhow=["dep:anyhow"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-fullstack-desktop-with-default")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.deps(r#"anyhow = { workspace = true, optional = true }"#)
.fetr(r#"default=["desktop"]"#)
.fetr(r#"desktop=["dioxus/desktop", "has_anyhow"]"#)
.fetr(r#"has_anyhow=["dep:anyhow"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
}),
TestHarnessBuilder::new("harness-no-dioxus")
.deps(r#"anyhow = { workspace = true, optional = true }"#)
.fetr(r#"web=["dep:anyhow"]"#)
.fetr(r#"server=[]"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
assert!(t.server.is_none());
}),
TestHarnessBuilder::new("harness-simple-dedicated-server"),
TestHarnessBuilder::new("harness-simple-dedicated-client")
.deps(r#"dioxus = { workspace = true, features = ["web"] }"#)
.asrt(r#"dx build"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
assert!(t.server.is_none());
})
.asrt(r#"dx build @client --package harness-simple-dedicated-client @server --package harness-simple-dedicated-server"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
let s = t.server.unwrap();
assert_eq!(s.bundle, BundleFormat::Server);
assert_eq!(s.triple, Triple::host());
},
)
.asrt(r#"dx build @client --package harness-simple-dedicated-client @server --package harness-simple-dedicated-server --target wasm32-unknown-unknown"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
let s = t.server.unwrap();
assert_eq!(s.bundle, BundleFormat::Server);
assert_eq!(s.triple, "wasm32-unknown-unknown".parse().unwrap());
}),
TestHarnessBuilder::new("harness-renderer-swap")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.fetr(r#"default=["desktop", "server"]"#)
.fetr(r#"desktop=["dioxus/desktop"]"#)
.fetr(r#"native=["dioxus/native"]"#)
.fetr(r#"server=["dioxus/server"]"#)
.asrt(
r#"dx build --desktop --renderer native"#,
|targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::host());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
},
),
TestHarnessBuilder::new("harness-default-to-non-default")
.deps(r#"dioxus = { workspace = true, features = [] }"#)
.fetr(r#"default=["web"]"#)
.fetr(r#"web=["dioxus/web"]"#)
.asrt(
r#"dx build --ios"#,
|targets| async move {
let t = targets.unwrap();
assert!(t.server.is_none());
assert_eq!(t.client.bundle, BundleFormat::Ios);
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
assert!(t.client.no_default_features);
},
),
TestHarnessBuilder::new("harness-fullstack-with-optional-tokio")
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
.deps(r#"serde = "1.0.219""#)
.deps(r#"tokio = { workspace = true, features = ["full"], optional = true }"#)
.fetr(r#"default = []"#)
.fetr(r#"server = ["dioxus/server", "dep:tokio"]"#)
.fetr(r#"web = ["dioxus/web"]"#)
// .asrt(r#"dx build"#, |targets| async move {
// assert!(targets.is_err())
// })
.asrt(r#"dx build --web"#, |targets| async move {
let t = targets.unwrap();
assert_eq!(t.client.bundle, BundleFormat::Web);
assert_eq!(t.client.triple, "wasm32-unknown-unknown".parse().unwrap());
let server = t.server.unwrap();
assert_eq!(server.bundle, BundleFormat::Server);
assert_eq!(server.triple, Triple::host());
})
])
.await;
}
#[derive(Default)]
struct TestHarnessBuilder {
name: String,
dependencies: String,
features: String,
futures: Vec<TestHarnessTestCase>,
}
struct TestHarnessTestCase {
args: String,
#[allow(clippy::type_complexity)]
callback: Box<dyn FnOnce(Result<BuildTargets>) -> Pin<Box<dyn Future<Output = ()>>>>,
}
impl TestHarnessBuilder {
fn new(name: &str) -> Self {
Self {
name: name.into(),
dependencies: Default::default(),
features: Default::default(),
futures: Default::default(),
}
}
/// Add a dependency to the test harness.
fn deps(mut self, dependencies: impl Into<String>) -> Self {
writeln!(&mut self.dependencies, "{}", dependencies.into()).unwrap();
self
}
/// Add a feature to the test harness.
fn fetr(mut self, features: impl Into<String>) -> Self {
writeln!(&mut self.features, "{}", features.into()).unwrap();
self
}
/// Assert the expected behavior of the test harness.
fn asrt<F>(
mut self,
args: impl Into<String>,
future: impl FnOnce(Result<BuildTargets>) -> F + 'static,
) -> Self
where
F: Future<Output = ()> + 'static,
{
self.futures.push(TestHarnessTestCase {
args: args.into(),
callback: Box::new(move |args| Box::pin(future(args))),
});
self
}
/// Write the test harness to the filesystem.
fn build(&self, harness_dir: &Path) {
let name = self.name.clone();
let dependencies = self.dependencies.clone();
let features = self.features.clone();
let test_dir = harness_dir.join(&name);
_ = std::fs::remove_dir_all(&test_dir);
std::fs::create_dir_all(&test_dir).unwrap();
std::fs::create_dir_all(test_dir.join("src")).unwrap();
let cargo_toml = format!(
r#"[package]
name = "{name}"
version = "0.0.1"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false
[dependencies]
{dependencies}
[features]
{features}
"#,
name = name,
dependencies = dependencies,
features = features
);
std::fs::write(test_dir.join("Cargo.toml"), cargo_toml).unwrap();
let contents = if features.contains("dioxus") {
r#"use dioxus::prelude::*;
fn main() {
dioxus::launch(|| rsx! { "hello world!" })
}
"#
} else {
r#"fn main() {
println!("Hello, world!");
}
"#
};
std::fs::write(test_dir.join("src/main.rs"), contents).unwrap();
}
async fn run(harnesses: Vec<Self>) {
_ = crate::VERBOSITY.set(crate::Verbosity {
verbose: true,
trace: true,
json_output: false,
log_to_file: None,
locked: false,
offline: false,
frozen: false,
});
let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap();
let harness_dir = cargo_manifest_dir.parent().unwrap().join("cli-harnesses");
// make sure we don't start deleting random stuff.
if !harness_dir.exists() {
panic!(
"cli-harnesses directory does not exist, aborting: {:?}",
harness_dir
);
}
// Erase old entries in the harness directory, but keep files (ie README.md) around
for entry in std::fs::read_dir(&harness_dir).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
std::fs::remove_dir_all(entry.path()).unwrap();
}
}
// Now that the harnesses are written to the filesystem, we can call cargo_metadata
// It will be cached from here
let mut futures = FuturesUnordered::new();
let mut seen_names = HashSet::new();
for harness in harnesses {
if !seen_names.insert(harness.name.clone()) {
panic!("Duplicate test harness name found: {}", harness.name);
}
harness.build(&harness_dir);
for case in harness.futures {
let mut escaped = shell_words::split(&case.args).unwrap();
if !(escaped.contains(&"--package".to_string())
|| escaped.contains(&"@server".to_string())
|| escaped.contains(&"@client".to_string()))
{
escaped.push("--package".to_string());
escaped.push(harness.name.clone());
}
let args = Cli::try_parse_from(escaped).unwrap();
let Commands::Build(build_args) = args.action else {
panic!("Expected build command");
};
futures.push(async move {
let targets = build_args.into_targets().await;
(case.callback)(targets).await;
});
}
}
// Give a moment for fs to catch up
std::thread::sleep(std::time::Duration::from_secs(1));
let _workspace = Workspace::current().await.unwrap();
while let Some(_res) = futures.next().await {}
}
fn host_ios_triple_sim() -> Triple {
if cfg!(target_arch = "aarch64") {
"aarch64-apple-ios-sim".parse().unwrap()
} else {
"x86_64-apple-ios".parse().unwrap()
}
}
}