glory_cli/config/
project.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use cargo_metadata::{Metadata, Package};
3use serde::Deserialize;
4use std::{fmt::Debug, net::SocketAddr, sync::Arc};
5
6use crate::{
7    config::lib_package::LibPackage,
8    ext::{
9        anyhow::{bail, Result},
10        PathBufExt, PathExt,
11    },
12    service::site::Site,
13};
14
15use super::{
16    assets::AssetsConfig,
17    bin_package::BinPackage,
18    cli::Opts,
19    dotenvs::{load_dotenvs, overlay_env},
20    end2end::End2EndConfig,
21    style::StyleConfig,
22};
23
24pub struct Project {
25    /// absolute path to the working dir
26    pub working_dir: Utf8PathBuf,
27    pub name: String,
28    pub lib: LibPackage,
29    pub bin: BinPackage,
30    pub style: StyleConfig,
31    pub watch: bool,
32    pub release: bool,
33    pub hot_reload: bool,
34    pub site: Arc<Site>,
35    pub end2end: Option<End2EndConfig>,
36    pub assets: Option<AssetsConfig>,
37    pub js_dir: Utf8PathBuf,
38}
39
40impl Debug for Project {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("Project")
43            .field("name", &self.name)
44            .field("lib", &self.lib)
45            .field("bin", &self.bin)
46            .field("style", &self.style)
47            .field("watch", &self.watch)
48            .field("release", &self.release)
49            .field("hot_reload", &self.hot_reload)
50            .field("site", &self.site)
51            .field("end2end", &self.end2end)
52            .field("assets", &self.assets)
53            .finish_non_exhaustive()
54    }
55}
56
57impl Project {
58    pub fn resolve(cli: &Opts, cwd: &Utf8Path, metadata: &Metadata, watch: bool) -> Result<Vec<Arc<Project>>> {
59        let projects = ProjectDefinition::parse(metadata)?;
60
61        let mut resolved = Vec::new();
62        for (project, mut config) in projects {
63            if config.output_name.is_empty() {
64                config.output_name = project.name.to_string();
65            }
66
67            let lib = LibPackage::resolve(cli, metadata, &project, &config)?;
68
69            let js_dir = config.js_dir.clone().unwrap_or_else(|| Utf8PathBuf::from("src"));
70
71            let proj = Project {
72                working_dir: metadata.workspace_root.clone(),
73                name: project.name.clone(),
74                lib,
75                bin: BinPackage::resolve(cli, metadata, &project, &config)?,
76                style: StyleConfig::new(&config)?,
77                watch,
78                release: cli.release,
79                hot_reload: cli.hot_reload,
80                site: Arc::new(Site::new(&config)),
81                end2end: End2EndConfig::resolve(&config),
82                assets: AssetsConfig::resolve(&config),
83                js_dir,
84            };
85            resolved.push(Arc::new(proj));
86        }
87
88        let projects_in_cwd = resolved
89            .iter()
90            .filter(|p| p.bin.abs_dir.starts_with(cwd) || p.lib.abs_dir.starts_with(cwd))
91            .collect::<Vec<_>>();
92
93        if projects_in_cwd.len() == 1 {
94            Ok(vec![projects_in_cwd[0].clone()])
95        } else {
96            Ok(resolved)
97        }
98    }
99
100    /// env vars to use when running external command
101    pub fn to_envs(&self) -> Vec<(&'static str, String)> {
102        let mut vec = vec![
103            ("GLORY_OUTPUT_NAME", self.lib.output_name.to_string()),
104            ("GLORY_SITE_ROOT", self.site.root_dir.to_string()),
105            ("GLORY_SITE_PKG_DIR", self.site.pkg_dir.to_string()),
106            ("GLORY_SITE_ADDR", self.site.addr.to_string()),
107            ("GLORY_RELOAD_PORT", self.site.reload.port().to_string()),
108            ("GLORY_LIB_DIR", self.lib.rel_dir.to_string()),
109            ("GLORY_BIN_DIR", self.bin.rel_dir.to_string()),
110        ];
111        if self.watch {
112            vec.push(("GLORY_WATCH", "ON".to_string()))
113        }
114        vec
115    }
116}
117
118#[derive(Deserialize, Debug)]
119pub struct ProjectConfig {
120    #[serde(default)]
121    pub output_name: String,
122    #[serde(default = "default_site_addr")]
123    pub site_addr: SocketAddr,
124    #[serde(default = "default_site_root")]
125    pub site_root: Utf8PathBuf,
126    #[serde(default = "default_pkg_dir")]
127    pub site_pkg_dir: Utf8PathBuf,
128    pub style_file: Option<Utf8PathBuf>,
129    pub tailwind_input_file: Option<Utf8PathBuf>,
130    pub tailwind_config_file: Option<Utf8PathBuf>,
131    /// assets dir. content will be copied to the target/site dir
132    pub assets_dir: Option<Utf8PathBuf>,
133    /// js dir. changes triggers rebuilds.
134    pub js_dir: Option<Utf8PathBuf>,
135    #[serde(default = "default_reload_port")]
136    pub reload_port: u16,
137    /// command for launching end-2-end integration tests
138    pub end2end_cmd: Option<String>,
139    /// the dir used when launching end-2-end integration tests
140    pub end2end_dir: Option<Utf8PathBuf>,
141    #[serde(default = "default_browser_query")]
142    pub browser_query: String,
143    /// the bin target to use for building the server
144    #[serde(default)]
145    pub bin_target: String,
146    /// the bin output target triple to use for building the server
147    pub bin_target_triple: Option<String>,
148    /// the directory to put the generated server artifacts
149    pub bin_target_dir: Option<String>,
150    /// the command to run instead of "cargo" when building the server
151    pub bin_cargo_command: Option<String>,
152    #[serde(default)]
153    pub features: Vec<String>,
154    #[serde(default)]
155    pub lib_features: Vec<String>,
156    #[serde(default)]
157    pub lib_default_features: bool,
158    #[serde(default)]
159    pub bin_features: Vec<String>,
160    #[serde(default)]
161    pub bin_default_features: bool,
162
163    #[serde(skip)]
164    pub config_dir: Utf8PathBuf,
165
166    // Profiles
167    pub lib_profile_dev: Option<String>,
168    pub lib_profile_release: Option<String>,
169    pub bin_profile_dev: Option<String>,
170    pub bin_profile_release: Option<String>,
171}
172
173impl ProjectConfig {
174    fn parse(dir: &Utf8Path, metadata: &serde_json::Value) -> Result<Self> {
175        let mut conf: ProjectConfig = serde_json::from_value(metadata.clone())?;
176        conf.config_dir = dir.to_path_buf();
177        let dotenvs = load_dotenvs(dir)?;
178        overlay_env(&mut conf, dotenvs)?;
179        if conf.site_root == "/" || conf.site_root == "." {
180            bail!(
181                "site-root cannot be '{}'. All the content is erased when building the site.",
182                conf.site_root
183            );
184        }
185        if conf.site_addr.port() == conf.reload_port {
186            bail!("The site-addr port and reload-port cannot be the same: {}", conf.reload_port);
187        }
188        Ok(conf)
189    }
190}
191
192#[derive(Debug, Deserialize)]
193#[serde(rename_all = "kebab-case")]
194pub struct ProjectDefinition {
195    name: String,
196    pub bin_package: String,
197    pub lib_package: String,
198}
199impl ProjectDefinition {
200    fn from_workspace(metadata: &serde_json::Value, dir: &Utf8Path) -> Result<Vec<(Self, ProjectConfig)>> {
201        let mut found = Vec::new();
202        if let Some(arr) = metadata.as_array() {
203            for section in arr {
204                let conf = ProjectConfig::parse(dir, section)?;
205                let def: Self = serde_json::from_value(section.clone())?;
206                found.push((def, conf))
207            }
208        }
209        Ok(found)
210    }
211
212    fn from_project(package: &Package, metadata: &serde_json::Value, dir: &Utf8Path) -> Result<(Self, ProjectConfig)> {
213        let conf = ProjectConfig::parse(dir, metadata)?;
214
215        // ensure!(
216        //     package.cdylib_target().is_some(),
217        //     "Cargo.toml has glory metadata but is missing a cdylib library target. {}",
218        //     GRAY.paint(package.manifest_path.as_str())
219        // );
220        // ensure!(
221        //     package.has_bin_target(),
222        //     "Cargo.toml has glory metadata but is missing a bin target. {}",
223        //     GRAY.paint(package.manifest_path.as_str())
224        // );
225
226        Ok((
227            ProjectDefinition {
228                name: package.name.to_string(),
229                bin_package: package.name.to_string(),
230                lib_package: package.name.to_string(),
231            },
232            conf,
233        ))
234    }
235
236    fn parse(metadata: &Metadata) -> Result<Vec<(Self, ProjectConfig)>> {
237        let workspace_dir = &metadata.workspace_root;
238        let mut found: Vec<(Self, ProjectConfig)> = if let Some(md) = glory_metadata(&metadata.workspace_metadata) {
239            Self::from_workspace(md, &Utf8PathBuf::default())?
240        } else {
241            Default::default()
242        };
243
244        for package in metadata.workspace_packages() {
245            let dir = package.manifest_path.unbase(workspace_dir)?.without_last();
246
247            if let Some(metadata) = glory_metadata(&package.metadata) {
248                found.push(Self::from_project(package, metadata, &dir)?);
249            }
250        }
251        Ok(found)
252    }
253}
254
255fn glory_metadata(metadata: &serde_json::Value) -> Option<&serde_json::Value> {
256    metadata.as_object().and_then(|o| o.get("glory"))
257}
258
259fn default_site_addr() -> SocketAddr {
260    SocketAddr::new([127, 0, 0, 1].into(), 8000)
261}
262
263fn default_pkg_dir() -> Utf8PathBuf {
264    Utf8PathBuf::from("pkg")
265}
266
267fn default_site_root() -> Utf8PathBuf {
268    Utf8PathBuf::from("target").join("site")
269}
270
271fn default_reload_port() -> u16 {
272    3001
273}
274
275fn default_browser_query() -> String {
276    "defaults".to_string()
277}