feat(app-tauri): packager les assets web en ressource et les résoudre depuis le resource dir (#68)

L'AppImage ne pouvait pas servir d'assets web : `resolve_web_root` ne
connaissait que `IDEA_WEB_ROOT`, le dossier de l'exécutable et le cwd —
aucun ne pointe vers un bundle packagé.

- `bundle.resources` embarque les assets sous `web/`, `beforeBuildCommand`
  déclenche le build du frontend.
- Le resource dir Tauri descend de `lib.rs` jusqu'à `EmbeddedServerController`
  via `AppState::build_with_resource_dir`, en gardant les constructeurs
  historiques comme façade.
- `resolve_web_root` insère le candidat packagé juste après `IDEA_WEB_ROOT`,
  qui garde donc la priorité pour le développement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 08:54:15 +02:00
parent a13a6c1801
commit 166adc3c4b
4 changed files with 78 additions and 10 deletions

View File

@ -162,6 +162,7 @@ impl FsServerExposureSettingsStore {
/// Desktop-owned embedded server manager.
pub struct EmbeddedServerController {
app_data_dir: PathBuf,
resource_dir: Option<PathBuf>,
store: FsServerExposureSettingsStore,
inner: Mutex<EmbeddedServerInner>,
}
@ -170,9 +171,16 @@ impl EmbeddedServerController {
/// Creates the controller.
#[must_use]
pub fn new(app_data_dir: PathBuf) -> Self {
Self::with_resource_dir(app_data_dir, None)
}
/// Creates the controller with the Tauri bundle resource directory.
#[must_use]
pub fn with_resource_dir(app_data_dir: PathBuf, resource_dir: Option<PathBuf>) -> Self {
Self {
store: FsServerExposureSettingsStore::new(app_data_dir.clone()),
app_data_dir,
resource_dir,
inner: Mutex::new(EmbeddedServerInner::default()),
}
}
@ -237,7 +245,7 @@ impl EmbeddedServerController {
let config = match server_config_from_settings(
&settings,
self.app_data_dir.clone(),
resolve_web_root()?,
resolve_web_root(self.resource_dir.as_deref())?,
) {
Ok(config) => config,
Err(err) => {
@ -487,11 +495,25 @@ fn parse_ip(value: &str, field: &str) -> Result<IpAddr, ErrorDto> {
.map_err(|_| invalid_error(format!("{field} must be an IP address")))
}
fn resolve_web_root() -> Result<PathBuf, ErrorDto> {
fn resolve_web_root(resource_dir: Option<&Path>) -> Result<PathBuf, ErrorDto> {
let explicit_web_root = std::env::var_os("IDEA_WEB_ROOT").map(PathBuf::from);
let candidates = web_root_candidates(explicit_web_root, resource_dir);
first_web_root(candidates).ok_or_else(|| {
invalid_error("web assets not found: build frontend/dist or set IDEA_WEB_ROOT")
})
}
fn web_root_candidates(
explicit_web_root: Option<PathBuf>,
resource_dir: Option<&Path>,
) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Some(path) = std::env::var_os("IDEA_WEB_ROOT").map(PathBuf::from) {
if let Some(path) = explicit_web_root {
candidates.push(path);
}
if let Some(resource_dir) = resource_dir {
candidates.push(resource_dir.join("web"));
}
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(dir.join("web"));
@ -501,12 +523,13 @@ fn resolve_web_root() -> Result<PathBuf, ErrorDto> {
if let Ok(cwd) = std::env::current_dir() {
candidates.push(cwd.join("frontend").join("dist"));
}
candidates
}
fn first_web_root(candidates: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
candidates
.into_iter()
.find(|candidate| candidate.join("index.html").is_file())
.ok_or_else(|| {
invalid_error("web assets not found: build frontend/dist or set IDEA_WEB_ROOT")
})
}
fn write_atomic_user_only(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
@ -588,6 +611,36 @@ mod tests {
web_root
}
#[test]
fn web_root_candidates_keep_packaged_resource_before_exe_fallbacks() {
let explicit = tmp_app_data().join("explicit-web-root");
let resource_dir = tmp_app_data().join("resources");
let candidates = web_root_candidates(Some(explicit.clone()), Some(&resource_dir));
assert_eq!(candidates[0], explicit);
assert_eq!(candidates[1], resource_dir.join("web"));
}
#[test]
fn first_web_root_uses_first_candidate_with_index_html() {
let missing = tmp_app_data().join("missing-web-root");
let resource_dir = tmp_app_data().join("resources");
let resource_web = resource_dir.join("web");
std::fs::create_dir_all(&resource_web).unwrap();
std::fs::write(
resource_web.join("index.html"),
"<!doctype html><title>Packaged</title>",
)
.unwrap();
let later = tmp_web_root();
let resolved = first_web_root([missing, resource_web.clone(), later])
.expect("web root should resolve");
assert_eq!(resolved, resource_web);
}
#[test]
fn non_loopback_remote_requires_trusted_proxy() {
let settings = ServerExposureSettingsDto {