diff --git a/Cargo.lock b/Cargo.lock index 04fd0121..ae4186f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,15 @@ dependencies = [ "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bincode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "bitflags" version = "0.7.0" @@ -1224,6 +1233,7 @@ dependencies = [ name = "rojo" version = "0.5.0-alpha.0" dependencies = [ + "bincode 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1251,6 +1261,16 @@ dependencies = [ name = "rojo-e2e" version = "0.1.0" +[[package]] +name = "rojo-plugin-moonscript" +version = "0.1.0" +dependencies = [ + "bincode 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rouille" version = "2.2.0" @@ -1868,6 +1888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" "checksum base64 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "621fc7ecb8008f86d7fb9b95356cd692ce9514b80a86d85b397f32a22da7b9e2" "checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +"checksum bincode 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9f2fb9e29e72fd6bc12071533d5dc7664cb01480c59406f656d7ac25c7bd8ff7" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" "checksum brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" diff --git a/Cargo.toml b/Cargo.toml index 27dc248a..6486845e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "server", "rojo-e2e", + "rojo-plugin-moonscript", ] \ No newline at end of file diff --git a/rojo-plugin-moonscript/Cargo.toml b/rojo-plugin-moonscript/Cargo.toml new file mode 100644 index 00000000..80ac0bc0 --- /dev/null +++ b/rojo-plugin-moonscript/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rojo-plugin-moonscript" +version = "0.1.0" +authors = ["Lucien Greathouse "] +edition = "2018" + +[dependencies] +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +bincode = "1.0" \ No newline at end of file diff --git a/rojo-plugin-moonscript/src/main.rs b/rojo-plugin-moonscript/src/main.rs new file mode 100644 index 00000000..1d2ab6c3 --- /dev/null +++ b/rojo-plugin-moonscript/src/main.rs @@ -0,0 +1,80 @@ +use std::{ + collections::HashMap, + io::{self, Write}, + path::PathBuf, + process::{Command, Stdio}, +}; + +use serde_derive::{Serialize, Deserialize}; + +#[derive(Debug, Deserialize)] +struct InputFile { + path: PathBuf, + contents: Vec, +} + +#[derive(Debug, Serialize)] +struct Metadata { + ignore_unknown_instances: bool, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "Type")] +enum Property { + #[serde(rename_all = "PascalCase")] + String { + value: String, + }, +} + +#[derive(Debug, Serialize)] +struct Instance { + name: String, + class_name: String, + properties: HashMap, + children: Vec<()>, + metadata: Metadata, +} + +fn main() -> io::Result<()> { + let file: InputFile = bincode::deserialize_from(io::stdin()) + .expect("Invalid input"); + + let mut child = Command::new("moonc") + .arg("--") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + { + let stdin = child.stdin.as_mut().unwrap(); + stdin.write_all(&file.contents)?; + } + + let result = child.wait_with_output()?; + + let script_name = file.path.file_stem().unwrap().to_string_lossy().into_owned(); + let compiled = String::from_utf8_lossy(&result.stdout).into_owned(); + + let mut properties = HashMap::new(); + properties.insert("Source".to_owned(), Property::String { + value: compiled, + }); + + let output = Instance { + name: script_name, + class_name: "ModuleScript".to_owned(), + properties, + children: Vec::new(), + metadata: Metadata { + ignore_unknown_instances: false, + }, + }; + + let response = serde_json::to_string(&output) + .expect("Couldn't serialize to JSON"); + + print!("{}", response); + + Ok(()) +} \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml index b4c4aa22..2c507925 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,6 +20,7 @@ default = [] bundle-plugin = [] [dependencies] +bincode = "1.0" clap = "2.27" csv = "1.0" env_logger = "0.5" diff --git a/server/src/rbx_snapshot.rs b/server/src/rbx_snapshot.rs index 06a8e2ce..5560e3fd 100644 --- a/server/src/rbx_snapshot.rs +++ b/server/src/rbx_snapshot.rs @@ -3,6 +3,7 @@ use std::{ collections::HashMap, fmt, path::{Path, PathBuf}, + process::{Command, Stdio}, str, }; @@ -33,6 +34,13 @@ use crate::{ rbx_session::{MetadataPerPath, MetadataPerInstance}, }; +// In a real plugin implementation, this would be part of the project file. +// Should the executable name be limited to try to prevent malicious projects +// from executing arbitrary code? +static PLUGINS: [(&str, &str); 1] = [ + ("moon", "target/debug/rojo-plugin-moonscript"), +]; + const INIT_MODULE_NAME: &str = "init.lua"; const INIT_SERVER_NAME: &str = "init.server.lua"; const INIT_CLIENT_NAME: &str = "init.client.lua"; @@ -246,7 +254,65 @@ fn snapshot_imfs_file<'source>( Some("txt") => snapshot_txt_file(file)?, Some("rbxmx") => snapshot_xml_model_file(file)?, Some("rbxm") => snapshot_binary_model_file(file)?, - Some(_) | None => return Ok(None), + // In a real plugin implementation, plugins would probably want to + // execute before any of the builtin instance routines. + // + // Alternatively, maybe we want to run the builtin routines and send the + // snapshot generated by that routine as well as the original file? That + // would open the door to make plugins that transform instances to do + // things like add source headers or optimize code without having to + // create another kind of plugin. + Some(unknown_ext) => { + let mut result = None; + + for (plugin_ext, plugin_path) in &PLUGINS { + if plugin_ext == &unknown_ext { + let mut child = Command::new(plugin_path) + // This argument is standard for telling processes to + // read from stdin, we can pass it as a courtesy even + // though I bet all plugin processes will be + // purpose-built to correctly deal with structured input + // from Rojo + .arg("--") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Could not spawn plugin process"); + + { + let stdin = child.stdin.as_mut().unwrap(); + // We're using bincode here since it makes Vec + // serialize more cleanly than JSON, but we could use + // whatever. + bincode::serialize_into(stdin, file) + .expect("Could not serialize file into plugin stdin"); + } + + let output = child.wait_with_output() + .expect("Could not wait for plugin process output"); + + assert!(output.status.success()); + + // I'm accepting JSON for the output because bincode doesn't + // implement all of the features needed to actually + // deserialize a RbxSnapshotInstance I guess. + // + // Whatever format is used for plugins, it should probably + // be consistent between input/output. + // + // This also doesn't account for representing failure or a + // plugin returning no instance at all. + let snapshot: RbxSnapshotInstance<'static> = serde_json::from_slice(&output.stdout) + .expect("Could not deserialize plugin output"); + + result = Some(snapshot); + break; + } + } + + result + }, + None => return Ok(None), }; if let Some(snapshot) = maybe_snapshot.as_mut() { diff --git a/server/src/snapshot_reconciler.rs b/server/src/snapshot_reconciler.rs index 2562c4cb..20075ead 100644 --- a/server/src/snapshot_reconciler.rs +++ b/server/src/snapshot_reconciler.rs @@ -55,7 +55,7 @@ impl InstanceChanges { } } -#[derive(Debug)] +#[derive(Debug, Deserialize)] pub struct RbxSnapshotInstance<'a> { pub name: Cow<'a, str>, pub class_name: Cow<'a, str>, diff --git a/test-projects/moonscript/roblox-project.json b/test-projects/moonscript/roblox-project.json new file mode 100644 index 00000000..81f413a3 --- /dev/null +++ b/test-projects/moonscript/roblox-project.json @@ -0,0 +1,6 @@ +{ + "name": "moonscript", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/moonscript/src/hello.moon b/test-projects/moonscript/src/hello.moon new file mode 100644 index 00000000..cafb6dda --- /dev/null +++ b/test-projects/moonscript/src/hello.moon @@ -0,0 +1 @@ +func_a = -> print "Hello, world!" \ No newline at end of file