Skip to main content

hblank_cli/
catalog.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    process::{Command, ExitStatus, Stdio},
5};
6
7use thiserror::Error;
8
9use crate::{Config, ConfigError, GenerationError, refresh_generated_fixtures};
10
11#[derive(Clone, Debug)]
12pub struct CatalogOptions {
13    pub project_root: PathBuf,
14}
15
16impl CatalogOptions {
17    #[must_use]
18    pub fn new(project_root: impl Into<PathBuf>) -> Self {
19        Self {
20            project_root: project_root.into(),
21        }
22    }
23}
24
25/// Builds the private preview and prints its registered component catalog.
26///
27/// # Errors
28/// Returns an error when config, generation, build, manifest, or preview execution fails.
29pub fn run_list(options: &CatalogOptions) -> Result<(), CatalogError> {
30    let project_root = canonical_project_root(&options.project_root)?;
31    let config = Config::load(&project_root)?;
32    refresh_generated_fixtures(&project_root, &config)?;
33    build_preview(&project_root)?;
34    let output = catalog_output(&project_root)?;
35    print!("{output}");
36    Ok(())
37}
38
39pub(crate) fn fixture_ids(project_root: &Path) -> Result<Vec<String>, CatalogError> {
40    let output = catalog_output(project_root)?;
41    Ok(output
42        .lines()
43        .filter_map(|line| {
44            let mut fields = line.split('\t');
45            (fields.next() == Some("fixture"))
46                .then(|| fields.next().map(str::to_owned))
47                .flatten()
48        })
49        .collect())
50}
51
52pub(crate) fn build_preview(project_root: &Path) -> Result<(), CatalogError> {
53    let manifest = project_root.join(".hblank/Cargo.toml");
54    let target = project_root.join(".hblank/target");
55    let status = Command::new("cargo")
56        .arg("build")
57        .arg("--manifest-path")
58        .arg(&manifest)
59        .arg("--target-dir")
60        .arg(&target)
61        .stdin(Stdio::null())
62        .status()
63        .map_err(CatalogError::Process)?;
64    if status.success() {
65        Ok(())
66    } else {
67        Err(CatalogError::BuildFailed(status))
68    }
69}
70
71pub(crate) fn preview_binary(project_root: &Path) -> Result<PathBuf, CatalogError> {
72    let package_name = preview_package_name(project_root)?;
73    let mut binary = project_root.join(".hblank/target/debug").join(package_name);
74    if cfg!(windows) {
75        binary.set_extension("exe");
76    }
77    Ok(binary)
78}
79
80fn catalog_output(project_root: &Path) -> Result<String, CatalogError> {
81    let binary = preview_binary(project_root)?;
82    let output = Command::new(&binary)
83        .env("HBLANK_PROJECT_ROOT", project_root)
84        .env("HBLANK_LIST_CATALOG", "1")
85        .stdin(Stdio::null())
86        .output()
87        .map_err(CatalogError::Process)?;
88    if !output.status.success() {
89        return Err(CatalogError::ListFailed(output.status));
90    }
91    String::from_utf8(output.stdout).map_err(CatalogError::NonUtf8)
92}
93
94fn canonical_project_root(path: &Path) -> Result<PathBuf, CatalogError> {
95    path.canonicalize()
96        .map_err(|source| CatalogError::ProjectRoot {
97            path: path.to_path_buf(),
98            source,
99        })
100}
101
102fn preview_package_name(project_root: &Path) -> Result<String, CatalogError> {
103    let path = project_root.join(".hblank/Cargo.toml");
104    let source = fs::read_to_string(&path).map_err(|source| CatalogError::ReadManifest {
105        path: path.clone(),
106        source,
107    })?;
108    let manifest =
109        toml::from_str::<toml::Value>(&source).map_err(|source| CatalogError::ParseManifest {
110            path: path.clone(),
111            source,
112        })?;
113    manifest
114        .get("package")
115        .and_then(|package| package.get("name"))
116        .and_then(toml::Value::as_str)
117        .map(str::to_owned)
118        .ok_or(CatalogError::MissingPackageName(path))
119}
120
121#[derive(Debug, Error)]
122pub enum CatalogError {
123    #[error("could not resolve Hblank project root {path}: {source}")]
124    ProjectRoot {
125        path: PathBuf,
126        source: std::io::Error,
127    },
128    #[error(transparent)]
129    Config(#[from] ConfigError),
130    #[error(transparent)]
131    Generation(#[from] GenerationError),
132    #[error("could not run Hblank catalog command: {0}")]
133    Process(std::io::Error),
134    #[error("preview build exited unsuccessfully: {0}")]
135    BuildFailed(ExitStatus),
136    #[error("catalog listing exited unsuccessfully: {0}")]
137    ListFailed(ExitStatus),
138    #[error("catalog listing was not UTF-8: {0}")]
139    NonUtf8(std::string::FromUtf8Error),
140    #[error("could not read preview manifest at {path}: {source}")]
141    ReadManifest {
142        path: PathBuf,
143        source: std::io::Error,
144    },
145    #[error("could not parse preview manifest at {path}: {source}")]
146    ParseManifest {
147        path: PathBuf,
148        source: toml::de::Error,
149    },
150    #[error("preview manifest at {0} has no package name")]
151    MissingPackageName(PathBuf),
152}