Compare commits

...

26 Commits

Author SHA1 Message Date
Lucien Greathouse
3b433e53be Memofs v0.1.1 2020-03-18 18:35:44 -07:00
Lucien Greathouse
28ddf40344 memofs: Update fs-err and use it more 2020-03-18 18:06:58 -07:00
Lucien Greathouse
c1286db9c1 Update Changelog 2020-03-18 16:26:41 -07:00
Lucien Greathouse
f13940262e Update CHANGELOG 2020-03-18 12:03:50 -07:00
Lucien Greathouse
9f0a6101b8 Add configurable color options 2020-03-18 12:03:07 -07:00
Lucien Greathouse
0b0fe01a7c Tidy up root repository files 2020-03-18 11:40:12 -07:00
Lucien Greathouse
85e098d5c8 Update README 2020-03-18 11:36:50 -07:00
Lucien Greathouse
e8d1faf4e2 Update changelog 2020-03-18 10:43:42 -07:00
Lucien Greathouse
2a46df1110 Expose two-way sync.
- Convert plugin DevSettings flag to settings panel feature
- Remove server feature, always enable write API
2020-03-18 10:39:40 -07:00
Lucien Greathouse
1601e6d26e Update changelog 2020-03-17 23:20:40 -07:00
Lucien Greathouse
0e4f6dea2b plugin: Add setting for opening scripts externally 2020-03-17 23:20:05 -07:00
Lucien Greathouse
a2356773dc Add checkbox and fill out settings panel 2020-03-17 23:14:32 -07:00
Lucien Greathouse
4a4da4737d Fix plugin settings persistent 2020-03-17 23:03:59 -07:00
Lucien Greathouse
2cefd1bf2e plugin: Add PluginSettings context item, render it in settings screen 2020-03-17 23:03:01 -07:00
Lucien Greathouse
c5ce15fe34 plugin: Add dummy settings panel 2020-03-17 22:38:53 -07:00
Lucien Greathouse
76dea568c9 Update changelog 2020-03-17 22:30:00 -07:00
Lucien Greathouse
8e81140eff Increase verbosity of logging 2020-03-17 22:29:23 -07:00
Lucien Greathouse
d58e1f0792 Add logging when running rojo build 2020-03-17 22:28:38 -07:00
Lucien Greathouse
830c242751 plugin: Stop using codename in dev mode 2020-03-17 22:25:04 -07:00
Lucien Greathouse
91d45afd0f Add plugin feature 'UnstableOpenScriptsExternally' 2020-03-17 18:13:52 -07:00
Lucien Greathouse
102c77b23e Implement /api/open/{id} to open a script by ID in your default editor 2020-03-17 17:50:54 -07:00
Lucien Greathouse
aa4039a2e7 bye snafu 2020-03-16 23:37:00 -07:00
Lucien Greathouse
c065ded440 Tidy up SnapshotError a lot 2020-03-16 21:35:46 -07:00
Lucien Greathouse
f69096dadb Use thiserror and anyhow for command-level error types 2020-03-16 21:13:38 -07:00
Lucien Greathouse
363f95ba14 memofs: Use fs_err instead of std::fs when possible 2020-03-16 20:35:58 -07:00
Lucien Greathouse
dcc15e8911 Refactor upload to use ServeSession and drop common_setup 2020-03-16 20:20:12 -07:00
32 changed files with 906 additions and 542 deletions

View File

@@ -1,6 +1,12 @@
# Rojo Changelog
## Unreleased Changes for 0.6.x
* Added basic settings panel to plugin, with two settings:
* "Open Scripts Externally": When enabled, opening a script in Studio will instead open it in your default text editor.
* "Two-Way Sync": When enabled, Rojo will attempt to save changes to your place back to the filesystem. **Very early feature, very broken, beware!**
* Added `--color` option to force-enable or force-disable color in Rojo's output.
* The server half of **experimental** two-way sync is now enabled by default.
* Increased default logging verbosity in commands like `rojo build`.
## [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))

View File

@@ -33,15 +33,13 @@ Please file issues and we'll try to help figure out what the best way forward is
## Pushing a Rojo Release
The Rojo release process is pretty manual right now. If you need to do it, here's how:
1. Bump server version in [`server/Cargo.toml`](server/Cargo.toml)
1. Bump server version in [`Cargo.toml`](Cargo.toml)
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
3. Run `cargo test` to update `Cargo.lock` and double-check tests
4. Update [`CHANGELOG.md`](CHANGELOG.md)
5. Commit!
* `git add . && git commit -m "Release vX.Y.Z"`
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
7. Build Windows release build of CLI
* `cargo build --release`
7. Publish the CLI
* `cargo publish`
8. Build and upload the plugin
@@ -52,4 +50,5 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
10. Copy GitHub release content from previous release
* Update the leading text with a summary about the release
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
* Write a small summary of each major feature
* Write a small summary of each major feature
* Attach release artifacts from GitHub Actions for each platform

41
Cargo.lock generated
View File

