diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ff751c..a01c48f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased Changes for 0.6.x * Added `--watch` argument to `rojo build`. ([#284](https://github.com/rojo-rbx/rojo/pull/284)) * Added dark theme support to plugin. ([#241](https://github.com/rojo-rbx/rojo/issues/241)) +* Added a revamped `rojo init` command, which will now create more complete projects. * Added the `rojo doc` command, which opens Rojo's documentation in your browser. * Simplified filesystem access code dramatically. * Improved error reporting and logging across the board. diff --git a/assets/default-model.lua b/assets/default-model.lua new file mode 100644 index 00000000..41faf217 --- /dev/null +++ b/assets/default-model.lua @@ -0,0 +1,5 @@ +return { + hello = function() + print("Hello world, from {project_name}!") + end, +} \ No newline at end of file diff --git a/assets/model.project.json b/assets/model.project.json new file mode 100644 index 00000000..e7bb5891 --- /dev/null +++ b/assets/model.project.json @@ -0,0 +1,6 @@ +{ + "name": "{project_name}", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/assets/place.project.json b/assets/place.project.json index 4cf32314..fc3f1ec3 100644 --- a/assets/place.project.json +++ b/assets/place.project.json @@ -1,39 +1,36 @@ { - "name": "[placeholder]", + "name": "{project_name}", "tree": { "$className": "DataModel", - "HttpService": { - "$className": "HttpService", - "$properties": { - "HttpEnabled": true - } - }, - "Lighting": { - "$className": "Lighting", - "$properties": { - "Ambient": [ - 0, - 0, - 0 - ], - "Brightness": 2, - "GlobalShadows": true, - "Outlines": false, - "Technology": "Voxel" - } - }, + "ReplicatedStorage": { "$className": "ReplicatedStorage", - "Source": { - "$path": "src" + + "Common": { + "$path": "src/common" } }, - "SoundService": { - "$className": "SoundService", - "$properties": { - "RespectFilteringEnabled": true + + "ServerScriptService": { + "$className": "ServerScriptService", + + "Server": { + "$path": "src/server" } }, + + "StarterPlayer": { + "$className": "StarterPlayer", + + "StarterPlayerScripts": { + "$className": "StarterPlayerScripts", + + "Client": { + "$path": "src/client" + } + } + }, + "Workspace": { "$className": "Workspace", "$properties": { @@ -61,6 +58,32 @@ ] } } + }, + "Lighting": { + "$className": "Lighting", + "$properties": { + "Ambient": [ + 0, + 0, + 0 + ], + "Brightness": 2, + "GlobalShadows": true, + "Outlines": false, + "Technology": "Voxel" + } + }, + "SoundService": { + "$className": "SoundService", + "$properties": { + "RespectFilteringEnabled": true + } + }, + "HttpService": { + "$className": "HttpService", + "$properties": { + "HttpEnabled": true + } } } } \ No newline at end of file diff --git a/src/cli/init.rs b/src/cli/init.rs index c27360b1..5fbbdafa 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,17 +1,156 @@ +use std::{ + fs::{self, OpenOptions}, + io::{self, Write}, + path::Path, +}; + use snafu::Snafu; -use crate::cli::InitCommand; +use crate::cli::{InitCommand, InitKind}; + +static DEFAULT_PLACE_PROJECT: &str = include_str!("../../assets/place.project.json"); + +static DEFAULT_MODEL_PROJECT: &str = include_str!("../../assets/model.project.json"); +static DEFAULT_MODEL_INIT: &str = include_str!("../../assets/default-model.lua"); #[derive(Debug, Snafu)] pub struct InitError(Error); #[derive(Debug, Snafu)] -enum Error {} +enum Error { + #[snafu(display("A project file named default.project.json already exists in this folder"))] + AlreadyExists, + + #[snafu(display("I/O error"))] + Io { source: io::Error }, +} + +impl From for Error { + fn from(source: io::Error) -> Self { + Self::Io { source } + } +} pub fn init(options: InitCommand) -> Result<(), InitError> { Ok(init_inner(options)?) } -fn init_inner(_options: InitCommand) -> Result<(), Error> { - unimplemented!("init command"); +fn init_inner(options: InitCommand) -> Result<(), Error> { + let base_path = options.absolute_path(); + let canonical = fs::canonicalize(&base_path)?; + let project_name = canonical + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("new-project"); + + let project_params = ProjectParams { + name: project_name.to_owned(), + }; + + match options.kind { + InitKind::Place => init_place(&base_path, project_params), + InitKind::Model => init_model(&base_path, project_params), + } +} + +fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> { + let project_file = project_params.render_template(DEFAULT_PLACE_PROJECT); + try_create_project(base_path, &project_file)?; + + let src = base_path.join("src"); + fs::create_dir_all(&src)?; + + let src_common = src.join("common"); + fs::create_dir_all(src.join(&src_common))?; + + let src_server = src.join("server"); + fs::create_dir_all(src.join(&src_server))?; + + let src_client = src.join("client"); + fs::create_dir_all(src.join(&src_client))?; + + write_if_not_exists( + &src_common.join("Hello.lua"), + "return function()\n\tprint(\"Hello, world!\")\nend", + )?; + + write_if_not_exists( + &src_server.join("init.server.lua"), + "print(\"Hello world, from server!\")", + )?; + + write_if_not_exists( + &src_client.join("init.client.lua"), + "print(\"Hello world, from client!\")", + )?; + + Ok(()) +} + +fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> { + let project_file = project_params.render_template(DEFAULT_MODEL_PROJECT); + try_create_project(base_path, &project_file)?; + + let src = base_path.join("src"); + fs::create_dir_all(&src)?; + + let init = project_params.render_template(DEFAULT_MODEL_INIT); + write_if_not_exists(&src.join("init.lua"), &init)?; + + Ok(()) +} + +/// Contains parameters used in templates to create a project. +struct ProjectParams { + name: String, +} + +impl ProjectParams { + /// Render a template by replacing variables with project parameters. + fn render_template(&self, template: &str) -> String { + template.replace("{project_name}", &self.name) + } +} + +/// Write a file if it does not exist yet, otherwise, leave it alone. +fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), Error> { + let file_res = OpenOptions::new().write(true).create_new(true).open(path); + + let mut file = match file_res { + Ok(file) => file, + Err(err) => { + return match err.kind() { + io::ErrorKind::AlreadyExists => return Ok(()), + _ => Err(err.into()), + } + } + }; + + file.write_all(contents.as_bytes())?; + + Ok(()) +} + +/// Try to create a project file and fail if it already exists. +fn try_create_project(base_path: &Path, contents: &str) -> Result<(), Error> { + let project_path = base_path.join("default.project.json"); + + let file_res = OpenOptions::new() + .write(true) + .create_new(true) + .open(project_path); + + let mut file = match file_res { + Ok(file) => file, + Err(err) => { + return match err.kind() { + io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists), + _ => Err(err.into()), + } + } + }; + + file.write_all(contents.as_bytes())?; + + Ok(()) }