unreact/app/mod.rs
1/// All route creation implementations for `Unreact` struct
2mod routes;
3
4use std::fs;
5
6use handlebars::Handlebars;
7
8use crate::{
9 convert::{register_inbuilt, register_templates, render_page, scss_to_css},
10 files::{check_source_folders, clean_build_dir, read_folder_recurse},
11 Config, Error, Object, Port, RouteMap, Unreact, DEV_BUILD_DIR,
12};
13
14impl<'a> Unreact<'a> {
15 /// Create a new empty `Unreact` app
16 ///
17 /// ## Parameters
18 ///
19 /// - `config`: Configuration for the app (See [`Config`])
20 /// - `is_dev`: Whether the app should build in *dev mode* (See [`is_dev`](fn.is_dev.html))
21 /// - `url`: The url that should be given to rendered templates. Overridden in *dev mode*. Trailing forward-slash is added if not present
22 ///
23 /// # Examples
24 ///
25 /// ### Quick Start
26 ///
27 /// ```rust,no_run
28 /// use unreact::prelude::*;
29 ///
30 /// fn main() -> Result<(), Error> {
31 /// // Create the app
32 /// // Using default config, not in dev mode, and an example url
33 /// let mut app = Unreact::new(Config::default(), false, "https://example.com")?;
34 /// // Create an index route
35 /// // This uses the template 'page.hbs' in 'templates/'
36 /// // A json object with a value for 'foo' is passed into the template
37 /// app.index("page", object! { foo: "World!" });
38 /// // Compile it!
39 /// app.run()
40 /// }
41 /// ```
42 ///
43 /// ### Long Example
44 ///
45 /// ```rust,no_run
46 /// use unreact::prelude::*;
47 ///
48 /// fn main() -> Result<(), Error> {
49 /// // Custom config
50 /// let config = Config {
51 /// // Strict mode enabled
52 /// strict: true,
53 /// ..Config::default()
54 /// };
55 ///
56 /// // Create app, with `is_dev` function
57 /// let mut app = Unreact::new(config, is_dev(), "https://bruh.news/").expect("Could not create app");
58 ///
59 /// // Set a global variable named 'smiley'
60 /// app.globalize(object! {
61 /// smiley: "(^_^)"
62 /// });
63 ///
64 /// // Some routes
65 /// app.index("page", object! {message: "World"})?
66 /// .not_found("404", object! {})?
67 /// .route_raw("hello", "this is my hello page".to_string())
68 /// .route("article", "other/article", object! {})?;
69 ///
70 /// // Run app
71 /// app.run().expect("Could not compile app");
72 ///
73 /// println!("Compiled successfully");
74 /// Ok(())
75 /// }
76 /// ```
77 pub fn new(mut config: Config, is_dev: bool, url: &str) -> Result<Self, Error> {
78 // Use dev build directory if dev mode is active
79 if is_dev {
80 config.build = DEV_BUILD_DIR.to_string();
81 }
82
83 // Check that source folders exist and can be accessed
84 check_source_folders(&config)?;
85
86 // Override url if in dev mode
87 let url = get_url(url, is_dev, config.port);
88
89 // Create handlebars registry, and register inbuilt partials and helpers
90 let mut registry = Handlebars::new();
91 register_inbuilt(&mut registry, &url)?;
92 registry.set_dev_mode(is_dev);
93
94 Ok(Self {
95 config,
96 routes: RouteMap::new(),
97 globals: Object::new(),
98 is_dev,
99 handlebars: registry,
100 url,
101 })
102 }
103
104 // pub fn registry(&mut self) -> &mut Handlebars {
105 // self.registry
106 // }
107
108 /// Set global variables for templates
109 ///
110 /// # Example
111 ///
112 /// ```rust,no_run
113 /// # use unreact::prelude::*;
114 /// # fn main() -> Result<(), Error> {
115 /// Unreact::new(Config::default(), false, "https://example.com")?
116 /// // Index page
117 /// .index("page", object! {})?
118 /// // Globalize does not need to be ran before routes
119 /// .globalize(object! {smiley: "(^_^)"})
120 /// // Compiles with a smiley face replacing `{{GLOBAL.smiley}}`
121 /// .run()
122 /// # }
123 /// ```
124 pub fn globalize(&mut self, data: Object) -> &mut Self {
125 self.globals = data;
126 self
127 }
128
129 /// Get [`Handlebars`](handlebars) registry as mutable reference
130 pub fn handlebars(&mut self) -> &mut Handlebars<'a> {
131 &mut self.handlebars
132 }
133
134 /// Get URL of app (overridden in *dev mode*)
135 pub fn url(&self) -> &String {
136 &self.url
137 }
138
139 /// Compile app to build directory
140 ///
141 /// Does not open a dev server, even in *dev mode*
142 fn compile(&self) -> Result<(), Error> {
143 clean_build_dir(&self.config, self.is_dev)?;
144
145 // Create handlebars registry
146 let mut registry = self.handlebars.clone();
147
148 // Enable strict mode if active
149 if self.config.strict {
150 registry.set_strict_mode(true);
151 }
152
153 // Register custom templates
154 let templates = read_folder_recurse(&self.config.templates)?;
155 register_templates(&mut registry, templates)?;
156
157 // Render page and write to files
158 for (name, page) in &self.routes {
159 // Render page with data
160 let content = render_page(
161 &mut registry,
162 page,
163 self.globals.clone(),
164 self.config.minify,
165 self.is_dev,
166 self.config.port_ws,
167 )?;
168
169 // Get filepath
170 let path = if name == "404" {
171 // Special case for 404 route
172 format!("{}/404.html", self.config.build)
173 } else {
174 // Create folder for `index.html` file
175 let parent = format!("{}/{}", self.config.build, name);
176 try_unwrap!(
177 fs::create_dir_all(&parent),
178 else Err(err) => return io_fail!(CreateDir, parent, err),
179 );
180 // Normal path
181 format!("{parent}/index.html")
182 };
183
184 // Write file
185 try_unwrap!(
186 fs::write(&path, content),
187 else Err(err) => return io_fail!(WriteFile, path, err),
188 );
189 }
190
191 // Convert scss to css and write to files
192 let styles = read_folder_recurse(&self.config.styles)?;
193 for (name, scss) in styles {
194 // Create folder for `style.css` file
195 let parent = format!("{}/styles/{}", self.config.build, name);
196 try_unwrap!(
197 fs::create_dir_all(&parent),
198 else Err(err) => return io_fail!(CreateDir, parent, err),
199 );
200
201 // Convert to scss
202 let css = scss_to_css(&name, &scss, self.config.minify)?;
203
204 // Write file
205 let path = format!("{}/style.css", parent);
206 try_unwrap!(
207 fs::write(&path, css),
208 else Err(err) => return io_fail!(WriteFile, path, err),
209 );
210 }
211
212 Ok(())
213 }
214
215 /// Compile app to build directory
216 ///
217 /// Compile app to build directory
218 ///
219 /// **NOTE**: The `"dev"` feature is not enabled, so app not open dev server, even in *dev mode*
220 ///
221 /// Add `features = "dev"` or `features = "watch"` to the `unreact` dependency in `Cargo.toml` to use the 'dev server'
222 ///
223 /// # Examples
224 ///
225 /// ### Quick Start
226 ///
227 /// ```rust,no_run
228 /// use unreact::prelude::*;
229 ///
230 /// fn main() -> Result<(), Error> {
231 /// // Create the app
232 /// // Using default config, not in dev mode, and an example url
233 /// let mut app = Unreact::new(Config::default(), false, "https://example.com")?;
234 /// // Create an index route
235 /// // This uses the template 'page.hbs' in 'templates/'
236 /// // A json object with a value for 'foo' is passed into the template
237 /// app.index("page", object! { foo: "World!" });
238 /// // Compile it!
239 /// app.run()
240 /// }
241 /// ```
242 #[cfg(not(feature = "dev"))]
243 pub fn run(&self) -> Result<(), Error> {
244 self.compile()
245 }
246
247 /// Compile app to build directly, and open local server if *dev mode* is active
248 ///
249 /// Only opens a dev server with the `"dev"` or `"watch"` features enabled
250 ///
251 /// If the `"watch"` feature is enabled, source files will also be watched for changes, and the client will be reloaded automatically
252 ///
253 /// # Examples
254 ///
255 /// ```rust,no_run
256 /// use unreact::prelude::*;
257 ///
258 /// fn main() -> Result<(), Error> {
259 /// // Create the app
260 /// // Using default config, not in dev mode, and an example url
261 /// let mut app = Unreact::new(Config::default(), true, "https://example.com")?;
262 /// // Create an index route
263 /// // This uses the template 'page.hbs' in 'templates/'
264 /// // A json object with a value for 'foo' is passed into the template
265 /// app.index("page", object! { foo: "World!" });
266 /// // Compile it!
267 /// app.run()
268 /// }
269 /// ```
270 #[cfg(feature = "dev")]
271 pub fn run(&self) -> Result<(), Error> {
272 use crate::server;
273 use stilo::{eprintln_styles, print_styles, println_styles};
274
275 // Just compile if not dev mode
276 if !self.is_dev {
277 return self.compile();
278 }
279
280 // Create callback with non-breaking error message
281 let run_compile = || {
282 // Clear terminal
283 print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
284
285 // Print message before compile
286 print_styles!(
287 "Unreact": Blue + bold + italic;
288 " dev server": Blue + bold;
289 );
290 if let Some(name) = crate::get_package_name() {
291 print_styles!(
292 " | ": Blue + dim;
293 "{}": Magenta, name;
294 );
295 }
296 println_styles!(
297 "\n Listening on ": Green + bold;
298 "http://localhost:{}": Green + bold + underline, self.config.port;
299 );
300 #[cfg(feature = "watch")]
301 {
302 println_styles!(" Watching files for changes...": Cyan);
303 }
304 #[cfg(not(feature = "watch"))]
305 {
306 println_styles!(
307 " Note: ": Yellow + bold;
308 "\"watch\"": Yellow + italic;
309 " feature not enabled": Yellow;
310 );
311 }
312 println!();
313
314 // Compile it now
315 match self.compile() {
316 // Success
317 Ok(()) => println_styles!("Compiled successfully!": Green + bold),
318 // Error
319 Err(err) => eprintln_styles!(
320 "Error compiling in dev mode:": Red + bold;
321 "\n{}": Yellow, err;
322 ),
323 }
324 };
325
326 // Compile for first time
327 run_compile();
328
329 // For "watch" feature
330 #[cfg(feature = "watch")]
331 {
332 // Open server in new thread
333 let port = self.config.port;
334 let port_ws = self.config.port_ws;
335 let public = self.config.public.clone();
336 std::thread::spawn(move || server::listen(port, &public, port_ws));
337
338 // Folders to watch
339 let watched_folders = &[
340 self.config.templates.as_str(),
341 self.config.styles.as_str(),
342 self.config.public.as_str(),
343 ];
344
345 // Watch files for changes
346 server::watch(run_compile, watched_folders, self.config.port_ws);
347 }
348
349 // For NOT "watch" feature
350 #[cfg(not(feature = "watch"))]
351 {
352 // Open server in current thread
353 server::listen(self.config.port, self.config.public, self.config.port_ws);
354 }
355
356 Ok(())
357 }
358}
359
360/// Get the url for the site
361///
362/// Returns url given, unless `"dev"` feature is enabled and *dev mode* is active
363fn get_url(
364 url: &str,
365 // Only for "dev" feature
366 #[allow(unused_variables)] is_dev: bool,
367 #[allow(unused_variables)] port: Port,
368) -> String {
369 // If `watch` feature is used, and `is_dev`
370 #[cfg(feature = "dev")]
371 {
372 if is_dev {
373 return format!("http://localhost:{}/", port);
374 }
375 }
376
377 // Default (add slash to end if not included)
378 url.to_string() + if url.ends_with('/') { "" } else { "/" }
379}