Compare commits

...

19 Commits

Author SHA1 Message Date
Lucien Greathouse
bd13047b58 Release 0.6.0-alpha.3 2020-03-13 23:50:54 -07:00
Lucien Greathouse
1a83789c01 Update Changelog 2020-03-13 20:50:03 -07:00
Lucien Greathouse
1cbe272e19 Fix malformed meta files causing panics.
Fixes #280.
2020-03-13 20:38:06 -07:00
Lucien Greathouse
6de74b41b3 Update snapshot error type and handle serde_json errors in JSON models 2020-03-13 20:31:31 -07:00
Lucien Greathouse
b0fc9ee507 Rename 'src/common' in default place template to 'src/shared' 2020-03-13 20:25:50 -07:00
Lucien Greathouse
a95ffe1d31 Add snapshot error handling to ChangeProcessor 2020-03-13 20:24:14 -07:00
Lucien Greathouse
4119a510f5 Fix broken error source tracking 2020-03-13 20:12:42 -07:00
Lucien Greathouse
cfa7f03815 Add help link to default model project 2020-03-13 20:11:08 -07:00
Lucien Greathouse
9b4c89820d Fix extra Git output in rojo init and update place template 2020-03-13 20:10:31 -07:00
Lucien Greathouse
fe874720aa Upgrade rojo init to create README.md and create Git repo 2020-03-13 20:00:48 -07:00
Lucien Greathouse
f7c0f33eb5 Add brand new rojo init command 2020-03-13 19:33:43 -07:00
Lucien Greathouse
c1bf9d9dfc Update duplicate env_logger dep 2020-03-12 19:39:27 -07:00
Lucien Greathouse
255bf439d3 Add 'rojo doc' command 2020-03-12 16:02:19 -07:00
Lucien Greathouse
2a31937b81 Use CARGO_PKG_REPOSITORY instead of hard-coded URL 2020-03-12 15:48:30 -07:00
Lucien Greathouse
eb8964e1d1 Improve panic messaging and process behavior.
- Replaced main() to use custom panic hook
- Updated log formatting
- Switched to panic=abort, since we don't need to unwind now.
- Process will now abort if any thread panics.
2020-03-12 15:42:56 -07:00
Lucien Greathouse
fe0ca280a1 Update disabled CSV middleware test 2020-03-10 23:52:55 -07:00
Lucien Greathouse
e8e3b7b985 plugin: Fix dark theme when resized to be wide 2020-03-10 20:34:12 -07:00
Lucien Greathouse
c437507442 Update changelogs 2020-03-10 18:15:06 -07:00
Lucien Greathouse
ca151b434e Update memofs dependency 2020-03-10 18:13:34 -07:00
28 changed files with 613 additions and 206 deletions

View File

@@ -1,8 +1,18 @@
# Rojo Changelog
## Unreleased Changes for 0.6.x
## [0.6.0 Alpha 3](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.3) (March 13, 2020)
* 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.
* Fixed many crashes from malformed projects and filesystem edge cases in `rojo serve`.
* Simplified filesystem access code dramatically.
* Improved error reporting and logging across the board.
* Log messages have a less noisy prefix.
* Any thread panicking now causes Rojo to abort instead of existing as a zombie.
* Errors now have a list of causes, helping make many errors more clear.
## [0.6.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.2) (March 6, 2020)
* Fixed `rojo upload` command always uploading models.

32
Cargo.lock generated
View File

