Skip to content

Commit 0bf879b

Browse files
committed
feat(cli): added perseus new and perseus init
Closes #128.
1 parent 0c1b578 commit 0bf879b

File tree

6 files changed

+272
-2
lines changed

6 files changed

+272
-2
lines changed

packages/perseus-cli/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ indicatif = "=0.17.0-beta.1" # Not stable, but otherwise error handling is just
2828
console = "0.14"
2929
serde = "1"
3030
serde_json = "1"
31-
clap = { version = "3", features = [ "color", "derive" ] }
31+
clap = { version = "3.2", features = [ "color", "derive", "unstable-v4" ] }
3232
fs_extra = "1"
3333
tokio = { version = "1", features = [ "macros", "rt-multi-thread", "sync" ] }
3434
warp = "0.3"

packages/perseus-cli/src/bin/main.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use fmterr::fmt_err;
44
use notify::{recommended_watcher, RecursiveMode, Watcher};
55
use perseus_cli::parse::{ExportOpts, ServeOpts, SnoopSubcommand};
66
use perseus_cli::{
7-
build, check_env, delete_artifacts, deploy, export,
7+
build, check_env, delete_artifacts, deploy, export, init, new,
88
parse::{Opts, Subcommand},
99
serve, serve_exported, tinker,
1010
};
@@ -298,6 +298,8 @@ async fn core_watch(dir: PathBuf, opts: Opts) -> Result<i32, Error> {
298298
SnoopSubcommand::Serve(snoop_serve_opts) => snoop_server(dir, snoop_serve_opts)?,
299299
},
300300
Subcommand::ExportErrorPage(opts) => export_error_page(dir, opts)?,
301+
Subcommand::New(opts) => new(dir, opts)?,
302+
Subcommand::Init(opts) => init(dir, opts)?,
301303
};
302304
Ok(exit_code)
303305
}

packages/perseus-cli/src/errors.rs

+48
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pub enum Error {
2525
DeployError(#[from] DeployError),
2626
#[error(transparent)]
2727
WatchError(#[from] WatchError),
28+
#[error(transparent)]
29+
InitError(#[from] InitError),
30+
#[error(transparent)]
31+
NewError(#[from] NewError),
2832
}
2933

3034
/// Errors that can occur while attempting to execute a Perseus app with
@@ -162,3 +166,47 @@ pub enum WatchError {
162166
source: std::io::Error,
163167
},
164168
}
169+
170+
#[derive(Error, Debug)]
171+
pub enum InitError {
172+
#[error("couldn't create directory structure for new project, do you have the necessary permissions?")]
173+
CreateDirStructureFailed {
174+
#[source]
175+
source: std::io::Error,
176+
},
177+
#[error("couldn't create file '{filename}' for project initialization")]
178+
CreateInitFileFailed {
179+
#[source]
180+
source: std::io::Error,
181+
filename: String,
182+
},
183+
}
184+
185+
#[derive(Error, Debug)]
186+
pub enum NewError {
187+
// The `new` command calls the `init` command in effect
188+
#[error(transparent)]
189+
InitError(#[from] InitError),
190+
#[error("couldn't create directory for new project, do you have the necessary permissions?")]
191+
CreateProjectDirFailed {
192+
#[source]
193+
source: std::io::Error,
194+
},
195+
#[error("fetching the custom initialization template failed")]
196+
GetCustomInitFailed {
197+
#[source]
198+
source: ExecutionError,
199+
},
200+
#[error(
201+
"fetching the custom initialization template returned non-zero exit code ({exit_code})"
202+
)]
203+
GetCustomInitNonZeroExitCode { exit_code: i32 },
204+
#[error(
205+
"couldn't remove git internals at '{target_dir:?}' for custom initialization template"
206+
)]
207+
RemoveCustomInitGitFailed {
208+
target_dir: Option<String>,
209+
#[source]
210+
source: std::io::Error,
211+
},
212+
}

