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}