ines_core/project/
mod.rs

1mod builder;
2pub mod config;
3pub mod error;
4mod include;
5
6use error::Result;
7
8use crate::project::config::Config;
9use crate::project::error::Error::InvalidProject;
10#[cfg(feature = "progress")]
11use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
12use std::path::PathBuf;
13#[cfg(feature = "progress")]
14use std::time::Duration;
15use tracing::debug;
16#[cfg(not(feature = "progress"))]
17use tracing::info;
18
19pub struct Project {
20    path: PathBuf,
21    config: Config,
22}
23
24impl Project {
25    pub fn create(path: PathBuf) -> Result<Self> {
26        debug!("creating project at {}", path.display());
27        std::fs::create_dir_all(&path)?;
28        let config = Config::default();
29        config.write_to_file(&path.join("ines.toml"))?;
30        std::fs::write(path.join("Cargo.toml"), include::CARGO_TOML)?;
31        std::fs::write(path.join("build.rs"), include::BUILD_RS)?;
32        std::fs::write(path.join("empty.rs"), "#![no_std]")?;
33        std::fs::create_dir_all(path.join("src").join("components"))?;
34        std::fs::create_dir_all(path.join("src").join("views"))?;
35        std::fs::write(path.join("src").join("index.html"), include::INDEX_HTML)?;
36        std::fs::write(
37            path.join("src").join("index.js"),
38            r#"import "./components/load.js""#,
39        )?;
40        std::fs::write(path.join("src").join("index.css"), "")?;
41        std::fs::write(path.join("src").join("components").join("load.js"), "")?;
42        std::fs::write(
43            path.join("src").join("components").join("component.js"),
44            include::COMPONENT_JS,
45        )?;
46
47        Ok(Self { path, config })
48    }
49
50    pub fn load_from_path(path: PathBuf) -> Result<Self> {
51        debug!("loading project at {}", path.display());
52
53        Self::is_file_or_dir(&path.join("ines.toml"))
54            .take_if(|e| *e)
55            .ok_or(InvalidProject("ines.toml not found".into()))?;
56        Self::is_file_or_dir(&path.join("Cargo.toml"))
57            .take_if(|e| *e)
58            .ok_or(InvalidProject("Cargo.toml not found".into()))?;
59        Self::is_file_or_dir(&path.join("build.rs"))
60            .take_if(|e| *e)
61            .ok_or(InvalidProject("build.rs not found".into()))?;
62        Self::is_file_or_dir(&path.join("empty.rs"))
63            .take_if(|e| *e)
64            .ok_or(InvalidProject("empty.rs not found".into()))?;
65        Self::is_file_or_dir(&path.join("src").join("index.html"))
66            .take_if(|e| *e)
67            .ok_or(InvalidProject("src/index.html not found".into()))?;
68        Self::is_file_or_dir(&path.join("src").join("index.css"))
69            .take_if(|e| *e)
70            .ok_or(InvalidProject("src/index.css not found".into()))?;
71        Self::is_file_or_dir(&path.join("src").join("index.js"))
72            .take_if(|e| *e)
73            .ok_or(InvalidProject("src/index.js not found".into()))?;
74        Self::is_file_or_dir(&path.join("src").join("components").join("component.js"))
75            .take_if(|e| *e)
76            .ok_or(InvalidProject(
77                "src/components/component.js not found".into(),
78            ))?;
79        Self::is_file_or_dir(&path.join("src").join("components").join("load.js"))
80            .take_if(|e| *e)
81            .ok_or(InvalidProject("src/components/load.js not found".into()))?;
82        Self::is_file_or_dir(&path.join("src").join("views"))
83            .take_if(|e| !*e)
84            .ok_or(InvalidProject("src/views/ not found".into()))?;
85
86        Self::_load_from_path(path)
87    }
88
89    fn is_file_or_dir(path: &PathBuf) -> Option<bool> {
90        std::fs::metadata(path)
91            .ok()
92            .take_if(|e| e.is_dir() || e.is_file())
93            .map(|e| e.is_file())
94    }
95
96    fn _load_from_path(path: PathBuf) -> Result<Self> {
97        let config = Config::from_file(&path.join("ines.toml"))?;
98
99        Ok(Self { path, config })
100    }
101
102    pub fn build(&self) -> Result<()> {
103        #[cfg(feature = "progress")]
104        let multi = MultiProgress::new();
105        #[cfg(feature = "progress")]
106        let run_style = ProgressStyle::with_template("{spinner:.cyan} {msg} ({elapsed})")
107            .unwrap()
108            .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
109        #[cfg(feature = "progress")]
110        let finish_style =
111            ProgressStyle::with_template("{prefix:.green} {msg} (in {elapsed})").unwrap();
112
113        #[cfg(feature = "progress")]
114        let pb = {
115            let pb = multi.add(
116                ProgressBar::new_spinner()
117                    .with_message("Copying base files")
118                    .with_style(run_style.clone()),
119            );
120            pb.enable_steady_tick(Duration::from_millis(200));
121            pb
122        };
123
124        let out_dir = self.path.join(&self.config.out);
125        _ = std::fs::remove_dir_all(&out_dir);
126
127        let strip = self.path.join("src");
128        std::fs::create_dir_all(out_dir.join("components"))?;
129
130        std::fs::copy(
131            strip.join("components").join("load.js"),
132            out_dir.join("components").join("load.js"),
133        )?;
134        std::fs::copy(
135            strip.join("components").join("component.js"),
136            out_dir.join("components").join("component.js"),
137        )?;
138
139        #[cfg(feature = "progress")]
140        {
141            pb.set_style(finish_style.clone());
142            pb.set_message("Base files copied");
143            pb.set_prefix("✓");
144            pb.finish();
145        }
146
147        #[cfg(feature = "progress")]
148        let pb = {
149            let pb = multi.add(
150                ProgressBar::new_spinner()
151                    .with_message("Getting components")
152                    .with_style(run_style.clone()),
153            );
154            pb.enable_steady_tick(Duration::from_millis(200));
155            pb
156        };
157
158        #[cfg(not(feature = "progress"))]
159        info!("getting components");
160        let components = builder::scan_components(strip.join("components"))?;
161
162        #[cfg(feature = "progress")]
163        {
164            pb.set_style(finish_style.clone());
165            pb.set_message(format!("Got {} components", components.len()));
166            pb.set_prefix("✓");
167            pb.finish();
168        }
169
170        #[cfg(not(feature = "progress"))]
171        info!("got {} components", components.len());
172
173        #[cfg(feature = "progress")]
174        let pb = {
175            let pb = multi.add(
176                ProgressBar::new_spinner()
177                    .with_message("Building component")
178                    .with_style(
179                        ProgressStyle::with_template(
180                            "{spinner:.cyan} {msg} {prefix:.grey} ({elapsed})",
181                        )
182                        .unwrap()
183                        .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
184                    ),
185            );
186            pb.enable_steady_tick(Duration::from_millis(200));
187            pb
188        };
189
190        let mut htmls = vec![];
191        for component in components {
192            #[cfg(feature = "progress")]
193            pb.set_prefix(component.name());
194
195            #[cfg(not(feature = "progress"))]
196            info!("building component {}", component.name());
197            std::fs::create_dir_all(
198                out_dir.join("components").join(
199                    component
200                        .get_base()
201                        .strip_prefix(strip.join("components"))
202                        .unwrap(),
203                ),
204            )?;
205            std::fs::write(
206                out_dir
207                    .join("components")
208                    .join(
209                        component
210                            .get_base()
211                            .strip_prefix(strip.join("components"))
212                            .unwrap(),
213                    )
214                    .join(component.filename()),
215                component.build_js()?,
216            )?;
217            htmls.push(component.build_template()?);
218        }
219
220        #[cfg(feature = "progress")]
221        {
222            pb.set_prefix("");
223            pb.finish();
224        }
225
226        #[cfg(feature = "progress")]
227        let pb = {
228            let pb = multi.add(
229                ProgressBar::new_spinner()
230                    .with_message("Generating index and assets")
231                    .with_style(run_style.clone()),
232            );
233            pb.enable_steady_tick(Duration::from_millis(200));
234            pb
235        };
236
237        #[cfg(not(feature = "progress"))]
238        info!("generating index.html");
239        std::fs::write(
240            out_dir.join("index.html"),
241            builder::build_html(self.path.join("src").join("index.html"), htmls.join("\n"))?,
242        )?;
243
244        #[cfg(not(feature = "progress"))]
245        info!("copying assets");
246        for entry in std::fs::read_dir(self.path.join("src"))?.flatten() {
247            match entry.file_name().to_str() {
248                Some("index.html") | Some("components") | Some("views") => {}
249                _ => {
250                    debug!(entry=?entry.path(), out=?out_dir.join(entry.file_name()));
251                    builder::copy(entry.path(), out_dir.join(entry.file_name()))?;
252                }
253            }
254        }
255
256        #[cfg(feature = "progress")]
257        {
258            pb.set_style(finish_style.clone());
259            pb.set_message("Generated index and assets");
260            pb.set_prefix("✓");
261            pb.finish();
262        }
263
264        #[cfg(feature = "progress")]
265        let pb = {
266            let pb = multi.add(
267                ProgressBar::new_spinner()
268                    .with_message("Generating redirect")
269                    .with_style(run_style),
270            );
271            pb.enable_steady_tick(Duration::from_millis(200));
272            pb
273        };
274
275        #[cfg(not(feature = "progress"))]
276        info!("Generating redirects");
277        if let Some(redirects) = &self.config.generate_redirects {
278            for redirect in redirects {
279                let path = redirect
280                    .split("/")
281                    .filter(|e| !e.is_empty())
282                    .collect::<Vec<_>>()
283                    .join("/");
284                std::fs::create_dir_all(out_dir.join(&path))?;
285                std::fs::write(
286                    out_dir.join(&path).join("index.html"),
287                    builder::generate_redirect(redirect),
288                )?;
289            }
290        }
291
292        #[cfg(feature = "progress")]
293        {
294            pb.set_style(finish_style);
295            pb.set_message("Generated redirects");
296            pb.set_prefix("✓");
297            pb.finish();
298        }
299
300        Ok(())
301    }
302
303    pub fn base_path(&self) -> &PathBuf {
304        &self.path
305    }
306
307    pub fn src_path(&self) -> PathBuf {
308        self.base_path().join("src")
309    }
310
311    pub fn components_path(&self) -> PathBuf {
312        self.src_path().join("components")
313    }
314
315    pub fn views_path(&self) -> PathBuf {
316        self.src_path().join("views")
317    }
318
319    pub fn out_path(&self) -> PathBuf {
320        self.base_path().join(&self.config.out)
321    }
322}