From 611f94a2daa3a4bc9e55399c0bb675acad260625 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 29 May 2026 12:56:03 -0700 Subject: [PATCH 1/3] defold entrypoint --- src/builds.rs | 45 ++++++++++++++++++++++++++++++++--------- src/config.rs | 30 ++++++++++++++++++--------- src/dev/entrypoint.rs | 47 ++++++++++++++++++++++++++++++++++++------- src/dev/mod.rs | 19 +++++++++++++---- src/init.rs | 47 +++++++++++++++++++++++++++++++------------ 5 files changed, 146 insertions(+), 42 deletions(-) diff --git a/src/builds.rs b/src/builds.rs index fc48f99..69ac8ac 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -1,5 +1,6 @@ use crate::auth::AuthManager; -use crate::config::{self, WavedashConfig}; +use crate::config::{self, EngineKind, WavedashConfig}; +use crate::dev::entrypoint::{fetch_entrypoint_params, locate_html_entrypoint}; use crate::file_staging::FileStaging; use anyhow::Result; use serde::{Deserialize, Serialize}; @@ -124,7 +125,11 @@ async fn notify_upload_complete( Ok(result) } -pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Option) -> Result<()> { +pub async fn handle_build_push( + config_path: PathBuf, + verbose: bool, + message: Option, +) -> Result<()> { // Load wavedash.toml config let wavedash_config = WavedashConfig::load(&config_path)?; @@ -157,14 +162,39 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Opt anyhow::bail!("No files found in {}", upload_dir.display()); } - // Get temporary R2 credentials (includes build size) let engine_kind = wavedash_config.engine_type()?; + let engine_version = wavedash_config.engine_version(); + let entrypoint_params = match engine_kind { + Some(EngineKind::Defold) => { + let html_entrypoint = locate_html_entrypoint(&upload_dir); + let html_path = html_entrypoint.as_deref().ok_or_else(|| { + anyhow::anyhow!("No HTML file found in upload_dir; required for DEFOLD builds") + })?; + let ver = engine_version + .ok_or_else(|| anyhow::anyhow!("DEFOLD engine requires a version"))?; + let html_relative_path = html_path + .strip_prefix(&upload_dir) + .unwrap_or(html_path) + .to_string_lossy() + .replace('\\', "/"); + Some( + fetch_entrypoint_params("DEFOLD", ver, html_path, Some(&html_relative_path)) + .await?, + ) + } + Some(EngineKind::JsDos | EngineKind::Ruffle | EngineKind::RenPy) => { + wavedash_config.executable_entrypoint_params() + } + _ => None, + }; + + // Get temporary R2 credentials (includes build size) let creds = get_temp_credentials( &wavedash_config.game_id, engine_kind.map(|e| e.as_label()), - wavedash_config.engine_version(), + engine_version, wavedash_config.entrypoint(), - wavedash_config.executable_entrypoint_params(), + entrypoint_params, message.as_deref(), total_bytes, &api_key, @@ -191,10 +221,7 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Opt // Print the play URL let site_host = config::get("open_browser_website_host")?; - let play_url = format!( - "{}/playtest/{}/{}", - site_host, result.game_slug, creds.uuid - ); + let play_url = format!("{}/playtest/{}/{}", site_host, result.game_slug, creds.uuid); println!("\n▶ Play at: {}", play_url); Ok(()) diff --git a/src/config.rs b/src/config.rs index ef5f4cf..d146166 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,8 +8,8 @@ use std::path::PathBuf; /// - Staging: ~/.wavedash-stg /// - Dev: ~/.wavedash-dev pub fn wavedash_dir() -> Result { - let base_dirs = BaseDirs::new() - .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + let base_dirs = + BaseDirs::new().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; Ok(base_dirs.home_dir().join(env!("CONFIG_DIR"))) } @@ -94,7 +94,9 @@ pub fn create_http_client() -> Result { } } - Ok(reqwest::Client::builder().default_headers(headers).build()?) + Ok(reqwest::Client::builder() + .default_headers(headers) + .build()?) } /// Check an API response for errors and return a human-friendly message. @@ -147,6 +149,11 @@ pub struct UnitySection { pub version: String, } +#[derive(Debug, Deserialize)] +pub struct DefoldSection { + pub version: String, +} + /// Shape for engines whose runtime is fetched as a single executable file /// (plus an optional loader script). Used by JSDOS, Ruffle, and Ren'Py. #[derive(Debug, Deserialize)] @@ -168,6 +175,9 @@ pub struct WavedashConfig { #[serde(rename = "unity")] pub unity: Option, + #[serde(rename = "defold")] + pub defold: Option, + #[serde(rename = "jsdos")] pub jsdos: Option, @@ -182,6 +192,7 @@ pub struct WavedashConfig { pub enum EngineKind { Godot, Unity, + Defold, JsDos, Ruffle, RenPy, @@ -192,6 +203,7 @@ impl EngineKind { match self { EngineKind::Godot => "GODOT", EngineKind::Unity => "UNITY", + EngineKind::Defold => "DEFOLD", EngineKind::JsDos => "JSDOS", EngineKind::Ruffle => "RUFFLE", EngineKind::RenPy => "RENPY", @@ -236,6 +248,7 @@ impl WavedashConfig { let engines: Vec = [ self.godot.is_some().then_some(EngineKind::Godot), self.unity.is_some().then_some(EngineKind::Unity), + self.defold.is_some().then_some(EngineKind::Defold), self.jsdos.is_some().then_some(EngineKind::JsDos), self.ruffle.is_some().then_some(EngineKind::Ruffle), self.renpy.is_some().then_some(EngineKind::RenPy), @@ -248,7 +261,7 @@ impl WavedashConfig { 0 => Ok(None), 1 => Ok(Some(engines[0])), _ => anyhow::bail!( - "Config must have at most one engine section: [godot], [unity], [jsdos], [ruffle], or [renpy]" + "Config must have at most one engine section: [godot], [unity], [defold], [jsdos], [ruffle], or [renpy]" ), } } @@ -270,6 +283,9 @@ impl WavedashConfig { if let Some(unity) = &self.unity { return Some(&unity.version); } + if let Some(defold) = &self.defold { + return Some(&defold.version); + } self.executable_section().map(|s| s.version.as_str()) } @@ -277,11 +293,7 @@ impl WavedashConfig { /// Uses the user-specified value from the config, or defaults to "index.html". pub fn entrypoint(&self) -> Option<&str> { match self.engine_type() { - Ok(None) => Some( - self.entrypoint - .as_deref() - .unwrap_or("index.html"), - ), + Ok(None) => Some(self.entrypoint.as_deref().unwrap_or("index.html")), _ => None, } } diff --git a/src/dev/entrypoint.rs b/src/dev/entrypoint.rs index 5266f9c..dcc20ba 100644 --- a/src/dev/entrypoint.rs +++ b/src/dev/entrypoint.rs @@ -20,6 +20,7 @@ pub fn locate_html_entrypoint(upload_dir: &Path) -> Option { return Some(default_index); } + let mut html_files: Vec = Vec::new(); for entry in WalkDir::new(upload_dir) .min_depth(1) .into_iter() @@ -28,23 +29,54 @@ pub fn locate_html_entrypoint(upload_dir: &Path) -> Option { if entry.file_type().is_file() { if let Some(ext) = entry.path().extension() { if ext.eq_ignore_ascii_case("html") { - return Some(entry.into_path()); + html_files.push(entry.into_path()); } } } } - None + html_files.sort_by(|a, b| { + let modified_a = a.metadata().and_then(|m| m.modified()).ok(); + let modified_b = b.metadata().and_then(|m| m.modified()).ok(); + let relative_a = a + .strip_prefix(upload_dir) + .unwrap_or(a) + .to_string_lossy() + .replace('\\', "/"); + let relative_b = b + .strip_prefix(upload_dir) + .unwrap_or(b) + .to_string_lossy() + .replace('\\', "/"); + let architecture_score = |relative: &str| { + if relative.starts_with("wasm-web/") { + 0 + } else if relative.starts_with("js-web/") { + 1 + } else { + 2 + } + }; + + modified_b + .cmp(&modified_a) + .then_with(|| architecture_score(&relative_a).cmp(&architecture_score(&relative_b))) + .then_with(|| relative_a.cmp(&relative_b)) + }); + + html_files.into_iter().next() } -pub async fn fetch_entrypoint_params(engine: &str, engine_version: &str, html_path: &Path) -> Result { +pub async fn fetch_entrypoint_params( + engine: &str, + engine_version: &str, + html_path: &Path, + html_relative_path: Option<&str>, +) -> Result { let html_content = fs::read_to_string(html_path) .with_context(|| format!("Failed to read {}", html_path.display()))?; let api_host = config::get("api_host")?; - let endpoint = format!( - "{}/cli/entrypoint-params", - api_host.trim_end_matches('/') - ); + let endpoint = format!("{}/cli/entrypoint-params", api_host.trim_end_matches('/')); let client = config::create_http_client()?; let response = client @@ -53,6 +85,7 @@ pub async fn fetch_entrypoint_params(engine: &str, engine_version: &str, html_pa "engine": engine, "engineVersion": engine_version, "htmlContent": html_content, + "htmlPath": html_relative_path, })) .send() .await diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 664a5d5..62e0016 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -9,7 +9,7 @@ use crate::config::{self, EngineKind, WavedashConfig}; use crate::file_staging::FileStaging; mod dev_app; -mod entrypoint; +pub(crate) mod entrypoint; mod launcher; use dev_app::{ensure_dev_app, user_data_dir}; @@ -99,7 +99,10 @@ pub async fn handle_dev(config_path: Option, verbose: bool) -> Result<( // pinned to the dev-app source dir, so a relative `./dist` would resolve // there (and serve the dev-app's own bundled `main.js`). let upload_dir = upload_dir.canonicalize().with_context(|| { - format!("Failed to canonicalize upload_dir: {}", upload_dir.display()) + format!( + "Failed to canonicalize upload_dir: {}", + upload_dir.display() + ) })?; let engine_kind = wavedash_config.engine_type()?; @@ -111,7 +114,7 @@ pub async fn handle_dev(config_path: Option, verbose: bool) -> Result<( let html_entrypoint = locate_html_entrypoint(&upload_dir); let engine_version = wavedash_config.engine_version(); let entrypoint_params = match engine_kind { - Some(EngineKind::Godot | EngineKind::Unity) => { + Some(EngineKind::Godot | EngineKind::Unity | EngineKind::Defold) => { let engine_label = engine_kind.unwrap().as_label(); let html_path = html_entrypoint.as_deref().ok_or_else(|| { anyhow::anyhow!( @@ -121,7 +124,15 @@ pub async fn handle_dev(config_path: Option, verbose: bool) -> Result<( })?; let ver = engine_version .ok_or_else(|| anyhow::anyhow!("{} engine requires a version", engine_label))?; - Some(fetch_entrypoint_params(engine_label, ver, html_path).await?) + let html_relative_path = html_path + .strip_prefix(&upload_dir) + .unwrap_or(html_path) + .to_string_lossy() + .replace('\\', "/"); + Some( + fetch_entrypoint_params(engine_label, ver, html_path, Some(&html_relative_path)) + .await?, + ) } Some(EngineKind::JsDos | EngineKind::Ruffle | EngineKind::RenPy) => { wavedash_config.executable_entrypoint_params() diff --git a/src/init.rs b/src/init.rs index 6a5ba4a..49875de 100644 --- a/src/init.rs +++ b/src/init.rs @@ -39,6 +39,7 @@ struct GamesResponse { enum EngineType { Godot, Unity, + Defold, Custom, } @@ -47,6 +48,7 @@ impl EngineType { match self { EngineType::Godot => "build", EngineType::Unity => "build", + EngineType::Defold => "dist", EngineType::Custom => "dist", } } @@ -130,10 +132,25 @@ fn detect_unity(dir: &Path) -> Option { None } +/// Look for a Defold project marker. +fn detect_defold(dir: &Path) -> Option { + if dir.join("game.project").is_file() { + return Some(DetectedEngine { + engine_type: EngineType::Defold, + version_hint: None, + }); + } + + None +} + fn detect_engine(dir: &Path) -> DetectedEngine { if let Some(engine) = detect_godot(dir) { return engine; } + if let Some(engine) = detect_defold(dir) { + return engine; + } if let Some(engine) = detect_unity(dir) { return engine; } @@ -233,6 +250,10 @@ fn generate_toml( let version = engine_version.unwrap_or("2022.3"); toml.push_str(&format!("\n[unity]\nversion = \"{}\"\n", version)); } + EngineType::Defold => { + let version = engine_version.unwrap_or("1.0"); + toml.push_str(&format!("\n[defold]\nversion = \"{}\"\n", version)); + } EngineType::Custom => { toml.push_str("\nentrypoint = \"index.html\"\n"); } @@ -253,9 +274,7 @@ pub async fn handle_init() -> Result<()> { let auth_info = auth_manager.get_auth_info(); let api_key = match auth_info.source { AuthSource::None => { - cliclack::outro_cancel( - "Not authenticated. Run `wavedash auth login` first.", - )?; + cliclack::outro_cancel("Not authenticated. Run `wavedash auth login` first.")?; std::process::exit(1); } _ => auth_info.api_key.unwrap(), @@ -264,10 +283,9 @@ pub async fn handle_init() -> Result<()> { // 2. Check for existing wavedash.toml let config_path = PathBuf::from("wavedash.toml"); if config_path.exists() { - let overwrite: bool = cliclack::confirm( - "A wavedash.toml already exists. Do you want to reinitialize?", - ) - .interact()?; + let overwrite: bool = + cliclack::confirm("A wavedash.toml already exists. Do you want to reinitialize?") + .interact()?; if !overwrite { cliclack::outro("Keeping existing configuration.")?; @@ -279,7 +297,7 @@ pub async fn handle_init() -> Result<()> { let current_dir = std::env::current_dir()?; let detected = detect_engine(¤t_dir); - // Only prompt for version when we detect Godot/Unity. + // Only prompt for version when we detect a first-class engine. // For web builds (threejs, phaser, custom, etc.) no engine config is needed. let engine_version: Option = match &detected.engine_type { EngineType::Godot => { @@ -302,6 +320,13 @@ pub async fn handle_init() -> Result<()> { Some(version) } } + EngineType::Defold => { + let version: String = cliclack::input("Defold version") + .placeholder("1.0") + .default_input("1.0") + .interact()?; + Some(version) + } EngineType::Custom => None, }; @@ -524,11 +549,7 @@ pub async fn handle_project_list(team_id: &str, json: bool) -> Result<()> { .load_preset(UTF8_FULL) .apply_modifier(UTF8_ROUND_CORNERS) .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec![ - Cell::new("ID"), - Cell::new("Slug"), - Cell::new("Title"), - ]); + .set_header(vec![Cell::new("ID"), Cell::new("Slug"), Cell::new("Title")]); for game in games { table.add_row(vec![game._id, game.slug, game.title]); } From 10f7bb04ad23b74efa657dd9e66fabba1cc8f929 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 29 May 2026 13:23:23 -0700 Subject: [PATCH 2/3] refactor --- src/dev/entrypoint.rs | 3 +++ src/init.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dev/entrypoint.rs b/src/dev/entrypoint.rs index dcc20ba..dacbebb 100644 --- a/src/dev/entrypoint.rs +++ b/src/dev/entrypoint.rs @@ -14,6 +14,9 @@ struct EntrypointParamsResponse { entrypoint_params: Value, } +// Shared by the Godot/Unity/Defold dev and build flows. A root-level `index.html` +// short-circuits, so single-HTML exports (Godot/Unity) keep their old behavior; the +// mtime/architecture ranking below only matters for nested multi-HTML dists (Defold). pub fn locate_html_entrypoint(upload_dir: &Path) -> Option { let default_index = upload_dir.join("index.html"); if default_index.is_file() { diff --git a/src/init.rs b/src/init.rs index 49875de..132af83 100644 --- a/src/init.rs +++ b/src/init.rs @@ -251,7 +251,7 @@ fn generate_toml( toml.push_str(&format!("\n[unity]\nversion = \"{}\"\n", version)); } EngineType::Defold => { - let version = engine_version.unwrap_or("1.0"); + let version = engine_version.unwrap_or("1.12.4"); toml.push_str(&format!("\n[defold]\nversion = \"{}\"\n", version)); } EngineType::Custom => { @@ -322,8 +322,8 @@ pub async fn handle_init() -> Result<()> { } EngineType::Defold => { let version: String = cliclack::input("Defold version") - .placeholder("1.0") - .default_input("1.0") + .placeholder("1.12.4") + .default_input("1.12.4") .interact()?; Some(version) } From b6db7b96ca7931e9817fa5f2e53fd4664e0837cb Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 29 May 2026 14:26:10 -0700 Subject: [PATCH 3/3] remove extra linting --- src/builds.rs | 11 +++++------ src/config.rs | 14 ++++++++------ src/dev/entrypoint.rs | 5 ++++- src/dev/mod.rs | 5 +---- src/init.rs | 17 ++++++++++++----- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/builds.rs b/src/builds.rs index 69ac8ac..3ebf6fb 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -125,11 +125,7 @@ async fn notify_upload_complete( Ok(result) } -pub async fn handle_build_push( - config_path: PathBuf, - verbose: bool, - message: Option, -) -> Result<()> { +pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Option) -> Result<()> { // Load wavedash.toml config let wavedash_config = WavedashConfig::load(&config_path)?; @@ -221,7 +217,10 @@ pub async fn handle_build_push( // Print the play URL let site_host = config::get("open_browser_website_host")?; - let play_url = format!("{}/playtest/{}/{}", site_host, result.game_slug, creds.uuid); + let play_url = format!( + "{}/playtest/{}/{}", + site_host, result.game_slug, creds.uuid + ); println!("\n▶ Play at: {}", play_url); Ok(()) diff --git a/src/config.rs b/src/config.rs index d146166..76e02ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,8 +8,8 @@ use std::path::PathBuf; /// - Staging: ~/.wavedash-stg /// - Dev: ~/.wavedash-dev pub fn wavedash_dir() -> Result { - let base_dirs = - BaseDirs::new().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + let base_dirs = BaseDirs::new() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; Ok(base_dirs.home_dir().join(env!("CONFIG_DIR"))) } @@ -94,9 +94,7 @@ pub fn create_http_client() -> Result { } } - Ok(reqwest::Client::builder() - .default_headers(headers) - .build()?) + Ok(reqwest::Client::builder().default_headers(headers).build()?) } /// Check an API response for errors and return a human-friendly message. @@ -293,7 +291,11 @@ impl WavedashConfig { /// Uses the user-specified value from the config, or defaults to "index.html". pub fn entrypoint(&self) -> Option<&str> { match self.engine_type() { - Ok(None) => Some(self.entrypoint.as_deref().unwrap_or("index.html")), + Ok(None) => Some( + self.entrypoint + .as_deref() + .unwrap_or("index.html"), + ), _ => None, } } diff --git a/src/dev/entrypoint.rs b/src/dev/entrypoint.rs index dacbebb..3387336 100644 --- a/src/dev/entrypoint.rs +++ b/src/dev/entrypoint.rs @@ -79,7 +79,10 @@ pub async fn fetch_entrypoint_params( let html_content = fs::read_to_string(html_path) .with_context(|| format!("Failed to read {}", html_path.display()))?; let api_host = config::get("api_host")?; - let endpoint = format!("{}/cli/entrypoint-params", api_host.trim_end_matches('/')); + let endpoint = format!( + "{}/cli/entrypoint-params", + api_host.trim_end_matches('/') + ); let client = config::create_http_client()?; let response = client diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 62e0016..6b8e825 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -99,10 +99,7 @@ pub async fn handle_dev(config_path: Option, verbose: bool) -> Result<( // pinned to the dev-app source dir, so a relative `./dist` would resolve // there (and serve the dev-app's own bundled `main.js`). let upload_dir = upload_dir.canonicalize().with_context(|| { - format!( - "Failed to canonicalize upload_dir: {}", - upload_dir.display() - ) + format!("Failed to canonicalize upload_dir: {}", upload_dir.display()) })?; let engine_kind = wavedash_config.engine_type()?; diff --git a/src/init.rs b/src/init.rs index 132af83..c5e635c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -274,7 +274,9 @@ pub async fn handle_init() -> Result<()> { let auth_info = auth_manager.get_auth_info(); let api_key = match auth_info.source { AuthSource::None => { - cliclack::outro_cancel("Not authenticated. Run `wavedash auth login` first.")?; + cliclack::outro_cancel( + "Not authenticated. Run `wavedash auth login` first.", + )?; std::process::exit(1); } _ => auth_info.api_key.unwrap(), @@ -283,9 +285,10 @@ pub async fn handle_init() -> Result<()> { // 2. Check for existing wavedash.toml let config_path = PathBuf::from("wavedash.toml"); if config_path.exists() { - let overwrite: bool = - cliclack::confirm("A wavedash.toml already exists. Do you want to reinitialize?") - .interact()?; + let overwrite: bool = cliclack::confirm( + "A wavedash.toml already exists. Do you want to reinitialize?", + ) + .interact()?; if !overwrite { cliclack::outro("Keeping existing configuration.")?; @@ -549,7 +552,11 @@ pub async fn handle_project_list(team_id: &str, json: bool) -> Result<()> { .load_preset(UTF8_FULL) .apply_modifier(UTF8_ROUND_CORNERS) .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec![Cell::new("ID"), Cell::new("Slug"), Cell::new("Title")]); + .set_header(vec![ + Cell::new("ID"), + Cell::new("Slug"), + Cell::new("Title"), + ]); for game in games { table.add_row(vec![game._id, game.slug, game.title]); }