1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
use crate::cmd::run_cmd_directly;
use crate::errors::*;
use crate::parse::{InitOpts, NewOpts, Opts};
use std::fs;
use std::path::{Path, PathBuf};

/// Creates the named file with the given contents if it doesn't already exist,
/// printing a warning if it does.
fn create_file_if_not_present(
    filename: &Path,
    contents: &str,
    name: &str,
) -> Result<(), InitError> {
    let filename_str = filename.to_str().unwrap();
    if fs::metadata(filename).is_ok() {
        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);
    } else {
        let contents = contents
            .replace("%name", name)
            .replace("%perseus_version", env!("CARGO_PKG_VERSION"));
        fs::write(filename, contents).map_err(|err| InitError::CreateInitFileFailed {
            source: err,
            filename: filename_str.to_string(),
        })?;
    }
    Ok(())
}

/// Initializes a new Perseus project in the given directory, based on either
/// the default template or one from a given URL.
pub fn init(dir: PathBuf, opts: &InitOpts) -> Result<i32, InitError> {
    // Create the basic directory structure (this will create both `src/` and
    // `src/templates/`)
    fs::create_dir_all(dir.join("src/templates"))
        .map_err(|err| InitError::CreateDirStructureFailed { source: err })?;
    fs::create_dir_all(dir.join(".cargo"))
        .map_err(|err| InitError::CreateDirStructureFailed { source: err })?;
    // Now create each file
    create_file_if_not_present(&dir.join("Cargo.toml"), DFLT_INIT_CARGO_TOML, &opts.name)?;
    create_file_if_not_present(&dir.join(".gitignore"), DFLT_INIT_GITIGNORE, &opts.name)?;
    create_file_if_not_present(&dir.join("src/main.rs"), DFLT_INIT_MAIN_RS, &opts.name)?;
    create_file_if_not_present(
        &dir.join("src/templates/mod.rs"),
        DFLT_INIT_MOD_RS,
        &opts.name,
    )?;
    create_file_if_not_present(
        &dir.join("src/templates/index.rs"),
        DFLT_INIT_INDEX_RS,
        &opts.name,
    )?;
    create_file_if_not_present(
        &dir.join(".cargo/config.toml"),
        DFLT_INIT_CONFIG_TOML,
        // Not used in this one
        &opts.name,
    )?;

    // And now tell the user about some stuff
    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/).");

    Ok(0)
}
/// Initializes a new Perseus project in a new directory that's a child of the
/// current one.
// The `dir` here is the current dir, the name of the one to create is in `opts`
pub fn new(dir: PathBuf, opts: &NewOpts, global_opts: &Opts) -> Result<i32, NewError> {
    // Create the directory (if the user provided a name explicitly, use that,
    // otherwise use the project name)
    let target = dir.join(opts.dir.as_ref().unwrap_or(&opts.name));

    // Check if we're using the default template or one from a URL
    if let Some(url) = &opts.template {
        let url_parts = url.split('@').collect::<Vec<&str>>();
        let engine_url = url_parts[0];
        // A custom branch can be specified after a `@`, or we'll use `stable`
        let cmd = format!(
            // We'll only clone the production branch, and only the top level, we don't need the
            // whole shebang
            "{} clone --single-branch {branch} --depth 1 {repo} {output}",
            global_opts.git_path,
            branch = if let Some(branch) = url_parts.get(1) {
                format!("--branch {}", branch)
            } else {
                String::new()
            },
            repo = engine_url,
            output = target.to_string_lossy()
        );
        println!(
            "Fetching custom initialization template with command: '{}'.",
            &cmd
        );
        // Tell the user what command we're running so that they can debug it
        let exit_code = run_cmd_directly(
            cmd,
            &dir, // We'll run this in the current directory and output into `.perseus/`
            vec![],
        )
        .map_err(|err| NewError::GetCustomInitFailed { source: err })?;
        if exit_code != 0 {
            return Err(NewError::GetCustomInitNonZeroExitCode { exit_code });
        }
        // Now delete the Git internals
        let git_target = target.join(".git");
        if let Err(err) = fs::remove_dir_all(&git_target) {
            return Err(NewError::RemoveCustomInitGitFailed {
                target_dir: git_target.to_str().map(|s| s.to_string()),
                source: err,
            });
        }
        Ok(0)
    } else {
        fs::create_dir(&target).map_err(|err| NewError::CreateProjectDirFailed { source: err })?;
        // Now initialize in there
        let exit_code = init(
            target,
            &InitOpts {
                name: opts.name.to_string(),
            },
        )?;
        Ok(exit_code)
    }
}

// --- BELOW ARE THE RAW FILES FOR DEFAULT INITIALIZATION ---
// The token `%name` in all of these will be replaced with the given project
// name
// NOTE: These must be updated for breaking changes

static DFLT_INIT_CARGO_TOML: &str = r#"[package]
name = "%name"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

# Dependencies for the engine and the browser go here
[dependencies]
perseus = { version = "=%perseus_version", features = [ "hydrate" ] }
sycamore = "^0.8.1"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"

# Engine-only dependencies go here
[target.'cfg(engine)'.dependencies]
tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
perseus-axum = { version = "=%perseus_version", features = [ "dflt-server" ] }

# Browser-only dependencies go here
[target.'cfg(client)'.dependencies]"#;
static DFLT_INIT_GITIGNORE: &str = r#"dist/
target/"#;
static DFLT_INIT_MAIN_RS: &str = r#"mod templates;

use perseus::prelude::*;

#[perseus::main(perseus_axum::dflt_server)]
pub fn main<G: Html>() -> PerseusApp<G> {
    PerseusApp::new()
        .template(crate::templates::index::get_template())
}"#;
static DFLT_INIT_MOD_RS: &str = r#"pub mod index;"#;
static DFLT_INIT_INDEX_RS: &str = r#"use perseus::prelude::*;
use sycamore::prelude::*;

fn index_page<G: Html>(cx: Scope) -> View<G> {
    view! { cx,
        // Don't worry, there are much better ways of styling in Perseus!
        div(style = "display: flex; flex-direction: column; justify-content: center; align-items: center; height: 95vh;") {
            h1 { "Welcome to Perseus!" }
            p {
                "This is just an example app. Try changing some code inside "
                code { "src/templates/index.rs" }
                " and you'll be able to see the results here!"
            }
        }
    }
}

#[engine_only_fn]
fn head(cx: Scope) -> View<SsrNode> {
    view! { cx,
        title { "Welcome to Perseus!" }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::build("index").view(index_page).head(head).build()
}"#;
static DFLT_INIT_CONFIG_TOML: &str = r#"[build]
# You can change these from `engine` to `client` if you want your IDE to give hints about your
# client-side code, rather than your engine-side code. Code that runs on both sides will be
# linted no matter what, and these settings only affect your IDE. The `perseus` CLI will ignore
# them.
rustflags = [ "--cfg", "engine" ]
rustdocflags = [ "--cfg", "engine" ]
"#;