Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/cli/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"description": "Unified permissions configuration. Permissions declared here are automatically mapped to platform-specific identifiers (AndroidManifest.xml, Info.plist, etc.)",
"$ref": "#/definitions/PermissionsConfig"
},
"renderer": {
"description": "Custom renderer configuration for projects that use `dioxus-core` with their own renderer.\n\nWhen present, this overrides the default renderer autodetection and feature injection. Existing Dioxus projects (without this section) are unaffected.\n\n```toml [renderer] name = \"my-renderer\" default_platform = \"desktop\"\n\n[renderer.features] desktop = [] web = [\"my-web\"] ios = [\"my-mobile\"] android = [\"my-mobile\"] ```",
"$ref": "#/definitions/RendererConfig"
},
"web": {
"$ref": "#/definitions/WebConfig"
},
Expand Down Expand Up @@ -2189,6 +2193,36 @@
}
}
},
"RendererConfig": {
"description": "Configuration for custom (non-dioxus) renderers.\n\nProjects that use `dioxus-core` directly with their own renderer can use this section to declare platform-to-feature mappings so `dx serve`, `dx build`, and `dx bundle` work without pulling in dioxus's built-in renderers.",
"type": "object",
"properties": {
"default_platform": {
"description": "Default platform when none is specified on the CLI.\n\nMust be one of: `\"web\"`, `\"macos\"`, `\"windows\"`, `\"linux\"`, `\"ios\"`, `\"android\"`, `\"server\"`, `\"liveview\"`.",
"type": [
"string",
"null"
]
},
"features": {
"description": "Map from platform name to cargo features to enable.\n\nKeys are platform identifiers (e.g., `\"desktop\"`, `\"web\"`, `\"ios\"`). Values are lists of cargo feature names to pass via `--features`. An empty list means \"build with default features, don't inject any extra\".",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"name": {
"description": "Display name for the renderer (shown in TUI).",
"type": [
"string",
"null"
]
}
}
},
"SimplePermission": {
"description": "Simple permission with just a description.",
"type": "object",
Expand Down
60 changes: 46 additions & 14 deletions packages/cli/src/build/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,45 +717,67 @@ impl BuildRequest {
}
}

// If no platform was specified on the CLI and a custom renderer declares a default, use it.
if matches!(platform, Platform::Unknown) {
if let Some(ref default) = config.renderer.default_platform {
if let Ok(p) = Platform::from_identifier(default) {
platform = p;
}
}
}

// Helper closure: check if the custom renderer config has features for this platform.
let custom_features = |keys: &[&str]| config.renderer.features_for_platform(keys);

// 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() {
if let Some(feats) = custom_features(&["web"]) {
features.extend(feats);
} else 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()?));
}
Platform::MacOS => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
if let Some(feats) = custom_features(&["macos", "desktop"]) {
features.extend(feats);
} else 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()));
}
Platform::Windows => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
if let Some(feats) = custom_features(&["windows", "desktop"]) {
features.extend(feats);
} else 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()));
}
Platform::Linux => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
if let Some(feats) = custom_features(&["linux", "desktop"]) {
features.extend(feats);
} else 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()));
}
Platform::Ios => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
if let Some(feats) = custom_features(&["ios", "mobile"]) {
features.extend(feats);
} else if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
Expand All @@ -774,7 +796,9 @@ impl BuildRequest {
}
}
Platform::Android => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
if let Some(feats) = custom_features(&["android", "mobile"]) {
features.extend(feats);
} else if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}

