1use crate::cmd::run_cmd_directly;
2use crate::errors::*;
3use crate::parse::{InitOpts, NewOpts, Opts};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7fn 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
29pub fn init(dir: PathBuf, opts: &InitOpts) -> Result<i32, InitError> {
32 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 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 &opts.name,
57 )?;
58
59 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}
64pub fn new(dir: PathBuf, opts: &NewOpts, global_opts: &Opts) -> Result<i32, NewError> {
68 let target = dir.join(opts.dir.as_ref().unwrap_or(&opts.name));
71
72 if let Some(url) = &opts.template {
74 let url_parts = url.split('@').collect::<Vec<&str>>();
75 let engine_url = url_parts[0];
76 let cmd = format!(
78 "{} 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 let exit_code = run_cmd_directly(
96 cmd,
97 &dir, vec![],
99 )
100 .map_err(|err| NewError::GetCustomInitFailed { source: err })?;
101 if exit_code != 0 {
102 return Err(NewError::GetCustomInitNonZeroExitCode { exit_code });
103 }
104 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 let exit_code = init(
117 target,
118 &InitOpts {
119 name: opts.name.to_string(),
120 },
121 )?;
122 Ok(exit_code)
123 }
124}
125
126static 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"#;