perseus_cli/
init.rs

1use crate::cmd::run_cmd_directly;
2use crate::errors::*;
3use crate::parse::{InitOpts, NewOpts, Opts};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Creates the named file with the given contents if it doesn't already exist,
8/// printing a warning if it does.
9fn create_file_if_not_present(
10    filename: &Path,
11    contents: &str,
12    name: &str,
13) -> Result<(), InitError> {
14    let filename_str = filename.to_str().unwrap();
15    if fs::metadata(filename).is_ok() {
16        eprintln!("[WARNING]: Didn't create '{}', since it already exists. If you didn't mean for this to happen, you should remove this file and try again.", filename_str);
17    } else {
18        let contents = contents
19            .replace("%name", name)
20            .replace("%perseus_version", env!("CARGO_PKG_VERSION"));
21        fs::write(filename, contents).map_err(|err| InitError::CreateInitFileFailed {
22            source: err,
23            filename: filename_str.to_string(),
24        })?;
25    }
26    Ok(())
27}
28
29/// Initializes a new Perseus project in the given directory, based on either
30/// the default template or one from a given URL.
31pub fn init(dir: PathBuf, opts: &InitOpts) -> Result<i32, InitError> {
32    // Create the basic directory structure (this will create both `src/` and
33    // `src/templates/`)
34    fs::create_dir_all(dir.join("src/templates"))
35        .map_err(|err| InitError::CreateDirStructureFailed { source: err })?;
36    fs::create_dir_all(dir.join(".cargo"))
37        .map_err(|err| InitError::CreateDirStructureFailed { source: err })?;
38    // Now create each file
39    create_file_if_not_present(&dir.join("Cargo.toml"), DFLT_INIT_CARGO_TOML, &opts.name)?;
40    create_file_if_not_present(&dir.join(".gitignore"), DFLT_INIT_GITIGNORE, &opts.name)?;
41    create_file_if_not_present(&dir.join("src/main.rs"), DFLT_INIT_MAIN_RS, &opts.name)?;
42    create_file_if_not_present(
43        &dir.join("src/templates/mod.rs"),
44        DFLT_INIT_MOD_RS,
45        &opts.name,
46    )?;
47    create_file_if_not_present(
48        &dir.join("src/templates/index.rs"),
49        DFLT_INIT_INDEX_RS,
50        &opts.name,
51    )?;
52    create_file_if_not_present(
53        &dir.join(".cargo/config.toml"),
54        DFLT_INIT_CONFIG_TOML,
55        // Not used in this one
56        &opts.name,
57    )?;
58
59    // And now tell the user about some stuff
60    println!("Your new app has been created! Run `perseus serve -w` to get to work! You can find more details, including about improving compilation speeds in the Perseus docs (https://framesurge.sh/perseus/en-US/docs/).");
61
62    Ok(0)
63}
64/// Initializes a new Perseus project in a new directory that's a child of the
65/// current one.
66// The `dir` here is the current dir, the name of the one to create is in `opts`
67pub fn new(dir: PathBuf, opts: &NewOpts, global_opts: &Opts) -> Result<i32, NewError> {
68    // Create the directory (if the user provided a name explicitly, use that,
69    // otherwise use the project name)
70    let target = dir.join(opts.dir.as_ref().unwrap_or(&opts.name));
71
72    // Check if we're using the default template or one from a URL
73    if let Some(url) = &opts.template {
74        let url_parts = url.split('@').collect::<Vec<&str>>();
75        let engine_url = url_parts[0];
76        // A custom branch can be specified after a `@`, or we'll use `stable`
77        let cmd = format!(
78            // We'll only clone the production branch, and only the top level, we don't need the
79            // whole shebang
80            "{} clone --single-branch {branch} --depth 1 {repo} {output}",
81            global_opts.git_path,
82            branch = if let Some(branch) = url_parts.get(1) {
83                format!("--branch {}", branch)
84            } else {
85                String::new()
86            },
87            repo = engine_url,
88            output = target.to_string_lossy()
89        );
90        println!(
91            "Fetching custom initialization template with command: '{}'.",
92            &cmd
93        );
94        // Tell the user what command we're running so that they can debug it
95        let exit_code = run_cmd_directly(
96            cmd,
97            &dir, // We'll run this in the current directory and output into `.perseus/`
98            vec![],
99        )
100        .map_err(|err| NewError::GetCustomInitFailed { source: err })?;
101        if exit_code != 0 {
102            return Err(NewError::GetCustomInitNonZeroExitCode { exit_code });
103        }
104        // Now delete the Git internals
105        let git_target = target.join(".git");
106        if let Err(err) = fs::remove_dir_all(&git_target) {
107            return Err(NewError::RemoveCustomInitGitFailed {
108                target_dir: git_target.to_str().map(|s| s.to_string()),
109                source: err,
110            });
111        }
112        Ok(0)
113    } else {
114        fs::create_dir(&target).map_err(|err| NewError::CreateProjectDirFailed { source: err })?;
115        // Now initialize in there
116        let exit_code = init(
117            target,
118            &InitOpts {
119                name: opts.name.to_string(),
120            },
121        )?;
122        Ok(exit_code)
123    }
124}
125
126// --- BELOW ARE THE RAW FILES FOR DEFAULT INITIALIZATION ---
127// The token `%name` in all of these will be replaced with the given project
128// name
129// NOTE: These must be updated for breaking changes
130
131static DFLT_INIT_CARGO_TOML: &str = r#"[package]
132name = "%name"
133version = "0.1.0"
134edition = "2021"
135
136# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
137
138# Dependencies for the engine and the browser go here
139[dependencies]
140perseus = { version = "=%perseus_version", features = [ "hydrate" ] }
141sycamore = "^0.8.1"
142serde = { version = "1", features = [ "derive" ] }
143serde_json = "1"
144
145# Engine-only dependencies go here
146[target.'cfg(engine)'.dependencies]
147tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
148perseus-axum = { version = "=%perseus_version", features = [ "dflt-server" ] }
149
150# Browser-only dependencies go here
151[target.'cfg(client)'.dependencies]"#;
152static DFLT_INIT_GITIGNORE: &str = r#"dist/
153target/"#;
154static DFLT_INIT_MAIN_RS: &str = r#"mod templates;
155
156use perseus::prelude::*;
157
158#[perseus::main(perseus_axum::dflt_server)]
159pub fn main<G: Html>() -> PerseusApp<G> {
160    PerseusApp::new()
161        .template(crate::templates::index::get_template())
162}"#;
163static DFLT_INIT_MOD_RS: &str = r#"pub mod index;"#;
164static DFLT_INIT_INDEX_RS: &str = r#"use perseus::prelude::*;
165use sycamore::prelude::*;
166
167fn index_page<G: Html>(cx: Scope) -> View<G> {
168    view! { cx,
169        // Don't worry, there are much better ways of styling in Perseus!
170        div(style = "display: flex; flex-direction: column; justify-content: center; align-items: center; height: 95vh;") {
171            h1 { "Welcome to Perseus!" }
172            p {
173                "This is just an example app. Try changing some code inside "
174                code { "src/templates/index.rs" }
175                " and you'll be able to see the results here!"
176            }
177        }
178    }
179}
180
181#[engine_only_fn]
182fn head(cx: Scope) -> View<SsrNode> {
183    view! { cx,
184        title { "Welcome to Perseus!" }
185    }
186}
187
188pub fn get_template<G: Html>() -> Template<G> {
189    Template::build("index").view(index_page).head(head).build()
190}"#;
191static DFLT_INIT_CONFIG_TOML: &str = r#"[build]
192# You can change these from `engine` to `client` if you want your IDE to give hints about your
193# client-side code, rather than your engine-side code. Code that runs on both sides will be
194# linted no matter what, and these settings only affect your IDE. The `perseus` CLI will ignore
195# them.
196rustflags = [ "--cfg", "engine" ]
197rustdocflags = [ "--cfg", "engine" ]
198"#;