@@ -21,6 +21,11 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "anyhow"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "atty"
version = "0.2.14"
@@ -520,6 +525,11 @@ name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fs-err"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fsevent"
version = "0.4.0"
@@ -912,9 +922,10 @@ dependencies = [
[[package]]
name = "memofs"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"fs-err 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1661,11 +1672,13 @@ dependencies = [
name = "rojo"
version = "0.6.0-alpha.3"
dependencies = [
"anyhow 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"fs-err 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
"globset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1675,7 +1688,7 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"memofs 0.1.0",
"memofs 0.1.1",
"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)",
@@ -1692,10 +1705,10 @@ dependencies = [
"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)",
"serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
"snafu 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2046,6 +2059,24 @@ dependencies = [
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thiserror"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"thiserror-impl 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thiserror-impl"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thread_local"
version = "1.0.1"
@@ -2525,6 +2556,7 @@ dependencies = [
"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
"checksum aho-corasick 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec"
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum anyhow 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "013a6e0a2cbe3d20f9c60b65458f7a7f7a5e636c5d0f45a5a6aee5d4b1f01785"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
@@ -2582,6 +2614,7 @@ dependencies = [
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
"checksum fs-err 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c1a51f8b7158efbe531f7baa74e38e49fbc41239e5d66720bb37ed39c27c241a"
"checksum fsevent 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
"checksum fsevent-sys 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
@@ -2740,6 +2773,8 @@ dependencies = [
"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
"checksum termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ee14bf8e6767ab4c687c9e8bc003879e042a96fd67a3ba5934eadb6536bef4db"
"checksum thiserror-impl 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "a7b51e1fbc44b5a0840be594fbc0f960be09050f2617e61e6aa43bef97cd3ef4"
"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum tinytemplate 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "57a3c6667d3e65eb1bc3aed6fd14011c6cbc3a0665218ab7f5daf040b9ec371a"

View File

@@ -27,9 +27,6 @@ default = []
# Turn on support for specifying glob ignore path rules in the project format.
unstable_glob_ignore_paths = []
# Turn on the server half of Rojo's unstable two-way sync feature.
unstable_two_way_sync = []
# Enable this feature to live-reload assets from the web UI.
dev_live_assets = []
@@ -61,12 +58,14 @@ name = "build"
harness = false
[dependencies]
memofs = { version = "0.1.0", path = "memofs" }
memofs = { version = "0.1.1", path = "memofs" }
anyhow = "1.0.27"
backtrace = "0.3"
crossbeam-channel = "0.4.0"
csv = "1.1.1"
env_logger = "0.7.1"
fs-err = "2.2.0"
futures = "0.1.29"
globset = "0.4.4"
humantime = "1.3.0"
@@ -87,9 +86,9 @@ ritz = "0.1.0"
rlua = "0.17.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
snafu = "0.6.0"
structopt = "0.3.5"
termcolor = "1.0.5"
thiserror = "1.0.11"
tokio = "0.1.22"
uuid = { version = "0.8.1", features = ["v4", "serde"] }

View File

@@ -13,8 +13,8 @@
<a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
</a>
<a href="https://rojo.space/docs/0.5.x">
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo 0.5.x Documentation" />
<a href="https://rojo.space/docs">
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo 0.5.x Documentation" />
</a>
</div>
@@ -34,11 +34,10 @@ Rojo enables:
* Streaming `rbxmx` and `rbxm` models into your game in real time
* Packaging and deploying your project to Roblox.com from the command line
Soon, Rojo will be able to:
In the future, Rojo will be able to:
* Automatically convert your existing game to work with Rojo
* Sync instances from Roblox Studio to the filesystem
* Automatically manage your assets on Roblox.com, like images and sounds
* Automatically convert your existing game to work with Rojo
* Import custom instances like MoonScript code
## [Documentation](https://rojo.space/docs)

View File

@@ -1,30 +0,0 @@
digraph Rojo {
concentrate = true;
node [fontname = "sans-serif"];
plugin [label="Roblox Studio Plugin"]
session [label="Session"]
rbx_tree [label="Instance Tree"]
imfs [label="In-Memory Filesystem"]
fs_impl [label="Filesystem Implementation\n(stubbed in tests)"]
fs [label="Real Filesystem"]
snapshot_subsystem [label="Snapshot Subsystem\n(reconciler)"]
snapshot_generator [label="Snapshot Generator"]
user_middleware [label="User Middleware\n(MoonScript, etc.)"]
builtin_middleware [label="Built-in Middleware\n(.lua, .rbxm, etc.)"]
api [label="Web API"]
file_watcher [label="File Watcher"]
session -> imfs
session -> rbx_tree
session -> snapshot_subsystem
session -> snapshot_generator
session -> file_watcher [dir="both"]
file_watcher -> imfs
snapshot_generator -> user_middleware
snapshot_generator -> builtin_middleware
plugin -> api [style="dotted"; dir="both"; minlen=2]
api -> session
imfs -> fs_impl
fs_impl -> fs
}

View File

@@ -2,5 +2,8 @@
## Unreleased Changes
## 0.1.1 (2020-03-18)
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
## 0.1.0 (2020-03-10)
* Initial release

View File

@@ -1,7 +1,7 @@
[package]
name = "memofs"
description = "Virtual filesystem with configurable backends."
version = "0.1.0"
version = "0.1.1"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
edition = "2018"
readme = "README.md"
@@ -11,5 +11,6 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
notify = "4.0.15"
crossbeam-channel = "0.4.0"
fs-err = "2.3.0"
notify = "4.0.15"

View File

@@ -1,4 +1,3 @@
use std::fs;
use std::io;
use std::path::Path;
use std::sync::mpsc;
@@ -55,15 +54,15 @@ impl StdBackend {
impl VfsBackend for StdBackend {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
fs::read(path)
fs_err::read(path)
}
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
fs::write(path, data)
fs_err::write(path, data)
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let entries: Result<Vec<_>, _> = fs::read_dir(path)?.collect();
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
let mut entries = entries?;
entries.sort_by_cached_key(|entry| entry.file_name());
@@ -78,15 +77,15 @@ impl VfsBackend for StdBackend {
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
fs::remove_file(path)
fs_err::remove_file(path)
}
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
fs::remove_dir_all(path)
fs_err::remove_dir_all(path)
}
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
let inner = fs::metadata(path)?;
let inner = fs_err::metadata(path)?;
Ok(Metadata {
is_file: inner.is_file(),

View File

@@ -233,4 +233,19 @@ function ApiContext:retrieveMessages()
end)
end
function ApiContext:open(id)
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
return Http.post(url, "")
:andThen(rejectFailedRequests)
:andThen(Http.Response.json)
:andThen(function(body)
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
return nil
end)
end
return ApiContext

View File

@@ -13,11 +13,11 @@ local Version = require(Plugin.Version)
local preloadAssets = require(Plugin.preloadAssets)
local strict = require(Plugin.strict)
local Theme = require(Plugin.Components.Theme)
local ConnectPanel = require(Plugin.Components.ConnectPanel)
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
local ErrorPanel = require(Plugin.Components.ErrorPanel)
local SettingsPanel = require(Plugin.Components.SettingsPanel)
local e = Roact.createElement
@@ -62,6 +62,7 @@ local AppStatus = strict("AppStatus", {
Connecting = "Connecting",
Connected = "Connected",
Error = "Error",
Settings = "Settings",
})
local App = Roact.Component:extend("App")
@@ -74,10 +75,7 @@ function App:init()
self.signals = {}
self.serveSession = nil
self.displayedVersion = DevSettings:isEnabled()
and Config.codename
or Version.display(Config.version)
self.displayedVersion = Version.display(Config.version)
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
@@ -109,12 +107,14 @@ function App:init()
end)
end
function App:startSession(address, port)
function App:startSession(address, port, sessionOptions)
Log.trace("Starting new session")
local baseUrl = ("http://%s:%s"):format(address, port)
self.serveSession = ServeSession.new({
apiContext = ApiContext.new(baseUrl),
openScriptsExternally = sessionOptions.openScriptsExternally,
twoWaySync = sessionOptions.twoWaySync,
})
self.serveSession:onStatusChanged(function(status, details)
@@ -155,8 +155,13 @@ function App:render()
if self.state.appStatus == AppStatus.NotStarted then
children = {
ConnectPanel = e(ConnectPanel, {
startSession = function(address, port)
self:startSession(address, port)
startSession = function(address, port, settings)
self:startSession(address, port, settings)
end,
openSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
end,
cancel = function()
Log.trace("Canceling session configuration")
@@ -169,7 +174,7 @@ function App:render()
}
elseif self.state.appStatus == AppStatus.Connecting then
children = {
ConnectingPanel = Roact.createElement(ConnectingPanel),
ConnectingPanel = e(ConnectingPanel),
}
elseif self.state.appStatus == AppStatus.Connected then
children = {
@@ -187,9 +192,19 @@ function App:render()
end,
}),
}
elseif self.state.appStatus == AppStatus.Settings then
children = {
e(SettingsPanel, {
back = function()
self:setState({
appStatus = AppStatus.NotStarted,
})
end,
}),
}
elseif self.state.appStatus == AppStatus.Error then
children = {
ErrorPanel = Roact.createElement(ErrorPanel, {
ErrorPanel = e(ErrorPanel, {
errorMessage = self.state.errorMessage,
onDismiss = function()
self:setState({
@@ -200,11 +215,9 @@ function App:render()
}
end
return Roact.createElement(Theme.StudioProvider, nil, {
UI = Roact.createElement(Roact.Portal, {
target = self.dockWidget,
}, children),
})
return e(Roact.Portal, {
target = self.dockWidget,
}, children)
end
function App:didMount()

View File

@@ -0,0 +1,39 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local function Checkbox(props)
local checked = props.checked
local layoutOrder = props.layoutOrder
local onChange = props.onChange
return Theme.with(function(theme)
return e("ImageButton", {
LayoutOrder = layoutOrder,
Size = UDim2.new(0, 20, 0, 20),
BorderSizePixel = 2,
BorderColor3 = theme.Text2,
BackgroundColor3 = theme.Background2,
[Roact.Event.Activated] = function()
onChange(not checked)
end,
}, {
Indicator = e("Frame", {
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BorderSizePixel = 0,
BackgroundColor3 = theme.Brand1,
BackgroundTransparency = checked and 0 or 1,
})
})
end)
end
return Checkbox

View File

@@ -11,6 +11,7 @@ local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton)
local FormTextInput = require(Plugin.Components.FormTextInput)
local PluginSettings = require(Plugin.Components.PluginSettings)
local e = Roact.createElement
@@ -25,138 +26,157 @@ end
function ConnectPanel:render()
local startSession = self.props.startSession
local openSettings = self.props.openSettings
return Theme.with(function(theme)
return e(Panel, nil, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
return PluginSettings.with(function(settings)
return e(Panel, nil, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
Inputs = e(FitList, {
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 1,
},
layoutProps = {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 10),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
Address = e(FitList, {
Inputs = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
LayoutOrder = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 10),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Address",
TextColor3 = theme.Text1,
Address = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Address",
TextColor3 = theme.Text1,
}),
Input = e(FormTextInput, {
layoutOrder = 2,
width = UDim.new(0, 220),
value = self.state.address,
placeholderValue = Config.defaultHost,
onValueChange = function(newValue)
self:setState({
address = newValue,
})
end,
}),
}),
Input = e(FormTextInput, {
layoutOrder = 2,
width = UDim.new(0, 220),
value = self.state.address,
placeholderValue = Config.defaultHost,
onValueChange = function(newValue)
self:setState({
address = newValue,
})
end,
Port = e(FitList, {
containerProps = {
LayoutOrder = 2,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Port",
TextColor3 = theme.Text1,
}),
Input = e(FormTextInput, {
layoutOrder = 2,
width = UDim.new(0, 80),
value = self.state.port,
placeholderValue = Config.defaultPort,
onValueChange = function(newValue)
self:setState({
port = newValue,
})
end,
}),
}),
}),
Port = e(FitList, {
Buttons = e(FitList, {
fitAxes = "Y",
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 2,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 0),
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 0),
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Port",
TextColor3 = theme.Text1,
e(FormButton, {
layoutOrder = 1,
text = "Settings",
secondary = true,
onClick = function()
if openSettings ~= nil then
openSettings()
end
end,
}),
Input = e(FormTextInput, {
e(FormButton, {
layoutOrder = 2,
width = UDim.new(0, 80),
value = self.state.port,
placeholderValue = Config.defaultPort,
onValueChange = function(newValue)
self:setState({
port = newValue,
})
text = "Connect",
onClick = function()
if startSession ~= nil then
local address = self.state.address
if address:len() == 0 then
address = Config.defaultHost
end
local port = self.state.port
if port:len() == 0 then
port = Config.defaultPort
end
local sessionOptions = {
openScriptsExternally = settings:get("openScriptsExternally"),
twoWaySync = settings:get("twoWaySync"),
}
startSession(address, port, sessionOptions)
end
end,
}),
}),
}),
Buttons = e(FitList, {
fitAxes = "Y",
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 0),
},
layoutProps = {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 0),
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
e(FormButton, {
layoutOrder = 2,
text = "Connect",
onClick = function()
if startSession ~= nil then
local address = self.state.address
if address:len() == 0 then
address = Config.defaultHost
end
local port = self.state.port
if port:len() == 0 then
port = Config.defaultPort
end
startSession(address, port)
end
end,
}),
}),
})
})
end)
end)
end

View File

@@ -0,0 +1,121 @@
--[[
Persistent plugin settings that can be accessed via Roact context.
]]
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
}
local Settings = {}
Settings.__index = Settings
function Settings.fromPlugin(plugin)
local values = {}
for name, defaultValue in pairs(defaultSettings) do
local savedValue = plugin:GetSetting("Rojo_" .. name)
if savedValue == nil then
plugin:SetSetting("Rojo_" .. name, defaultValue)
values[name] = defaultValue
else
values[name] = savedValue
end
end
return setmetatable({
__values = values,
__plugin = plugin,
__updateListeners = {},
}, Settings)
end
function Settings:get(name)
if defaultSettings[name] == nil then
error("Invalid setings name " .. tostring(name), 2)
end
return self.__values[name]
end
function Settings:set(name, value)
self.__plugin:SetSetting("Rojo_" .. name, value)
self.__values[name] = value
for callback in pairs(self.__updateListeners) do
callback(name, value)
end
end
function Settings:onUpdate(newCallback)
local newListeners = {}
for callback in pairs(self.__updateListeners) do
newListeners[callback] = true
end
newListeners[newCallback] = true
self.__updateListeners = newListeners
return function()
local newListeners = {}
for callback in pairs(self.__updateListeners) do
if callback ~= newCallback then
newListeners[callback] = true
end
end
self.__updateListeners = newListeners
end
end
local Context = Roact.createContext(nil)
local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:init()
self.settings = Settings.fromPlugin(self.props.plugin)
end
function StudioProvider:render()
return Roact.createElement(Context.Provider, {
value = self.settings,
}, self.props[Roact.Children])
end
local InternalConsumer = Roact.Component:extend("InternalConsumer")
function InternalConsumer:render()
return self.props.render(self.props.settings)
end
function InternalConsumer:didMount()
self.disconnect = self.props.settings:onUpdate(function()
-- Trigger a dummy state update to update the settings consumer.
self:setState({})
end)
end
function InternalConsumer:willUnmount()
self.disconnect()
end
local function with(callback)
return Roact.createElement(Context.Consumer, {
render = function(settings)
return Roact.createElement(InternalConsumer, {
settings = settings,
render = callback,
})
end,
})
end
return {
StudioProvider = StudioProvider,
with = with,
}

View File

@@ -0,0 +1,119 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local Checkbox = require(Plugin.Components.Checkbox)
local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton)
local Panel = require(Plugin.Components.Panel)
local PluginSettings = require(Plugin.Components.PluginSettings)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local SettingsPanel = Roact.Component:extend("SettingsPanel")
function SettingsPanel:render()
local back = self.props.back
return Theme.with(function(theme)
return PluginSettings.with(function(settings)
return e(Panel, nil, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 16),
}),
OpenScriptsExternally = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.MainFont,
TextSize = 16,
Text = "Open Scripts Externally",
TextColor3 = theme.Text1,
}),
Padding = e("Frame", {
Size = UDim2.new(0, 8, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Input = e(Checkbox, {
layoutOrder = 3,
checked = settings:get("openScriptsExternally"),
onChange = function(newValue)
settings:set("openScriptsExternally", not settings:get("openScriptsExternally"))
end,
}),
}),
TwoWaySync = e(FitList, {
containerProps = {
LayoutOrder = 2,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.MainFont,
TextSize = 16,
Text = "Two-Way Sync (Experimental!)",
TextColor3 = theme.Text1,
}),
Padding = e("Frame", {
Size = UDim2.new(0, 8, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Input = e(Checkbox, {
layoutOrder = 3,
checked = settings:get("twoWaySync"),
onChange = function(newValue)
settings:set("twoWaySync", not settings:get("twoWaySync"))
end,
}),
}),
BackButton = e(FormButton, {
layoutOrder = 4,
text = "Okay",
secondary = true,
onClick = function()
back()
end,
}),
})
end)
end)
end
return SettingsPanel