Expand Down Expand Up @@ -803,15 +827,19 @@ impl BuildRequest {
}
}
Platform::Server => {
if main_package.features.contains_key("server") && renderer.is_none() {
if let Some(feats) = custom_features(&["server"]) {
features.extend(feats);
} else 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()));
}
Platform::Liveview => {
if main_package.features.contains_key("liveview") && renderer.is_none() {
if let Some(feats) = custom_features(&["liveview"]) {
features.extend(feats);
} else if main_package.features.contains_key("liveview") && renderer.is_none() {
features.push("liveview".into());
}
renderer = renderer.or(Some(Renderer::Liveview));
Expand Down Expand Up @@ -842,13 +870,17 @@ impl BuildRequest {
bundle_format.unwrap_or(BundleFormat::host())
};

// Add any features required to turn on the client
// Add any features required to turn on the client.
// When a custom renderer is configured, skip dioxus-specific feature injection since
// the custom features were already added in the platform match above.
if let Some(renderer) = renderer {
if let Some(feature) =
Self::feature_for_platform_and_renderer(main_package, &triple, renderer)
{
features.push(feature);
features.dedup();
if !config.renderer.is_custom() {
if let Some(feature) =
Self::feature_for_platform_and_renderer(main_package, &triple, renderer)
{
features.push(feature);
features.dedup();
}
}
}

Expand Down
111 changes: 111 additions & 0 deletions packages/cli/src/config/dioxus_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::config::component::ComponentConfig;
use super::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub(crate) struct DioxusConfig {
Expand Down Expand Up @@ -55,6 +56,72 @@ pub(crate) struct DioxusConfig {
/// Linux-specific configuration.
#[serde(default)]
pub(crate) linux: LinuxConfig,

/// Custom renderer configuration for projects that use `dioxus-core` with their own renderer.
///
/// When present, this overrides the default renderer autodetection and feature injection.
/// Existing Dioxus projects (without this section) are unaffected.
///
/// ```toml
/// [renderer]
/// name = "my-renderer"
/// default_platform = "desktop"
///
/// [renderer.features]
/// desktop = []
/// web = ["my-web"]
/// ios = ["my-mobile"]
/// android = ["my-mobile"]
/// ```
#[serde(default)]
pub(crate) renderer: RendererConfig,
}

/// Configuration for custom (non-dioxus) renderers.
///
/// Projects that use `dioxus-core` directly with their own renderer can use this section
/// to declare platform-to-feature mappings so `dx serve`, `dx build`, and `dx bundle` work
/// without pulling in dioxus's built-in renderers.
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub(crate) struct RendererConfig {
/// Display name for the renderer (shown in TUI).
#[serde(default)]
pub(crate) name: Option<String>,

/// Default platform when none is specified on the CLI.
///
/// Must be one of: `"web"`, `"macos"`, `"windows"`, `"linux"`, `"ios"`, `"android"`,
/// `"server"`, `"liveview"`.
#[serde(default)]
pub(crate) default_platform: Option<String>,

/// Map from platform name to cargo features to enable.
///
/// Keys are platform identifiers (e.g., `"desktop"`, `"web"`, `"ios"`).
/// Values are lists of cargo feature names to pass via `--features`.
/// An empty list means "build with default features, don't inject any extra".
#[serde(default)]
pub(crate) features: HashMap<String, Vec<String>>,
}

impl RendererConfig {
/// Returns `true` if a custom renderer is configured.
pub(crate) fn is_custom(&self) -> bool {
self.name.is_some() || !self.features.is_empty()
}

/// Look up custom features for a platform, trying each key in order.
///
/// This allows fallback chains like `["macos", "desktop"]` so platform-specific
/// keys take priority over generic ones.
pub(crate) fn features_for_platform(&self, keys: &[&str]) -> Option<Vec<String>> {
for key in keys {
if let Some(feats) = self.features.get(*key) {
return Some(feats.clone());
}
}
None
}
}

/// Platform identifier for bundle resolution.
Expand Down Expand Up @@ -149,6 +216,7 @@ impl Default for DioxusConfig {
macos: MacosConfig::default(),
windows: WindowsConfig::default(),
linux: LinuxConfig::default(),
renderer: RendererConfig::default(),
}
}
}
Expand Down Expand Up @@ -190,4 +258,47 @@ mod tests {
let config: DioxusConfig = toml::from_str(source).expect("parse config");
assert_eq!(config.application.public_dir.as_deref(), None);
}

#[test]
fn renderer_config_absent_is_not_custom() {
let config = DioxusConfig::default();
assert!(!config.renderer.is_custom());
assert!(config.renderer.features.is_empty());
}

#[test]
fn renderer_config_parses_from_toml() {
let source = r#"
[renderer]
name = "tanzo"
default_platform = "desktop"

[renderer.features]
desktop = []
web = ["tanzo-web"]
ios = ["tanzo-mobile"]
android = ["tanzo-mobile"]
"#;

let config: DioxusConfig = toml::from_str(source).expect("parse config");
assert!(config.renderer.is_custom());
assert_eq!(config.renderer.name.as_deref(), Some("tanzo"));
assert_eq!(config.renderer.default_platform.as_deref(), Some("desktop"));
assert_eq!(
config.renderer.features_for_platform(&["desktop"]),
Some(vec![])
);
assert_eq!(
config.renderer.features_for_platform(&["web"]),
Some(vec!["tanzo-web".to_string()])
);
assert_eq!(
config.renderer.features_for_platform(&["macos", "desktop"]),
Some(vec![])
);
assert_eq!(
config.renderer.features_for_platform(&["nonexistent"]),
None
);
}
}
3 changes: 2 additions & 1 deletion packages/cli/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ pub(crate) enum Platform {
}

impl Platform {
fn from_identifier(identifier: &str) -> std::result::Result<Self, clap::Error> {
/// Parse a platform from a string identifier (e.g., "web", "macos", "desktop").
pub(crate) fn from_identifier(identifier: &str) -> std::result::Result<Self, clap::Error> {
match identifier {
"web" => Ok(Self::Web),
"macos" => Ok(Self::MacOS),
Expand Down
Loading