packages/perseus-cli/src/init.rs

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use crate::cmd::run_cmd_directly;
2+
use crate::errors::*;
3+
use crate::parse::{InitOpts, NewOpts};
4+
use std::fs;
5+
use 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.
9+
fn 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.replace("%name", name);
19+
fs::write(filename, contents).map_err(|err| InitError::CreateInitFileFailed {
20+
source: err,
21+
filename: filename_str.to_string(),
22+
})?;
23+
}
24+
Ok(())
25+
}
26+
27+
/// Initializes a new Perseus project in the given directory, based on either
28+
/// the default template or one from a given URL.
29+
pub fn init(dir: PathBuf, opts: InitOpts) -> Result<i32, InitError> {
30+
// Create the basic directory structure (this will create both `src/` and
31+
// `src/templates/`)
32+
fs::create_dir_all(dir.join("src/templates"))
33+
.map_err(|err| InitError::CreateDirStructureFailed { source: err })?;
34+
// Now create each file
35+
create_file_if_not_present(&dir.join("Cargo.toml"), DFLT_INIT_CARGO_TOML, &opts.name)?;
36+
create_file_if_not_present(&dir.join(".gitignore"), DFLT_INIT_GITIGNORE, &opts.name)?;
37+
create_file_if_not_present(&dir.join("src/lib.rs"), DFLT_INIT_LIB_RS, &opts.name)?;
38+
create_file_if_not_present(
39+
&dir.join("src/templates/mod.rs"),
40+
DFLT_INIT_MOD_RS,
41+
&opts.name,
42+
)?;
43+
create_file_if_not_present(
44+
&dir.join("src/templates/index.rs"),
45+
DFLT_INIT_INDEX_RS,
46+
&opts.name,
47+
)?;
48+
49+
// And now tell the user about some stuff
50+
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://arctic-hen7.github.io/perseus/en-US/docs/).");
51+
52+
Ok(0)
53+
}
54+
/// Initializes a new Perseus project in a new directory that's a child of the
55+
/// current one.
56+
// The `dir` here is the current dir, the name of the one to create is in `opts`
57+
pub fn new(dir: PathBuf, opts: NewOpts) -> Result<i32, NewError> {
58+
// Create the directory (if the user provided a name explicitly, use that,
59+
// otherwise use the project name)
60+
let target = dir.join(opts.dir.unwrap_or(opts.name.clone()));
61+
62+
// Check if we're using the default template or one from a URL
63+
if let Some(url) = opts.template {
64+
let url_parts = url.split('@').collect::<Vec<&str>>();
65+
let engine_url = url_parts[0];
66+
// A custom branch can be specified after a `@`, or we'll use `stable`
67+
let cmd = format!(
68+
// We'll only clone the production branch, and only the top level, we don't need the
69+
// whole shebang
70+
"{} clone --single-branch {branch} --depth 1 {repo} {output}",
71+
std::env::var("PERSEUS_GIT_PATH").unwrap_or_else(|_| "git".to_string()),
72+
branch = if let Some(branch) = url_parts.get(1) {
73+
format!("--branch {}", branch)
74+
} else {
75+
String::new()
76+
},
77+
repo = engine_url,
78+
output = target.to_string_lossy()
79+
);
80+
println!(
81+
"Fetching custom initialization template with command: '{}'.",
82+
&cmd
83+
);
84+
// Tell the user what command we're running so that they can debug it
85+
let exit_code = run_cmd_directly(
86+
cmd,
87+
&dir, // We'll run this in the current directory and output into `.perseus/`
88+
vec![],
89+
)
90+
.map_err(|err| NewError::GetCustomInitFailed { source: err })?;
91+
if exit_code != 0 {
92+
return Err(NewError::GetCustomInitNonZeroExitCode { exit_code });
93+
}
94+
// Now delete the Git internals
95+
let git_target = target.join(".git");
96+
if let Err(err) = fs::remove_dir_all(&git_target) {
97+
return Err(NewError::RemoveCustomInitGitFailed {
98+
target_dir: git_target.to_str().map(|s| s.to_string()),
99+
source: err,
100+
});
101+
}
102+
Ok(0)
103+
} else {
104+
fs::create_dir(&target).map_err(|err| NewError::CreateProjectDirFailed { source: err })?;
105+
// Now initialize in there
106+
let exit_code = init(target, InitOpts { name: opts.name })?;
107+
Ok(exit_code)
108+
}
109+
}
110+
111+
// --- BELOW ARE THE RAW FILES FOR DEFAULT INTIALIZATION ---
112+
// The token `%name` in all of these will be replaced with the given project
113+
// name NOTE: These must be updated for breaking changes
114+
115+
static DFLT_INIT_CARGO_TOML: &str = r#"[package]
116+
name = "%name"
117+
version = "0.1.0"
118+
edition = "2021"
119+
120+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
121+
122+
# Dependencies for the engine and the browser go here
123+
[dependencies]
124+
perseus = { version = "=0.4.0-beta.3", features = [ "hydrate" ] }
125+
sycamore = "=0.8.0-beta.7"
126+
serde = { version = "1", features = [ "derive" ] }
127+
serde_json = "1"
128+
129+
# Engine-only dependencies go here
130+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
131+
tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
132+
perseus-warp = { version = "=0.4.0-beta.3", features = [ "dflt-server" ] }
133+
134+
# Browser-only dependencies go here
135+
[target.'cfg(target_arch = "wasm32")'.dependencies]
136+
wasm-bindgen = "0.2"
137+
138+
# We'll use `src/lib.rs` as both a binary *and* a library at the same time (which we need to tell Cargo explicitly)
139+
[lib]
140+
name = "lib"
141+
path = "src/lib.rs"
142+
crate-type = [ "cdylib", "rlib" ]
143+
144+
[[bin]]
145+
name = "%name"
146+
path = "src/lib.rs"
147+
148+
# This section adds some optimizations to make your app nice and speedy in production
149+
[package.metadata.wasm-pack.profile.release]
150+
wasm-opt = [ "-Oz" ]"#;
151+
static DFLT_INIT_GITIGNORE: &str = r#"dist/
152+
target_wasm/
153+
target_engine/"#;
154+
static DFLT_INIT_LIB_RS: &str = r#"mod templates;
155+
156+
use perseus::{Html, PerseusApp};
157+
158+
#[perseus::main(perseus_warp::dflt_server)]
159+
pub fn main<G: Html>() -> PerseusApp<G> {
160+
PerseusApp::new()
161+
.template(crate::templates::index::get_template)
162+
}"#;
163+
static DFLT_INIT_MOD_RS: &str = r#"pub mod index;"#;
164+
static DFLT_INIT_INDEX_RS: &str = r#"use perseus::Template;
165+
use sycamore::prelude::{view, Html, Scope, SsrNode, View};
166+
167+
#[perseus::template_rx]
168+
pub fn index_page<G: Html>(cx: Scope) -> View<G> {
169+
view! { cx,
170+
// Don't worry, there are much better ways of styling in Perseus!
171+
div(style = "display: flex; flex-direction: column; justify-content: center; align-items: center; height: 95vh;") {
172+
h1 { "Welome to Perseus!" }
173+
p {
174+
"This is just an example app. Try changing some code inside "
175+
code { "src/templates/index.rs" }
176+
" and you'll be able to see the results here!"
177+
}
178+
}
179+
}
180+
}
181+
182+
#[perseus::head]
183+
pub fn head(cx: Scope) -> View<SsrNode> {
184+
view! { cx,
185+
title { "Welcome to Perseus!" }
186+
}
187+
}
188+
189+
pub fn get_template<G: Html>() -> Template<G> {
190+
Template::new("index").template(index_page).head(head)
191+
}"#;