View File

@@ -25,14 +25,6 @@ local VALUES = {
[Environment.Test] = true,
},
},
UnstableTwoWaySync = {
type = "BoolValue",
values = {
[Environment.User] = false,
[Environment.Dev] = false,
[Environment.Test] = false,
},
},
}
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
@@ -140,10 +132,6 @@ function DevSettings:shouldTypecheck()
return getValue("TypecheckingEnabled")
end
function DevSettings:twoWaySyncEnabled()
return getValue("UnstableTwoWaySync")
end
function _G.ROJO_DEV_CREATE()
DevSettings:createDevSettings()
end

View File

@@ -1,3 +1,5 @@
local StudioService = game:GetService("StudioService")
local Log = require(script.Parent.Parent.Log)
local Fmt = require(script.Parent.Parent.Fmt)
local t = require(script.Parent.Parent.t)
@@ -43,6 +45,8 @@ ServeSession.Status = Status
local validateServeOptions = t.strictInterface({
apiContext = t.table,
openScriptsExternally = t.boolean,
twoWaySync = t.boolean,
})
function ServeSession.new(options)
@@ -57,12 +61,28 @@ function ServeSession.new(options)
local instanceMap = InstanceMap.new(onInstanceChanged)
local reconciler = Reconciler.new(instanceMap)
local connections = {}
local connection = StudioService
:GetPropertyChangedSignal("ActiveScript")
:Connect(function()
local activeScript = StudioService.ActiveScript
if activeScript ~= nil then
self:__onActiveScriptChanged(activeScript)
end
end)
table.insert(connections, connection)
self = {
__status = Status.NotStarted,
__apiContext = options.apiContext,
__openScriptsExternally = options.openScriptsExternally,
__twoWaySync = options.twoWaySync,
__reconciler = reconciler,
__instanceMap = instanceMap,
__statusChangedCallback = nil,
__connections = connections,
}
setmetatable(self, ServeSession)
@@ -108,8 +128,39 @@ function ServeSession:stop()
self:__stopInternal()
end
function ServeSession:__onActiveScriptChanged(activeScript)
if not self.__openScriptsExternally then
Log.trace("Not opening script {} because feature not enabled.", activeScript)
return
end
if self.__status ~= Status.Connected then
Log.trace("Not opening script {} because session is not connected.", activeScript)
return
end
local scriptId = self.__instanceMap.fromInstances[activeScript]
if scriptId == nil then
Log.trace("Not opening script {} because it is not known by Rojo.", activeScript)
return
end
Log.debug("Trying to open script {} externally...", activeScript)
-- Force-close the script inside Studio
local existingParent = activeScript.Parent
activeScript.Parent = nil
activeScript.Parent = existingParent
-- Notify the Rojo server to open this script
self.__apiContext:open(scriptId)
end
function ServeSession:__onInstanceChanged(instance, propertyName)
if not DevSettings:twoWaySyncEnabled() then
if not self.__twoWaySync then
return
end
@@ -200,6 +251,11 @@ function ServeSession:__stopInternal(err)
self:__setStatus(Status.Disconnected, err)
self.__apiContext:disconnect()
self.__instanceMap:stop()
for _, connection in ipairs(self.__connections) do
connection:Disconnect()
end
self.__connections = {}
end
function ServeSession:__setStatus(status, detail)