@@ -195,7 +195,7 @@ dependencies = [
name = "clibrojo"
version = "0.1.0"
dependencies = [
"rojo 0.6.0-alpha.2",
"rojo 0.6.0-alpha.3",
]
[[package]]
@@ -434,18 +434,6 @@ dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "env_logger"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "env_logger"
version = "0.7.1"
@@ -1073,6 +1061,14 @@ name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "opener"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl"
version = "0.10.28"
@@ -1663,8 +1659,9 @@ dependencies = [
[[package]]
name = "rojo"
version = "0.6.0-alpha.2"
version = "0.6.0-alpha.3"
dependencies = [
"backtrace 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"csv 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1680,6 +1677,7 @@ dependencies = [
"maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"memofs 0.1.0",
"notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"opener 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"paste 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rbx_binary 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1716,13 +1714,13 @@ dependencies = [
name = "rojo-test"
version = "0.1.0"
dependencies = [
"env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"insta 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"paste 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"rbx_dom_weak 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)",
"rojo 0.6.0-alpha.2",
"rojo 0.6.0-alpha.3",
"rojo-insta-ext 0.1.0",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2574,7 +2572,6 @@ dependencies = [
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
"checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
"checksum encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28"
"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3"
"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
"checksum error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd"
"checksum failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b"
@@ -2646,6 +2643,7 @@ dependencies = [
"checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
"checksum oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405"
"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
"checksum opener 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13117407ca9d0caf3a0e74f97b490a7e64c0ae3aa90a8b7085544d0c37b6f3ae"
"checksum openssl 0.10.28 (registry+https://github.com/rust-lang/crates.io-index)" = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52"
"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
"checksum openssl-sys 0.9.54 (registry+https://github.com/rust-lang/crates.io-index)" = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986"

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "0.6.0-alpha.2"
version = "0.6.0-alpha.3"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -15,6 +15,12 @@ exclude = [
"/test-projects/**",
]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[features]
default = []
@@ -55,6 +61,9 @@ name = "build"
harness = false
[dependencies]
memofs = { version = "0.1.0", path = "memofs" }
backtrace = "0.3"
crossbeam-channel = "0.4.0"
csv = "1.1.1"
env_logger = "0.7.1"
@@ -67,6 +76,7 @@ lazy_static = "1.4.0"
log = "0.4.8"
maplit = "1.0.1"
notify = "4.0.14"
opener = "0.4.1"
rbx_binary = "0.5.0"
rbx_dom_weak = "1.10.1"
rbx_reflection = "3.3.408"
@@ -82,7 +92,6 @@ structopt = "0.3.5"
termcolor = "1.0.5"
tokio = "0.1.22"
uuid = { version = "0.8.1", features = ["v4", "serde"] }
memofs = { path = "memofs" }
[target.'cfg(windows)'.dependencies]
winreg = "0.6.2"

View File

@@ -0,0 +1,11 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build this library or plugin, use:
```bash
rojo build -o "{project_name}.rbxmx"
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

@@ -0,0 +1,6 @@
{
"name": "{project_name}",
"tree": {
"$path": "src"
}
}

View File

@@ -0,0 +1,3 @@
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock

View File

@@ -0,0 +1,5 @@
return {
hello = function()
print("Hello world, from {project_name}!")
end,
}

View File

@@ -0,0 +1,17 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build the place from scratch, use:
```bash
rojo build -o "{project_name}.rbxlx"
```
Next, open `{project_name}.rbxlx` in Roblox Studio and start the Rojo server:
```bash
rojo serve
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,6 @@
# Project place file
/{project_name}.rbxlx
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock

6
memofs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,6 @@
# memofs Changelog
## Unreleased Changes
## 0.1.0 (2020-03-10)
* Initial release

View File

@@ -17,7 +17,8 @@ function Panel:render()
return Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
BackgroundColor3 = theme.Background1,
BorderSizePixel = 1,
}, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
@@ -26,8 +27,7 @@ function Panel:render()
Body = e("Frame", {
Size = UDim2.new(0, 360, 1, -32),
BackgroundColor3 = theme.Background1,
BorderSizePixel = 0,
BackgroundTransparency = 1,
}, self.props[Roact.Children]),
Footer = e(RojoFooter),

View File

@@ -5,7 +5,7 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", {
isDevBuild = isDevBuild,
codename = "Epiphany",
version = {0, 6, 0, "-alpha.2"},
version = {0, 6, 0, "-alpha.3"},
expectedServerVersionString = "0.6.0 or newer",
protocolVersion = 3,
defaultHost = "localhost",

View File

@@ -11,7 +11,7 @@ default = []
unstable_glob_ignore_paths = []
[dependencies]
env_logger = "0.6.2"
env_logger = "0.7.1"
insta = { version = "0.13.1", features = ["redactions"] }
log = "0.4.8"
paste = "0.1.5"

View File

@@ -1,65 +1,93 @@
use std::{error::Error, panic, process};
use std::{env, error::Error, panic, process};
use log::error;
use backtrace::Backtrace;
use structopt::StructOpt;
use librojo::cli::{self, Options, Subcommand};
fn main() {
let options = Options::from_args();
{
let log_filter = match options.verbosity {
0 => "warn",
1 => "warn,librojo=info",
2 => "warn,librojo=trace",
_ => "trace",
};
let log_env = env_logger::Env::default().default_filter_or(log_filter);
env_logger::Builder::from_env(log_env)
.format_timestamp(None)
.init();
}
let panic_result = panic::catch_unwind(|| {
if let Err(err) = run(options.subcommand) {
log::error!("{}", err);
process::exit(1);
}
});
if let Err(error) = panic_result {
let message = match error.downcast_ref::<&str>() {
Some(message) => message.to_string(),
None => match error.downcast_ref::<String>() {
Some(message) => message.clone(),
None => "<no message>".to_string(),
},
};
show_crash_message(&message);
process::exit(1);
}
}
fn run(subcommand: Subcommand) -> Result<(), Box<dyn Error>> {
match subcommand {
Subcommand::Init(init_options) => cli::init(init_options)?,
Subcommand::Serve(serve_options) => cli::serve(serve_options)?,
Subcommand::Build(build_options) => cli::build(build_options)?,
Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
Subcommand::Doc => cli::doc()?,
}
Ok(())
}
fn show_crash_message(message: &str) {
error!("Rojo crashed!");
error!("This is a bug in Rojo.");
error!("");
error!("Please consider filing a bug: https://github.com/rojo-rbx/rojo/issues");
error!("");
error!("Details: {}", message);
fn main() {
panic::set_hook(Box::new(|panic_info| {
// PanicInfo's payload is usually a &'static str or String.
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload
let message = match panic_info.payload().downcast_ref::<&str>() {
Some(message) => message.to_string(),
None => match panic_info.payload().downcast_ref::<String>() {
Some(message) => message.clone(),
None => "<no message>".to_string(),
},
};
log::error!("Rojo crashed!");
log::error!("This is probably a Rojo bug.");
log::error!("");
log::error!(
"Please consider filing an issue: {}/issues",
env!("CARGO_PKG_REPOSITORY")
);
log::error!("");
log::error!("Details: {}", message);
if let Some(location) = panic_info.location() {
log::error!("in file {} on line {}", location.file(), location.line());
}
// When using the backtrace crate, we need to check the RUST_BACKTRACE
// environment variable ourselves. Once we switch to the (currently
// unstable) std::backtrace module, we won't need to do this anymore.
let should_backtrace = env::var("RUST_BACKTRACE")
.map(|var| var == "1")
.unwrap_or(false);
if should_backtrace {
eprintln!("{:?}", Backtrace::new());
} else {
eprintln!(
"note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace."
);
}
process::exit(1);
}));
let options = Options::from_args();
let log_filter = match options.verbosity {
0 => "warn",
1 => "warn,librojo=info",
2 => "warn,librojo=trace",
_ => "trace",
};
let log_env = env_logger::Env::default().default_filter_or(log_filter);
env_logger::Builder::from_env(log_env)
.format_module_path(false)
.format_timestamp(None)
// Indent following lines equal to the log level label, like `[ERROR] `
.format_indent(Some(8))
.init();
if let Err(err) = run(options.subcommand) {
log::error!("{}", err);
let mut current_err: &dyn Error = &*err;
while let Some(source) = current_err.source() {
log::error!(" caused by {}", source);
current_err = &*source;
}
process::exit(1);
}
}

View File

@@ -9,6 +9,7 @@ use memofs::{IoResultExt, Vfs, VfsEvent};
use rbx_dom_weak::{RbxId, RbxValue};
use crate::{
error::ErrorDisplay,
message_queue::MessageQueue,
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstigatingSource, PatchSet, RojoTree,
@@ -294,9 +295,15 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
// starting at that path and use it as the source for
// our patch.
let snapshot = snapshot_from_vfs(&metadata.context, &vfs, &path)
.expect("snapshot failed")
.expect("snapshot did not return an instance");
let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) {
Ok(maybe_snapshot) => {
maybe_snapshot.expect("snapshot did not return an instance")
}
Err(err) => {
log::error!("Snapshot error: {}", ErrorDisplay(err));
return None;
}
};
let patch_set = compute_patch_set(&snapshot, &tree, id);
apply_patch_set(tree, patch_set)
@@ -320,15 +327,21 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
// there might be information associated with our instance from
// the project file, we snapshot the entire project node again.
let snapshot = snapshot_project_node(
let snapshot_result = snapshot_project_node(
&metadata.context,
&project_path,
instance_name,
project_node,
&vfs,
)
.expect("snapshot failed")
.expect("snapshot did not return an instance");
);
let snapshot = match snapshot_result {
Ok(maybe_snapshot) => maybe_snapshot.expect("snapshot did not return an instance"),
Err(err) => {
log::error!("Snapshot error: {}", ErrorDisplay(err));
return None;
}
};
let patch_set = compute_patch_set(&snapshot, &tree, id);
apply_patch_set(tree, patch_set)

26
src/cli/doc.rs Normal file
View File

@@ -0,0 +1,26 @@
use opener::{open, OpenError};
use snafu::Snafu;
#[derive(Debug, Snafu)]
pub struct DocError(Error);
#[derive(Debug, Snafu)]
enum Error {
Open { source: OpenError },
}
impl From<OpenError> for Error {
fn from(source: OpenError) -> Self {
Error::Open { source }
}
}
pub fn doc() -> Result<(), DocError> {
doc_inner()?;
Ok(())
}
fn doc_inner() -> Result<(), Error> {
open("https://rojo.space/docs")?;
Ok(())
}

View File

@@ -1,17 +1,229 @@
use std::{
fs::{self, OpenOptions},
io::{self, Write},
path::Path,
process::{Command, Stdio},
};
use snafu::Snafu;
use crate::cli::InitCommand;
use crate::cli::{InitCommand, InitKind};
static MODEL_PROJECT: &str =
include_str!("../../assets/default-model-project/default.project.json");
static MODEL_README: &str = include_str!("../../assets/default-model-project/README.md");
static MODEL_INIT: &str = include_str!("../../assets/default-model-project/src-init.lua");
static MODEL_GIT_IGNORE: &str = include_str!("../../assets/default-model-project/gitignore.txt");
static PLACE_PROJECT: &str =
include_str!("../../assets/default-place-project/default.project.json");
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
#[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("git init failed"))]
GitInit,
#[snafu(display("I/O error"))]
Io { source: io::Error },
}
impl From<io::Error> 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();
fs::create_dir_all(&base_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> {
eprintln!("Creating new place project '{}'", project_params.name);
let project_file = project_params.render_template(PLACE_PROJECT);
try_create_project(base_path, &project_file)?;
let readme = project_params.render_template(PLACE_README);
write_if_not_exists(&base_path.join("README.md"), &readme)?;
let src = base_path.join("src");
fs::create_dir_all(&src)?;
let src_shared = src.join("shared");
fs::create_dir_all(src.join(&src_shared))?;
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_shared.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!\")",
)?;
let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
eprintln!("Created project successfully.");
Ok(())
}
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> {
eprintln!("Creating new model project '{}'", project_params.name);
let project_file = project_params.render_template(MODEL_PROJECT);
try_create_project(base_path, &project_file)?;
let readme = project_params.render_template(MODEL_README);
write_if_not_exists(&base_path.join("README.md"), &readme)?;
let src = base_path.join("src");
fs::create_dir_all(&src)?;
let init = project_params.render_template(MODEL_INIT);
write_if_not_exists(&src.join("init.lua"), &init)?;
let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
eprintln!("Created project successfully.");
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)
.replace("{rojo_version}", env!("CARGO_PKG_VERSION"))
}
}
/// Attempt to initialize a Git repository if necessary, and create .gitignore.
fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), Error> {
if should_git_init(path) {
log::debug!("Initializing Git repository...");
let status = Command::new("git").arg("init").current_dir(path).status()?;
if !status.success() {
return Err(Error::GitInit);
}
}
write_if_not_exists(&path.join(".gitignore"), git_ignore)?;
Ok(())
}
/// Tells whether we should initialize a Git repository inside the given path.
///
/// Will return false if the user doesn't have Git installed or if the path is
/// already inside a Git repository.
fn should_git_init(path: &Path) -> bool {
let result = Command::new("git")
.args(&["rev-parse", "--is-inside-work-tree"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.current_dir(path)
.status();
match result {
// If the command ran, but returned a non-zero exit code, we are not in
// a Git repo and we should initialize one.
Ok(status) => !status.success(),
// If the command failed to run, we probably don't have Git installed.
Err(_) => false,
}
}
/// 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(())
}

View File

@@ -1,6 +1,7 @@
//! Defines Rojo's CLI through structopt types.
mod build;
mod doc;
mod init;
mod serve;
mod upload;
@@ -17,6 +18,7 @@ use std::{
use structopt::StructOpt;
pub use self::build::*;
pub use self::doc::*;
pub use self::init::*;
pub use self::serve::*;
pub use self::upload::*;
@@ -48,6 +50,9 @@ pub enum Subcommand {
/// Generates a place or model file out of the project and uploads it to Roblox.
Upload(UploadCommand),
/// Open Rojo's documentation in your browser.
Doc,
}
/// Initializes a new Rojo project.

18
src/error.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::{error::Error, fmt};
/// Wrapper type to print errors with source-chasing.
pub struct ErrorDisplay<E>(pub E);
impl<E: Error> fmt::Display for ErrorDisplay<E> {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
writeln!(formatter, "{}", self.0)?;
let mut current_err: &dyn Error = &self.0;
while let Some(source) = current_err.source() {
writeln!(formatter, " caused by {}", source)?;
current_err = &*source;
}
Ok(())
}
}

View File

@@ -10,6 +10,7 @@ mod tree_view;
mod auth_cookie;
mod change_processor;
mod common_setup;
mod error;
mod glob;
mod message_queue;
mod multimap;

View File

@@ -46,8 +46,8 @@ impl SnapshotMiddleware for SnapshotCsv {
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()]),
);
if let Some(meta_contents) = vfs.read(meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
@@ -131,52 +131,58 @@ fn convert_localization_csv(contents: &[u8]) -> String {
serde_json::to_string(&entries).expect("Could not encode JSON for localization table")
}
#[cfg(all(test, feature = "FIXME"))]
#[cfg(test)]
mod test {
use super::*;
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
use insta::assert_yaml_snapshot;
use memofs::{InMemoryFs, VfsSnapshot};
#[test]
fn csv_from_vfs() {
let mut vfs = Vfs::new(NoopFetcher);
let file = VfsSnapshot::file(
r#"
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.csv",
VfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
);
),
)
.unwrap();
vfs.debug_load_snapshot("/foo.csv", file);
let mut vfs = Vfs::new(imfs);
let entry = vfs.get("/foo.csv").unwrap();
let instance_snapshot =
SnapshotCsv::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
SnapshotCsv::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv"))
.unwrap()
.unwrap();
assert_yaml_snapshot!(instance_snapshot);
insta::assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn csv_with_meta() {
let mut vfs = Vfs::new(NoopFetcher);
let file = VfsSnapshot::file(
r#"
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.csv",
VfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
);
let meta = VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#);
),
)
.unwrap();
imfs.load_snapshot(
"/foo.meta.json",
VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#),
)
.unwrap();
vfs.debug_load_snapshot("/foo.csv", file);
vfs.debug_load_snapshot("/foo.meta.json", meta);
let mut vfs = Vfs::new(imfs);
let entry = vfs.get("/foo.csv").unwrap();
let instance_snapshot =
SnapshotCsv::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
SnapshotCsv::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv"))
.unwrap()
.unwrap();
assert_yaml_snapshot!(instance_snapshot);
}
}

