feat(web): add solitaire_web Bevy WASM build targeting play.html canvas
Build and Deploy / build-and-push (push) Failing after 58s

Adds a new `solitaire_web` crate that compiles the full `solitaire_engine`
to `wasm32-unknown-unknown` and renders to a `<canvas id="bevy-canvas">`
element in `play.html` — the same ECS code path as desktop and Android.

Changes to enable the WASM target:
- .cargo/config.toml: add wasm32-unknown-unknown rustflags for getrandom
- Workspace Cargo.toml: add solitaire_web member
- solitaire_data/Cargo.toml: gate tokio/reqwest/dirs/keyring to non-wasm
- solitaire_data/src: add wasm32 branch to data_dir() (returns None);
  cfg-gate sync_client network types, auth_tokens, matomo_client
- solitaire_engine/Cargo.toml: gate tokio/reqwest/kira/arboard/dirs/zip
  to non-wasm (mio/cpal/arboard don't compile for wasm32-unknown-unknown)
- solitaire_engine/src/lib.rs: cfg-gate module declarations and re-exports
  for analytics, audio, sync, sync_setup, avatar, leaderboard plugins
- solitaire_engine/src/core_game_plugin.rs: cfg-gate plugin registrations
  that require TokioRuntime (audio, sync, analytics, leaderboard, avatar)
- solitaire_engine/src/resources.rs: cfg-gate TokioRuntimeResource
- solitaire_engine/src/game_plugin.rs: cfg-gate std::fs::remove_file (x10)
- solitaire_engine/src/theme/mod.rs: cfg-gate importer module (uses dirs+zip)
- solitaire_engine/src/settings_plugin.rs: cfg-gate theme ZIP import UI
- solitaire_engine/src/assets/sources.rs: cfg-gate FileAssetReader/user_theme_dir
- solitaire_engine/src/auto_complete_plugin.rs: cfg-gate audio system
- solitaire_engine/src/daily_challenge_plugin.rs: cfg-gate server fetch
- solitaire_engine/src/hud_plugin.rs: cfg-gate AvatarResource import
- solitaire_engine/src/profile_plugin.rs: cfg-gate AvatarResource import
- solitaire_server/web/play.html: minimal HTML canvas shell
- solitaire_web/: new crate (Cargo.toml + src/lib.rs)
- build_wasm.sh: add Bevy WASM build step (cargo + wasm-bindgen + wasm-opt)

All tests pass; clippy --workspace -- -D warnings clean; native build
(solitaire_engine, solitaire_app) unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-01 13:46:45 -07:00
parent 9260ca7994
commit 835a48fe9d
23 changed files with 573 additions and 51 deletions
+10
View File
@@ -1512,6 +1512,7 @@ mod tests {
use solitaire_data::load_game_state_from;
let path = tmp_gs_path("exit_save");
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
let mut app = test_app(7);
@@ -1527,6 +1528,7 @@ mod tests {
let loaded = load_game_state_from(&path).expect("file should exist after exit");
assert_eq!(loaded.seed, 7654);
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
}
@@ -1571,6 +1573,7 @@ mod tests {
use solitaire_data::load_game_state_from;
let path = tmp_gs_path("auto_save_30s");
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
let mut app = test_app(42);
@@ -1601,6 +1604,7 @@ mod tests {
let loaded = load_game_state_from(&path).expect("file must be loadable");
assert_eq!(loaded.seed, 42);
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
}
@@ -1608,6 +1612,7 @@ mod tests {
#[test]
fn auto_save_skips_when_no_moves() {
let path = tmp_gs_path("auto_save_skip");
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
let mut app = test_app(99);
@@ -2165,6 +2170,7 @@ mod tests {
use solitaire_data::load_replay_history_from;
let path = std::env::temp_dir().join("engine_test_replay_freeze.json");
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
let mut app = test_app(7654);
@@ -2223,6 +2229,7 @@ mod tests {
other => panic!("second entry must be a Move, got {other:?}"),
}
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
}
@@ -2234,6 +2241,7 @@ mod tests {
use solitaire_data::load_replay_history_from;
let path = std::env::temp_dir().join("engine_test_replay_history_append.json");
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
let mut app = test_app(11);
@@ -2270,6 +2278,7 @@ mod tests {
assert_eq!(history.replays[0].final_score, 200);
assert_eq!(history.replays[1].final_score, 100);
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
}
@@ -2281,6 +2290,7 @@ mod tests {
#[test]
fn replay_with_empty_recording_skips_save() {
let path = std::env::temp_dir().join("engine_test_replay_empty_skip.json");
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(&path);
let mut app = test_app(1);