View File

@@ -14,9 +14,17 @@ local Roact = require(script.Parent.Roact)
local Config = require(script.Config)
local App = require(script.Components.App)
local Theme = require(script.Components.Theme)
local PluginSettings = require(script.Components.PluginSettings)
local app = Roact.createElement(App, {
plugin = plugin,
local app = Roact.createElement(Theme.StudioProvider, nil, {
Roact.createElement(PluginSettings.StudioProvider, {
plugin = plugin,
}, {
RojoUI = Roact.createElement(App, {
plugin = plugin,
}),
})
})
local tree = Roact.mount(app, nil, "Rojo UI")

View File

@@ -3,12 +3,12 @@ use std::{env, error::Error, panic, process};
use backtrace::Backtrace;
use structopt::StructOpt;
use librojo::cli::{self, Options, Subcommand};
use librojo::cli::{self, GlobalOptions, Options, Subcommand};
fn run(subcommand: Subcommand) -> Result<(), Box<dyn Error>> {
fn run(global: GlobalOptions, 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::Serve(serve_options) => cli::serve(global, serve_options)?,
Subcommand::Build(build_options) => cli::build(build_options)?,
Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
Subcommand::Doc => cli::doc()?,
@@ -63,10 +63,10 @@ fn main() {
let options = Options::from_args();
let log_filter = match options.verbosity {
0 => "warn",
1 => "warn,librojo=info",
2 => "warn,librojo=trace",
let log_filter = match options.global.verbosity {
0 => "info",
1 => "info,librojo=debug",
2 => "info,librojo=trace",
_ => "trace",
};
@@ -77,9 +77,10 @@ fn main() {
.format_timestamp(None)
// Indent following lines equal to the log level label, like `[ERROR] `
.format_indent(Some(8))
.write_style(options.global.color.into())
.init();
if let Err(err) = run(options.subcommand) {
if let Err(err) = run(options.global, options.subcommand) {
log::error!("{}", err);
let mut current_err: &dyn Error = &*err;

View File

@@ -1,15 +1,13 @@
use std::{
fs::File,
io::{self, BufWriter, Write},
io::{BufWriter, Write},
};
use memofs::Vfs;
use snafu::{ResultExt, Snafu};
use thiserror::Error;
use tokio::runtime::Runtime;
use crate::{
cli::BuildCommand, project::ProjectError, serve_session::ServeSession, snapshot::RojoTree,
};
use crate::{cli::BuildCommand, serve_session::ServeSession, snapshot::RojoTree};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputKind {
@@ -31,45 +29,17 @@ fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
}
}
#[derive(Debug, Snafu)]
pub struct BuildError(Error);
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
enum Error {
#[snafu(display("Could not detect what kind of file to create"))]
#[error("Could not detect what kind of file to build. Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.")]
UnknownOutputKind,
#[snafu(display("{}", source))]
Io { source: io::Error },
#[snafu(display("{}", source))]
XmlModelEncode { source: rbx_xml::EncodeError },
#[snafu(display("Binary model error: {:?}", source))]
BinaryModelEncode {
#[snafu(source(false))]
source: rbx_binary::EncodeError,
},
#[snafu(display("{}", source))]
Project { source: ProjectError },
}
impl From<rbx_binary::EncodeError> for Error {
fn from(source: rbx_binary::EncodeError) -> Self {
Error::BinaryModelEncode { source }
}
}
fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
pub fn build(options: BuildCommand) -> Result<(), BuildError> {
Ok(build_inner(options)?)
}
fn build_inner(options: BuildCommand) -> Result<(), Error> {
pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
@@ -98,14 +68,14 @@ fn build_inner(options: BuildCommand) -> Result<(), Error> {
Ok(())
}
fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Error> {
let output_kind = detect_output_kind(&options).ok_or(Error::UnknownOutputKind)?;
log::debug!("Hoping to generate file of type {:?}", output_kind);
let root_id = tree.get_root_id();
log::trace!("Opening output file for write");
let file = File::create(&options.output).context(Io)?;
let file = File::create(&options.output)?;
let mut file = BufWriter::new(file);
match output_kind {
@@ -113,8 +83,7 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
// Model files include the root instance of the tree and all its
// descendants.
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())
.context(XmlModelEncode)?;
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
}
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
@@ -123,8 +92,7 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())
.context(XmlModelEncode)?;
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
}
OutputKind::Rbxm => {
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
@@ -141,7 +109,14 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
}
}
file.flush().context(Io)?;
file.flush()?;
let filename = options
.output
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("<invalid utf-8>");
log::info!("Built project to {}", filename);
Ok(())
}

View File

@@ -1,26 +1,4 @@
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")?;
pub fn doc() -> Result<(), anyhow::Error> {
opener::open("https://rojo.space/docs")?;
Ok(())
}

View File

@@ -5,7 +5,7 @@ use std::{
process::{Command, Stdio},
};
use snafu::Snafu;
use thiserror::Error;
use crate::cli::{InitCommand, InitKind};
@@ -20,32 +20,16 @@ static PLACE_PROJECT: &str =
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)]
#[derive(Debug, Error)]
enum Error {
#[snafu(display("A project file named default.project.json already exists in this folder"))]
#[error("A project file named default.project.json already exists in this folder")]
AlreadyExists,
#[snafu(display("git init failed"))]
#[error("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> {
pub fn init(options: InitCommand) -> Result<(), anyhow::Error> {
let base_path = options.absolute_path();
fs::create_dir_all(&base_path)?;
@@ -65,7 +49,7 @@ fn init_inner(options: InitCommand) -> Result<(), Error> {
}
}
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> {
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
eprintln!("Creating new place project '{}'", project_params.name);
let project_file = project_params.render_template(PLACE_PROJECT);
@@ -109,7 +93,7 @@ fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), Err
Ok(())
}
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> {
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
eprintln!("Creating new model project '{}'", project_params.name);
let project_file = project_params.render_template(MODEL_PROJECT);
@@ -147,14 +131,14 @@ impl ProjectParams {
}
/// Attempt to initialize a Git repository if necessary, and create .gitignore.
fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), Error> {
fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::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);
return Err(Error::GitInit.into());
}
}
@@ -186,7 +170,7 @@ fn should_git_init(path: &Path) -> bool {
}
/// Write a file if it does not exist yet, otherwise, leave it alone.
fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), Error> {
fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> {
let file_res = OpenOptions::new().write(true).create_new(true).open(path);
let mut file = match file_res {
@@ -205,7 +189,7 @@ fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), Error> {
}
/// Try to create a project file and fail if it already exists.
fn try_create_project(base_path: &Path, contents: &str) -> Result<(), Error> {
fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Error> {
let project_path = base_path.join("default.project.json");
let file_res = OpenOptions::new()
@@ -217,7 +201,7 @@ fn try_create_project(base_path: &Path, contents: &str) -> Result<(), Error> {
Ok(file) => file,
Err(err) => {
return match err.kind() {
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists),
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists.into()),
_ => Err(err.into()),
}
}

View File

@@ -16,6 +16,7 @@ use std::{
};
use structopt::StructOpt;
use thiserror::Error;
pub use self::build::*;
pub use self::doc::*;
@@ -27,16 +28,73 @@ pub use self::upload::*;
#[derive(Debug, StructOpt)]
#[structopt(name = "Rojo", about, author)]
pub struct Options {
/// Sets verbosity level. Can be specified multiple times.
#[structopt(long = "verbose", short, global(true), parse(from_occurrences))]
pub verbosity: u8,
#[structopt(flatten)]
pub global: GlobalOptions,
/// Subcommand to run in this invocation.
#[structopt(subcommand)]
pub subcommand: Subcommand,
}
/// All of Rojo's subcommands.
#[derive(Debug, StructOpt)]
pub struct GlobalOptions {
/// Sets verbosity level. Can be specified multiple times.
#[structopt(long("verbose"), short, global(true), parse(from_occurrences))]
pub verbosity: u8,
/// Set color behavior. Valid values are auto, always, and never.
#[structopt(long("color"), global(true), default_value("auto"))]
pub color: ColorChoice,
}
#[derive(Debug, Clone, Copy)]
pub enum ColorChoice {
Auto,
Always,
Never,
}
impl FromStr for ColorChoice {
type Err = ColorChoiceParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"auto" => Ok(ColorChoice::Auto),
"always" => Ok(ColorChoice::Always),
"never" => Ok(ColorChoice::Never),
_ => Err(ColorChoiceParseError {
attempted: source.to_owned(),
}),
}
}
}
impl From<ColorChoice> for termcolor::ColorChoice {
fn from(value: ColorChoice) -> Self {
match value {
ColorChoice::Auto => termcolor::ColorChoice::Auto,
ColorChoice::Always => termcolor::ColorChoice::Always,
ColorChoice::Never => termcolor::ColorChoice::Never,
}
}
}
impl From<ColorChoice> for env_logger::WriteStyle {
fn from(value: ColorChoice) -> Self {
match value {
ColorChoice::Auto => env_logger::WriteStyle::Auto,
ColorChoice::Always => env_logger::WriteStyle::Always,
ColorChoice::Never => env_logger::WriteStyle::Never,
}
}
}
#[derive(Debug, Error)]
#[error("Invalid color choice '{attempted}'. Valid values are: auto, always, never")]
pub struct ColorChoiceParseError {
attempted: String,
}
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Creates a new Rojo project.

View File

@@ -3,25 +3,19 @@ use std::{
sync::Arc,
};
use anyhow::Result;
use memofs::Vfs;
use snafu::Snafu;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use crate::{cli::ServeCommand, serve_session::ServeSession, web::LiveServer};
use crate::{
cli::{GlobalOptions, ServeCommand},
serve_session::ServeSession,
web::LiveServer,
};
const DEFAULT_PORT: u16 = 34872;
#[derive(Debug, Snafu)]
pub struct ServeError(Error);
#[derive(Debug, Snafu)]
enum Error {}
pub fn serve(options: ServeCommand) -> Result<(), ServeError> {
Ok(serve_inner(options)?)
}
fn serve_inner(options: ServeCommand) -> Result<(), Error> {
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
let vfs = Vfs::new_default();
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project()));
@@ -33,14 +27,14 @@ fn serve_inner(options: ServeCommand) -> Result<(), Error> {
let server = LiveServer::new(session);
let _ = show_start_message(port);
let _ = show_start_message(port, global.color.into());
server.start(port);
Ok(())
}
fn show_start_message(port: u16) -> io::Result<()> {
let writer = BufferWriter::stdout(ColorChoice::Auto);
fn show_start_message(port: u16, color: ColorChoice) -> io::Result<()> {
let writer = BufferWriter::stdout(color);
let mut buffer = writer.buffer();
writeln!(&mut buffer, "Rojo server listening:")?;

View File

@@ -1,45 +1,30 @@
use memofs::Vfs;
use reqwest::header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT};
use snafu::{ResultExt, Snafu};
use thiserror::Error;
use crate::{auth_cookie::get_auth_cookie, cli::UploadCommand, common_setup};
use crate::{auth_cookie::get_auth_cookie, cli::UploadCommand, serve_session::ServeSession};
#[derive(Debug, Snafu)]
pub struct UploadError(Error);
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
enum Error {
#[snafu(display(
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
))]
#[error("Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
NeedAuthCookie,
#[snafu(display("XML model file encode error: {}", source))]
XmlModel { source: rbx_xml::EncodeError },
#[snafu(display("HTTP error: {}", source))]
Http { source: reqwest::Error },
#[snafu(display("Roblox API error: {}", body))]
#[error("The Roblox API returned an unexpected error: {body}")]
RobloxApi { body: String },
}
pub fn upload(options: UploadCommand) -> Result<(), UploadError> {
Ok(upload_inner(options)?)
}
fn upload_inner(options: UploadCommand) -> Result<(), Error> {
pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
let cookie = options
.cookie
.clone()
.or_else(get_auth_cookie)
.ok_or(Error::NeedAuthCookie)?;
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
let (_maybe_project, tree) = common_setup::start(&options.absolute_project(), &vfs);
let session = ServeSession::new(vfs, &options.absolute_project());
let tree = session.tree();
let inner_tree = tree.inner();
let root_id = inner_tree.get_root_id();
let root_instance = inner_tree.get_instance(root_id).unwrap();
@@ -55,7 +40,7 @@ fn upload_inner(options: UploadCommand) -> Result<(), Error> {
let config = rbx_xml::EncodeOptions::new()
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config).context(XmlModel)?;
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config)?;
let url = format!(
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
@@ -72,13 +57,13 @@ fn upload_inner(options: UploadCommand) -> Result<(), Error> {
.header(CONTENT_TYPE, "application/xml")
.header(ACCEPT, "application/json")
.body(buffer)
.send()
.context(Http)?;
.send()?;
if !response.status().is_success() {
return Err(Error::RobloxApi {
body: response.text().context(Http)?,
});
body: response.text()?,
}
.into());
}
Ok(())