packages/perseus-cli/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod deploy;
2121
pub mod errors;
2222
mod export;
2323
mod export_error_page;
24+
mod init;
2425
/// Parsing utilities for arguments.
2526
pub mod parse;
2627
mod prepare;
@@ -41,6 +42,7 @@ pub use build::build;
4142
pub use deploy::deploy;
4243
pub use export::export;
4344
pub use export_error_page::export_error_page;
45+
pub use init::{init, new};
4446
pub use prepare::check_env;
4547
pub use reload_server::{order_reload, run_reload_server};
4648
pub use serve::serve;

packages/perseus-cli/src/parse.rs

+27
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub enum Subcommand {
3232
/// to see more detailed logs
3333
#[clap(subcommand)]
3434
Snoop(SnoopSubcommand),
35+
New(NewOpts),
36+
Init(InitOpts),
3537
}
3638
/// Builds your app
3739
#[derive(Parser)]
@@ -123,6 +125,31 @@ pub struct TinkerOpts {
123125
#[clap(long)]
124126
pub no_clean: bool,
125127
}
128+
/// Creates a new Perseus project in a directory of the given name, which will
129+
/// be created in the current path
130+
#[derive(Parser)]
131+
pub struct NewOpts {
132+
/// The name of the new project, which will also be used for the directory
133+
#[clap(value_parser)]
134+
pub name: String,
135+
/// An optional custom URL to a Git repository to be used as a custom
136+
/// template (note that custom templates will not respect your project's
137+
/// name). This can be followed with `@branch` to fetch from `branch`
138+
/// rather than the default
139+
#[clap(short, long)]
140+
pub template: Option<String>,
141+
/// The path to a custom directory to create (if this is not provided, the
142+
/// project name will be used by default)
143+
#[clap(long)]
144+
pub dir: Option<String>,
145+
}
146+
/// Intializes a new Perseus project in the current directory
147+
#[derive(Parser)]
148+
pub struct InitOpts {
149+
/// The name of the new project
150+
#[clap(value_parser)]
151+
pub name: String,
152+
}
126153

127154
#[derive(Parser)]
128155
pub enum SnoopSubcommand {

0 commit comments

Comments
 (0)