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}