View File

@@ -1,57 +0,0 @@
//! Initialization routines that are used by more than one Rojo command or
//! utility.
use std::path::Path;
use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
use crate::{
project::Project,
snapshot::{
apply_patch_set, compute_patch_set, InstanceContext, InstancePropertiesWithMeta,
PathIgnoreRule, RojoTree,
},
snapshot_middleware::snapshot_from_vfs,
};
pub fn start(fuzzy_project_path: &Path, vfs: &Vfs) -> (Option<Project>, RojoTree) {
log::trace!("Loading project file from {}", fuzzy_project_path.display());
let maybe_project = Project::load_fuzzy(fuzzy_project_path).expect("TODO: Project load failed");
log::trace!("Constructing initial tree");
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
let mut instance_context = InstanceContext::default();
if let Some(project) = &maybe_project {
let rules = project.glob_ignore_paths.iter().map(|glob| PathIgnoreRule {
glob: glob.clone(),
base_path: project.folder_location().to_path_buf(),
});
instance_context.add_path_ignore_rules(rules);
}
log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, vfs, &fuzzy_project_path)
.expect("snapshot failed")
.expect("snapshot did not return an instance");
log::trace!("Computing patch set");
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
log::trace!("Applying patch set");
apply_patch_set(&mut tree, patch_set);
(maybe_project, tree)
}

