diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e100e33..e1b2998 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,7 @@ repos: - id: check-case-conflict - id: check-executables-have-shebangs - id: check-toml + exclude: ^cot-cli/src/project_template/ - id: detect-private-key - id: end-of-file-fixer - id: mixed-line-ending @@ -27,3 +28,4 @@ repos: rev: v0.1.0 hooks: - id: fmt + exclude: ^cot-cli/src/project_template/ diff --git a/Cargo.lock b/Cargo.lock index 242fc5b..d20d225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,11 +492,13 @@ dependencies = [ name = "cot-cli" version = "0.1.0" dependencies = [ + "anstyle", "anyhow", "cargo_toml", "chrono", "clap", "clap-verbosity-flag", + "convert_case", "cot", "cot_codegen", "darling", diff --git a/Cargo.toml b/Cargo.toml index fce21b5..bb3c709 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ resolver = "2" [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" +version = "0.1.0" [workspace.lints.clippy] all = "deny" pedantic = "warn" [workspace.dependencies] +anstyle = "1" anyhow = "1.0.95" async-stream = "0.3" async-trait = "0.1" @@ -33,13 +35,13 @@ chrono = { version = "0.4", default-features = false } clap = "4" clap-verbosity-flag = { version = "3", default-features = false } convert_case = "0.6" +cot = { path = "cot" } +cot_codegen = { path = "cot-codegen" } +cot_macros = { path = "cot-macros" } darling = "0.20" derive_builder = "0.20" derive_more = "1" fake = "3.1" -cot = { path = "cot" } -cot_codegen = { path = "cot-codegen" } -cot_macros = { path = "cot-macros" } form_urlencoded = "1" futures = { version = "0.3", default-features = false } futures-core = { version = "0.3", default-features = false } diff --git a/cot-cli/Cargo.toml b/cot-cli/Cargo.toml index 87d3e3a..147e583 100644 --- a/cot-cli/Cargo.toml +++ b/cot-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cot-cli" -version = "0.1.0" +version.workspace = true edition.workspace = true license.workspace = true description = "Modern web framework focused on speed and ease of use - CLI tool." @@ -13,11 +13,13 @@ path = "src/main.rs" workspace = true [dependencies] +anstyle.workspace = true anyhow.workspace = true cargo_toml.workspace = true chrono.workspace = true clap = { workspace = true, features = ["derive", "env"] } clap-verbosity-flag = { workspace = true, features = ["tracing"] } +convert_case.workspace = true darling.workspace = true cot.workspace = true cot_codegen = { workspace = true, features = ["symbol-resolver"] } diff --git a/cot-cli/src/lib.rs b/cot-cli/src/lib.rs index 4b4a2e1..4717d59 100644 --- a/cot-cli/src/lib.rs +++ b/cot-cli/src/lib.rs @@ -1,2 +1,3 @@ pub mod migration_generator; +pub mod new_project; mod utils; diff --git a/cot-cli/src/main.rs b/cot-cli/src/main.rs index 3e1f8e6..179b35a 100644 --- a/cot-cli/src/main.rs +++ b/cot-cli/src/main.rs @@ -1,4 +1,5 @@ mod migration_generator; +mod new_project; mod utils; use std::path::PathBuf; @@ -9,6 +10,7 @@ use clap_verbosity_flag::Verbosity; use tracing_subscriber::util::SubscriberInitExt; use crate::migration_generator::{make_migrations, MigrationGeneratorOptions}; +use crate::new_project::{new_project, CotSource}; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] @@ -21,6 +23,18 @@ struct Cli { #[derive(Debug, Subcommand)] enum Commands { + /// Create a new Cot project + New { + /// Path to the directory to create the new project in + path: PathBuf, + /// Set the resulting crate name (defaults to the directory name) + #[arg(long)] + name: Option, + /// Use the latest `cot` version from git instead of a published crate + #[arg(long)] + cot_git: bool, + }, + /// Generate migrations for a Cot project MakeMigrations { /// Path to the crate directory to generate migrations for (default: /// current directory) @@ -47,6 +61,29 @@ fn main() -> anyhow::Result<()> { .init(); match cli.command { + Commands::New { + path, + name, + cot_git, + } => { + let project_name = match name { + None => { + let dir_name = path + .file_name() + .with_context(|| format!("file name not present: {}", path.display()))?; + dir_name.to_string_lossy().into_owned() + } + Some(name) => name, + }; + + let cot_source = if cot_git { + CotSource::Git + } else { + CotSource::PublishedCrate + }; + new_project(&path, &project_name, cot_source) + .with_context(|| "unable to create project")?; + } Commands::MakeMigrations { path, app_name, diff --git a/cot-cli/src/new_project.rs b/cot-cli/src/new_project.rs new file mode 100644 index 0000000..90c20bd --- /dev/null +++ b/cot-cli/src/new_project.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use convert_case::{Case, Casing}; +use tracing::trace; + +use crate::utils::print_status_msg; + +macro_rules! project_file { + ($name:literal) => { + ($name, include_str!(concat!("project_template/", $name))) + }; +} + +const PROJECT_FILES: [(&'static str, &'static str); 6] = [ + project_file!("Cargo.toml"), + project_file!("bacon.toml"), + project_file!(".gitignore"), + project_file!("src/main.rs"), + project_file!("static/css/main.css"), + project_file!("templates/index.html"), +]; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum CotSource { + Git, + PublishedCrate, +} + +pub fn new_project( + path: &PathBuf, + project_name: &str, + cot_source: CotSource, +) -> anyhow::Result<()> { + print_status_msg("Creating", &format!("Cot project `{project_name}`")); + + if path.exists() { + anyhow::bail!("destination `{}` already exists", path.display()); + } + + let app_name = format!("{}App", project_name.to_case(Case::Pascal)); + let cot_source = match cot_source { + CotSource::Git => { + "package = \"cot\", git = \"https://github.com/cot-rs/cot.git\"".to_owned() + } + CotSource::PublishedCrate => format!("version = \"{}\"", env!("CARGO_PKG_VERSION")), + }; + + for (file_name, content) in PROJECT_FILES { + let file_path = path.join(file_name); + trace!("Writing file: {:?}", file_path); + + std::fs::create_dir_all( + file_path + .parent() + .expect("joined path should always have a parent"), + )?; + + std::fs::write( + file_path, + content + .replace("{{ project_name }}", project_name) + .replace("{{ app_name }}", &app_name) + .replace("{{ cot_source }}", &cot_source), + )?; + } + + Ok(()) +} diff --git a/cot-cli/src/project_template/.gitignore b/cot-cli/src/project_template/.gitignore new file mode 100644 index 0000000..a611abc --- /dev/null +++ b/cot-cli/src/project_template/.gitignore @@ -0,0 +1,15 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Test databases +*.db +*.sqlite3 +*.sqlite3-journal diff --git a/cot-cli/src/project_template/Cargo.toml b/cot-cli/src/project_template/Cargo.toml new file mode 100644 index 0000000..18f9b88 --- /dev/null +++ b/cot-cli/src/project_template/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "{{ project_name }}" +version = "0.1.0" +edition = "2021" + +[dependencies] +cot = { {{ cot_source }}, features = ["full"] } +rinja = "0.3" diff --git a/cot-cli/src/project_template/bacon.toml b/cot-cli/src/project_template/bacon.toml new file mode 100644 index 0000000..fbbca7d --- /dev/null +++ b/cot-cli/src/project_template/bacon.toml @@ -0,0 +1,5 @@ +[jobs.serve] +command = ["cargo", "run"] +background = false +on_change_strategy = "kill_then_restart" +watch = ["templates", "static"] diff --git a/cot-cli/src/project_template/src/main.rs b/cot-cli/src/project_template/src/main.rs new file mode 100644 index 0000000..dc19d1b --- /dev/null +++ b/cot-cli/src/project_template/src/main.rs @@ -0,0 +1,49 @@ +use cot::bytes::Bytes; +use cot::config::ProjectConfig; +use cot::middleware::LiveReloadMiddleware; +use cot::request::Request; +use cot::response::{Response, ResponseExt}; +use cot::router::{Route, Router}; +use cot::static_files::StaticFilesMiddleware; +use cot::{static_files, Body, CotApp, CotProject, StatusCode}; +use rinja::Template; + +#[derive(Debug, Template)] +#[template(path = "index.html")] +struct IndexTemplate {} + +async fn index(request: Request) -> cot::Result { + let index_template = IndexTemplate {}; + let rendered = index_template.render()?; + + Ok(Response::new_html(StatusCode::OK, Body::fixed(rendered))) +} + +struct {{ app_name }}; + +impl CotApp for {{ app_name }} { + fn name(&self) -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn router(&self) -> Router { + Router::with_urls([Route::with_handler_and_name("/", index, "index")]) + } + + fn static_files(&self) -> Vec<(String, Bytes)> { + static_files!("css/main.css") + } +} + +#[cot::main] +async fn main() -> cot::Result { + let project = CotProject::builder() + .config(ProjectConfig::builder().build()) + .register_app_with_views({{ app_name }}, "") + .middleware_with_context(StaticFilesMiddleware::from_app_context) + .middleware(LiveReloadMiddleware::new()) + .build() + .await?; + + Ok(project) +} diff --git a/cot-cli/src/project_template/static/css/main.css b/cot-cli/src/project_template/static/css/main.css new file mode 100644 index 0000000..054c03f --- /dev/null +++ b/cot-cli/src/project_template/static/css/main.css @@ -0,0 +1,39 @@ +* { + box-sizing: border-box; +} + +html, body { + height: 100%; + margin: 0; + padding: 0; +} + +body { + text-align: center; + background-image: linear-gradient(to right bottom, #0f172a, #2d3d57); + font-family: sans-serif; + color: #fff; +} + +main { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +} + +ul { + list-style: none; +} + +h1 { + margin: 0; +} + +a { + color: #f97316; +} + +a:hover, a:focus { + color: #ec9355; +} diff --git a/cot-cli/src/project_template/templates/index.html b/cot-cli/src/project_template/templates/index.html new file mode 100644 index 0000000..bf5ddd3 --- /dev/null +++ b/cot-cli/src/project_template/templates/index.html @@ -0,0 +1,24 @@ + + + + + + Welcome to Cot! + + + + + +
+

You have successfully installed Cot!

+ +

Now, you can start building your web application using Rust and Cot.

+ + +
+ + + diff --git a/cot-cli/src/utils.rs b/cot-cli/src/utils.rs index d35c4ac..dee7448 100644 --- a/cot-cli/src/utils.rs +++ b/cot-cli/src/utils.rs @@ -1,5 +1,12 @@ use std::path::{Path, PathBuf}; +pub(crate) fn print_status_msg(status: &str, message: &str) { + let status_style = anstyle::Style::new() | anstyle::Effects::BOLD; + let status_style = status_style.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))); + + eprintln!("{status_style}{status:>12}{status_style:#} {message}"); +} + pub fn find_cargo_toml(starting_dir: &Path) -> Option { let mut current_dir = starting_dir; diff --git a/cot-cli/tests/new_project.rs b/cot-cli/tests/new_project.rs new file mode 100644 index 0000000..ffcf79c --- /dev/null +++ b/cot-cli/tests/new_project.rs @@ -0,0 +1,44 @@ +use std::env; +use std::path::Path; +use std::process::Command; + +use cot_cli::new_project::{new_project, CotSource}; + +#[test] +#[cfg_attr(miri, ignore)] // unsupported operation: extern static `pidfd_spawnp` is not supported by Miri +fn new_project_compile_test() { + let temp_dir = tempfile::tempdir().unwrap(); + let project_path = temp_dir.path().join("my_project"); + + new_project(&project_path, &"my_project", CotSource::Git).unwrap(); + + let output = cargo(&project_path) + .arg("build") + .arg("--quiet") + .output() + .unwrap(); + + assert!( + output.status.success(), + "status: {}, stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); +} + +fn raw_cargo() -> Command { + match env::var_os("CARGO") { + Some(cargo) => Command::new(cargo), + None => Command::new("cargo"), + } +} + +fn cargo(project_path: &Path) -> Command { + let mut cmd = raw_cargo(); + cmd.current_dir(project_path); + cmd.env("CARGO_TARGET_DIR", project_path.join("target")); + cmd.env_remove("RUSTFLAGS"); + cmd.env("CARGO_INCREMENTAL", "0"); + + cmd +} diff --git a/cot/Cargo.toml b/cot/Cargo.toml index b6e4be9..93795b5 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cot" -version = "0.1.0" +version.workspace = true edition.workspace = true license.workspace = true description = "Modern web framework focused on speed and ease of use."