View File

@@ -73,8 +73,8 @@ impl SnapshotMiddleware for SnapshotDir {
.context(context),
);
if let Some(meta_contents) = vfs.read(meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}

View File

@@ -1,5 +1,7 @@
use std::{error::Error, fmt, io, path::PathBuf};
use snafu::Snafu;
#[derive(Debug)]
pub struct SnapshotError {
detail: SnapshotErrorDetail,
@@ -8,49 +10,63 @@ pub struct SnapshotError {
impl SnapshotError {
pub fn new(detail: SnapshotErrorDetail, path: Option<impl Into<PathBuf>>) -> Self {
SnapshotError {
Self {
detail,
path: path.map(Into::into),
}
}
pub(crate) fn wrap(inner: impl Into<SnapshotErrorDetail>, path: impl Into<PathBuf>) -> Self {
SnapshotError {
detail: inner.into(),
pub(crate) fn wrap(source: impl Into<SnapshotErrorDetail>, path: impl Into<PathBuf>) -> Self {
Self {
detail: source.into(),
path: Some(path.into()),
}
}
pub(crate) fn file_did_not_exist(path: impl Into<PathBuf>) -> SnapshotError {
SnapshotError {
pub(crate) fn file_did_not_exist(path: impl Into<PathBuf>) -> Self {
Self {
detail: SnapshotErrorDetail::FileDidNotExist,
path: Some(path.into()),
}
}
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> SnapshotError {
SnapshotError {
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> Self {
Self {
detail: SnapshotErrorDetail::FileNameBadUnicode,
path: Some(path.into()),
}
}
pub(crate) fn file_contents_bad_unicode(
inner: std::str::Utf8Error,
source: std::str::Utf8Error,
path: impl Into<PathBuf>,
) -> SnapshotError {
SnapshotError {
detail: SnapshotErrorDetail::FileContentsBadUnicode { inner },
) -> Self {
Self {
detail: SnapshotErrorDetail::FileContentsBadUnicode { source },
path: Some(path.into()),
}
}
pub(crate) fn malformed_project(
inner: serde_json::Error,
pub(crate) fn malformed_project(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
Self {
detail: SnapshotErrorDetail::MalformedProject { source },
path: Some(path.into()),
}
}
pub(crate) fn malformed_model_json(
source: serde_json::Error,
path: impl Into<PathBuf>,
) -> SnapshotError {
SnapshotError {
detail: SnapshotErrorDetail::MalformedProject { inner },
) -> Self {
Self {
detail: SnapshotErrorDetail::MalformedModelJson { source },
path: Some(path.into()),
}
}
pub(crate) fn malformed_meta_json(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
Self {
detail: SnapshotErrorDetail::MalformedMetaJson { source },
path: Some(path.into()),
}
}
@@ -83,55 +99,41 @@ impl From<rlua::Error> for SnapshotError {
}
}
#[derive(Debug)]
#[derive(Debug, Snafu)]
pub enum SnapshotErrorDetail {
IoError { inner: io::Error },
Lua { inner: rlua::Error },
#[snafu(display("I/O error"))]
IoError { source: io::Error },
#[snafu(display("Lua error"))]
Lua { source: rlua::Error },
#[snafu(display("file did not exist"))]
FileDidNotExist,
#[snafu(display("file name had malformed Unicode"))]
FileNameBadUnicode,
FileContentsBadUnicode { inner: std::str::Utf8Error },
MalformedProject { inner: serde_json::Error },
#[snafu(display("file had malformed Unicode contents"))]
FileContentsBadUnicode { source: std::str::Utf8Error },
#[snafu(display("malformed project file"))]
MalformedProject { source: serde_json::Error },
#[snafu(display("malformed .model.json file"))]
MalformedModelJson { source: serde_json::Error },
#[snafu(display("malformed .meta.json file"))]
MalformedMetaJson { source: serde_json::Error },
}
impl From<io::Error> for SnapshotErrorDetail {
fn from(inner: io::Error) -> Self {
SnapshotErrorDetail::IoError { inner }
fn from(source: io::Error) -> Self {
SnapshotErrorDetail::IoError { source }
}
}
impl From<rlua::Error> for SnapshotErrorDetail {
fn from(inner: rlua::Error) -> Self {
SnapshotErrorDetail::Lua { inner }
}
}
impl SnapshotErrorDetail {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use self::SnapshotErrorDetail::*;
match self {
IoError { inner } => Some(inner),
Lua { inner } => Some(inner),
FileContentsBadUnicode { inner } => Some(inner),
MalformedProject { inner } => Some(inner),
_ => None,
}
}
}
impl fmt::Display for SnapshotErrorDetail {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
use self::SnapshotErrorDetail::*;
match self {
IoError { inner } => write!(formatter, "I/O error: {}", inner),
Lua { inner } => write!(formatter, "{}", inner),
FileDidNotExist => write!(formatter, "file did not exist"),
FileNameBadUnicode => write!(formatter, "file name had malformed Unicode"),
FileContentsBadUnicode { inner } => {
write!(formatter, "file had malformed unicode: {}", inner)
}
MalformedProject { inner } => write!(formatter, "{}", inner),
}
fn from(source: rlua::Error) -> Self {
SnapshotErrorDetail::Lua { source }
}
}

View File

@@ -8,6 +8,7 @@ use serde::Deserialize;
use crate::snapshot::{InstanceContext, InstanceSnapshot};
use super::{
error::SnapshotError,
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
util::match_file_name,
};
@@ -27,8 +28,9 @@ impl SnapshotMiddleware for SnapshotJsonModel {
None => return Ok(None),
};
let instance: JsonModel =
serde_json::from_slice(&vfs.read(path)?).expect("TODO: Handle serde_json errors");
let contents = vfs.read(path)?;
let instance: JsonModel = serde_json::from_slice(&contents)
.map_err(|source| SnapshotError::malformed_model_json(source, path))?;
if let Some(json_name) = &instance.name {
if json_name != instance_name {

View File

@@ -87,8 +87,8 @@ fn snapshot_lua_file(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snaps
.context(context),
);
if let Some(meta_contents) = vfs.read(meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashMap};
use std::{borrow::Cow, collections::HashMap, path::Path};
use rbx_dom_weak::UnresolvedRbxValue;
use rbx_reflection::try_resolve_value;
@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
use crate::snapshot::InstanceSnapshot;
use super::error::SnapshotError;
/// Represents metadata in a sibling file with the same basename.
///
/// As an example, hello.meta.json next to hello.lua would allow assigning
@@ -21,10 +23,9 @@ pub struct AdjacentMetadata {
}
impl AdjacentMetadata {
pub fn from_slice(slice: &[u8]) -> Self {
pub fn from_slice(slice: &[u8], path: &Path) -> Result<Self, SnapshotError> {
serde_json::from_slice(slice)
// TODO: Turn into error type
.expect(".meta.json file was malformed")
.map_err(|source| SnapshotError::malformed_meta_json(source, path))
}
pub fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) {
@@ -74,10 +75,9 @@ pub struct DirectoryMetadata {
}
impl DirectoryMetadata {
pub fn from_slice(slice: &[u8]) -> Self {
pub fn from_slice(slice: &[u8], path: &Path) -> Result<Self, SnapshotError> {
serde_json::from_slice(slice)
// TODO: Turn into error type
.expect("init.meta.json file was malformed")
.map_err(|source| SnapshotError::malformed_meta_json(source, path))
}
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) {

View File

@@ -52,8 +52,8 @@ impl SnapshotMiddleware for SnapshotTxt {
.context(context),
);
if let Some(meta_contents) = vfs.read(meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}