Compare commits

...

26 Commits

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

View File

@@ -1,6 +1,12 @@
# Rojo Changelog # Rojo Changelog
## Unreleased Changes for 0.6.x ## 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) ## [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 `--watch` argument to `rojo build`. ([#284](https://github.com/rojo-rbx/rojo/pull/284))

View File

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

41
Cargo.lock generated
View File

@@ -21,6 +21,11 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@@ -520,6 +525,11 @@ name = "foreign-types-shared"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "fsevent" name = "fsevent"
version = "0.4.0" version = "0.4.0"
@@ -912,9 +922,10 @@ dependencies = [
[[package]] [[package]]
name = "memofs" name = "memofs"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@@ -1661,11 +1672,13 @@ dependencies = [
name = "rojo" name = "rojo"
version = "0.6.0-alpha.3" version = "0.6.0-alpha.3"
dependencies = [ 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)", "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)", "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)", "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)", "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)", "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)", "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)", "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)", "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1675,7 +1688,7 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "paste 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1692,10 +1705,10 @@ dependencies = [
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde 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_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)", "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)", "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)", "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)", "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)", "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)", "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)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2046,6 +2059,24 @@ dependencies = [
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.0.1" version = "1.0.1"
@@ -2525,6 +2556,7 @@ dependencies = [
"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" "checksum 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 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 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 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 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" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
@@ -2582,6 +2614,7 @@ dependencies = [
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum 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 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 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 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 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" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
@@ -2740,6 +2773,8 @@ dependencies = [
"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum 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 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 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 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 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" "checksum tinytemplate 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "57a3c6667d3e65eb1bc3aed6fd14011c6cbc3a0665218ab7f5daf040b9ec371a"

View File

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

View File

@@ -13,8 +13,8 @@
<a href="https://crates.io/crates/rojo"> <a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /> <img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
</a> </a>
<a href="https://rojo.space/docs/0.5.x"> <a href="https://rojo.space/docs">
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo 0.5.x Documentation" /> <img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo 0.5.x Documentation" />
</a> </a>
</div> </div>
@@ -34,11 +34,10 @@ Rojo enables:
* Streaming `rbxmx` and `rbxm` models into your game in real time * Streaming `rbxmx` and `rbxm` models into your game in real time
* Packaging and deploying your project to Roblox.com from the command line * 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 * 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 * Import custom instances like MoonScript code
## [Documentation](https://rojo.space/docs) ## [Documentation](https://rojo.space/docs)

View File

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

View File

@@ -2,5 +2,8 @@
## Unreleased Changes ## 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) ## 0.1.0 (2020-03-10)
* Initial release * Initial release

View File

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

View File

@@ -1,4 +1,3 @@
use std::fs;
use std::io; use std::io;
use std::path::Path; use std::path::Path;
use std::sync::mpsc; use std::sync::mpsc;
@@ -55,15 +54,15 @@ impl StdBackend {
impl VfsBackend for StdBackend { impl VfsBackend for StdBackend {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> { 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<()> { 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> { 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?; let mut entries = entries?;
entries.sort_by_cached_key(|entry| entry.file_name()); 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<()> { 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<()> { 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> { fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
let inner = fs::metadata(path)?; let inner = fs_err::metadata(path)?;
Ok(Metadata { Ok(Metadata {
is_file: inner.is_file(), is_file: inner.is_file(),

View File

@@ -233,4 +233,19 @@ function ApiContext:retrieveMessages()
end) end)
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 return ApiContext

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
local StudioService = game:GetService("StudioService")
local Log = require(script.Parent.Parent.Log) local Log = require(script.Parent.Parent.Log)
local Fmt = require(script.Parent.Parent.Fmt) local Fmt = require(script.Parent.Parent.Fmt)
local t = require(script.Parent.Parent.t) local t = require(script.Parent.Parent.t)
@@ -43,6 +45,8 @@ ServeSession.Status = Status
local validateServeOptions = t.strictInterface({ local validateServeOptions = t.strictInterface({
apiContext = t.table, apiContext = t.table,
openScriptsExternally = t.boolean,
twoWaySync = t.boolean,
}) })
function ServeSession.new(options) function ServeSession.new(options)
@@ -57,12 +61,28 @@ function ServeSession.new(options)
local instanceMap = InstanceMap.new(onInstanceChanged) local instanceMap = InstanceMap.new(onInstanceChanged)
local reconciler = Reconciler.new(instanceMap) 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 = { self = {
__status = Status.NotStarted, __status = Status.NotStarted,
__apiContext = options.apiContext, __apiContext = options.apiContext,
__openScriptsExternally = options.openScriptsExternally,
__twoWaySync = options.twoWaySync,
__reconciler = reconciler, __reconciler = reconciler,
__instanceMap = instanceMap, __instanceMap = instanceMap,
__statusChangedCallback = nil, __statusChangedCallback = nil,
__connections = connections,
} }
setmetatable(self, ServeSession) setmetatable(self, ServeSession)
@@ -108,8 +128,39 @@ function ServeSession:stop()
self:__stopInternal() self:__stopInternal()
end 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) function ServeSession:__onInstanceChanged(instance, propertyName)
if not DevSettings:twoWaySyncEnabled() then if not self.__twoWaySync then
return return
end end
@@ -200,6 +251,11 @@ function ServeSession:__stopInternal(err)
self:__setStatus(Status.Disconnected, err) self:__setStatus(Status.Disconnected, err)
self.__apiContext:disconnect() self.__apiContext:disconnect()
self.__instanceMap:stop() self.__instanceMap:stop()
for _, connection in ipairs(self.__connections) do
connection:Disconnect()
end
self.__connections = {}
end end
function ServeSession:__setStatus(status, detail) function ServeSession:__setStatus(status, detail)

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
use std::{ use std::{
fs::File, fs::File,
io::{self, BufWriter, Write}, io::{BufWriter, Write},
}; };
use memofs::Vfs; use memofs::Vfs;
use snafu::{ResultExt, Snafu}; use thiserror::Error;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::{ use crate::{cli::BuildCommand, serve_session::ServeSession, snapshot::RojoTree};
cli::BuildCommand, project::ProjectError, serve_session::ServeSession, snapshot::RojoTree,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputKind { enum OutputKind {
@@ -31,45 +29,17 @@ fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
} }
} }
#[derive(Debug, Snafu)] #[derive(Debug, Error)]
pub struct BuildError(Error);
#[derive(Debug, Snafu)]
enum 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, 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 { fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown) rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
} }
pub fn build(options: BuildCommand) -> Result<(), BuildError> { pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
Ok(build_inner(options)?)
}
fn build_inner(options: BuildCommand) -> Result<(), Error> {
log::trace!("Constructing in-memory filesystem"); log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default(); let vfs = Vfs::new_default();
@@ -98,14 +68,14 @@ fn build_inner(options: BuildCommand) -> Result<(), Error> {
Ok(()) 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)?; let output_kind = detect_output_kind(&options).ok_or(Error::UnknownOutputKind)?;
log::debug!("Hoping to generate file of type {:?}", output_kind); log::debug!("Hoping to generate file of type {:?}", output_kind);
let root_id = tree.get_root_id(); let root_id = tree.get_root_id();
log::trace!("Opening output file for write"); 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); let mut file = BufWriter::new(file);
match output_kind { 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 // Model files include the root instance of the tree and all its
// descendants. // descendants.
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config()) rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
.context(XmlModelEncode)?;
} }
OutputKind::Rbxlx => { OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our // 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 root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children(); let top_level_ids = root_instance.children();
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config()) rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
.context(XmlModelEncode)?;
} }
OutputKind::Rbxm => { OutputKind::Rbxm => {
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?; 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(()) Ok(())
} }

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ use std::{
}; };
use structopt::StructOpt; use structopt::StructOpt;
use thiserror::Error;
pub use self::build::*; pub use self::build::*;
pub use self::doc::*; pub use self::doc::*;
@@ -27,16 +28,73 @@ pub use self::upload::*;
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
#[structopt(name = "Rojo", about, author)] #[structopt(name = "Rojo", about, author)]
pub struct Options { pub struct Options {
/// Sets verbosity level. Can be specified multiple times. #[structopt(flatten)]
#[structopt(long = "verbose", short, global(true), parse(from_occurrences))] pub global: GlobalOptions,
pub verbosity: u8,
/// Subcommand to run in this invocation. /// Subcommand to run in this invocation.
#[structopt(subcommand)] #[structopt(subcommand)]
pub subcommand: 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)] #[derive(Debug, StructOpt)]
pub enum Subcommand { pub enum Subcommand {
/// Creates a new Rojo project. /// Creates a new Rojo project.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,18 @@ use std::{
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use memofs::Vfs; use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
use crate::{ use crate::{
change_processor::ChangeProcessor, change_processor::ChangeProcessor,
common_setup,
message_queue::MessageQueue, message_queue::MessageQueue,
project::Project, project::Project,
session_id::SessionId, 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. /// 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 /// block to prevent needing to spread Send + Sync + 'static into everything
/// that handles ServeSession. /// that handles ServeSession.
impl 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. /// path.
/// ///
/// The project file is expected to be loaded out-of-band since it's /// The project file is expected to be loaded out-of-band since it's
@@ -94,12 +98,45 @@ impl ServeSession {
/// in-memory filesystem layer. /// in-memory filesystem layer.
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Self { pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Self {
let start_path = start_path.as_ref(); let start_path = start_path.as_ref();
log::trace!("Starting new ServeSession at path {}", start_path.display(),);
let start_time = Instant::now(); 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 session_id = SessionId::new();
let message_queue = MessageQueue::new(); let message_queue = MessageQueue::new();

View File

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

View File

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

View File

@@ -128,6 +128,13 @@ pub struct SubscribeResponse<'a> {
pub messages: Vec<SubscribeMessage<'a>>, 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 /// General response type returned from all Rojo routes
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]