Skip to main content

waterui_cli/hydrolysis/
backend.rs

1//! Hydrolysis backend configuration and initialization.
2
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    backend::Backend,
9    build::BuildOptions,
10    device::Artifact,
11    hydrolysis::platform::{
12        build_hydrolysis, clean_hydrolysis, is_hydrolysis_platform, package_hydrolysis,
13    },
14    platform::{PackageOptions, TargetBackend, TargetPlatform},
15    project::Project,
16    templates::{self, TemplateContext},
17};
18
19/// Configuration for the hydrolysis backend in a `WaterUI` project.
20#[derive(Debug, Serialize, Deserialize, Clone)]
21pub struct HydrolysisBackend {
22    #[serde(
23        default = "default_hydrolysis_project_path",
24        skip_serializing_if = "is_default_hydrolysis_project_path"
25    )]
26    project_path: PathBuf,
27}
28
29impl HydrolysisBackend {
30    /// Create a new hydrolysis backend configuration with default settings.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            project_path: default_hydrolysis_project_path(),
35        }
36    }
37
38    /// Set a custom project path (defaults to "hydrolysis").
39    #[must_use]
40    pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
41        self.project_path = path.into();
42        self
43    }
44
45    /// Get the path to the hydrolysis project within the `WaterUI` project.
46    #[must_use]
47    pub const fn project_path(&self) -> &PathBuf {
48        &self.project_path
49    }
50
51    /// Check whether generated hydrolysis backend files should be regenerated.
52    ///
53    /// This is used by playground mode where backend glue code is fully managed by the CLI.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error when backend `Cargo.toml` exists but cannot be parsed.
58    pub async fn requires_regeneration(project: &Project) -> eyre::Result<bool> {
59        let backend_dir = project.backend_path::<Self>();
60        let ctx = Self::template_context(project).await?;
61        let outputs = templates::hydrolysis::rendered_outputs(
62            &ctx,
63            &project.hydrolysis_backend_crate_name(),
64        )?;
65        for (relative, expected) in outputs {
66            let path = backend_dir.join(&relative);
67            // Preview binding files are rewritten with target-specific
68            // content on every preview invocation; only their presence is
69            // managed here.
70            let per_run_binding = relative == Path::new("src/preview_symbol.rs")
71                || relative == Path::new("src/preview_test.rs");
72            match std::fs::read(&path) {
73                Ok(existing) if per_run_binding || existing == expected => {}
74                Ok(_) | Err(_) => return Ok(true),
75            }
76        }
77        Ok(false)
78    }
79
80    /// The template context the CLI manages this backend with; regeneration
81    /// compares the backend on disk against exactly this rendering.
82    async fn template_context(project: &Project) -> eyre::Result<TemplateContext> {
83        let manifest = project.manifest();
84        let app_name = manifest
85            .package
86            .name
87            .chars()
88            .filter(|c| c.is_alphanumeric())
89            .collect::<String>();
90        Ok(TemplateContext::for_project_manifest(
91            manifest,
92            project.crate_name().clone(),
93            app_name,
94            &project.resolved_framework().await?,
95        )
96        .with_backend_project_path(project.backend_path::<Self>())
97        .with_project_root_path(project.root().to_path_buf())
98        .with_webview_enabled(project.uses_standard_webview().await?)
99        .with_chromium_enabled(project.links_runtime_package("waterui-chromium").await?)
100        .with_browser_engine(project.linked_browser_engine().await?))
101    }
102}
103
104impl Default for HydrolysisBackend {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl Backend for HydrolysisBackend {
111    const DEFAULT_PATH: &'static str = "hydrolysis";
112
113    // The build cache lives in the repository `target/`; the lockfile is
114    // dependency state, not generated content, and survives regeneration so
115    // resolved versions stay stable across template updates.
116    const CACHE_PATHS: &'static [&'static str] = &["Cargo.lock"];
117
118    fn path(&self) -> &Path {
119        &self.project_path
120    }
121
122    async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
123        let project_path = default_hydrolysis_project_path();
124        let ctx = Self::template_context(project)
125            .await
126            .map_err(crate::backend::FailToInitBackend::Config)?;
127
128        templates::hydrolysis::scaffold(
129            &project.backend_path::<Self>(),
130            &ctx,
131            &project.hydrolysis_backend_crate_name(),
132        )
133        .await
134        .map_err(crate::backend::FailToInitBackend::Io)?;
135
136        Ok(Self { project_path })
137    }
138
139    fn supports(&self, platform: TargetPlatform) -> bool {
140        is_hydrolysis_platform(platform)
141    }
142
143    async fn build(
144        &self,
145        project: &Project,
146        platform: TargetPlatform,
147        options: BuildOptions,
148    ) -> eyre::Result<PathBuf> {
149        project
150            .browser_runtime_plan(platform, TargetBackend::Hydrolysis)
151            .await?;
152        build_hydrolysis(project, platform, options).await
153    }
154
155    async fn package(
156        &self,
157        project: &Project,
158        platform: TargetPlatform,
159        options: PackageOptions,
160    ) -> eyre::Result<Artifact> {
161        package_hydrolysis(project, platform, options).await
162    }
163
164    async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
165        clean_hydrolysis(project).await
166    }
167}
168
169fn default_hydrolysis_project_path() -> PathBuf {
170    PathBuf::from("hydrolysis")
171}
172
173fn is_default_hydrolysis_project_path(s: &Path) -> bool {
174    s == Path::new("hydrolysis")
175}