mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 12:45:05 +00:00
Compare commits
45 Commits
memofs-v0.
...
memofs-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b433e53be | ||
|
|
28ddf40344 | ||
|
|
c1286db9c1 | ||
|
|
f13940262e | ||
|
|
9f0a6101b8 | ||
|
|
0b0fe01a7c | ||
|
|
85e098d5c8 | ||
|
|
e8d1faf4e2 | ||
|
|
2a46df1110 | ||
|
|
1601e6d26e | ||
|
|
0e4f6dea2b | ||
|
|
a2356773dc | ||
|
|
4a4da4737d | ||
|
|
2cefd1bf2e | ||
|
|
c5ce15fe34 | ||
|
|
76dea568c9 | ||
|
|
8e81140eff | ||
|
|
d58e1f0792 | ||
|
|
830c242751 | ||
|
|
91d45afd0f | ||
|
|
102c77b23e | ||
|
|
aa4039a2e7 | ||
|
|
c065ded440 | ||
|
|
f69096dadb | ||
|
|
363f95ba14 | ||
|
|
dcc15e8911 | ||
|
|
bd13047b58 | ||
|
|
1a83789c01 | ||
|
|
1cbe272e19 | ||
|
|
6de74b41b3 | ||
|
|
b0fc9ee507 | ||
|
|
a95ffe1d31 | ||
|
|
4119a510f5 | ||
|
|
cfa7f03815 | ||
|
|
9b4c89820d | ||
|
|
fe874720aa | ||
|
|
f7c0f33eb5 | ||
|
|
c1bf9d9dfc | ||
|
|
255bf439d3 | ||
|
|
2a31937b81 | ||
|
|
eb8964e1d1 | ||
|
|
fe0ca280a1 | ||
|
|
e8e3b7b985 | ||
|
|
c437507442 | ||
|
|
ca151b434e |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,8 +1,24 @@
|
||||
# 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))
|
||||
* 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.
|
||||
|
||||
@@ -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
|
||||
73
Cargo.lock
generated
73
Cargo.lock
generated
@@ -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"
|
||||
@@ -195,7 +200,7 @@ dependencies = [
|
||||
name = "clibrojo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rojo 0.6.0-alpha.2",
|
||||
"rojo 0.6.0-alpha.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -434,18 +439,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"
|
||||
@@ -532,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"
|
||||
@@ -924,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)",
|
||||
]
|
||||
|
||||
@@ -1073,6 +1072,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,12 +1670,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rojo"
|
||||
version = "0.6.0-alpha.2"
|
||||
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)",
|
||||
@@ -1678,8 +1688,9 @@ 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)",
|
||||
"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)",
|
||||
@@ -1694,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)",
|
||||
@@ -1716,13 +1727,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)",
|
||||
@@ -2048,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"
|
||||
@@ -2527,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"
|
||||
@@ -2574,7 +2604,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"
|
||||
@@ -2585,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"
|
||||
@@ -2646,6 +2676,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"
|
||||
@@ -2742,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"
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -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,15 +15,18 @@ exclude = [
|
||||
"/test-projects/**",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[features]
|
||||
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 = []
|
||||
|
||||
@@ -55,9 +58,14 @@ name = "build"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
@@ -67,6 +75,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"
|
||||
@@ -77,12 +86,11 @@ 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"] }
|
||||
memofs = { path = "memofs" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.6.2"
|
||||
|
||||
@@ -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)
|
||||
|
||||
11
assets/default-model-project/README.md
Normal file
11
assets/default-model-project/README.md
Normal 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).
|
||||
6
assets/default-model-project/default.project.json
Normal file
6
assets/default-model-project/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "{project_name}",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
3
assets/default-model-project/gitignore.txt
Normal file
3
assets/default-model-project/gitignore.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
5
assets/default-model-project/src-init.lua
Normal file
5
assets/default-model-project/src-init.lua
Normal file
@@ -0,0 +1,5 @@
|
||||
return {
|
||||
hello = function()
|
||||
print("Hello world, from {project_name}!")
|
||||
end,
|
||||
}
|
||||
17
assets/default-place-project/README.md
Normal file
17
assets/default-place-project/README.md
Normal 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).
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
assets/default-place-project/gitignore.txt
Normal file
6
assets/default-place-project/gitignore.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# Project place file
|
||||
/{project_name}.rbxlx
|
||||
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
30
design.gv
30
design.gv
@@ -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
|
||||
}
|
||||
9
memofs/CHANGELOG.md
Normal file
9
memofs/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# memofs Changelog
|
||||
|
||||
## 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
39
plugin/src/Components/Checkbox.lua
Normal file
39
plugin/src/Components/Checkbox.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
121
plugin/src/Components/PluginSettings.lua
Normal file
121
plugin/src/Components/PluginSettings.lua
Normal 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,
|
||||
}
|
||||
119
plugin/src/Components/SettingsPanel.lua
Normal file
119
plugin/src/Components/SettingsPanel.lua
Normal 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
131
src/bin.rs
131
src/bin.rs
@@ -1,65 +1,94 @@
|
||||
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};
|
||||
use librojo::cli::{self, GlobalOptions, 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>> {
|
||||
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()?,
|
||||
}
|
||||
|
||||
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.global.verbosity {
|
||||
0 => "info",
|
||||
1 => "info,librojo=debug",
|
||||
2 => "info,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))
|
||||
.write_style(options.global.color.into())
|
||||
.init();
|
||||
|
||||
if let Err(err) = run(options.global, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
4
src/cli/doc.rs
Normal file
4
src/cli/doc.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub fn doc() -> Result<(), anyhow::Error> {
|
||||
opener::open("https://rojo.space/docs")?;
|
||||
Ok(())
|
||||
}
|
||||
216
src/cli/init.rs
216
src/cli/init.rs
@@ -1,17 +1,213 @@
|
||||
use snafu::Snafu;
|
||||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use crate::cli::InitCommand;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub struct InitError(Error);
|
||||
use crate::cli::{InitCommand, InitKind};
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
enum Error {}
|
||||
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");
|
||||
|
||||
pub fn init(options: InitCommand) -> Result<(), InitError> {
|
||||
Ok(init_inner(options)?)
|
||||
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, Error)]
|
||||
enum Error {
|
||||
#[error("A project file named default.project.json already exists in this folder")]
|
||||
AlreadyExists,
|
||||
|
||||
#[error("git init failed")]
|
||||
GitInit,
|
||||
}
|
||||
|
||||
fn init_inner(_options: InitCommand) -> Result<(), Error> {
|
||||
unimplemented!("init command");
|
||||
pub fn init(options: InitCommand) -> Result<(), anyhow::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<(), anyhow::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<(), anyhow::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<(), 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.into());
|
||||
}
|
||||
}
|
||||
|
||||
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<(), anyhow::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<(), anyhow::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.into()),
|
||||
_ => Err(err.into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
file.write_all(contents.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Defines Rojo's CLI through structopt types.
|
||||
|
||||
mod build;
|
||||
mod doc;
|
||||
mod init;
|
||||
mod serve;
|
||||
mod upload;
|
||||
@@ -15,8 +16,10 @@ use std::{
|
||||
};
|
||||
|
||||
use structopt::StructOpt;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use self::build::*;
|
||||
pub use self::doc::*;
|
||||
pub use self::init::*;
|
||||
pub use self::serve::*;
|
||||
pub use self::upload::*;
|
||||
@@ -25,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.
|
||||
@@ -48,6 +108,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.
|
||||
|
||||
@@ -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:")?;
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
18
src/error.rs
Normal file
18
src/error.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ mod tree_view;
|
||||
|
||||
mod auth_cookie;
|
||||
mod change_processor;
|
||||
mod common_setup;
|
||||
mod error;
|
||||
mod glob;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,137 +1,79 @@
|
||||
use std::{error::Error, fmt, io, path::PathBuf};
|
||||
use std::{io, path::PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SnapshotError {
|
||||
detail: SnapshotErrorDetail,
|
||||
path: Option<PathBuf>,
|
||||
use thiserror::Error;
|
||||
|
||||
#[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 {
|
||||
SnapshotError {
|
||||
detail,
|
||||
path: path.map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wrap(inner: impl Into<SnapshotErrorDetail>, path: impl Into<PathBuf>) -> Self {
|
||||
SnapshotError {
|
||||
detail: inner.into(),
|
||||
path: Some(path.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_did_not_exist(path: impl Into<PathBuf>) -> SnapshotError {
|
||||
SnapshotError {
|
||||
detail: SnapshotErrorDetail::FileDidNotExist,
|
||||
path: Some(path.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> SnapshotError {
|
||||
SnapshotError {
|
||||
detail: SnapshotErrorDetail::FileNameBadUnicode,
|
||||
path: Some(path.into()),
|
||||
}
|
||||
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> Self {
|
||||
Self::FileNameBadUnicode { path: 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 },
|
||||
path: Some(path.into()),
|
||||
) -> Self {
|
||||
Self::FileContentsBadUnicode {
|
||||
source,
|
||||
path: 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::MalformedProject {
|
||||
source,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn malformed_model_json(
|
||||
source: serde_json::Error,
|
||||
path: impl Into<PathBuf>,
|
||||
) -> SnapshotError {
|
||||
SnapshotError {
|
||||
detail: SnapshotErrorDetail::MalformedProject { inner },
|
||||
path: Some(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)]
|
||||
pub enum SnapshotErrorDetail {
|
||||
IoError { inner: io::Error },
|
||||
Lua { inner: rlua::Error },
|
||||
FileDidNotExist,
|
||||
FileNameBadUnicode,
|
||||
FileContentsBadUnicode { inner: std::str::Utf8Error },
|
||||
MalformedProject { inner: serde_json::Error },
|
||||
}
|
||||
|
||||
impl From<io::Error> for SnapshotErrorDetail {
|
||||
fn from(inner: io::Error) -> Self {
|
||||
SnapshotErrorDetail::IoError { inner }
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
) -> Self {
|
||||
Self::MalformedModelJson {
|
||||
source,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn malformed_meta_json(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
|
||||
Self::MalformedMetaJson {
|
||||
source,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user