View File

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

View File

@@ -6,22 +6,26 @@ use std::{
use rbx_dom_weak::UnresolvedRbxValue;
use serde::{Deserialize, Serialize};
use snafu::{ResultExt, Snafu};
use thiserror::Error;
use crate::glob::Glob;
static PROJECT_FILENAME: &str = "default.project.json";
/// Error type returned by any function that handles projects.
#[derive(Debug, Snafu)]
pub struct ProjectError(Error);
#[derive(Debug, Error)]
#[error(transparent)]
pub struct ProjectError(#[from] Error);
#[derive(Debug, Snafu)]
#[derive(Debug, Error)]
enum Error {
/// A general IO error occurred.
Io { source: io::Error, path: PathBuf },
#[error("Rojo project I/O error")]
Io {
#[from]
source: io::Error,
},
/// An error with JSON parsing occurred.
#[error("Error parsing Rojo project")]
Json {
source: serde_json::Error,
path: PathBuf,
@@ -125,14 +129,14 @@ impl Project {
}
}
fn load_exact(project_file_location: &Path) -> Result<Self, ProjectError> {
let contents = fs::read_to_string(project_file_location).context(Io {
path: project_file_location,
})?;
fn load_exact(project_file_location: &Path) -> Result<Self, Error> {
let contents = fs::read_to_string(project_file_location)?;
let mut project: Project = serde_json::from_str(&contents).context(Json {
path: project_file_location,
})?;
let mut project: Project =
serde_json::from_str(&contents).map_err(|source| Error::Json {
source,
path: project_file_location.to_owned(),
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
@@ -140,10 +144,6 @@ impl Project {
Ok(project)
}
pub fn save(&self) -> Result<(), ProjectError> {
unimplemented!()
}
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
fn check_compatibility(&self) {

View File

@@ -7,14 +7,18 @@ use std::{
use crossbeam_channel::Sender;
use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
use crate::{
change_processor::ChangeProcessor,
common_setup,
message_queue::MessageQueue,
project::Project,
session_id::SessionId,
snapshot::{AppliedPatchSet, PatchSet, RojoTree},
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext,
InstancePropertiesWithMeta, PatchSet, PathIgnoreRule, RojoTree,
},
snapshot_middleware::snapshot_from_vfs,
};
/// Contains all of the state for a Rojo serve session.
@@ -86,7 +90,7 @@ pub struct ServeSession {
/// block to prevent needing to spread Send + Sync + 'static into everything
/// that handles ServeSession.
impl ServeSession {
/// Start a new serve session from the given in-memory filesystem and start
/// Start a new serve session from the given in-memory filesystem and start
/// path.
///
/// The project file is expected to be loaded out-of-band since it's
@@ -94,12 +98,45 @@ impl ServeSession {
/// in-memory filesystem layer.
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Self {
let start_path = start_path.as_ref();
log::trace!("Starting new ServeSession at path {}", start_path.display(),);
let start_time = Instant::now();
let (root_project, tree) = common_setup::start(start_path, &vfs);
log::trace!("Starting new ServeSession at path {}", start_path.display());
log::trace!("Loading project file from {}", start_path.display());
let root_project = Project::load_fuzzy(start_path).expect("TODO: Project load failed");
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
let mut instance_context = InstanceContext::default();
if let Some(project) = &root_project {
let rules = project.glob_ignore_paths.iter().map(|glob| PathIgnoreRule {
glob: glob.clone(),
base_path: project.folder_location().to_path_buf(),
});
instance_context.add_path_ignore_rules(rules);
}
log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)
.expect("snapshot failed")
.expect("snapshot did not return an instance");
log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set);
let session_id = SessionId::new();
let message_queue = MessageQueue::new();

View File

@@ -1,56 +1,62 @@
use std::{error::Error, fmt, io, path::PathBuf};
use std::{io, path::PathBuf};
use snafu::Snafu;
use thiserror::Error;
#[derive(Debug)]
pub struct SnapshotError {
detail: SnapshotErrorDetail,
path: Option<PathBuf>,
#[derive(Debug, Error)]
pub enum SnapshotError {
#[error("file name had malformed Unicode")]
FileNameBadUnicode { path: PathBuf },
#[error("file had malformed Unicode contents")]
FileContentsBadUnicode {
source: std::str::Utf8Error,
path: PathBuf,
},
#[error("malformed project file")]
MalformedProject {
source: serde_json::Error,
path: PathBuf,
},
#[error("malformed .model.json file")]
MalformedModelJson {
source: serde_json::Error,
path: PathBuf,
},
#[error("malformed .meta.json file")]
MalformedMetaJson {
source: serde_json::Error,
path: PathBuf,
},
#[error(transparent)]
Io {
#[from]
source: io::Error,
},
}
impl SnapshotError {
pub fn new(detail: SnapshotErrorDetail, path: Option<impl Into<PathBuf>>) -> Self {
Self {
detail,
path: path.map(Into::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>) -> Self {
Self {
detail: SnapshotErrorDetail::FileDidNotExist,
path: Some(path.into()),
}
}
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> Self {
Self {
detail: SnapshotErrorDetail::FileNameBadUnicode,
path: Some(path.into()),
}
Self::FileNameBadUnicode { path: path.into() }
}
pub(crate) fn file_contents_bad_unicode(
source: std::str::Utf8Error,
path: impl Into<PathBuf>,
) -> Self {
Self {
detail: SnapshotErrorDetail::FileContentsBadUnicode { source },
path: Some(path.into()),
Self::FileContentsBadUnicode {
source,
path: path.into(),
}
}
pub(crate) fn malformed_project(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
Self {
detail: SnapshotErrorDetail::MalformedProject { source },
path: Some(path.into()),
Self::MalformedProject {
source,
path: path.into(),
}
}
@@ -58,82 +64,16 @@ impl SnapshotError {
source: serde_json::Error,
path: impl Into<PathBuf>,
) -> Self {
Self {
detail: SnapshotErrorDetail::MalformedModelJson { source },
path: Some(path.into()),
Self::MalformedModelJson {
source,
path: 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()),
Self::MalformedMetaJson {
source,
path: path.into(),
}
}
}
impl Error for SnapshotError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.detail.source()
}
}
impl fmt::Display for SnapshotError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &self.path {
Some(path) => write!(formatter, "{} in path {}", self.detail, path.display()),
None => write!(formatter, "{}", self.detail),
}
}
}
impl From<io::Error> for SnapshotError {
fn from(inner: io::Error) -> Self {
Self::new(inner.into(), Option::<PathBuf>::None)
}
}
impl From<rlua::Error> for SnapshotError {
fn from(error: rlua::Error) -> Self {
Self::new(error.into(), Option::<PathBuf>::None)
}
}
#[derive(Debug, Snafu)]
pub enum SnapshotErrorDetail {
#[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,
#[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(source: io::Error) -> Self {
SnapshotErrorDetail::IoError { source }
}
}
impl From<rlua::Error> for SnapshotErrorDetail {
fn from(source: rlua::Error) -> Self {
SnapshotErrorDetail::Lua { source }
}
}

View File

@@ -1,7 +1,7 @@
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
//! JSON.
use std::{collections::HashMap, sync::Arc};
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use futures::{Future, Stream};
@@ -10,12 +10,12 @@ use rbx_dom_weak::RbxId;
use crate::{
serve_session::ServeSession,
snapshot::{PatchSet, PatchUpdate},
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{
interface::{
ErrorResponse, Instance, InstanceMetadata as WebInstanceMetadata, InstanceUpdate,
ReadResponse, ServerInfoResponse, SubscribeMessage, SubscribeResponse, WriteRequest,
WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
OpenResponse, ReadResponse, ServerInfoResponse, SubscribeMessage, SubscribeResponse,
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
},
util::{json, json_ok},
},
@@ -39,11 +39,12 @@ impl Service for ApiService {
(&Method::GET, path) if path.starts_with("/api/subscribe/") => {
self.handle_api_subscribe(request)
}
(&Method::POST, "/api/write") if cfg!(feature = "unstable_two_way_sync") => {
self.handle_api_write(request)
(&Method::POST, path) if path.starts_with("/api/open/") => {
self.handle_api_open(request)
}
(&Method::POST, "/api/write") => self.handle_api_write(request),
(_method, path) => json(
ErrorResponse::not_found(format!("Route not found: {}", path)),
StatusCode::NOT_FOUND,
@@ -233,4 +234,76 @@ impl ApiService {
instances,
})
}
/// Open a script with the given ID in the user's default text editor.
fn handle_api_open(&self, request: Request<Body>) -> <Self as Service>::Future {
let argument = &request.uri().path()["/api/open/".len()..];
let requested_id = match RbxId::parse_str(argument) {
Some(id) => id,
None => {
return json(
ErrorResponse::bad_request("Invalid instance ID"),
StatusCode::BAD_REQUEST,
);
}
};
let tree = self.serve_session.tree();
let instance = match tree.get_instance(requested_id) {
Some(instance) => instance,
None => {
return json(
ErrorResponse::bad_request("Instance not found"),
StatusCode::NOT_FOUND,
);
}
};
let script_path = match pick_script_path(instance) {
Some(path) => path,
None => {
return json(
ErrorResponse::bad_request(
"No appropriate file could be found to open this script",
),
StatusCode::NOT_FOUND,
);
}
};
let _ = opener::open(script_path);
json_ok(&OpenResponse {
session_id: self.serve_session.session_id(),
})
}
}
/// If this instance is represented by a script, try to find the correct .lua
/// file to open to edit it.
fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
match instance.class_name() {
"Script" | "LocalScript" | "ModuleScript" => {}
_ => return None,
}
// Pick the first listed relevant path that has an extension of .lua that
// exists.
instance
.metadata()
.relevant_paths
.iter()
.find(|path| {
// We should only ever open Lua files to be safe.
match path.extension().and_then(|ext| ext.to_str()) {
Some("lua") => {}
_ => return false,
}
fs::metadata(path)
.map(|meta| meta.is_file())
.unwrap_or(false)
})
.map(|path| path.to_owned())
}

View File

@@ -128,6 +128,13 @@ pub struct SubscribeResponse<'a> {
pub messages: Vec<SubscribeMessage<'a>>,
}
/// Response body from /api/open/{id}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenResponse {
pub session_id: SessionId,
}
/// General response type returned from all Rojo routes
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]