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};
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(())
}
pub fn init(dir: PathBuf, opts: &InitOpts) -> Result<i32, InitError> {
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 })?;
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,
&opts.name,
)?;
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)
}
pub fn new(dir: PathBuf, opts: &NewOpts, global_opts: &Opts) -> Result<i32, NewError> {
let target = dir.join(opts.dir.as_ref().unwrap_or(&opts.name));
if let Some(url) = &opts.template {
let url_parts = url.split('@').collect::<Vec<&str>>();
let engine_url = url_parts[0];
let cmd = format!(
"{} 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
);
let exit_code = run_cmd_directly(
cmd,
&dir, vec![],
)
.map_err(|err| NewError::GetCustomInitFailed { source: err })?;
if exit_code != 0 {
return Err(NewError::GetCustomInitNonZeroExitCode { exit_code });
}
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 })?;
let exit_code = init(
target,
&InitOpts {
name: opts.name.to_string(),
},
)?;
Ok(exit_code)
}
}
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" ]
"#;