mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 20:55:50 +00:00
Compare commits
54 Commits
v0.6.0-alp
...
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 | ||
|
|
10ba74c21e | ||
|
|
6191b6371d | ||
|
|
5be4175ac3 | ||
|
|
f61f3671a6 | ||
|
|
477e0ada32 | ||
|
|
a884f693ae | ||
|
|
3107b1b21b | ||
|
|
04529de7b3 | ||
|
|
199a39208c |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,6 +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
|
||||
86
Cargo.lock
generated
86
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"
|
||||
@@ -922,6 +920,15 @@ dependencies = [
|
||||
"rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memofs"
|
||||
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)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
@@ -1065,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"
|
||||
@@ -1655,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)",
|
||||
@@ -1670,7 +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.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)",
|
||||
@@ -1685,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)",
|
||||
@@ -1707,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)",
|
||||
@@ -2039,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"
|
||||
@@ -2364,14 +2402,6 @@ name = "version_check"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "vfs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.1"
|
||||
@@ -2526,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"
|
||||
@@ -2573,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"
|
||||
@@ -2584,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"
|
||||
@@ -2645,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"
|
||||
@@ -2741,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"
|
||||
|
||||
25
Cargo.toml
25
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 = []
|
||||
|
||||
@@ -32,14 +35,14 @@ members = [
|
||||
"rojo-test",
|
||||
"rojo-insta-ext",
|
||||
"clibrojo",
|
||||
"vfs",
|
||||
"memofs",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
".",
|
||||
"rojo-test",
|
||||
"rojo-insta-ext",
|
||||
"vfs",
|
||||
"memofs",
|
||||
]
|
||||
|
||||
[lib]
|
||||
@@ -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,9 +86,10 @@ 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"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
@@ -95,5 +105,4 @@ paste = "0.1"
|
||||
pretty_assertions = "0.6.1"
|
||||
serde_yaml = "0.8.9"
|
||||
tempfile = "3.0"
|
||||
tokio = "0.1.22"
|
||||
walkdir = "2.1"
|
||||
|
||||
@@ -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,14 +1,16 @@
|
||||
[package]
|
||||
name = "vfs"
|
||||
version = "0.1.0"
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.1.1"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
homepage = "https://github.com/rojo-rbx/rojo"
|
||||
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"
|
||||
22
memofs/README.md
Normal file
22
memofs/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# memofs
|
||||
[](https://crates.io/crates/memofs)
|
||||
|
||||
Implementation of a virtual filesystem with a configurable backend and file
|
||||
watching.
|
||||
|
||||
memofs is currently an unstable minimum viable library. Its primary consumer is
|
||||
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
|
||||
|
||||
### Current Features
|
||||
* API similar to `std::fs`
|
||||
* Configurable backends
|
||||
* `StdBackend`, which uses `std::fs` and the `notify` crate
|
||||
* `NoopBackend`, which always throws errors
|
||||
* `InMemoryFs`, a simple in-memory filesystem useful for testing
|
||||
|
||||
### Future Features
|
||||
* Hash-based hierarchical memoization keys (hence the name)
|
||||
* Configurable caching (write-through, write-around, write-back)
|
||||
|
||||
## License
|
||||
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||
7
memofs/README.tpl
Normal file
7
memofs/README.tpl
Normal file
@@ -0,0 +1,7 @@
|
||||
# {{crate}}
|
||||
[](https://crates.io/crates/memofs)
|
||||
|
||||
{{readme}}
|
||||
|
||||
## License
|
||||
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||
@@ -1,32 +1,77 @@
|
||||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent, VfsSnapshot};
|
||||
|
||||
/// `VfsBackend` that reads from an in-memory filesystem, intended for setting
|
||||
/// up testing scenarios quickly.
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryBackend {
|
||||
entries: HashMap<PathBuf, Entry>,
|
||||
orphans: BTreeSet<PathBuf>,
|
||||
/// In-memory filesystem that can be used as a VFS backend.
|
||||
///
|
||||
/// Internally reference counted to enable giving a copy to
|
||||
/// [`Vfs`](struct.Vfs.html) and keeping the original to mutate the filesystem's
|
||||
/// state with.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InMemoryFs {
|
||||
inner: Arc<Mutex<InMemoryFsInner>>,
|
||||
}
|
||||
|
||||
impl MemoryBackend {
|
||||
impl InMemoryFs {
|
||||
/// Create a new empty `InMemoryFs`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: HashMap::new(),
|
||||
orphans: BTreeSet::new(),
|
||||
inner: Arc::new(Mutex::new(InMemoryFsInner::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a [`VfsSnapshot`](enum.VfsSnapshot.html) into a subtree of the
|
||||
/// in-memory filesystem.
|
||||
///
|
||||
/// This function will return an error if the operations required to apply
|
||||
/// the snapshot result in errors, like trying to create a file inside a
|
||||
/// file.
|
||||
pub fn load_snapshot<P: Into<PathBuf>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
snapshot: VfsSnapshot,
|
||||
) -> io::Result<()> {
|
||||
let path = path.into();
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.load_snapshot(path.into(), snapshot)
|
||||
}
|
||||
|
||||
/// Raises a filesystem change event.
|
||||
///
|
||||
/// If this `InMemoryFs` is being used as the backend of a
|
||||
/// [`Vfs`](struct.Vfs.html), then any listeners be notified of this event.
|
||||
pub fn raise_event(&mut self, event: VfsEvent) {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.event_sender.send(event).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InMemoryFsInner {
|
||||
entries: HashMap<PathBuf, Entry>,
|
||||
orphans: BTreeSet<PathBuf>,
|
||||
|
||||
event_receiver: Receiver<VfsEvent>,
|
||||
event_sender: Sender<VfsEvent>,
|
||||
}
|
||||
|
||||
impl InMemoryFsInner {
|
||||
fn new() -> Self {
|
||||
let (event_sender, event_receiver) = crossbeam_channel::unbounded();
|
||||
|
||||
Self {
|
||||
entries: HashMap::new(),
|
||||
orphans: BTreeSet::new(),
|
||||
event_receiver,
|
||||
event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_snapshot(&mut self, path: PathBuf, snapshot: VfsSnapshot) -> io::Result<()> {
|
||||
if let Some(parent_path) = path.parent() {
|
||||
if let Some(parent_entry) = self.entries.get_mut(parent_path) {
|
||||
if let Entry::Dir { children } = parent_entry {
|
||||
@@ -84,9 +129,11 @@ enum Entry {
|
||||
Dir { children: BTreeSet<PathBuf> },
|
||||
}
|
||||
|
||||
impl VfsBackend for MemoryBackend {
|
||||
impl VfsBackend for InMemoryFs {
|
||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
match self.entries.get(path) {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::File { contents }) => Ok(contents.clone()),
|
||||
Some(Entry::Dir { .. }) => must_be_file(path),
|
||||
None => not_found(path),
|
||||
@@ -94,8 +141,10 @@ impl VfsBackend for MemoryBackend {
|
||||
}
|
||||
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||
self.load_snapshot(
|
||||
path,
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
inner.load_snapshot(
|
||||
path.to_path_buf(),
|
||||
VfsSnapshot::File {
|
||||
contents: data.to_owned(),
|
||||
},
|
||||
@@ -103,7 +152,9 @@ impl VfsBackend for MemoryBackend {
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
match self.entries.get(path) {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::Dir { children }) => {
|
||||
let iter = children
|
||||
.clone()
|
||||
@@ -120,9 +171,11 @@ impl VfsBackend for MemoryBackend {
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
match self.entries.get(path) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::File { .. }) => {
|
||||
self.remove(path.to_owned());
|
||||
inner.remove(path.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
Some(Entry::Dir { .. }) => must_be_file(path),
|
||||
@@ -131,9 +184,11 @@ impl VfsBackend for MemoryBackend {
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
match self.entries.get(path) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::Dir { .. }) => {
|
||||
self.remove(path.to_owned());
|
||||
inner.remove(path.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
Some(Entry::File { .. }) => must_be_dir(path),
|
||||
@@ -142,7 +197,9 @@ impl VfsBackend for MemoryBackend {
|
||||
}
|
||||
|
||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||
match self.entries.get(path) {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::File { .. }) => Ok(Metadata { is_file: true }),
|
||||
Some(Entry::Dir { .. }) => Ok(Metadata { is_file: false }),
|
||||
None => not_found(path),
|
||||
@@ -150,7 +207,9 @@ impl VfsBackend for MemoryBackend {
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
inner.event_receiver.clone()
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
@@ -1,4 +1,23 @@
|
||||
mod memory_backend;
|
||||
/*!
|
||||
Implementation of a virtual filesystem with a configurable backend and file
|
||||
watching.
|
||||
|
||||
memofs is currently an unstable minimum viable library. Its primary consumer is
|
||||
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
|
||||
|
||||
## Current Features
|
||||
* API similar to `std::fs`
|
||||
* Configurable backends
|
||||
* `StdBackend`, which uses `std::fs` and the `notify` crate
|
||||
* `NoopBackend`, which always throws errors
|
||||
* `InMemoryFs`, a simple in-memory filesystem useful for testing
|
||||
|
||||
## Future Features
|
||||
* Hash-based hierarchical memoization keys (hence the name)
|
||||
* Configurable caching (write-through, write-around, write-back)
|
||||
*/
|
||||
|
||||
mod in_memory_fs;
|
||||
mod noop_backend;
|
||||
mod snapshot;
|
||||
mod std_backend;
|
||||
@@ -7,7 +26,7 @@ use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
pub use memory_backend::MemoryBackend;
|
||||
pub use in_memory_fs::InMemoryFs;
|
||||
pub use noop_backend::NoopBackend;
|
||||
pub use snapshot::VfsSnapshot;
|
||||
pub use std_backend::StdBackend;
|
||||
@@ -18,9 +37,9 @@ mod sealed {
|
||||
/// Sealing trait for VfsBackend.
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for MemoryBackend {}
|
||||
impl Sealed for NoopBackend {}
|
||||
impl Sealed for StdBackend {}
|
||||
impl Sealed for InMemoryFs {}
|
||||
}
|
||||
|
||||
/// Trait that transforms `io::Result<T>` into `io::Result<Option<T>>`.
|
||||
@@ -107,6 +126,8 @@ impl Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an event that a filesystem can raise that might need to be
|
||||
/// handled.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum VfsEvent {
|
||||
@@ -176,6 +197,10 @@ impl VfsInner {
|
||||
}
|
||||
|
||||
/// A virtual filesystem with a configurable backend.
|
||||
///
|
||||
/// All operations on the Vfs take a lock on an internal backend. For performing
|
||||
/// large batches of operations, it might be more performant to call `lock()`
|
||||
/// and use [`VfsLock`](struct.VfsLock.html) instead.
|
||||
pub struct Vfs {
|
||||
inner: Mutex<VfsInner>,
|
||||
}
|
||||
@@ -197,6 +222,7 @@ impl Vfs {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually lock the Vfs, useful for large batches of operations.
|
||||
pub fn lock(&self) -> VfsLock<'_> {
|
||||
VfsLock {
|
||||
inner: self.inner.lock().unwrap(),
|
||||
@@ -284,7 +310,9 @@ impl Vfs {
|
||||
}
|
||||
}
|
||||
|
||||
/// A locked handle to a `Vfs`, created by `Vfs::lock`.
|
||||
/// A locked handle to a [`Vfs`](struct.Vfs.html), created by `Vfs::lock`.
|
||||
///
|
||||
/// Implements roughly the same API as [`Vfs`](struct.Vfs.html).
|
||||
pub struct VfsLock<'a> {
|
||||
inner: MutexGuard<'a, VfsInner>,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A slice of a tree of files. Can be loaded into an
|
||||
/// [`InMemoryFs`](struct.InMemoryFs.html).
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum VfsSnapshot {
|
||||
File {
|
||||
@@ -26,4 +29,16 @@ impl VfsSnapshot {
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_file() -> Self {
|
||||
Self::File {
|
||||
contents: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_dir() -> Self {
|
||||
Self::Dir {
|
||||
children: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
@@ -55,19 +54,22 @@ 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 inner = fs::read_dir(path)?.map(|entry| {
|
||||
Ok(DirEntry {
|
||||
path: entry?.path(),
|
||||
})
|
||||
});
|
||||
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||
let mut entries = entries?;
|
||||
|
||||
entries.sort_by_cached_key(|entry| entry.file_name());
|
||||
|
||||
let inner = entries
|
||||
.into_iter()
|
||||
.map(|entry| Ok(DirEntry { path: entry.path() }));
|
||||
|
||||
Ok(ReadDir {
|
||||
inner: Box::new(inner),
|
||||
@@ -75,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(),
|
||||
Submodule plugin/modules/roact updated: b1db3f82a2...f7d2f1ce1d
@@ -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
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
|
||||
@@ -61,6 +62,7 @@ local AppStatus = strict("AppStatus", {
|
||||
Connecting = "Connecting",
|
||||
Connected = "Connected",
|
||||
Error = "Error",
|
||||
Settings = "Settings",
|
||||
})
|
||||
|
||||
local App = Roact.Component:extend("App")
|
||||
@@ -73,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)
|
||||
|
||||
@@ -108,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)
|
||||
@@ -154,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")
|
||||
@@ -168,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 = {
|
||||
@@ -186,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({
|
||||
@@ -199,7 +215,7 @@ function App:render()
|
||||
}
|
||||
end
|
||||
|
||||
return Roact.createElement(Roact.Portal, {
|
||||
return e(Roact.Portal, {
|
||||
target = self.dockWidget,
|
||||
}, children)
|
||||
end
|
||||
|
||||
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
|
||||
@@ -4,13 +4,14 @@ local Plugin = Rojo.Plugin
|
||||
local Roact = require(Rojo.Roact)
|
||||
|
||||
local Config = require(Plugin.Config)
|
||||
local Theme = require(Plugin.Theme)
|
||||
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
local Panel = require(Plugin.Components.Panel)
|
||||
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,137 +26,158 @@ end
|
||||
|
||||
function ConnectPanel:render()
|
||||
local startSession = self.props.startSession
|
||||
local openSettings = self.props.openSettings
|
||||
|
||||
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, {
|
||||
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.PrimaryColor,
|
||||
return Theme.with(function(theme)
|
||||
return PluginSettings.with(function(settings)
|
||||
return e(Panel, nil, {
|
||||
Layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
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 = {
|
||||
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,
|
||||
}),
|
||||
|
||||
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.PrimaryColor,
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
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,
|
||||
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 = 1,
|
||||
text = "Settings",
|
||||
secondary = true,
|
||||
onClick = function()
|
||||
if openSettings ~= nil then
|
||||
openSettings()
|
||||
end
|
||||
end,
|
||||
}),
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return ConnectPanel
|
||||
@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||
|
||||
local Plugin = script:FindFirstAncestor("Plugin")
|
||||
|
||||
local Theme = require(Plugin.Theme)
|
||||
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
local Panel = require(Plugin.Components.Panel)
|
||||
local FitText = require(Plugin.Components.FitText)
|
||||
|
||||
@@ -12,23 +11,25 @@ local e = Roact.createElement
|
||||
local ConnectingPanel = Roact.Component:extend("ConnectingPanel")
|
||||
|
||||
function ConnectingPanel:render()
|
||||
return e(Panel, nil, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 8),
|
||||
}),
|
||||
return Theme.with(function(theme)
|
||||
return e(Panel, nil, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 8),
|
||||
}),
|
||||
|
||||
Text = e(FitText, {
|
||||
Padding = Vector2.new(12, 6),
|
||||
Font = Theme.ButtonFont,
|
||||
TextSize = 18,
|
||||
Text = "Connecting...",
|
||||
TextColor3 = Theme.PrimaryColor,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
})
|
||||
Text = e(FitText, {
|
||||
Padding = Vector2.new(12, 6),
|
||||
Font = theme.ButtonFont,
|
||||
TextSize = 18,
|
||||
Text = "Connecting...",
|
||||
TextColor3 = theme.Text1,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return ConnectingPanel
|
||||
@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||
|
||||
local Plugin = script:FindFirstAncestor("Plugin")
|
||||
|
||||
local Theme = require(Plugin.Theme)
|
||||
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
local Panel = require(Plugin.Components.Panel)
|
||||
local FitText = require(Plugin.Components.FitText)
|
||||
local FormButton = require(Plugin.Components.FormButton)
|
||||
@@ -15,32 +14,34 @@ local ConnectionActivePanel = Roact.Component:extend("ConnectionActivePanel")
|
||||
function ConnectionActivePanel:render()
|
||||
local stopSession = self.props.stopSession
|
||||
|
||||
return e(Panel, nil, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 8),
|
||||
}),
|
||||
return Theme.with(function(theme)
|
||||
return e(Panel, nil, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 8),
|
||||
}),
|
||||
|
||||
Text = e(FitText, {
|
||||
Padding = Vector2.new(12, 6),
|
||||
Font = Theme.ButtonFont,
|
||||
TextSize = 18,
|
||||
Text = "Connected to Live-Sync Server",
|
||||
TextColor3 = Theme.PrimaryColor,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
Text = e(FitText, {
|
||||
Padding = Vector2.new(12, 6),
|
||||
Font = theme.ButtonFont,
|
||||
TextSize = 18,
|
||||
Text = "Connected to Live-Sync Server",
|
||||
TextColor3 = theme.Text1,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
DisconnectButton = e(FormButton, {
|
||||
layoutOrder = 2,
|
||||
text = "Disconnect",
|
||||
secondary = true,
|
||||
onClick = function()
|
||||
stopSession()
|
||||
end,
|
||||
}),
|
||||
})
|
||||
DisconnectButton = e(FormButton, {
|
||||
layoutOrder = 2,
|
||||
text = "Disconnect",
|
||||
secondary = true,
|
||||
onClick = function()
|
||||
stopSession()
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return ConnectionActivePanel
|
||||
@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||
|
||||
local Plugin = script:FindFirstAncestor("Plugin")
|
||||
|
||||
local Theme = require(Plugin.Theme)
|
||||
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
local Panel = require(Plugin.Components.Panel)
|
||||
local FitText = require(Plugin.Components.FitText)
|
||||
local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame)
|
||||
@@ -20,50 +19,52 @@ function ErrorPanel:render()
|
||||
local errorMessage = self.props.errorMessage
|
||||
local onDismiss = self.props.onDismiss
|
||||
|
||||
return e(Panel, nil, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 8),
|
||||
}),
|
||||
|
||||
ErrorContainer = e(FitScrollingFrame, {
|
||||
containerProps = {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
|
||||
Position = UDim2.new(0, HOR_PADDING, 0, 0),
|
||||
ScrollBarImageColor3 = Theme.PrimaryColor,
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||
},
|
||||
}, {
|
||||
Text = e(FitText, {
|
||||
Size = UDim2.new(1, 0, 0, 0),
|
||||
|
||||
LayoutOrder = 1,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
FitAxis = "Y",
|
||||
Font = Theme.ButtonFont,
|
||||
TextSize = 18,
|
||||
Text = errorMessage,
|
||||
TextWrap = true,
|
||||
TextColor3 = Theme.PrimaryColor,
|
||||
BackgroundTransparency = 1,
|
||||
return Theme.with(function(theme)
|
||||
return e(Panel, nil, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 8),
|
||||
}),
|
||||
}),
|
||||
|
||||
DismissButton = e(FormButton, {
|
||||
layoutOrder = 2,
|
||||
text = "Dismiss",
|
||||
secondary = true,
|
||||
onClick = function()
|
||||
onDismiss()
|
||||
end,
|
||||
}),
|
||||
})
|
||||
ErrorContainer = e(FitScrollingFrame, {
|
||||
containerProps = {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
|
||||
Position = UDim2.new(0, HOR_PADDING, 0, 0),
|
||||
ScrollBarImageColor3 = theme.Text1,
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||
},
|
||||
}, {
|
||||
Text = e(FitText, {
|
||||
Size = UDim2.new(1, 0, 0, 0),
|
||||
|
||||
LayoutOrder = 1,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
FitAxis = "Y",
|
||||
Font = theme.ButtonFont,
|
||||
TextSize = 18,
|
||||
Text = errorMessage,
|
||||
TextWrap = true,
|
||||
TextColor3 = theme.Text1,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
}),
|
||||
|
||||
DismissButton = e(FormButton, {
|
||||
layoutOrder = 2,
|
||||
text = "Dismiss",
|
||||
secondary = true,
|
||||
onClick = function()
|
||||
onDismiss()
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return ErrorPanel
|
||||
@@ -4,7 +4,7 @@ local Plugin = Rojo.Plugin
|
||||
local Roact = require(Rojo.Roact)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.Theme)
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
local FitList = require(Plugin.Components.FitList)
|
||||
local FitText = require(Plugin.Components.FitText)
|
||||
|
||||
@@ -20,43 +20,45 @@ local function FormButton(props)
|
||||
local textColor
|
||||
local backgroundColor
|
||||
|
||||
if props.secondary then
|
||||
textColor = Theme.AccentColor
|
||||
backgroundColor = Theme.SecondaryColor
|
||||
else
|
||||
textColor = Theme.SecondaryColor
|
||||
backgroundColor = Theme.AccentColor
|
||||
end
|
||||
return Theme.with(function(theme)
|
||||
if props.secondary then
|
||||
textColor = theme.Brand1
|
||||
backgroundColor = theme.Background2
|
||||
else
|
||||
textColor = theme.TextOnAccent
|
||||
backgroundColor = theme.Brand1
|
||||
end
|
||||
|
||||
return e(FitList, {
|
||||
containerKind = "ImageButton",
|
||||
containerProps = {
|
||||
LayoutOrder = layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
Image = RoundBox.asset,
|
||||
ImageRectOffset = RoundBox.offset,
|
||||
ImageRectSize = RoundBox.size,
|
||||
SliceCenter = RoundBox.center,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
ImageColor3 = backgroundColor,
|
||||
return e(FitList, {
|
||||
containerKind = "ImageButton",
|
||||
containerProps = {
|
||||
LayoutOrder = layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
Image = RoundBox.asset,
|
||||
ImageRectOffset = RoundBox.offset,
|
||||
ImageRectSize = RoundBox.size,
|
||||
SliceCenter = RoundBox.center,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
ImageColor3 = backgroundColor,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
if onClick ~= nil then
|
||||
onClick()
|
||||
end
|
||||
end,
|
||||
},
|
||||
}, {
|
||||
Text = e(FitText, {
|
||||
Kind = "TextLabel",
|
||||
Text = text,
|
||||
TextSize = 18,
|
||||
TextColor3 = textColor,
|
||||
Font = Theme.ButtonFont,
|
||||
Padding = Vector2.new(16, 8),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
})
|
||||
[Roact.Event.Activated] = function()
|
||||
if onClick ~= nil then
|
||||
onClick()
|
||||
end
|
||||
end,
|
||||
},
|
||||
}, {
|
||||
Text = e(FitText, {
|
||||
Kind = "TextLabel",
|
||||
Text = text,
|
||||
TextSize = 18,
|
||||
TextColor3 = textColor,
|
||||
Font = theme.ButtonFont,
|
||||
Padding = Vector2.new(16, 8),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return FormButton
|
||||
@@ -4,7 +4,7 @@ local Plugin = Rojo.Plugin
|
||||
local Roact = require(Rojo.Roact)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.Theme)
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -35,46 +35,48 @@ function FormTextInput:render()
|
||||
shownPlaceholder = placeholderValue
|
||||
end
|
||||
|
||||
return e("ImageLabel", {
|
||||
LayoutOrder = layoutOrder,
|
||||
Image = RoundBox.asset,
|
||||
ImageRectOffset = RoundBox.offset,
|
||||
ImageRectSize = RoundBox.size,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = RoundBox.center,
|
||||
ImageColor3 = Theme.SecondaryColor,
|
||||
Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
InputInner = e("TextBox", {
|
||||
return Theme.with(function(theme)
|
||||
return e("ImageLabel", {
|
||||
LayoutOrder = layoutOrder,
|
||||
Image = RoundBox.asset,
|
||||
ImageRectOffset = RoundBox.offset,
|
||||
ImageRectSize = RoundBox.size,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = RoundBox.center,
|
||||
ImageColor3 = theme.Background2,
|
||||
Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2),
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Font = Theme.InputFont,
|
||||
ClearTextOnFocus = false,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextSize = TEXT_SIZE,
|
||||
Text = value,
|
||||
PlaceholderText = shownPlaceholder,
|
||||
PlaceholderColor3 = Theme.LightTextColor,
|
||||
TextColor3 = Theme.PrimaryColor,
|
||||
}, {
|
||||
InputInner = e("TextBox", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Font = theme.InputFont,
|
||||
ClearTextOnFocus = false,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextSize = TEXT_SIZE,
|
||||
Text = value,
|
||||
PlaceholderText = shownPlaceholder,
|
||||
PlaceholderColor3 = theme.Text2,
|
||||
TextColor3 = theme.Text1,
|
||||
|
||||
[Roact.Change.Text] = function(rbx)
|
||||
onValueChange(rbx.Text)
|
||||
end,
|
||||
[Roact.Event.Focused] = function()
|
||||
self:setState({
|
||||
focused = true,
|
||||
})
|
||||
end,
|
||||
[Roact.Event.FocusLost] = function()
|
||||
self:setState({
|
||||
focused = false,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
})
|
||||
[Roact.Change.Text] = function(rbx)
|
||||
onValueChange(rbx.Text)
|
||||
end,
|
||||
[Roact.Event.Focused] = function()
|
||||
self:setState({
|
||||
focused = true,
|
||||
})
|
||||
end,
|
||||
[Roact.Event.FocusLost] = function()
|
||||
self:setState({
|
||||
focused = false,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return FormTextInput
|
||||
@@ -3,6 +3,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||
local Plugin = script:FindFirstAncestor("Plugin")
|
||||
|
||||
local RojoFooter = require(Plugin.Components.RojoFooter)
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -13,22 +14,25 @@ function Panel:init()
|
||||
end
|
||||
|
||||
function Panel:render()
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}),
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = theme.Background1,
|
||||
BorderSizePixel = 1,
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}),
|
||||
|
||||
Body = e("Frame", {
|
||||
Size = UDim2.new(0, 360, 1, -32),
|
||||
BackgroundTransparency = 1,
|
||||
}, self.props[Roact.Children]),
|
||||
Body = e("Frame", {
|
||||
Size = UDim2.new(0, 360, 1, -32),
|
||||
BackgroundTransparency = 1,
|
||||
}, self.props[Roact.Children]),
|
||||
|
||||
Footer = e(RojoFooter),
|
||||
})
|
||||
Footer = e(RojoFooter),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return Panel
|
||||
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,
|
||||
}
|
||||
@@ -6,7 +6,7 @@ local Roact = require(Rojo.Roact)
|
||||
local Config = require(Plugin.Config)
|
||||
local Version = require(Plugin.Version)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.Theme)
|
||||
local Theme = require(Plugin.Components.Theme)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -18,50 +18,53 @@ function RojoFooter:init()
|
||||
end
|
||||
|
||||
function RojoFooter:render()
|
||||
return e("Frame", {
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
BackgroundColor3 = Theme.SecondaryColor,
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 4),
|
||||
PaddingBottom = UDim.new(0, 4),
|
||||
PaddingLeft = UDim.new(0, 8),
|
||||
PaddingRight = UDim.new(0, 8),
|
||||
}),
|
||||
|
||||
LogoContainer = e("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Size = UDim2.new(0, 0, 0, 32),
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
BackgroundColor3 = theme.Background2,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 2,
|
||||
}, {
|
||||
Logo = e("ImageLabel", {
|
||||
Image = Assets.Images.Logo,
|
||||
Size = UDim2.new(0, 80, 0, 40),
|
||||
ScaleType = Enum.ScaleType.Fit,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 1, -10),
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 4),
|
||||
PaddingBottom = UDim.new(0, 4),
|
||||
PaddingLeft = UDim.new(0, 8),
|
||||
PaddingRight = UDim.new(0, 8),
|
||||
}),
|
||||
}),
|
||||
|
||||
Version = e("TextLabel", {
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Font = Theme.TitleFont,
|
||||
TextSize = 18,
|
||||
Text = Version.display(Config.version),
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
TextColor3 = Theme.LightTextColor,
|
||||
BackgroundTransparency = 1,
|
||||
LogoContainer = e("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||
self.setFooterVersionSize(rbx.AbsoluteSize)
|
||||
end,
|
||||
}),
|
||||
})
|
||||
Size = UDim2.new(0, 0, 0, 32),
|
||||
}, {
|
||||
Logo = e("ImageLabel", {
|
||||
Image = Assets.Images.Logo,
|
||||
Size = UDim2.new(0, 80, 0, 40),
|
||||
ScaleType = Enum.ScaleType.Fit,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 1, -10),
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
}),
|
||||
}),
|
||||
|
||||
Version = e("TextLabel", {
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Font = theme.TitleFont,
|
||||
TextSize = 18,
|
||||
Text = Version.display(Config.version),
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
TextColor3 = theme.Text2,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||
self.setFooterVersionSize(rbx.AbsoluteSize)
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return RojoFooter
|
||||
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
|
||||
104
plugin/src/Components/Theme.lua
Normal file
104
plugin/src/Components/Theme.lua
Normal file
@@ -0,0 +1,104 @@
|
||||
--[[
|
||||
Theming system taking advantage of Roact's new context API.
|
||||
|
||||
Doesn't use colors provided by Studio and instead just branches on theme
|
||||
name. This isn't exactly best practice.
|
||||
]]
|
||||
|
||||
local Studio = settings():GetService("Studio")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Log = require(Rojo.Log)
|
||||
|
||||
local strict = require(script.Parent.Parent.strict)
|
||||
|
||||
local lightTheme = strict("Theme", {
|
||||
ButtonFont = Enum.Font.GothamSemibold,
|
||||
InputFont = Enum.Font.Code,
|
||||
TitleFont = Enum.Font.GothamBold,
|
||||
MainFont = Enum.Font.Gotham,
|
||||
|
||||
Brand1 = Color3.fromRGB(225, 56, 53),
|
||||
|
||||
Text1 = Color3.fromRGB(64, 64, 64),
|
||||
Text2 = Color3.fromRGB(160, 160, 160),
|
||||
TextOnAccent = Color3.fromRGB(235, 235, 235),
|
||||
|
||||
Background1 = Color3.fromRGB(255, 255, 255),
|
||||
Background2 = Color3.fromRGB(235, 235, 235),
|
||||
})
|
||||
|
||||
local darkTheme = strict("Theme", {
|
||||
ButtonFont = Enum.Font.GothamSemibold,
|
||||
InputFont = Enum.Font.Code,
|
||||
TitleFont = Enum.Font.GothamBold,
|
||||
MainFont = Enum.Font.Gotham,
|
||||
|
||||
Brand1 = Color3.fromRGB(225, 56, 53),
|
||||
|
||||
Text1 = Color3.fromRGB(235, 235, 235),
|
||||
Text2 = Color3.fromRGB(200, 200, 200),
|
||||
TextOnAccent = Color3.fromRGB(235, 235, 235),
|
||||
|
||||
Background1 = Color3.fromRGB(48, 48, 48),
|
||||
Background2 = Color3.fromRGB(64, 64, 64),
|
||||
})
|
||||
|
||||
local Context = Roact.createContext(lightTheme)
|
||||
|
||||
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||
|
||||
-- Pull the current theme from Roblox Studio and update state with it.
|
||||
function StudioProvider:updateTheme()
|
||||
local studioTheme = Studio.Theme
|
||||
|
||||
if studioTheme.Name == "Light" then
|
||||
self:setState({
|
||||
theme = lightTheme,
|
||||
})
|
||||
elseif studioTheme.Name == "Dark" then
|
||||
self:setState({
|
||||
theme = darkTheme,
|
||||
})
|
||||
else
|
||||
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
|
||||
|
||||
self:setState({
|
||||
theme = lightTheme,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function StudioProvider:init()
|
||||
self:updateTheme()
|
||||
end
|
||||
|
||||
function StudioProvider:render()
|
||||
return Roact.createElement(Context.Provider, {
|
||||
value = self.state.theme,
|
||||
}, self.props[Roact.Children])
|
||||
end
|
||||
|
||||
function StudioProvider:didMount()
|
||||
self.connection = Studio.ThemeChanged:Connect(function()
|
||||
self:updateTheme()
|
||||
end)
|
||||
end
|
||||
|
||||
function StudioProvider:willUnmount()
|
||||
self.connection:Disconnect()
|
||||
end
|
||||
|
||||
local function with(callback)
|
||||
return Roact.createElement(Context.Consumer, {
|
||||
render = callback,
|
||||
})
|
||||
end
|
||||
|
||||
return {
|
||||
StudioProvider = StudioProvider,
|
||||
Consumer = Context.Consumer,
|
||||
with = with,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
local strict = require(script.Parent.strict)
|
||||
|
||||
return strict("Theme", {
|
||||
ButtonFont = Enum.Font.GothamSemibold,
|
||||
InputFont = Enum.Font.Code,
|
||||
TitleFont = Enum.Font.GothamBold,
|
||||
MainFont = Enum.Font.Gotham,
|
||||
|
||||
AccentColor = Color3.fromRGB(225, 56, 53),
|
||||
AccentLightColor = Color3.fromRGB(255, 146, 145),
|
||||
PrimaryColor = Color3.fromRGB(64, 64, 64),
|
||||
SecondaryColor = Color3.fromRGB(235, 235, 235),
|
||||
LightTextColor = Color3.fromRGB(160, 160, 160),
|
||||
})
|
||||
@@ -14,12 +14,20 @@ 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, game:GetService("CoreGui"), "Rojo UI")
|
||||
local tree = Roact.mount(app, nil, "Rojo UI")
|
||||
|
||||
plugin.Unloading:Connect(function()
|
||||
Roact.unmount(tree)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ use std::{
|
||||
|
||||
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
||||
use jod_thread::JoinHandle;
|
||||
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,
|
||||
},
|
||||
snapshot_middleware::{snapshot_from_vfs, snapshot_project_node},
|
||||
vfs::{FsResultExt, Vfs, VfsEvent, VfsFetcher},
|
||||
};
|
||||
|
||||
/// Owns the connection between Rojo's VFS and its DOM by holding onto another
|
||||
@@ -43,14 +44,14 @@ pub struct ChangeProcessor {
|
||||
impl ChangeProcessor {
|
||||
/// Spin up the ChangeProcessor, connecting it to the given tree, VFS, and
|
||||
/// outbound message queue.
|
||||
pub fn start<F: VfsFetcher + Send + Sync + 'static>(
|
||||
pub fn start(
|
||||
tree: Arc<Mutex<RojoTree>>,
|
||||
vfs: Arc<Vfs<F>>,
|
||||
vfs: Arc<Vfs>,
|
||||
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
||||
tree_mutation_receiver: Receiver<PatchSet>,
|
||||
) -> Self {
|
||||
let (shutdown_sender, shutdown_receiver) = crossbeam_channel::bounded(1);
|
||||
let vfs_receiver = vfs.change_receiver();
|
||||
let vfs_receiver = vfs.event_receiver();
|
||||
let task = JobThreadContext {
|
||||
tree,
|
||||
vfs,
|
||||
@@ -107,25 +108,25 @@ impl Drop for ChangeProcessor {
|
||||
}
|
||||
|
||||
/// Contains all of the state needed to synchronize the DOM and VFS.
|
||||
struct JobThreadContext<F> {
|
||||
struct JobThreadContext {
|
||||
/// A handle to the DOM we're managing.
|
||||
tree: Arc<Mutex<RojoTree>>,
|
||||
|
||||
/// A handle to the VFS we're managing.
|
||||
vfs: Arc<Vfs<F>>,
|
||||
vfs: Arc<Vfs>,
|
||||
|
||||
/// Whenever changes are applied to the DOM, we should push those changes
|
||||
/// into this message queue to inform any connected clients.
|
||||
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> JobThreadContext<F> {
|
||||
impl JobThreadContext {
|
||||
fn handle_vfs_event(&self, event: VfsEvent) {
|
||||
log::trace!("Vfs event: {:?}", event);
|
||||
|
||||
// Update the VFS immediately with the event.
|
||||
self.vfs
|
||||
.commit_change(&event)
|
||||
.commit_event(&event)
|
||||
.expect("Error applying VFS change");
|
||||
|
||||
// For a given VFS event, we might have many changes to different parts
|
||||
@@ -135,7 +136,7 @@ impl<F: VfsFetcher> JobThreadContext<F> {
|
||||
let mut applied_patches = Vec::new();
|
||||
|
||||
match event {
|
||||
VfsEvent::Created(path) | VfsEvent::Modified(path) | VfsEvent::Removed(path) => {
|
||||
VfsEvent::Create(path) | VfsEvent::Write(path) | VfsEvent::Remove(path) => {
|
||||
// Find the nearest ancestor to this path that has
|
||||
// associated instances in the tree. This helps make sure
|
||||
// that we handle additions correctly, especially if we
|
||||
@@ -164,6 +165,7 @@ impl<F: VfsFetcher> JobThreadContext<F> {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => log::warn!("Unhandled VFS event: {:?}", event),
|
||||
}
|
||||
|
||||
applied_patches
|
||||
@@ -262,11 +264,7 @@ impl<F: VfsFetcher> JobThreadContext<F> {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_and_apply_changes<F: VfsFetcher>(
|
||||
tree: &mut RojoTree,
|
||||
vfs: &Vfs<F>,
|
||||
id: RbxId,
|
||||
) -> Option<AppliedPatchSet> {
|
||||
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Option<AppliedPatchSet> {
|
||||
let metadata = tree
|
||||
.get_metadata(id)
|
||||
.expect("metadata missing for instance present in tree");
|
||||
@@ -288,21 +286,24 @@ fn compute_and_apply_changes<F: VfsFetcher>(
|
||||
// file/folder in the first place.
|
||||
let applied_patch_set = match instigating_source {
|
||||
InstigatingSource::Path(path) => {
|
||||
let maybe_entry = vfs
|
||||
.get(path)
|
||||
.with_not_found()
|
||||
.expect("unexpected VFS error");
|
||||
let maybe_meta = vfs.metadata(path).with_not_found().unwrap();
|
||||
|
||||
match maybe_entry {
|
||||
Some(entry) => {
|
||||
match maybe_meta {
|
||||
Some(_meta) => {
|
||||
// Our instance was previously created from a path and
|
||||
// that path still exists. We can generate a snapshot
|
||||
// starting at that path and use it as the source for
|
||||
// our patch.
|
||||
|
||||
let snapshot = snapshot_from_vfs(&metadata.context, &vfs, &entry)
|
||||
.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)
|
||||
@@ -326,15 +327,21 @@ fn compute_and_apply_changes<F: VfsFetcher>(
|
||||
// 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,16 +1,13 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufWriter, Write},
|
||||
io::{BufWriter, Write},
|
||||
};
|
||||
|
||||
use snafu::{ResultExt, Snafu};
|
||||
use memofs::Vfs;
|
||||
use thiserror::Error;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::{
|
||||
cli::BuildCommand,
|
||||
common_setup,
|
||||
project::ProjectError,
|
||||
vfs::{RealFetcher, Vfs, WatchMode},
|
||||
};
|
||||
use crate::{cli::BuildCommand, serve_session::ServeSession, snapshot::RojoTree};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum OutputKind {
|
||||
@@ -32,58 +29,53 @@ 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)?)
|
||||
pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
|
||||
log::trace!("Constructing in-memory filesystem");
|
||||
|
||||
let vfs = Vfs::new_default();
|
||||
|
||||
let session = ServeSession::new(vfs, &options.absolute_project());
|
||||
let mut cursor = session.message_queue().cursor();
|
||||
|
||||
{
|
||||
let tree = session.tree();
|
||||
write_model(&tree, &options)?;
|
||||
}
|
||||
|
||||
if options.watch {
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
|
||||
loop {
|
||||
let receiver = session.message_queue().subscribe(cursor);
|
||||
let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
|
||||
cursor = new_cursor;
|
||||
|
||||
let tree = session.tree();
|
||||
write_model(&tree, &options)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_inner(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);
|
||||
|
||||
log::trace!("Constructing in-memory filesystem");
|
||||
let vfs = Vfs::new(RealFetcher::new(WatchMode::Disabled));
|
||||
|
||||
let (_maybe_project, tree) = common_setup::start(&options.absolute_project(), &vfs);
|
||||
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 {
|
||||
@@ -91,8 +83,7 @@ fn build_inner(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
|
||||
@@ -101,8 +92,7 @@ fn build_inner(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)?;
|
||||
@@ -119,9 +109,14 @@ fn build_inner(options: BuildCommand) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
file.flush().context(Io)?;
|
||||
file.flush()?;
|
||||
|
||||
log::trace!("Done!");
|
||||
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.
|
||||
@@ -140,6 +203,10 @@ pub struct BuildCommand {
|
||||
/// Where to output the result.
|
||||
#[structopt(long, short)]
|
||||
pub output: PathBuf,
|
||||
|
||||
/// Whether to automatically rebuild when any input files change.
|
||||
#[structopt(long)]
|
||||
pub watch: bool,
|
||||
}
|
||||
|
||||
impl BuildCommand {
|
||||
|
||||
@@ -3,30 +3,20 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use snafu::Snafu;
|
||||
use anyhow::Result;
|
||||
use memofs::Vfs;
|
||||
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
||||
|
||||
use crate::{
|
||||
cli::ServeCommand,
|
||||
cli::{GlobalOptions, ServeCommand},
|
||||
serve_session::ServeSession,
|
||||
vfs::{RealFetcher, Vfs, WatchMode},
|
||||
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> {
|
||||
let vfs = Vfs::new(RealFetcher::new(WatchMode::Enabled));
|
||||
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
|
||||
let vfs = Vfs::new_default();
|
||||
|
||||
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project()));
|
||||
|
||||
@@ -37,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,49 +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,
|
||||
vfs::{RealFetcher, Vfs, WatchMode},
|
||||
};
|
||||
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(RealFetcher::new(WatchMode::Disabled));
|
||||
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();
|
||||
@@ -59,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={}",
|
||||
@@ -76,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,65 +0,0 @@
|
||||
//! Initialization routines that are used by more than one Rojo command or
|
||||
//! utility.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
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,
|
||||
vfs::{Vfs, VfsFetcher},
|
||||
};
|
||||
|
||||
pub fn start<F: VfsFetcher>(
|
||||
fuzzy_project_path: &Path,
|
||||
vfs: &Vfs<F>,
|
||||
) -> (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();
|
||||
|
||||
log::trace!("Reading project root");
|
||||
let entry = vfs
|
||||
.get(fuzzy_project_path)
|
||||
.expect("could not get project path");
|
||||
|
||||
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, &entry)
|
||||
.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,18 +9,16 @@ mod tree_view;
|
||||
|
||||
mod auth_cookie;
|
||||
mod change_processor;
|
||||
mod common_setup;
|
||||
mod error;
|
||||
mod glob;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
mod path_map;
|
||||
mod path_serializer;
|
||||
mod project;
|
||||
mod serve_session;
|
||||
mod session_id;
|
||||
mod snapshot;
|
||||
mod snapshot_middleware;
|
||||
mod vfs;
|
||||
mod web;
|
||||
|
||||
pub use project::*;
|
||||
|
||||
282
src/path_map.rs
282
src/path_map.rs
@@ -1,282 +0,0 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PathMapNode<T> {
|
||||
value: T,
|
||||
children: BTreeSet<PathBuf>,
|
||||
}
|
||||
|
||||
/// A map from paths to another type, like instance IDs, with a bit of
|
||||
/// additional data that enables removing a path and all of its child paths from
|
||||
/// the tree more quickly.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PathMap<T> {
|
||||
nodes: HashMap<PathBuf, PathMapNode<T>>,
|
||||
|
||||
/// Contains the set of all paths whose parent either does not exist, or is
|
||||
/// not present in the PathMap.
|
||||
///
|
||||
/// Note that these paths may have other _ancestors_ in the tree, but if an
|
||||
/// orphan's parent path is ever inserted, it will stop being an orphan. It
|
||||
/// will be... adopted!
|
||||
orphan_paths: BTreeSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl<T> Default for PathMap<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PathMap<T> {
|
||||
pub fn new() -> PathMap<T> {
|
||||
PathMap {
|
||||
nodes: HashMap::new(),
|
||||
orphan_paths: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, path: impl AsRef<Path>) -> Option<&T> {
|
||||
self.nodes.get(path.as_ref()).map(|v| &v.value)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, path: impl AsRef<Path>) -> Option<&mut T> {
|
||||
self.nodes.get_mut(path.as_ref()).map(|v| &mut v.value)
|
||||
}
|
||||
|
||||
pub fn children(&self, path: impl AsRef<Path>) -> Option<Vec<&Path>> {
|
||||
self.nodes
|
||||
.get(path.as_ref())
|
||||
.map(|v| v.children.iter().map(AsRef::as_ref).collect())
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, path: impl AsRef<Path>) -> bool {
|
||||
self.nodes.contains_key(path.as_ref())
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, path: impl Into<PathBuf>, value: T) {
|
||||
let path = path.into();
|
||||
|
||||
self.add_to_parent(path.clone());
|
||||
|
||||
// Collect any children that are currently marked as orphaned paths, but
|
||||
// are actually children of this new node.
|
||||
let mut children = BTreeSet::new();
|
||||
for orphan_path in &self.orphan_paths {
|
||||
if orphan_path.parent() == Some(&path) {
|
||||
children.insert(orphan_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for child in &children {
|
||||
self.orphan_paths.remove(child);
|
||||
}
|
||||
|
||||
self.nodes.insert(path, PathMapNode { value, children });
|
||||
}
|
||||
|
||||
/// Remove the given path and all of its linked descendants, returning all
|
||||
/// values stored in the map.
|
||||
pub fn remove(&mut self, root_path: impl AsRef<Path>) -> Vec<(PathBuf, T)> {
|
||||
let root_path = root_path.as_ref();
|
||||
|
||||
self.remove_from_parent(root_path);
|
||||
|
||||
let (root_path, root_node) = match self.nodes.remove_entry(root_path) {
|
||||
Some(node) => node,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut removed_entries = vec![(root_path, root_node.value)];
|
||||
let mut to_visit: Vec<PathBuf> = root_node.children.into_iter().collect();
|
||||
|
||||
while let Some(path) = to_visit.pop() {
|
||||
match self.nodes.remove_entry(&path) {
|
||||
Some((path, node)) => {
|
||||
removed_entries.push((path, node.value));
|
||||
|
||||
for child in node.children.into_iter() {
|
||||
to_visit.push(child);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
"Consistency issue; tried to remove {} but it was already removed",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removed_entries
|
||||
}
|
||||
|
||||
pub fn orphans(&self) -> impl Iterator<Item = &Path> {
|
||||
self.orphan_paths.iter().map(|item| item.as_ref())
|
||||
}
|
||||
|
||||
/// Adds the path to its parent if it's present in the tree, or the set of
|
||||
/// orphaned paths if it is not.
|
||||
fn add_to_parent(&mut self, path: PathBuf) {
|
||||
if let Some(parent_path) = path.parent() {
|
||||
if let Some(parent) = self.nodes.get_mut(parent_path) {
|
||||
parent.children.insert(path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In this branch, the path is orphaned because it either doesn't have a
|
||||
// parent according to Path, or because its parent doesn't exist in the
|
||||
// PathMap.
|
||||
self.orphan_paths.insert(path);
|
||||
}
|
||||
|
||||
/// Removes the path from its parent, or from the orphaned paths set if it
|
||||
/// has no parent.
|
||||
fn remove_from_parent(&mut self, path: &Path) {
|
||||
if let Some(parent_path) = path.parent() {
|
||||
if let Some(parent) = self.nodes.get_mut(parent_path) {
|
||||
parent.children.remove(path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In this branch, the path is orphaned because it either doesn't have a
|
||||
// parent according to Path, or because its parent doesn't exist in the
|
||||
// PathMap.
|
||||
self.orphan_paths.remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use maplit::btreeset;
|
||||
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
assert_eq!(map.get("/foo"), None);
|
||||
map.insert("/foo", 5);
|
||||
assert_eq!(map.get("/foo"), Some(&5));
|
||||
|
||||
map.insert("/foo/bar", 6);
|
||||
assert_eq!(map.get("/foo"), Some(&5));
|
||||
assert_eq!(map.get("/foo/bar"), Some(&6));
|
||||
assert_eq!(map.children("/foo"), Some(vec![Path::new("/foo/bar")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orphans() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
map.insert("/foo/bar", 5);
|
||||
assert_eq!(map.orphan_paths, btreeset!["/foo/bar".into()]);
|
||||
|
||||
map.insert("/foo", 6);
|
||||
assert_eq!(map.orphan_paths, btreeset!["/foo".into()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_one() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
map.insert("/foo", 6);
|
||||
|
||||
assert_eq!(map.remove("/foo"), vec![(PathBuf::from("/foo"), 6),]);
|
||||
|
||||
assert_eq!(map.get("/foo"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_child() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
map.insert("/foo", 6);
|
||||
map.insert("/foo/bar", 12);
|
||||
|
||||
assert_eq!(
|
||||
map.remove("/foo"),
|
||||
vec![(PathBuf::from("/foo"), 6), (PathBuf::from("/foo/bar"), 12),]
|
||||
);
|
||||
|
||||
assert_eq!(map.get("/foo"), None);
|
||||
assert_eq!(map.get("/foo/bar"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_descendant() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
map.insert("/foo", 6);
|
||||
map.insert("/foo/bar", 12);
|
||||
map.insert("/foo/bar/baz", 18);
|
||||
|
||||
assert_eq!(
|
||||
map.remove("/foo"),
|
||||
vec![
|
||||
(PathBuf::from("/foo"), 6),
|
||||
(PathBuf::from("/foo/bar"), 12),
|
||||
(PathBuf::from("/foo/bar/baz"), 18),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(map.get("/foo"), None);
|
||||
assert_eq!(map.get("/foo/bar"), None);
|
||||
assert_eq!(map.get("/foo/bar/baz"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_not_orphan_descendants() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
map.insert("/foo", 6);
|
||||
map.insert("/foo/bar/baz", 12);
|
||||
|
||||
assert_eq!(map.remove("/foo"), vec![(PathBuf::from("/foo"), 6),]);
|
||||
|
||||
assert_eq!(map.get("/foo"), None);
|
||||
assert_eq!(map.get("/foo/bar/baz"), Some(&12));
|
||||
}
|
||||
|
||||
// Makes sure that regardless of addition order, paths are always sorted
|
||||
// when asking for children.
|
||||
#[test]
|
||||
fn add_order_sorted() {
|
||||
let mut map = PathMap::new();
|
||||
|
||||
map.insert("/foo", 5);
|
||||
map.insert("/foo/b", 2);
|
||||
map.insert("/foo/d", 0);
|
||||
map.insert("/foo/c", 3);
|
||||
|
||||
assert_eq!(
|
||||
map.children("/foo"),
|
||||
Some(vec![
|
||||
Path::new("/foo/b"),
|
||||
Path::new("/foo/c"),
|
||||
Path::new("/foo/d"),
|
||||
])
|
||||
);
|
||||
|
||||
map.insert("/foo/a", 1);
|
||||
|
||||
assert_eq!(
|
||||
map.children("/foo"),
|
||||
Some(vec![
|
||||
Path::new("/foo/a"),
|
||||
Path::new("/foo/b"),
|
||||
Path::new("/foo/c"),
|
||||
Path::new("/foo/d"),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,15 +6,19 @@ 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},
|
||||
vfs::{Vfs, VfsFetcher},
|
||||
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.
|
||||
@@ -24,7 +28,7 @@ use crate::{
|
||||
/// why Rojo couldn't expose an IPC or channels-based API for embedding in the
|
||||
/// future. `ServeSession` would be roughly the right interface to expose for
|
||||
/// those cases.
|
||||
pub struct ServeSession<F> {
|
||||
pub struct ServeSession {
|
||||
/// The object responsible for listening to changes from the in-memory
|
||||
/// filesystem, applying them, updating the Roblox instance tree, and
|
||||
/// routing messages through the session's message queue to any connected
|
||||
@@ -68,7 +72,7 @@ pub struct ServeSession<F> {
|
||||
///
|
||||
/// The main use for accessing it from the session is for debugging issues
|
||||
/// with Rojo's live-sync protocol.
|
||||
vfs: Arc<Vfs<F>>,
|
||||
vfs: Arc<Vfs>,
|
||||
|
||||
/// A queue of changes that have been applied to `tree` that affect clients.
|
||||
///
|
||||
@@ -85,21 +89,54 @@ pub struct ServeSession<F> {
|
||||
/// Methods that need thread-safety bounds on VfsFetcher are limited to this
|
||||
/// block to prevent needing to spread Send + Sync + 'static into everything
|
||||
/// that handles ServeSession.
|
||||
impl<F: VfsFetcher + Send + Sync + 'static> ServeSession<F> {
|
||||
/// Start a new serve session from the given in-memory filesystem and start
|
||||
impl ServeSession {
|
||||
/// 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
|
||||
/// currently loaded from the filesystem directly instead of through the
|
||||
/// in-memory filesystem layer.
|
||||
pub fn new<P: AsRef<Path>>(vfs: Vfs<F>, start_path: P) -> Self {
|
||||
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();
|
||||
@@ -131,7 +168,7 @@ impl<F: VfsFetcher + Send + Sync + 'static> ServeSession<F> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> ServeSession<F> {
|
||||
impl ServeSession {
|
||||
pub fn tree_handle(&self) -> Arc<Mutex<RojoTree>> {
|
||||
Arc::clone(&self.tree)
|
||||
}
|
||||
@@ -144,7 +181,7 @@ impl<F: VfsFetcher> ServeSession<F> {
|
||||
self.tree_mutation_sender.clone()
|
||||
}
|
||||
|
||||
pub fn vfs(&self) -> &Vfs<F> {
|
||||
pub fn vfs(&self) -> &Vfs {
|
||||
&self.vfs
|
||||
}
|
||||
|
||||
@@ -189,201 +226,201 @@ mod serve_session {
|
||||
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use insta::assert_yaml_snapshot;
|
||||
use maplit::hashmap;
|
||||
use memofs::{InMemoryFs, VfsEvent, VfsSnapshot};
|
||||
use rojo_insta_ext::RedactionMap;
|
||||
use tokio::{runtime::Runtime, timer::Timeout};
|
||||
|
||||
use crate::{
|
||||
tree_view::view_tree,
|
||||
vfs::{NoopFetcher, TestFetcher, VfsDebug, VfsEvent, VfsSnapshot},
|
||||
};
|
||||
use crate::tree_view::view_tree;
|
||||
|
||||
#[test]
|
||||
fn just_folder() {
|
||||
let vfs = Vfs::new(NoopFetcher);
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo", VfsSnapshot::empty_dir())
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", VfsSnapshot::empty_dir());
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let session = ServeSession::new(vfs, "/foo");
|
||||
|
||||
let mut rm = RedactionMap::new();
|
||||
assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
||||
insta::assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_folder() {
|
||||
let vfs = Vfs::new(NoopFetcher);
|
||||
|
||||
vfs.debug_load_snapshot(
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "HelloWorld",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
{
|
||||
"name": "HelloWorld",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
"#),
|
||||
"src" => VfsSnapshot::dir(hashmap! {
|
||||
"hello.txt" => VfsSnapshot::file("Hello, world!"),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let session = ServeSession::new(vfs, "/foo");
|
||||
|
||||
let mut rm = RedactionMap::new();
|
||||
assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
||||
insta::assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_with_meta() {
|
||||
let vfs = Vfs::new(NoopFetcher);
|
||||
|
||||
vfs.debug_load_snapshot(
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/root",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"test.lua" => VfsSnapshot::file("This is a test."),
|
||||
"test.meta.json" => VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#),
|
||||
}),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let session = ServeSession::new(vfs, "/root");
|
||||
|
||||
let mut rm = RedactionMap::new();
|
||||
assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_script_meta() {
|
||||
let (state, fetcher) = TestFetcher::new();
|
||||
|
||||
state.load_snapshot(
|
||||
"/root",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"test.lua" => VfsSnapshot::file("This is a test."),
|
||||
"test.meta.json" => VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#),
|
||||
}),
|
||||
);
|
||||
|
||||
let vfs = Vfs::new(fetcher);
|
||||
let session = ServeSession::new(vfs, "/root");
|
||||
|
||||
let mut redactions = RedactionMap::new();
|
||||
assert_yaml_snapshot!(
|
||||
"change_script_meta_before",
|
||||
view_tree(&session.tree(), &mut redactions)
|
||||
);
|
||||
|
||||
state.load_snapshot(
|
||||
"/root/test.meta.json",
|
||||
VfsSnapshot::file(r#"{ "ignoreUnknownInstances": false }"#),
|
||||
);
|
||||
|
||||
let receiver = Timeout::new(
|
||||
session.message_queue().subscribe_any(),
|
||||
Duration::from_millis(200),
|
||||
);
|
||||
state.raise_event(VfsEvent::Modified(PathBuf::from("/root/test.meta.json")));
|
||||
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let changes = rt.block_on(receiver).unwrap();
|
||||
|
||||
assert_yaml_snapshot!(
|
||||
"change_script_meta_patch",
|
||||
redactions.redacted_yaml(changes)
|
||||
);
|
||||
assert_yaml_snapshot!(
|
||||
"change_script_meta_after",
|
||||
view_tree(&session.tree(), &mut redactions)
|
||||
);
|
||||
insta::assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_txt_file() {
|
||||
let (state, fetcher) = TestFetcher::new();
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("Hello!"))
|
||||
.unwrap();
|
||||
|
||||
state.load_snapshot("/foo.txt", VfsSnapshot::file("Hello!"));
|
||||
let vfs = Vfs::new(imfs.clone());
|
||||
|
||||
let vfs = Vfs::new(fetcher);
|
||||
let session = ServeSession::new(vfs, "/foo.txt");
|
||||
|
||||
let mut redactions = RedactionMap::new();
|
||||
assert_yaml_snapshot!(
|
||||
let mut rm = RedactionMap::new();
|
||||
insta::assert_yaml_snapshot!(
|
||||
"change_txt_file_before",
|
||||
view_tree(&session.tree(), &mut redactions)
|
||||
view_tree(&session.tree(), &mut rm)
|
||||
);
|
||||
|
||||
state.load_snapshot("/foo.txt", VfsSnapshot::file("World!"));
|
||||
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("World!"))
|
||||
.unwrap();
|
||||
|
||||
let receiver = session.message_queue().subscribe_any();
|
||||
|
||||
state.raise_event(VfsEvent::Modified(PathBuf::from("/foo.txt")));
|
||||
imfs.raise_event(VfsEvent::Write(PathBuf::from("/foo.txt")));
|
||||
|
||||
let receiver = Timeout::new(receiver, Duration::from_millis(200));
|
||||
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let result = rt.block_on(receiver).unwrap();
|
||||
|
||||
assert_yaml_snapshot!("change_txt_file_patch", redactions.redacted_yaml(result));
|
||||
assert_yaml_snapshot!(
|
||||
"change_txt_file_after",
|
||||
view_tree(&session.tree(), &mut redactions)
|
||||
insta::assert_yaml_snapshot!("change_txt_file_patch", rm.redacted_yaml(result));
|
||||
insta::assert_yaml_snapshot!("change_txt_file_after", view_tree(&session.tree(), &mut rm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_script_meta() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/root",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"test.lua" => VfsSnapshot::file("This is a test."),
|
||||
"test.meta.json" => VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs.clone());
|
||||
|
||||
let session = ServeSession::new(vfs, "/root");
|
||||
|
||||
let mut rm = RedactionMap::new();
|
||||
insta::assert_yaml_snapshot!(
|
||||
"change_script_meta_before",
|
||||
view_tree(&session.tree(), &mut rm)
|
||||
);
|
||||
|
||||
imfs.load_snapshot(
|
||||
"/root/test.meta.json",
|
||||
VfsSnapshot::file(r#"{ "ignoreUnknownInstances": false }"#),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let receiver = session.message_queue().subscribe_any();
|
||||
|
||||
imfs.raise_event(VfsEvent::Write(PathBuf::from("/root/test.meta.json")));
|
||||
|
||||
let receiver = Timeout::new(receiver, Duration::from_millis(200));
|
||||
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let result = rt.block_on(receiver).unwrap();
|
||||
|
||||
insta::assert_yaml_snapshot!("change_script_meta_patch", rm.redacted_yaml(result));
|
||||
insta::assert_yaml_snapshot!(
|
||||
"change_script_meta_after",
|
||||
view_tree(&session.tree(), &mut rm)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_file_in_project() {
|
||||
let (state, fetcher) = TestFetcher::new();
|
||||
|
||||
state.load_snapshot(
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "change_file_in_project",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
{
|
||||
"name": "change_file_in_project",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
|
||||
"Child": {
|
||||
"$path": "file.txt"
|
||||
"Child": {
|
||||
"$path": "file.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
"#),
|
||||
"file.txt" => VfsSnapshot::file("initial content"),
|
||||
}),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs.clone());
|
||||
|
||||
let vfs = Vfs::new(fetcher);
|
||||
let session = ServeSession::new(vfs, "/foo");
|
||||
|
||||
let mut redactions = RedactionMap::new();
|
||||
assert_yaml_snapshot!(
|
||||
let mut rm = RedactionMap::new();
|
||||
insta::assert_yaml_snapshot!(
|
||||
"change_file_in_project_before",
|
||||
view_tree(&session.tree(), &mut redactions)
|
||||
view_tree(&session.tree(), &mut rm)
|
||||
);
|
||||
|
||||
state.load_snapshot("/foo/file.txt", VfsSnapshot::file("Changed!"));
|
||||
imfs.load_snapshot("/foo/file.txt", VfsSnapshot::file("Changed!"))
|
||||
.unwrap();
|
||||
|
||||
let receiver = session.message_queue().subscribe_any();
|
||||
|
||||
state.raise_event(VfsEvent::Modified(PathBuf::from("/foo/file.txt")));
|
||||
imfs.raise_event(VfsEvent::Write(PathBuf::from("/foo/file.txt")));
|
||||
|
||||
let receiver = Timeout::new(receiver, Duration::from_millis(200));
|
||||
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let result = rt.block_on(receiver).unwrap();
|
||||
|
||||
assert_yaml_snapshot!(
|
||||
"change_file_in_project_patch",
|
||||
redactions.redacted_yaml(result)
|
||||
);
|
||||
assert_yaml_snapshot!(
|
||||
insta::assert_yaml_snapshot!("change_file_in_project_patch", rm.redacted_yaml(result));
|
||||
insta::assert_yaml_snapshot!(
|
||||
"change_file_in_project_after",
|
||||
view_tree(&session.tree(), &mut redactions)
|
||||
view_tree(&session.tree(), &mut rm)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use maplit::hashmap;
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use rbx_dom_weak::RbxValue;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{FsResultExt, Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
meta_file::AdjacentMetadata,
|
||||
@@ -18,25 +16,21 @@ use super::{
|
||||
pub struct SnapshotCsv;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotCsv {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
_context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
fn from_vfs(_context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let instance_name = match match_file_name(entry.path(), ".csv") {
|
||||
let instance_name = match match_file_name(path, ".csv") {
|
||||
Some(name) => name,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let meta_path = entry
|
||||
.path()
|
||||
.with_file_name(format!("{}.meta.json", instance_name));
|
||||
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
|
||||
|
||||
let table_contents = convert_localization_csv(&entry.contents(vfs)?);
|
||||
let table_contents = convert_localization_csv(&vfs.read(path)?);
|
||||
|
||||
let mut snapshot = InstanceSnapshot::new()
|
||||
.name(instance_name)
|
||||
@@ -48,13 +42,12 @@ impl SnapshotMiddleware for SnapshotCsv {
|
||||
})
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf(), meta_path.clone()]),
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()]),
|
||||
);
|
||||
|
||||
if let Some(meta_entry) = vfs.get(meta_path).with_not_found()? {
|
||||
let meta_contents = meta_entry.contents(vfs)?;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -142,48 +135,54 @@ fn convert_localization_csv(contents: &[u8]) -> String {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use rbx_dom_weak::{RbxId, RbxTree};
|
||||
use memofs::{DirEntry, IoResultExt, Vfs};
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{DirectorySnapshot, FsResultExt, Vfs, VfsEntry, VfsFetcher, VfsSnapshot},
|
||||
};
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
error::SnapshotError,
|
||||
meta_file::DirectoryMetadata,
|
||||
middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware},
|
||||
snapshot_from_instance, snapshot_from_vfs,
|
||||
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||
snapshot_from_vfs,
|
||||
};
|
||||
|
||||
pub struct SnapshotDir;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotDir {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_file() {
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let passes_filter_rules = |child: &VfsEntry| {
|
||||
let passes_filter_rules = |child: &DirEntry| {
|
||||
context
|
||||
.path_ignore_rules
|
||||
.iter()
|
||||
@@ -35,31 +30,36 @@ impl SnapshotMiddleware for SnapshotDir {
|
||||
|
||||
let mut snapshot_children = Vec::new();
|
||||
|
||||
for child in entry.children(vfs)?.into_iter().filter(passes_filter_rules) {
|
||||
if let Some(child_snapshot) = snapshot_from_vfs(context, vfs, &child)? {
|
||||
for entry in vfs.read_dir(path)? {
|
||||
let entry = entry?;
|
||||
|
||||
if !passes_filter_rules(&entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(child_snapshot) = snapshot_from_vfs(context, vfs, entry.path())? {
|
||||
snapshot_children.push(child_snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
let instance_name = entry
|
||||
.path()
|
||||
let instance_name = path
|
||||
.file_name()
|
||||
.expect("Could not extract file name")
|
||||
.to_str()
|
||||
.ok_or_else(|| SnapshotError::file_name_bad_unicode(entry.path()))?
|
||||
.ok_or_else(|| SnapshotError::file_name_bad_unicode(path))?
|
||||
.to_string();
|
||||
|
||||
let meta_path = entry.path().join("init.meta.json");
|
||||
let meta_path = path.join("init.meta.json");
|
||||
|
||||
let relevant_paths = vec![
|
||||
entry.path().to_path_buf(),
|
||||
path.to_path_buf(),
|
||||
meta_path.clone(),
|
||||
// TODO: We shouldn't need to know about Lua existing in this
|
||||
// middleware. Should we figure out a way for that function to add
|
||||
// relevant paths to this middleware?
|
||||
entry.path().join("init.lua"),
|
||||
entry.path().join("init.server.lua"),
|
||||
entry.path().join("init.client.lua"),
|
||||
path.join("init.lua"),
|
||||
path.join("init.server.lua"),
|
||||
path.join("init.client.lua"),
|
||||
];
|
||||
|
||||
let mut snapshot = InstanceSnapshot::new()
|
||||
@@ -68,81 +68,61 @@ impl SnapshotMiddleware for SnapshotDir {
|
||||
.children(snapshot_children)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.instigating_source(path)
|
||||
.relevant_paths(relevant_paths)
|
||||
.context(context),
|
||||
);
|
||||
|
||||
if let Some(meta_entry) = vfs.get(meta_path).with_not_found()? {
|
||||
let meta_contents = meta_entry.contents(vfs)?;
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(Some(snapshot))
|
||||
}
|
||||
|
||||
fn from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
|
||||
let instance = tree.get_instance(id).unwrap();
|
||||
|
||||
if instance.class_name != "Folder" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut children = HashMap::new();
|
||||
|
||||
for child_id in instance.get_children_ids() {
|
||||
if let Some((name, child)) = snapshot_from_instance(tree, *child_id) {
|
||||
children.insert(name, child);
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = VfsSnapshot::Directory(DirectorySnapshot { children });
|
||||
|
||||
Some((instance.name.clone(), snapshot))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use insta::assert_yaml_snapshot;
|
||||
use maplit::hashmap;
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn empty_folder() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir::<String>(HashMap::new());
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo", VfsSnapshot::empty_dir())
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotDir::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotDir::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folder_in_folder() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"Child" => VfsSnapshot::dir::<String>(HashMap::new()),
|
||||
});
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"Child" => VfsSnapshot::empty_dir(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotDir::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotDir::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,79 @@
|
||||
use std::{error::Error, fmt, io, path::PathBuf};
|
||||
use std::{io, path::PathBuf};
|
||||
|
||||
use crate::vfs::FsError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SnapshotError {
|
||||
detail: SnapshotErrorDetail,
|
||||
path: Option<PathBuf>,
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SnapshotError {
|
||||
#[error("file name had malformed Unicode")]
|
||||
FileNameBadUnicode { path: PathBuf },
|
||||
|
||||
#[error("file had malformed Unicode contents")]
|
||||
FileContentsBadUnicode {
|
||||
source: std::str::Utf8Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("malformed project file")]
|
||||
MalformedProject {
|
||||
source: serde_json::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("malformed .model.json file")]
|
||||
MalformedModelJson {
|
||||
source: serde_json::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("malformed .meta.json file")]
|
||||
MalformedMetaJson {
|
||||
source: serde_json::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
Io {
|
||||
#[from]
|
||||
source: io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl SnapshotError {
|
||||
pub fn new(detail: SnapshotErrorDetail, path: Option<impl Into<PathBuf>>) -> Self {
|
||||
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<FsError> for SnapshotError {
|
||||
fn from(error: FsError) -> Self {
|
||||
let (inner, path) = error.into_raw();
|
||||
|
||||
Self::new(inner.into(), Some(path))
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use std::{borrow::Cow, collections::HashMap, path::Path};
|
||||
|
||||
use memofs::Vfs;
|
||||
use rbx_dom_weak::UnresolvedRbxValue;
|
||||
use rbx_reflection::try_resolve_value;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceSnapshot},
|
||||
vfs::{Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use crate::snapshot::{InstanceContext, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
error::SnapshotError,
|
||||
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||
util::match_file_name,
|
||||
};
|
||||
@@ -17,28 +16,27 @@ use super::{
|
||||
pub struct SnapshotJsonModel;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotJsonModel {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let instance_name = match match_file_name(entry.path(), ".model.json") {
|
||||
let instance_name = match match_file_name(path, ".model.json") {
|
||||
Some(name) => name,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let instance: JsonModel =
|
||||
serde_json::from_slice(&entry.contents(vfs)?).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 {
|
||||
log::warn!(
|
||||
"Name from JSON model did not match its file name: {}",
|
||||
entry.path().display()
|
||||
path.display()
|
||||
);
|
||||
log::warn!(
|
||||
"In Rojo < alpha 14, this model is named \"{}\" (from its 'Name' property)",
|
||||
@@ -56,8 +54,8 @@ impl SnapshotMiddleware for SnapshotJsonModel {
|
||||
|
||||
snapshot.metadata = snapshot
|
||||
.metadata
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf()])
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context);
|
||||
|
||||
Ok(Some(snapshot))
|
||||
@@ -137,39 +135,43 @@ impl JsonModelCore {
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use insta::assert_yaml_snapshot;
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn model_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"Name": "children",
|
||||
"ClassName": "IntValue",
|
||||
"Properties": {
|
||||
"Value": 5
|
||||
},
|
||||
"Children": [
|
||||
{
|
||||
"Name": "The Child",
|
||||
"ClassName": "StringValue"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo.model.json",
|
||||
VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"Name": "children",
|
||||
"ClassName": "IntValue",
|
||||
"Properties": {
|
||||
"Value": 5
|
||||
},
|
||||
"Children": [
|
||||
{
|
||||
"Name": "The Child",
|
||||
"ClassName": "StringValue"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.model.json", file);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.model.json").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotJsonModel::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotJsonModel::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.model.json"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use std::str;
|
||||
use std::{path::Path, str};
|
||||
|
||||
use maplit::hashmap;
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use rbx_dom_weak::RbxValue;
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{FsResultExt, Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
dir::SnapshotDir,
|
||||
@@ -18,12 +16,8 @@ use super::{
|
||||
pub struct SnapshotLua;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotLua {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
let file_name = entry.path().file_name().unwrap().to_string_lossy();
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let file_name = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
// These paths alter their parent instance, so we don't need to turn
|
||||
// them into a script instance here.
|
||||
@@ -32,18 +26,20 @@ impl SnapshotMiddleware for SnapshotLua {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if entry.is_file() {
|
||||
snapshot_lua_file(context, vfs, entry)
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_file() {
|
||||
snapshot_lua_file(context, vfs, path)
|
||||
} else {
|
||||
// At this point, our entry is definitely a directory!
|
||||
|
||||
if let Some(snapshot) = snapshot_init(context, vfs, entry, "init.lua")? {
|
||||
if let Some(snapshot) = snapshot_init(context, vfs, path, "init.lua")? {
|
||||
// An `init.lua` file turns its parent into a ModuleScript
|
||||
Ok(Some(snapshot))
|
||||
} else if let Some(snapshot) = snapshot_init(context, vfs, entry, "init.server.lua")? {
|
||||
} else if let Some(snapshot) = snapshot_init(context, vfs, path, "init.server.lua")? {
|
||||
// An `init.server.lua` file turns its parent into a Script
|
||||
Ok(Some(snapshot))
|
||||
} else if let Some(snapshot) = snapshot_init(context, vfs, entry, "init.client.lua")? {
|
||||
} else if let Some(snapshot) = snapshot_init(context, vfs, path, "init.client.lua")? {
|
||||
// An `init.client.lua` file turns its parent into a LocalScript
|
||||
Ok(Some(snapshot))
|
||||
} else {
|
||||
@@ -54,12 +50,8 @@ impl SnapshotMiddleware for SnapshotLua {
|
||||
}
|
||||
|
||||
/// Core routine for turning Lua files into snapshots.
|
||||
fn snapshot_lua_file<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
let file_name = entry.path().file_name().unwrap().to_string_lossy();
|
||||
fn snapshot_lua_file(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let file_name = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
let (class_name, instance_name) = if let Some(name) = match_trailing(&file_name, ".server.lua")
|
||||
{
|
||||
@@ -72,15 +64,13 @@ fn snapshot_lua_file<F: VfsFetcher>(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let contents = entry.contents(vfs)?;
|
||||
let contents = vfs.read(path)?;
|
||||
let contents_str = str::from_utf8(&contents)
|
||||
// TODO: Turn into error type
|
||||
.expect("File content was not valid UTF-8")
|
||||
.to_string();
|
||||
|
||||
let meta_path = entry
|
||||
.path()
|
||||
.with_file_name(format!("{}.meta.json", instance_name));
|
||||
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
|
||||
|
||||
let mut snapshot = InstanceSnapshot::new()
|
||||
.name(instance_name)
|
||||
@@ -92,14 +82,13 @@ fn snapshot_lua_file<F: VfsFetcher>(
|
||||
})
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf(), meta_path.clone()])
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
if let Some(meta_entry) = vfs.get(meta_path).with_not_found()? {
|
||||
let meta_contents = meta_entry.contents(vfs)?;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -111,17 +100,17 @@ fn snapshot_lua_file<F: VfsFetcher>(
|
||||
///
|
||||
/// Scripts named `init.lua`, `init.server.lua`, or `init.client.lua` usurp
|
||||
/// their parents, which acts similarly to `__init__.py` from the Python world.
|
||||
fn snapshot_init<F: VfsFetcher>(
|
||||
fn snapshot_init(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
folder_entry: &VfsEntry,
|
||||
vfs: &Vfs,
|
||||
folder_path: &Path,
|
||||
init_name: &str,
|
||||
) -> SnapshotInstanceResult {
|
||||
let init_path = folder_entry.path().join(init_name);
|
||||
let init_path = folder_path.join(init_name);
|
||||
|
||||
if let Some(init_entry) = vfs.get(init_path).with_not_found()? {
|
||||
if let Some(dir_snapshot) = SnapshotDir::from_vfs(context, vfs, folder_entry)? {
|
||||
if let Some(mut init_snapshot) = snapshot_lua_file(context, vfs, &init_entry)? {
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
if let Some(dir_snapshot) = SnapshotDir::from_vfs(context, vfs, folder_path)? {
|
||||
if let Some(mut init_snapshot) = snapshot_lua_file(context, vfs, &init_path)? {
|
||||
if dir_snapshot.class_name != "Folder" {
|
||||
panic!(
|
||||
"init.lua, init.server.lua, and init.client.lua can \
|
||||
@@ -146,149 +135,171 @@ fn snapshot_init<F: VfsFetcher>(
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use insta::{assert_yaml_snapshot, with_settings};
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn module_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.lua", file);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.lua").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo.lua"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.server.lua", file);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.server.lua").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotLua::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.server.lua"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.client.lua", file);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.client.lua").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotLua::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.client.lua"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_module_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"init.lua" => VfsSnapshot::file("Hello!"),
|
||||
});
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/root",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"init.lua" => VfsSnapshot::file("Hello!"),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/root", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/root").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/root"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_with_meta() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let meta = VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
imfs.load_snapshot(
|
||||
"/foo.meta.json",
|
||||
VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.lua", file);
|
||||
vfs.debug_load_snapshot("/foo.meta.json", meta);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.lua").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo.lua"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_with_meta() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let meta = VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
imfs.load_snapshot(
|
||||
"/foo.meta.json",
|
||||
VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"ignoreUnknownInstances": true
|
||||
}
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.server.lua", file);
|
||||
vfs.debug_load_snapshot("/foo.meta.json", meta);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.server.lua").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotLua::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.server.lua"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_disabled() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let meta = VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"properties": {
|
||||
"Disabled": true
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
imfs.load_snapshot(
|
||||
"/bar.meta.json",
|
||||
VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"properties": {
|
||||
"Disabled": true
|
||||
}
|
||||
}
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/bar.server.lua", file);
|
||||
vfs.debug_load_snapshot("/bar.meta.json", meta);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/bar.server.lua").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotLua::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotLua::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/bar.server.lua"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
with_settings!({ sort_maps => true }, {
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::with_settings!({ sort_maps => true }, {
|
||||
insta::assert_yaml_snapshot!(instance_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) {
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
use rbx_dom_weak::{RbxId, RbxTree};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceSnapshot},
|
||||
vfs::{Vfs, VfsEntry, VfsFetcher, VfsSnapshot},
|
||||
};
|
||||
use memofs::Vfs;
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceSnapshot};
|
||||
|
||||
use super::error::SnapshotError;
|
||||
|
||||
pub type SnapshotInstanceResult = Result<Option<InstanceSnapshot>, SnapshotError>;
|
||||
pub type SnapshotFileResult = Option<(String, VfsSnapshot)>;
|
||||
|
||||
pub trait SnapshotMiddleware {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult;
|
||||
|
||||
fn from_instance(_tree: &RbxTree, _id: RbxId) -> SnapshotFileResult {
|
||||
None
|
||||
}
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult;
|
||||
}
|
||||
|
||||
@@ -15,46 +15,37 @@ mod rbxlx;
|
||||
mod rbxm;
|
||||
mod rbxmx;
|
||||
mod txt;
|
||||
mod user_plugins;
|
||||
mod util;
|
||||
|
||||
pub use self::error::*;
|
||||
|
||||
use rbx_dom_weak::{RbxId, RbxTree};
|
||||
use std::path::Path;
|
||||
|
||||
use memofs::Vfs;
|
||||
|
||||
use self::middleware::{SnapshotInstanceResult, SnapshotMiddleware};
|
||||
use self::{
|
||||
csv::SnapshotCsv,
|
||||
dir::SnapshotDir,
|
||||
json_model::SnapshotJsonModel,
|
||||
lua::SnapshotLua,
|
||||
middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware},
|
||||
project::SnapshotProject,
|
||||
rbxlx::SnapshotRbxlx,
|
||||
rbxm::SnapshotRbxm,
|
||||
rbxmx::SnapshotRbxmx,
|
||||
csv::SnapshotCsv, dir::SnapshotDir, json_model::SnapshotJsonModel, lua::SnapshotLua,
|
||||
project::SnapshotProject, rbxlx::SnapshotRbxlx, rbxm::SnapshotRbxm, rbxmx::SnapshotRbxmx,
|
||||
txt::SnapshotTxt,
|
||||
user_plugins::SnapshotUserPlugins,
|
||||
};
|
||||
use crate::{
|
||||
snapshot::InstanceContext,
|
||||
vfs::{Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use crate::snapshot::InstanceContext;
|
||||
|
||||
pub use self::project::snapshot_project_node;
|
||||
|
||||
macro_rules! middlewares {
|
||||
( $($middleware: ident,)* ) => {
|
||||
/// Generates a snapshot of instances from the given VfsEntry.
|
||||
pub fn snapshot_from_vfs<F: VfsFetcher>(
|
||||
/// Generates a snapshot of instances from the given path.
|
||||
pub fn snapshot_from_vfs(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
) -> SnapshotInstanceResult {
|
||||
$(
|
||||
log::trace!("trying middleware {} on {}", stringify!($middleware), entry.path().display());
|
||||
log::trace!("trying middleware {} on {}", stringify!($middleware), path.display());
|
||||
|
||||
if let Some(snapshot) = $middleware::from_vfs(context, vfs, entry)? {
|
||||
log::trace!("middleware {} success on {}", stringify!($middleware), entry.path().display());
|
||||
if let Some(snapshot) = $middleware::from_vfs(context, vfs, path)? {
|
||||
log::trace!("middleware {} success on {}", stringify!($middleware), path.display());
|
||||
return Ok(Some(snapshot));
|
||||
}
|
||||
)*
|
||||
@@ -62,24 +53,11 @@ macro_rules! middlewares {
|
||||
log::trace!("no middleware returned Ok(Some)");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Generates an in-memory filesystem snapshot of the given Roblox
|
||||
/// instance.
|
||||
pub fn snapshot_from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
|
||||
$(
|
||||
if let Some(result) = $middleware::from_instance(tree, id) {
|
||||
return Some(result);
|
||||
}
|
||||
)*
|
||||
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
middlewares! {
|
||||
SnapshotProject,
|
||||
SnapshotUserPlugins,
|
||||
SnapshotJsonModel,
|
||||
SnapshotRbxlx,
|
||||
SnapshotRbxmx,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{borrow::Cow, collections::HashMap, path::Path};
|
||||
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use rbx_reflection::try_resolve_value;
|
||||
|
||||
use crate::{
|
||||
@@ -7,7 +8,6 @@ use crate::{
|
||||
snapshot::{
|
||||
InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule,
|
||||
},
|
||||
vfs::{FsResultExt, Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -22,30 +22,28 @@ use super::{
|
||||
pub struct SnapshotProject;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotProject {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
let project_path = entry.path().join("default.project.json");
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
match vfs.get(project_path).with_not_found()? {
|
||||
if meta.is_dir() {
|
||||
let project_path = path.join("default.project.json");
|
||||
|
||||
match vfs.metadata(&project_path).with_not_found()? {
|
||||
// TODO: Do we need to muck with the relevant paths if we're a
|
||||
// project file within a folder? Should the folder path be the
|
||||
// relevant path instead of the project file path?
|
||||
Some(entry) => return SnapshotProject::from_vfs(context, vfs, &entry),
|
||||
Some(_meta) => return SnapshotProject::from_vfs(context, vfs, &project_path),
|
||||
None => return Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
if !entry.path().to_string_lossy().ends_with(".project.json") {
|
||||
if !path.to_string_lossy().ends_with(".project.json") {
|
||||
// This isn't a project file, so it's not our job.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let project = Project::load_from_slice(&entry.contents(vfs)?, entry.path())
|
||||
.map_err(|err| SnapshotError::malformed_project(err, entry.path()))?;
|
||||
let project = Project::load_from_slice(&vfs.read(path)?, path)
|
||||
.map_err(|err| SnapshotError::malformed_project(err, path))?;
|
||||
|
||||
let mut context = context.clone();
|
||||
|
||||
@@ -75,7 +73,7 @@ impl SnapshotMiddleware for SnapshotProject {
|
||||
// relevant path -> snapshot path mapping per instance, we pick the more
|
||||
// conservative approach of snapshotting the project file if any
|
||||
// relevant paths changed.
|
||||
snapshot.metadata.instigating_source = Some(entry.path().to_path_buf().into());
|
||||
snapshot.metadata.instigating_source = Some(path.to_path_buf().into());
|
||||
|
||||
// Mark this snapshot (the root node of the project file) as being
|
||||
// related to the project file.
|
||||
@@ -83,21 +81,18 @@ impl SnapshotMiddleware for SnapshotProject {
|
||||
// We SHOULD NOT mark the project file as a relevant path for any
|
||||
// nodes that aren't roots. They'll be updated as part of the project
|
||||
// file being updated.
|
||||
snapshot
|
||||
.metadata
|
||||
.relevant_paths
|
||||
.push(entry.path().to_path_buf());
|
||||
snapshot.metadata.relevant_paths.push(path.to_path_buf());
|
||||
|
||||
Ok(Some(snapshot))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot_project_node<F: VfsFetcher>(
|
||||
pub fn snapshot_project_node(
|
||||
context: &InstanceContext,
|
||||
project_folder: &Path,
|
||||
instance_name: &str,
|
||||
node: &ProjectNode,
|
||||
vfs: &Vfs<F>,
|
||||
vfs: &Vfs,
|
||||
) -> SnapshotInstanceResult {
|
||||
let name = Cow::Owned(instance_name.to_owned());
|
||||
let mut class_name = node
|
||||
@@ -117,9 +112,7 @@ pub fn snapshot_project_node<F: VfsFetcher>(
|
||||
Cow::Borrowed(path)
|
||||
};
|
||||
|
||||
let entry = vfs.get(path.as_path())?;
|
||||
|
||||
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &entry)? {
|
||||
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &path)? {
|
||||
// If a class name was already specified, then it'll override the
|
||||
// class name of this snapshot ONLY if it's a Folder.
|
||||
//
|
||||
@@ -217,259 +210,284 @@ pub fn snapshot_project_node<F: VfsFetcher>(
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use insta::assert_yaml_snapshot;
|
||||
use maplit::hashmap;
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn project_from_folder() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "indirect-project",
|
||||
"tree": {
|
||||
"$className": "Folder"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "indirect-project",
|
||||
"tree": {
|
||||
"$className": "Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_from_direct_file() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"hello.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "direct-project",
|
||||
"tree": {
|
||||
"$className": "Model"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"hello.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "direct-project",
|
||||
"tree": {
|
||||
"$className": "Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo/hello.project.json").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
let instance_snapshot = SnapshotProject::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo/hello.project.json"),
|
||||
)
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_resolved_properties() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "resolved-properties",
|
||||
"tree": {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "resolved-properties",
|
||||
"tree": {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_unresolved_properties() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "unresolved-properties",
|
||||
"tree": {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": "Hi!"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "unresolved-properties",
|
||||
"tree": {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": "Hi!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_children() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "children",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "children",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
|
||||
"Child": {
|
||||
"$className": "Model"
|
||||
"Child": {
|
||||
"$className": "Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_path_to_txt() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-project",
|
||||
"tree": {
|
||||
"$path": "other.txt"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-project",
|
||||
"tree": {
|
||||
"$path": "other.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
"other.txt" => VfsSnapshot::file("Hello, world!"),
|
||||
});
|
||||
"#),
|
||||
"other.txt" => VfsSnapshot::file("Hello, world!"),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_path_to_project() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-project",
|
||||
"tree": {
|
||||
"$path": "other.project.json"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-project",
|
||||
"tree": {
|
||||
"$path": "other.project.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
"other.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "other-project",
|
||||
"tree": {
|
||||
"$className": "Model"
|
||||
"#),
|
||||
"other.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "other-project",
|
||||
"tree": {
|
||||
"$className": "Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_path_to_project_with_children() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-child-project",
|
||||
"tree": {
|
||||
"$path": "other.project.json"
|
||||
}
|
||||
}
|
||||
"#),
|
||||
"other.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "other-project",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
|
||||
"SomeChild": {
|
||||
"$className": "Model"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-child-project",
|
||||
"tree": {
|
||||
"$path": "other.project.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
"other.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "other-project",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
"SomeChild": {
|
||||
"$className": "Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
/// Ensures that if a property is defined both in the resulting instance
|
||||
@@ -479,40 +497,43 @@ mod test {
|
||||
fn project_path_property_overrides() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-property-override",
|
||||
"tree": {
|
||||
"$path": "other.project.json",
|
||||
"$properties": {
|
||||
"Value": "Changed"
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo",
|
||||
VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "path-property-override",
|
||||
"tree": {
|
||||
"$path": "other.project.json",
|
||||
"$properties": {
|
||||
"Value": "Changed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
"other.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "other-project",
|
||||
"tree": {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": "Original"
|
||||
"#),
|
||||
"other.project.json" => VfsSnapshot::file(r#"
|
||||
{
|
||||
"name": "other-project",
|
||||
"tree": {
|
||||
"$className": "StringValue",
|
||||
"$properties": {
|
||||
"Value": "Original"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#),
|
||||
});
|
||||
"#),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo", dir);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotProject::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo"))
|
||||
.expect("snapshot error")
|
||||
.expect("snapshot returned no instances");
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use memofs::Vfs;
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||
@@ -11,16 +12,14 @@ use super::{
|
||||
pub struct SnapshotRbxlx;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotRbxlx {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let instance_name = match match_file_name(entry.path(), ".rbxlx") {
|
||||
let instance_name = match match_file_name(path, ".rbxlx") {
|
||||
Some(name) => name,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -28,7 +27,7 @@ impl SnapshotMiddleware for SnapshotRbxlx {
|
||||
let options = rbx_xml::DecodeOptions::new()
|
||||
.property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown);
|
||||
|
||||
let temp_tree = rbx_xml::from_reader(entry.contents(vfs)?.as_slice(), options)
|
||||
let temp_tree = rbx_xml::from_reader(vfs.read(path)?.as_slice(), options)
|
||||
.expect("TODO: Handle rbx_xml errors");
|
||||
|
||||
let root_id = temp_tree.get_root_id();
|
||||
@@ -37,8 +36,8 @@ impl SnapshotMiddleware for SnapshotRbxlx {
|
||||
.name(instance_name)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf()])
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use memofs::Vfs;
|
||||
use rbx_dom_weak::{RbxInstanceProperties, RbxTree};
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||
@@ -15,16 +13,14 @@ use super::{
|
||||
pub struct SnapshotRbxm;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotRbxm {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let instance_name = match match_file_name(entry.path(), ".rbxm") {
|
||||
let instance_name = match match_file_name(path, ".rbxm") {
|
||||
Some(name) => name,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -36,7 +32,7 @@ impl SnapshotMiddleware for SnapshotRbxm {
|
||||
});
|
||||
|
||||
let root_id = temp_tree.get_root_id();
|
||||
rbx_binary::decode(&mut temp_tree, root_id, entry.contents(vfs)?.as_slice())
|
||||
rbx_binary::decode(&mut temp_tree, root_id, vfs.read(path)?.as_slice())
|
||||
.expect("TODO: Handle rbx_binary errors");
|
||||
|
||||
let root_instance = temp_tree.get_instance(root_id).unwrap();
|
||||
@@ -47,8 +43,8 @@ impl SnapshotMiddleware for SnapshotRbxm {
|
||||
.name(instance_name)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf()])
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
@@ -63,20 +59,26 @@ impl SnapshotMiddleware for SnapshotRbxm {
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn model_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file(include_bytes!("../../assets/test-folder.rbxm").to_vec());
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo.rbxm",
|
||||
VfsSnapshot::file(include_bytes!("../../assets/test-folder.rbxm").to_vec()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.rbxm", file);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.rbxm").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotRbxm::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotRbxm::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.rbxm"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(instance_snapshot.name, "foo");
|
||||
assert_eq!(instance_snapshot.class_name, "Folder");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{Vfs, VfsEntry, VfsFetcher},
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use memofs::Vfs;
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||
@@ -11,16 +12,14 @@ use super::{
|
||||
pub struct SnapshotRbxmx;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotRbxmx {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let instance_name = match match_file_name(entry.path(), ".rbxmx") {
|
||||
let instance_name = match match_file_name(path, ".rbxmx") {
|
||||
Some(name) => name,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -28,7 +27,7 @@ impl SnapshotMiddleware for SnapshotRbxmx {
|
||||
let options = rbx_xml::DecodeOptions::new()
|
||||
.property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown);
|
||||
|
||||
let temp_tree = rbx_xml::from_reader(entry.contents(vfs)?.as_slice(), options)
|
||||
let temp_tree = rbx_xml::from_reader(vfs.read(path)?.as_slice(), options)
|
||||
.expect("TODO: Handle rbx_xml errors");
|
||||
|
||||
let root_instance = temp_tree.get_instance(temp_tree.get_root_id()).unwrap();
|
||||
@@ -39,8 +38,8 @@ impl SnapshotMiddleware for SnapshotRbxmx {
|
||||
.name(instance_name)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf()])
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
@@ -55,36 +54,40 @@ impl SnapshotMiddleware for SnapshotRbxmx {
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn model_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file(
|
||||
r#"
|
||||
<roblox version="4">
|
||||
<Item class="Folder" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">THIS NAME IS IGNORED</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
"#,
|
||||
);
|
||||
fn plain_folder() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
"/foo.rbxmx",
|
||||
VfsSnapshot::file(
|
||||
r#"
|
||||
<roblox version="4">
|
||||
<Item class="Folder" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">THIS NAME IS IGNORED</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.rbxmx", file);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let entry = vfs.get("/foo.rbxmx").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotRbxmx::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = SnapshotRbxmx::from_vfs(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.rbxmx"),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(instance_snapshot.name, "foo");
|
||||
assert_eq!(instance_snapshot.class_name, "Folder");
|
||||
assert_eq!(instance_snapshot.properties, HashMap::new());
|
||||
assert_eq!(instance_snapshot.properties, Default::default());
|
||||
assert_eq!(instance_snapshot.children, Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
use std::str;
|
||||
use std::{path::Path, str};
|
||||
|
||||
use maplit::hashmap;
|
||||
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use rbx_dom_weak::RbxValue;
|
||||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
vfs::{FileSnapshot, FsResultExt, Vfs, VfsEntry, VfsFetcher, VfsSnapshot},
|
||||
};
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
error::SnapshotError,
|
||||
meta_file::AdjacentMetadata,
|
||||
middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware},
|
||||
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||
util::match_file_name,
|
||||
};
|
||||
|
||||
pub struct SnapshotTxt;
|
||||
|
||||
impl SnapshotMiddleware for SnapshotTxt {
|
||||
fn from_vfs<F: VfsFetcher>(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs<F>,
|
||||
entry: &VfsEntry,
|
||||
) -> SnapshotInstanceResult {
|
||||
if entry.is_directory() {
|
||||
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||
let meta = vfs.metadata(path)?;
|
||||
|
||||
if meta.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let instance_name = match match_file_name(entry.path(), ".txt") {
|
||||
let instance_name = match match_file_name(path, ".txt") {
|
||||
Some(name) => name,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let contents = entry.contents(vfs)?;
|
||||
let contents = vfs.read(path)?;
|
||||
let contents_str = str::from_utf8(&contents)
|
||||
.map_err(|err| SnapshotError::file_contents_bad_unicode(err, entry.path()))?
|
||||
.map_err(|err| SnapshotError::file_contents_bad_unicode(err, path))?
|
||||
.to_string();
|
||||
|
||||
let properties = hashmap! {
|
||||
@@ -43,9 +39,7 @@ impl SnapshotMiddleware for SnapshotTxt {
|
||||
},
|
||||
};
|
||||
|
||||
let meta_path = entry
|
||||
.path()
|
||||
.with_file_name(format!("{}.meta.json", instance_name));
|
||||
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
|
||||
|
||||
let mut snapshot = InstanceSnapshot::new()
|
||||
.name(instance_name)
|
||||
@@ -53,99 +47,39 @@ impl SnapshotMiddleware for SnapshotTxt {
|
||||
.properties(properties)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(entry.path())
|
||||
.relevant_paths(vec![entry.path().to_path_buf(), meta_path.clone()])
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
if let Some(meta_entry) = vfs.get(meta_path).with_not_found()? {
|
||||
let meta_contents = meta_entry.contents(vfs)?;
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(Some(snapshot))
|
||||
}
|
||||
|
||||
fn from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
|
||||
let instance = tree.get_instance(id).unwrap();
|
||||
|
||||
if instance.class_name != "StringValue" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !instance.get_children_ids().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let value = match instance.properties.get("Value") {
|
||||
Some(RbxValue::String { value }) => value.clone(),
|
||||
Some(_) => panic!("wrong type ahh"),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let snapshot = VfsSnapshot::File(FileSnapshot {
|
||||
contents: value.into_bytes(),
|
||||
});
|
||||
|
||||
let mut file_name = instance.name.clone();
|
||||
file_name.push_str(".txt");
|
||||
|
||||
Some((file_name, snapshot))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use insta::assert_yaml_snapshot;
|
||||
use maplit::hashmap;
|
||||
use rbx_dom_weak::RbxInstanceProperties;
|
||||
|
||||
use crate::vfs::{NoopFetcher, VfsDebug};
|
||||
use memofs::{InMemoryFs, VfsSnapshot};
|
||||
|
||||
#[test]
|
||||
fn instance_from_vfs() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("Hello there!");
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("Hello there!"))
|
||||
.unwrap();
|
||||
|
||||
vfs.debug_load_snapshot("/foo.txt", file);
|
||||
let mut vfs = Vfs::new(imfs.clone());
|
||||
|
||||
let entry = vfs.get("/foo.txt").unwrap();
|
||||
let instance_snapshot =
|
||||
SnapshotTxt::from_vfs(&InstanceContext::default(), &mut vfs, &entry)
|
||||
SnapshotTxt::from_vfs(&InstanceContext::default(), &mut vfs, Path::new("/foo.txt"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vfs_from_instance() {
|
||||
let tree = RbxTree::new(string_value("Root", "Hello, world!"));
|
||||
let root_id = tree.get_root_id();
|
||||
|
||||
let (_file_name, _file) = SnapshotTxt::from_instance(&tree, root_id).unwrap();
|
||||
}
|
||||
|
||||
fn folder(name: impl Into<String>) -> RbxInstanceProperties {
|
||||
RbxInstanceProperties {
|
||||
name: name.into(),
|
||||
class_name: "Folder".to_owned(),
|
||||
properties: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn string_value(name: impl Into<String>, value: impl Into<String>) -> RbxInstanceProperties {
|
||||
RbxInstanceProperties {
|
||||
name: name.into(),
|
||||
class_name: "StringValue".to_owned(),
|
||||
properties: hashmap! {
|
||||
"Value".to_owned() => RbxValue::String {
|
||||
value: value.into(),
|
||||
},
|
||||
},
|
||||
}
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use std::{error::Error, fmt, io, path::PathBuf};
|
||||
|
||||
pub type FsResult<T> = Result<T, FsError>;
|
||||
pub use io::ErrorKind as FsErrorKind;
|
||||
|
||||
pub trait FsResultExt<T> {
|
||||
fn with_not_found(self) -> Result<Option<T>, FsError>;
|
||||
}
|
||||
|
||||
impl<T> FsResultExt<T> for Result<T, FsError> {
|
||||
fn with_not_found(self) -> Result<Option<T>, FsError> {
|
||||
match self {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(ref err) if err.kind() == FsErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around io::Error that also attaches the path associated with the
|
||||
/// error.
|
||||
#[derive(Debug)]
|
||||
pub struct FsError {
|
||||
source: io::Error,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FsError {
|
||||
pub fn new<P: Into<PathBuf>>(source: io::Error, path: P) -> FsError {
|
||||
FsError {
|
||||
source,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> FsErrorKind {
|
||||
self.source.kind()
|
||||
}
|
||||
|
||||
pub fn into_raw(self) -> (io::Error, PathBuf) {
|
||||
(self.source, self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for FsError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
Some(&self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FsError {
|
||||
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(output, "{}: {}", self.path.display(), self.source)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VfsEvent {
|
||||
Modified(PathBuf),
|
||||
Created(PathBuf),
|
||||
Removed(PathBuf),
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use super::event::VfsEvent;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
/// The generic interface that `Vfs` uses to lazily read files from the disk.
|
||||
/// In tests, it's stubbed out to do different versions of absolutely nothing
|
||||
/// depending on the test.
|
||||
pub trait VfsFetcher {
|
||||
fn file_type(&self, path: &Path) -> io::Result<FileType>;
|
||||
fn read_children(&self, path: &Path) -> io::Result<Vec<PathBuf>>;
|
||||
fn read_contents(&self, path: &Path) -> io::Result<Vec<u8>>;
|
||||
|
||||
fn create_directory(&self, path: &Path) -> io::Result<()>;
|
||||
fn write_file(&self, path: &Path, contents: &[u8]) -> io::Result<()>;
|
||||
fn remove(&self, path: &Path) -> io::Result<()>;
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent>;
|
||||
|
||||
fn watch(&self, _path: &Path) {}
|
||||
fn unwatch(&self, _path: &Path) {}
|
||||
|
||||
/// A method intended for debugging what paths the fetcher is watching.
|
||||
fn watched_paths(&self) -> Vec<PathBuf> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
mod error;
|
||||
mod event;
|
||||
mod fetcher;
|
||||
mod noop_fetcher;
|
||||
mod real_fetcher;
|
||||
mod snapshot;
|
||||
|
||||
// I don't think module inception is a real problem?
|
||||
#[allow(clippy::module_inception)]
|
||||
mod vfs;
|
||||
|
||||
pub use error::*;
|
||||
pub use event::*;
|
||||
pub use fetcher::*;
|
||||
pub use noop_fetcher::*;
|
||||
pub use real_fetcher::*;
|
||||
pub use snapshot::*;
|
||||
pub use vfs::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_fetcher;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use test_fetcher::*;
|
||||
@@ -1,62 +0,0 @@
|
||||
//! Implements the VFS fetcher interface for a fake filesystem using Rust's
|
||||
//! std::fs interface.
|
||||
|
||||
// This interface is only used for testing, so it's okay if it isn't used.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use super::{
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
};
|
||||
|
||||
pub struct NoopFetcher;
|
||||
|
||||
impl VfsFetcher for NoopFetcher {
|
||||
fn file_type(&self, _path: &Path) -> io::Result<FileType> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"NoopFetcher always returns NotFound",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_children(&self, _path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"NoopFetcher always returns NotFound",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_contents(&self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"NoopFetcher always returns NotFound",
|
||||
))
|
||||
}
|
||||
|
||||
fn create_directory(&self, _path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_file(&self, _path: &Path, _contents: &[u8]) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, _path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch(&self, _path: &Path) {}
|
||||
|
||||
fn unwatch(&self, _path: &Path) {}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
//! Implements the VFS fetcher interface for the real filesystem using Rust's
|
||||
//! std::fs interface and notify as the file watcher.
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
sync::{mpsc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use jod_thread::JoinHandle;
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use super::{
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
};
|
||||
|
||||
/// Workaround to disable the file watcher for processes that don't need it,
|
||||
/// since notify appears hang on to mpsc Sender objects too long, causing Rojo
|
||||
/// to deadlock on drop.
|
||||
///
|
||||
/// We can make constructing the watcher optional in order to hotfix rojo build.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WatchMode {
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub struct RealFetcher {
|
||||
// Drop order is relevant here!
|
||||
//
|
||||
// `watcher` must be dropped before `_converter_thread` or else joining the
|
||||
// thread will cause a deadlock.
|
||||
watcher: Option<Mutex<RecommendedWatcher>>,
|
||||
|
||||
/// Thread handle to convert notify's mpsc channel messages into
|
||||
/// crossbeam_channel messages.
|
||||
_converter_thread: JoinHandle<()>,
|
||||
|
||||
/// The crossbeam receiver filled with events from the converter thread.
|
||||
receiver: Receiver<VfsEvent>,
|
||||
|
||||
/// All of the paths that the fetcher is watching, tracked here because
|
||||
/// notify does not expose this information.
|
||||
watched_paths: Mutex<HashSet<PathBuf>>,
|
||||
}
|
||||
|
||||
impl RealFetcher {
|
||||
pub fn new(watch_mode: WatchMode) -> RealFetcher {
|
||||
log::trace!("Starting RealFetcher with watch mode {:?}", watch_mode);
|
||||
|
||||
let (notify_sender, notify_receiver) = mpsc::channel();
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let handle = jod_thread::Builder::new()
|
||||
.name("notify message converter".to_owned())
|
||||
.spawn(move || {
|
||||
log::trace!("RealFetcher converter thread started");
|
||||
converter_thread(notify_receiver, sender);
|
||||
log::trace!("RealFetcher converter thread stopped");
|
||||
})
|
||||
.expect("Could not start message converter thread");
|
||||
|
||||
// TODO: Investigate why notify hangs onto notify_sender too long,
|
||||
// causing our program to deadlock. Once this is fixed, watcher no
|
||||
// longer needs to be optional, but is still maybe useful?
|
||||
let watcher = match watch_mode {
|
||||
WatchMode::Enabled => {
|
||||
let watcher = notify::watcher(notify_sender, Duration::from_millis(300))
|
||||
.expect("Couldn't start 'notify' file watcher");
|
||||
|
||||
Some(Mutex::new(watcher))
|
||||
}
|
||||
WatchMode::Disabled => None,
|
||||
};
|
||||
|
||||
RealFetcher {
|
||||
watcher,
|
||||
_converter_thread: handle,
|
||||
receiver,
|
||||
watched_paths: Mutex::new(HashSet::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn converter_thread(notify_receiver: mpsc::Receiver<DebouncedEvent>, sender: Sender<VfsEvent>) {
|
||||
use DebouncedEvent::*;
|
||||
|
||||
for event in notify_receiver {
|
||||
log::trace!("Notify event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Create(path) => sender.send(VfsEvent::Created(path)).unwrap(),
|
||||
Write(path) => sender.send(VfsEvent::Modified(path)).unwrap(),
|
||||
Remove(path) => sender.send(VfsEvent::Removed(path)).unwrap(),
|
||||
Rename(from_path, to_path) => {
|
||||
sender.send(VfsEvent::Created(from_path)).unwrap();
|
||||
sender.send(VfsEvent::Removed(to_path)).unwrap();
|
||||
}
|
||||
Rescan => {
|
||||
log::warn!("Unhandled filesystem rescan event.");
|
||||
log::warn!(
|
||||
"Please file an issue! Rojo may need to handle this case, but does not yet."
|
||||
);
|
||||
}
|
||||
Error(err, maybe_path) => {
|
||||
log::warn!("Unhandled filesystem error: {}", err);
|
||||
|
||||
match maybe_path {
|
||||
Some(path) => log::warn!("On path {}", path.display()),
|
||||
None => log::warn!("No path was associated with this error."),
|
||||
}
|
||||
|
||||
log::warn!(
|
||||
"Rojo may need to handle this. If this happens again, please file an issue!"
|
||||
);
|
||||
}
|
||||
NoticeWrite(_) | NoticeRemove(_) | Chmod(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsFetcher for RealFetcher {
|
||||
fn file_type(&self, path: &Path) -> io::Result<FileType> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
|
||||
if metadata.is_file() {
|
||||
Ok(FileType::File)
|
||||
} else {
|
||||
Ok(FileType::Directory)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_children(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
log::trace!("Reading directory {}", path.display());
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
let iter = fs::read_dir(path)?;
|
||||
|
||||
for entry in iter {
|
||||
result.push(entry?.path());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn read_contents(&self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
log::trace!("Reading file {}", path.display());
|
||||
|
||||
fs::read(path)
|
||||
}
|
||||
|
||||
fn create_directory(&self, path: &Path) -> io::Result<()> {
|
||||
log::trace!("Creating directory {}", path.display());
|
||||
|
||||
fs::create_dir(path)
|
||||
}
|
||||
|
||||
fn write_file(&self, path: &Path, contents: &[u8]) -> io::Result<()> {
|
||||
log::trace!("Writing path {}", path.display());
|
||||
|
||||
fs::write(path, contents)
|
||||
}
|
||||
|
||||
fn remove(&self, path: &Path) -> io::Result<()> {
|
||||
log::trace!("Removing path {}", path.display());
|
||||
|
||||
let metadata = fs::metadata(path)?;
|
||||
|
||||
if metadata.is_file() {
|
||||
fs::remove_file(path)
|
||||
} else {
|
||||
fs::remove_dir_all(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn watch(&self, path: &Path) {
|
||||
log::trace!("Watching path {}", path.display());
|
||||
|
||||
if let Some(watcher_handle) = &self.watcher {
|
||||
let mut watcher = watcher_handle.lock().unwrap();
|
||||
|
||||
match watcher.watch(path, RecursiveMode::NonRecursive) {
|
||||
Ok(_) => {
|
||||
let mut watched_paths = self.watched_paths.lock().unwrap();
|
||||
watched_paths.insert(path.to_path_buf());
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Couldn't watch path {}: {:?}", path.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwatch(&self, path: &Path) {
|
||||
log::trace!("Stopped watching path {}", path.display());
|
||||
|
||||
if let Some(watcher_handle) = &self.watcher {
|
||||
let mut watcher = watcher_handle.lock().unwrap();
|
||||
|
||||
// Remove the path from our watched paths regardless of the outcome
|
||||
// of notify's unwatch to ensure we drop old paths in the event of a
|
||||
// rename.
|
||||
let mut watched_paths = self.watched_paths.lock().unwrap();
|
||||
watched_paths.remove(path);
|
||||
|
||||
if let Err(err) = watcher.unwatch(path) {
|
||||
log::warn!("Couldn't unwatch path {}: {:?}", path.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
self.receiver.clone()
|
||||
}
|
||||
|
||||
fn watched_paths(&self) -> Vec<PathBuf> {
|
||||
let watched_paths = self.watched_paths.lock().unwrap();
|
||||
watched_paths.iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// This file is non-critical and used for testing, so it's okay if it's unused.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VfsSnapshot {
|
||||
File(FileSnapshot),
|
||||
Directory(DirectorySnapshot),
|
||||
}
|
||||
|
||||
impl VfsSnapshot {
|
||||
/// Create a new file VfsSnapshot with the given contents.
|
||||
pub fn file(contents: impl Into<Vec<u8>>) -> VfsSnapshot {
|
||||
VfsSnapshot::File(FileSnapshot {
|
||||
contents: contents.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new directory VfsSnapshot with the given children.
|
||||
pub fn dir<S: Into<String>>(children: HashMap<S, VfsSnapshot>) -> VfsSnapshot {
|
||||
let children = children.into_iter().map(|(k, v)| (k.into(), v)).collect();
|
||||
|
||||
VfsSnapshot::Directory(DirectorySnapshot { children })
|
||||
}
|
||||
|
||||
pub fn empty_dir() -> VfsSnapshot {
|
||||
VfsSnapshot::Directory(DirectorySnapshot {
|
||||
children: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSnapshot {
|
||||
pub contents: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectorySnapshot {
|
||||
pub children: HashMap<String, VfsSnapshot>,
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
//! Implements the VFS fetcher interface for a fake filesystem that can be
|
||||
//! mutated and have changes signaled through it.
|
||||
//!
|
||||
//! This is useful for testing how things using Vfs react to changed events
|
||||
//! without relying on the real filesystem implementation, which is very
|
||||
//! platform-specific.
|
||||
|
||||
// This interface is only used for testing, so it's okay if it isn't used.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{self, Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
|
||||
use crate::path_map::PathMap;
|
||||
|
||||
use super::{
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
snapshot::VfsSnapshot,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestFetcherState {
|
||||
inner: Arc<Mutex<TestFetcherStateInner>>,
|
||||
}
|
||||
|
||||
impl TestFetcherState {
|
||||
pub fn load_snapshot<P: AsRef<Path>>(&self, path: P, snapshot: VfsSnapshot) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.load_snapshot(path.as_ref().to_path_buf(), snapshot);
|
||||
}
|
||||
|
||||
pub fn remove<P: AsRef<Path>>(&self, path: P) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.remove(path.as_ref());
|
||||
}
|
||||
|
||||
pub fn raise_event(&self, event: VfsEvent) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.raise_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TestFetcherEntry {
|
||||
File(Vec<u8>),
|
||||
Dir,
|
||||
}
|
||||
|
||||
struct TestFetcherStateInner {
|
||||
entries: PathMap<TestFetcherEntry>,
|
||||
sender: Sender<VfsEvent>,
|
||||
}
|
||||
|
||||
impl TestFetcherStateInner {
|
||||
fn new(sender: Sender<VfsEvent>) -> Self {
|
||||
let mut entries = PathMap::new();
|
||||
entries.insert(Path::new("/"), TestFetcherEntry::Dir);
|
||||
|
||||
Self { sender, entries }
|
||||
}
|
||||
|
||||
fn load_snapshot(&mut self, path: PathBuf, snapshot: VfsSnapshot) {
|
||||
match snapshot {
|
||||
VfsSnapshot::File(file) => {
|
||||
self.entries
|
||||
.insert(path, TestFetcherEntry::File(file.contents));
|
||||
}
|
||||
VfsSnapshot::Directory(directory) => {
|
||||
self.entries.insert(path.clone(), TestFetcherEntry::Dir);
|
||||
|
||||
for (child_name, child) in directory.children.into_iter() {
|
||||
self.load_snapshot(path.join(child_name), child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, path: &Path) {
|
||||
self.entries.remove(path);
|
||||
}
|
||||
|
||||
fn raise_event(&mut self, event: VfsEvent) {
|
||||
self.sender.send(event).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestFetcher {
|
||||
state: TestFetcherState,
|
||||
receiver: Receiver<VfsEvent>,
|
||||
}
|
||||
|
||||
impl TestFetcher {
|
||||
pub fn new() -> (TestFetcherState, Self) {
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let state = TestFetcherState {
|
||||
inner: Arc::new(Mutex::new(TestFetcherStateInner::new(sender))),
|
||||
};
|
||||
|
||||
(state.clone(), Self { receiver, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsFetcher for TestFetcher {
|
||||
fn file_type(&self, path: &Path) -> io::Result<FileType> {
|
||||
let inner = self.state.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(TestFetcherEntry::File(_)) => Ok(FileType::File),
|
||||
Some(TestFetcherEntry::Dir) => Ok(FileType::Directory),
|
||||
None => Err(io::Error::new(io::ErrorKind::NotFound, "Path not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_children(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
let inner = self.state.inner.lock().unwrap();
|
||||
|
||||
Ok(inner
|
||||
.entries
|
||||
.children(path)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Path not found"))?
|
||||
.into_iter()
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn read_contents(&self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
let inner = self.state.inner.lock().unwrap();
|
||||
|
||||
let node = inner.entries.get(path);
|
||||
|
||||
match node {
|
||||
Some(TestFetcherEntry::File(contents)) => Ok(contents.clone()),
|
||||
Some(TestFetcherEntry::Dir) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Cannot read contents of a directory",
|
||||
)),
|
||||
None => Err(io::Error::new(io::ErrorKind::NotFound, "Path not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_directory(&self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"TestFetcher is not mutable yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn write_file(&self, _path: &Path, _contents: &[u8]) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"TestFetcher is not mutable yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove(&self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"TestFetcher is not mutable yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
self.receiver.clone()
|
||||
}
|
||||
}
|
||||
614
src/vfs/vfs.rs
614
src/vfs/vfs.rs
@@ -1,614 +0,0 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use crate::path_map::PathMap;
|
||||
|
||||
use super::{
|
||||
error::{FsError, FsResult},
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
snapshot::VfsSnapshot,
|
||||
};
|
||||
|
||||
/// An in-memory filesystem that can be incrementally populated and updated as
|
||||
/// filesystem modification events occur.
|
||||
///
|
||||
/// All operations on the `Vfs` are lazy and do I/O as late as they can to
|
||||
/// avoid reading extraneous files or directories from the disk. This means that
|
||||
/// they all take `self` mutably, and means that it isn't possible to hold
|
||||
/// references to the internal state of the Vfs while traversing it!
|
||||
///
|
||||
/// Most operations return `VfsEntry` objects to work around this, which is
|
||||
/// effectively a index into the `Vfs`.
|
||||
pub struct Vfs<F> {
|
||||
/// A hierarchical map from paths to items that have been read or partially
|
||||
/// read into memory by the Vfs.
|
||||
data: Mutex<PathMap<VfsItem>>,
|
||||
|
||||
/// This Vfs's fetcher, which is used for all actual interactions with the
|
||||
/// filesystem. It's referred to by the type parameter `F` all over, and is
|
||||
/// generic in order to make it feasible to mock.
|
||||
fetcher: F,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> Vfs<F> {
|
||||
pub fn new(fetcher: F) -> Self {
|
||||
Self {
|
||||
data: Mutex::new(PathMap::new()),
|
||||
fetcher,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_receiver(&self) -> Receiver<VfsEvent> {
|
||||
self.fetcher.receiver()
|
||||
}
|
||||
|
||||
pub fn commit_change(&self, event: &VfsEvent) -> FsResult<()> {
|
||||
use VfsEvent::*;
|
||||
|
||||
log::trace!("Committing Vfs change {:?}", event);
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
|
||||
match event {
|
||||
Created(path) | Modified(path) => {
|
||||
Self::raise_file_changed(&mut data, &self.fetcher, path)?;
|
||||
}
|
||||
Removed(path) => {
|
||||
Self::raise_file_removed(&mut data, &self.fetcher, path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, path: impl AsRef<Path>) -> FsResult<VfsEntry> {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
Self::get_internal(&mut data, &self.fetcher, path)
|
||||
}
|
||||
|
||||
pub fn get_contents(&self, path: impl AsRef<Path>) -> FsResult<Arc<Vec<u8>>> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
Self::read_if_not_exists(&mut data, &self.fetcher, path)?;
|
||||
|
||||
match data.get_mut(path).unwrap() {
|
||||
VfsItem::File(file) => {
|
||||
if file.contents.is_none() {
|
||||
file.contents = Some(
|
||||
self.fetcher
|
||||
.read_contents(path)
|
||||
.map(Arc::new)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(file.contents.clone().unwrap())
|
||||
}
|
||||
VfsItem::Directory(_) => Err(FsError::new(
|
||||
io::Error::new(io::ErrorKind::Other, "Can't read a directory"),
|
||||
path.to_path_buf(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_children(&self, path: impl AsRef<Path>) -> FsResult<Vec<VfsEntry>> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
Self::read_if_not_exists(&mut data, &self.fetcher, path)?;
|
||||
|
||||
match data.get_mut(path).unwrap() {
|
||||
VfsItem::Directory(dir) => {
|
||||
self.fetcher.watch(path);
|
||||
|
||||
// If the directory hasn't been marked as enumerated yet, find
|
||||
// all of its children and insert them into the VFS.
|
||||
if !dir.children_enumerated {
|
||||
dir.children_enumerated = true;
|
||||
|
||||
let children = self
|
||||
.fetcher
|
||||
.read_children(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?;
|
||||
|
||||
for path in children {
|
||||
Self::get_internal(&mut data, &self.fetcher, path)?;
|
||||
}
|
||||
}
|
||||
|
||||
data.children(path)
|
||||
.unwrap() // TODO: Handle None here, which means the PathMap entry did not exist.
|
||||
.into_iter()
|
||||
.map(PathBuf::from) // Convert paths from &Path to PathBuf
|
||||
.collect::<Vec<PathBuf>>() // Collect all PathBufs, since self.get needs to borrow self mutably.
|
||||
.into_iter()
|
||||
.map(|path| Self::get_internal(&mut data, &self.fetcher, path))
|
||||
.collect::<FsResult<Vec<VfsEntry>>>()
|
||||
}
|
||||
VfsItem::File(_) => Err(FsError::new(
|
||||
io::Error::new(io::ErrorKind::Other, "Can't read a directory"),
|
||||
path.to_path_buf(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_internal(
|
||||
data: &mut PathMap<VfsItem>,
|
||||
fetcher: &F,
|
||||
path: impl AsRef<Path>,
|
||||
) -> FsResult<VfsEntry> {
|
||||
let path = path.as_ref();
|
||||
|
||||
Self::read_if_not_exists(data, fetcher, path)?;
|
||||
|
||||
let item = data.get(path).unwrap();
|
||||
|
||||
let is_file = match item {
|
||||
VfsItem::File(_) => true,
|
||||
VfsItem::Directory(_) => false,
|
||||
};
|
||||
|
||||
Ok(VfsEntry {
|
||||
path: item.path().to_path_buf(),
|
||||
is_file,
|
||||
})
|
||||
}
|
||||
|
||||
fn raise_file_changed(
|
||||
data: &mut PathMap<VfsItem>,
|
||||
fetcher: &F,
|
||||
path: impl AsRef<Path>,
|
||||
) -> FsResult<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !Self::would_be_resident(&data, path) {
|
||||
log::trace!(
|
||||
"Path would not be resident, skipping change: {}",
|
||||
path.display()
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_type = fetcher
|
||||
.file_type(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?;
|
||||
|
||||
match data.get_mut(path) {
|
||||
Some(existing_item) => {
|
||||
match (existing_item, &new_type) {
|
||||
(VfsItem::File(existing_file), FileType::File) => {
|
||||
// Invalidate the existing file contents.
|
||||
// We can probably be smarter about this by reading the changed file.
|
||||
existing_file.contents = None;
|
||||
}
|
||||
(VfsItem::Directory(_), FileType::Directory) => {
|
||||
// No changes required, a directory updating doesn't mean anything to us.
|
||||
fetcher.watch(path);
|
||||
}
|
||||
(VfsItem::File(_), FileType::Directory) => {
|
||||
data.remove(path);
|
||||
data.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::new_from_type(FileType::Directory, path),
|
||||
);
|
||||
fetcher.watch(path);
|
||||
}
|
||||
(VfsItem::Directory(_), FileType::File) => {
|
||||
data.remove(path);
|
||||
data.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::new_from_type(FileType::File, path),
|
||||
);
|
||||
fetcher.unwatch(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::trace!("Inserting new path {}", path.display());
|
||||
data.insert(path.to_path_buf(), VfsItem::new_from_type(new_type, path));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raise_file_removed(
|
||||
data: &mut PathMap<VfsItem>,
|
||||
fetcher: &F,
|
||||
path: impl AsRef<Path>,
|
||||
) -> FsResult<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !Self::would_be_resident(data, path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
data.remove(path);
|
||||
fetcher.unwatch(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to read the path into the `Vfs` if it doesn't exist.
|
||||
///
|
||||
/// This does not necessitate that file contents or directory children will
|
||||
/// be read. Depending on the `VfsFetcher` implementation that the `Vfs`
|
||||
/// is using, this call may read exactly only the given path and no more.
|
||||
fn read_if_not_exists(data: &mut PathMap<VfsItem>, fetcher: &F, path: &Path) -> FsResult<()> {
|
||||
if !data.contains_key(path) {
|
||||
let kind = fetcher
|
||||
.file_type(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?;
|
||||
|
||||
if kind == FileType::Directory {
|
||||
fetcher.watch(path);
|
||||
}
|
||||
|
||||
data.insert(path.to_path_buf(), VfsItem::new_from_type(kind, path));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tells whether the given path, if it were loaded, would be loaded if it
|
||||
/// existed.
|
||||
///
|
||||
/// Returns true if the path is loaded or if its parent is loaded, is a
|
||||
/// directory, and is marked as having been enumerated before.
|
||||
///
|
||||
/// This idea corresponds to whether a file change event should result in
|
||||
/// tangible changes to the in-memory filesystem. If a path would be
|
||||
/// resident, we need to read it, and if its contents were known before, we
|
||||
/// need to update them.
|
||||
fn would_be_resident(data: &PathMap<VfsItem>, path: &Path) -> bool {
|
||||
if data.contains_key(path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Some(VfsItem::Directory(dir)) = data.get(parent) {
|
||||
return dir.children_enumerated;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains extra methods that should only be used for debugging. They're
|
||||
/// broken out into a separate trait to make it more explicit to depend on them.
|
||||
pub trait VfsDebug {
|
||||
fn debug_load_snapshot<P: AsRef<Path>>(&self, path: P, snapshot: VfsSnapshot);
|
||||
fn debug_is_file(&self, path: &Path) -> bool;
|
||||
fn debug_contents(&self, path: &Path) -> Option<Arc<Vec<u8>>>;
|
||||
fn debug_children(&self, path: &Path) -> Option<(bool, Vec<PathBuf>)>;
|
||||
fn debug_orphans(&self) -> Vec<PathBuf>;
|
||||
fn debug_watched_paths(&self) -> Vec<PathBuf>;
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> VfsDebug for Vfs<F> {
|
||||
fn debug_load_snapshot<P: AsRef<Path>>(&self, path: P, snapshot: VfsSnapshot) {
|
||||
fn load_snapshot<P: AsRef<Path>>(
|
||||
data: &mut PathMap<VfsItem>,
|
||||
path: P,
|
||||
snapshot: VfsSnapshot,
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
|
||||
match snapshot {
|
||||
VfsSnapshot::File(file) => {
|
||||
data.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::File(VfsFile {
|
||||
path: path.to_path_buf(),
|
||||
contents: Some(Arc::new(file.contents)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
VfsSnapshot::Directory(directory) => {
|
||||
data.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::Directory(VfsDirectory {
|
||||
path: path.to_path_buf(),
|
||||
children_enumerated: true,
|
||||
}),
|
||||
);
|
||||
|
||||
for (child_name, child) in directory.children.into_iter() {
|
||||
load_snapshot(data, path.join(child_name), child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
load_snapshot(&mut data, path, snapshot)
|
||||
}
|
||||
|
||||
fn debug_is_file(&self, path: &Path) -> bool {
|
||||
let data = self.data.lock().unwrap();
|
||||
match data.get(path) {
|
||||
Some(VfsItem::File(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_contents(&self, path: &Path) -> Option<Arc<Vec<u8>>> {
|
||||
let data = self.data.lock().unwrap();
|
||||
match data.get(path) {
|
||||
Some(VfsItem::File(file)) => file.contents.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_children(&self, path: &Path) -> Option<(bool, Vec<PathBuf>)> {
|
||||
let data = self.data.lock().unwrap();
|
||||
match data.get(path) {
|
||||
Some(VfsItem::Directory(dir)) => Some((
|
||||
dir.children_enumerated,
|
||||
data.children(path)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_orphans(&self) -> Vec<PathBuf> {
|
||||
let data = self.data.lock().unwrap();
|
||||
data.orphans().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
|
||||
fn debug_watched_paths(&self) -> Vec<PathBuf> {
|
||||
self.fetcher.watched_paths()
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to file or folder in an `Vfs`. Can only be produced by the
|
||||
/// entry existing in the Vfs, but can later point to nothing if something
|
||||
/// would invalidate that path.
|
||||
///
|
||||
/// This struct does not borrow from the Vfs since every operation has the
|
||||
/// possibility to mutate the underlying data structure and move memory around.
|
||||
pub struct VfsEntry {
|
||||
path: PathBuf,
|
||||
is_file: bool,
|
||||
}
|
||||
|
||||
impl VfsEntry {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn contents(&self, vfs: &Vfs<impl VfsFetcher>) -> FsResult<Arc<Vec<u8>>> {
|
||||
vfs.get_contents(&self.path)
|
||||
}
|
||||
|
||||
pub fn children(&self, vfs: &Vfs<impl VfsFetcher>) -> FsResult<Vec<VfsEntry>> {
|
||||
vfs.get_children(&self.path)
|
||||
}
|
||||
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.is_file
|
||||
}
|
||||
|
||||
pub fn is_directory(&self) -> bool {
|
||||
!self.is_file
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal structure describing potentially partially-resident files and
|
||||
/// folders in the `Vfs`.
|
||||
#[derive(Debug)]
|
||||
pub enum VfsItem {
|
||||
File(VfsFile),
|
||||
Directory(VfsDirectory),
|
||||
}
|
||||
|
||||
impl VfsItem {
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
VfsItem::File(file) => &file.path,
|
||||
VfsItem::Directory(dir) => &dir.path,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_from_type(kind: FileType, path: impl Into<PathBuf>) -> VfsItem {
|
||||
match kind {
|
||||
FileType::Directory => VfsItem::Directory(VfsDirectory {
|
||||
path: path.into(),
|
||||
children_enumerated: false,
|
||||
}),
|
||||
FileType::File => VfsItem::File(VfsFile {
|
||||
path: path.into(),
|
||||
contents: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VfsFile {
|
||||
pub(super) path: PathBuf,
|
||||
pub(super) contents: Option<Arc<Vec<u8>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VfsDirectory {
|
||||
pub(super) path: PathBuf,
|
||||
pub(super) children_enumerated: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use maplit::hashmap;
|
||||
|
||||
use super::super::{error::FsErrorKind, event::VfsEvent, noop_fetcher::NoopFetcher};
|
||||
|
||||
#[test]
|
||||
fn from_snapshot_file() {
|
||||
let vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("hello, world!");
|
||||
|
||||
vfs.debug_load_snapshot("/hello.txt", file);
|
||||
|
||||
let contents = vfs.get_contents("/hello.txt").unwrap();
|
||||
assert_eq!(contents.as_slice(), b"hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_snapshot_dir() {
|
||||
let vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"a.txt" => VfsSnapshot::file("contents of a.txt"),
|
||||
"b.lua" => VfsSnapshot::file("contents of b.lua"),
|
||||
});
|
||||
|
||||
vfs.debug_load_snapshot("/dir", dir);
|
||||
|
||||
let children = vfs.get_children("/dir").unwrap();
|
||||
|
||||
let mut has_a = false;
|
||||
let mut has_b = false;
|
||||
|
||||
for child in children.into_iter() {
|
||||
if child.path() == Path::new("/dir/a.txt") {
|
||||
has_a = true;
|
||||
} else if child.path() == Path::new("/dir/b.lua") {
|
||||
has_b = true;
|
||||
} else {
|
||||
panic!("Unexpected child in /dir");
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_a, "/dir/a.txt was missing");
|
||||
assert!(has_b, "/dir/b.lua was missing");
|
||||
|
||||
let a_contents = vfs.get_contents("/dir/a.txt").unwrap();
|
||||
assert_eq!(a_contents.as_slice(), b"contents of a.txt");
|
||||
|
||||
let b_contents = vfs.get_contents("/dir/b.lua").unwrap();
|
||||
assert_eq!(b_contents.as_slice(), b"contents of b.lua");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_event() {
|
||||
#[derive(Default)]
|
||||
struct MockState {
|
||||
a_contents: &'static str,
|
||||
}
|
||||
|
||||
struct MockFetcher {
|
||||
inner: Rc<RefCell<MockState>>,
|
||||
}
|
||||
|
||||
impl VfsFetcher for MockFetcher {
|
||||
fn file_type(&self, path: &Path) -> io::Result<FileType> {
|
||||
if path == Path::new("/dir/a.txt") {
|
||||
return Ok(FileType::File);
|
||||
}
|
||||
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn read_contents(&self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
if path == Path::new("/dir/a.txt") {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
return Ok(Vec::from(inner.a_contents));
|
||||
}
|
||||
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn read_children(&self, _path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn create_directory(&self, _path: &Path) -> io::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn write_file(&self, _path: &Path, _contents: &[u8]) -> io::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn remove(&self, _path: &Path) -> io::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
}
|
||||
|
||||
let mock_state = Rc::new(RefCell::new(MockState {
|
||||
a_contents: "Initial contents",
|
||||
}));
|
||||
|
||||
let mut vfs = Vfs::new(MockFetcher {
|
||||
inner: mock_state.clone(),
|
||||
});
|
||||
|
||||
let a = vfs.get("/dir/a.txt").expect("mock file did not exist");
|
||||
|
||||
let contents = a.contents(&mut vfs).expect("mock file contents error");
|
||||
|
||||
assert_eq!(contents.as_slice(), b"Initial contents");
|
||||
|
||||
{
|
||||
let mut mock_state = mock_state.borrow_mut();
|
||||
mock_state.a_contents = "Changed contents";
|
||||
}
|
||||
|
||||
vfs.commit_change(&VfsEvent::Modified(PathBuf::from("/dir/a.txt")))
|
||||
.expect("error processing file change");
|
||||
|
||||
let contents = a.contents(&mut vfs).expect("mock file contents error");
|
||||
|
||||
assert_eq!(contents.as_slice(), b"Changed contents");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_event_existing() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
|
||||
let file = VfsSnapshot::file("hello, world!");
|
||||
vfs.debug_load_snapshot("/hello.txt", file);
|
||||
|
||||
let hello = vfs.get("/hello.txt").expect("couldn't get hello.txt");
|
||||
|
||||
let contents = hello
|
||||
.contents(&mut vfs)
|
||||
.expect("couldn't get hello.txt contents");
|
||||
|
||||
assert_eq!(contents.as_slice(), b"hello, world!");
|
||||
|
||||
vfs.commit_change(&VfsEvent::Removed(PathBuf::from("/hello.txt")))
|
||||
.expect("error processing file removal");
|
||||
|
||||
match vfs.get("hello.txt") {
|
||||
Err(ref err) if err.kind() == FsErrorKind::NotFound => {}
|
||||
Ok(_) => {
|
||||
panic!("hello.txt was not removed from Vfs");
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Unexpected error: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,23 +10,22 @@ use rbx_dom_weak::RbxId;
|
||||
|
||||
use crate::{
|
||||
serve_session::ServeSession,
|
||||
snapshot::{PatchSet, PatchUpdate},
|
||||
vfs::VfsFetcher,
|
||||
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},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ApiService<F> {
|
||||
serve_session: Arc<ServeSession<F>>,
|
||||
pub struct ApiService {
|
||||
serve_session: Arc<ServeSession>,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> Service for ApiService<F> {
|
||||
impl Service for ApiService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
@@ -40,11 +39,12 @@ impl<F: VfsFetcher> Service for ApiService<F> {
|
||||
(&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,
|
||||
@@ -53,8 +53,8 @@ impl<F: VfsFetcher> Service for ApiService<F> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> ApiService<F> {
|
||||
pub fn new(serve_session: Arc<ServeSession<F>>) -> Self {
|
||||
impl ApiService {
|
||||
pub fn new(serve_session: Arc<ServeSession>) -> Self {
|
||||
ApiService { serve_session }
|
||||
}
|
||||
|
||||
@@ -234,4 +234,76 @@ impl<F: VfsFetcher> ApiService<F> {
|
||||
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")]
|
||||
|
||||
@@ -13,16 +13,16 @@ use futures::{
|
||||
use hyper::{service::Service, Body, Request, Response, Server};
|
||||
use log::trace;
|
||||
|
||||
use crate::{serve_session::ServeSession, vfs::VfsFetcher};
|
||||
use crate::serve_session::ServeSession;
|
||||
|
||||
use self::{api::ApiService, ui::UiService};
|
||||
|
||||
pub struct RootService<F> {
|
||||
api: ApiService<F>,
|
||||
ui: UiService<F>,
|
||||
pub struct RootService {
|
||||
api: ApiService,
|
||||
ui: UiService,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> Service for RootService<F> {
|
||||
impl Service for RootService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
@@ -39,8 +39,8 @@ impl<F: VfsFetcher> Service for RootService<F> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> RootService<F> {
|
||||
pub fn new(serve_session: Arc<ServeSession<F>>) -> Self {
|
||||
impl RootService {
|
||||
pub fn new(serve_session: Arc<ServeSession>) -> Self {
|
||||
RootService {
|
||||
api: ApiService::new(Arc::clone(&serve_session)),
|
||||
ui: UiService::new(Arc::clone(&serve_session)),
|
||||
@@ -48,12 +48,12 @@ impl<F: VfsFetcher> RootService<F> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LiveServer<F> {
|
||||
serve_session: Arc<ServeSession<F>>,
|
||||
pub struct LiveServer {
|
||||
serve_session: Arc<ServeSession>,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher + Send + Sync + 'static> LiveServer<F> {
|
||||
pub fn new(serve_session: Arc<ServeSession<F>>) -> Self {
|
||||
impl LiveServer {
|
||||
pub fn new(serve_session: Arc<ServeSession>) -> Self {
|
||||
LiveServer { serve_session }
|
||||
}
|
||||
|
||||
|
||||
109
src/web/ui.rs
109
src/web/ui.rs
@@ -1,6 +1,6 @@
|
||||
//! Defines the HTTP-based UI. These endpoints generally return HTML and SVG.
|
||||
|
||||
use std::{borrow::Cow, path::Path, sync::Arc, time::Duration};
|
||||
use std::{borrow::Cow, sync::Arc, time::Duration};
|
||||
|
||||
use futures::{future, Future};
|
||||
use hyper::{header, service::Service, Body, Method, Request, Response, StatusCode};
|
||||
@@ -11,7 +11,6 @@ use ritz::{html, Fragment, HtmlContent, HtmlSelfClosingTag};
|
||||
use crate::{
|
||||
serve_session::ServeSession,
|
||||
snapshot::RojoTree,
|
||||
vfs::{Vfs, VfsDebug, VfsFetcher},
|
||||
web::{
|
||||
assets,
|
||||
interface::{ErrorResponse, SERVER_VERSION},
|
||||
@@ -19,11 +18,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
pub struct UiService<F> {
|
||||
serve_session: Arc<ServeSession<F>>,
|
||||
pub struct UiService {
|
||||
serve_session: Arc<ServeSession>,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> Service for UiService<F> {
|
||||
impl Service for UiService {
|
||||
type ReqBody = Body;
|
||||
type ResBody = Body;
|
||||
type Error = hyper::Error;
|
||||
@@ -35,7 +34,6 @@ impl<F: VfsFetcher> Service for UiService<F> {
|
||||
(&Method::GET, "/logo.png") => self.handle_logo(),
|
||||
(&Method::GET, "/icon.png") => self.handle_icon(),
|
||||
(&Method::GET, "/show-instances") => self.handle_show_instances(),
|
||||
(&Method::GET, "/show-vfs") => self.handle_show_vfs(),
|
||||
(_method, path) => {
|
||||
return json(
|
||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||
@@ -48,8 +46,8 @@ impl<F: VfsFetcher> Service for UiService<F> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> UiService<F> {
|
||||
pub fn new(serve_session: Arc<ServeSession<F>>) -> Self {
|
||||
impl UiService {
|
||||
pub fn new(serve_session: Arc<ServeSession>) -> Self {
|
||||
UiService { serve_session }
|
||||
}
|
||||
|
||||
@@ -71,7 +69,6 @@ impl<F: VfsFetcher> UiService<F> {
|
||||
let page = self.normal_page(html! {
|
||||
<div class="button-list">
|
||||
{ Self::button("Rojo Documentation", "https://rojo.space/docs") }
|
||||
{ Self::button("View virtual filesystem state", "/show-vfs") }
|
||||
{ Self::button("View instance tree state", "/show-instances") }
|
||||
</div>
|
||||
});
|
||||
@@ -96,100 +93,6 @@ impl<F: VfsFetcher> UiService<F> {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn handle_show_vfs(&self) -> Response<Body> {
|
||||
let vfs = self.serve_session.vfs();
|
||||
|
||||
let orphans: Vec<_> = vfs
|
||||
.debug_orphans()
|
||||
.into_iter()
|
||||
.map(|path| Self::render_vfs_path(&vfs, &path, true))
|
||||
.collect();
|
||||
|
||||
let watched_list: Vec<_> = vfs
|
||||
.debug_watched_paths()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
html! {
|
||||
<li>{ format!("{}", path.display()) }</li>
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let page = self.normal_page(html! {
|
||||
<>
|
||||
<section class="main-section">
|
||||
<h1 class="section-title">"Known FS Items"</h1>
|
||||
<div>{ Fragment::new(orphans) }</div>
|
||||
</section>
|
||||
|
||||
<section class="main-section">
|
||||
<h1 class="section-title">"Watched Paths"</h1>
|
||||
<ul class="path-list">{ Fragment::new(watched_list) }</ul>
|
||||
</section>
|
||||
</>
|
||||
});
|
||||
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/html")
|
||||
.body(Body::from(format!("<!DOCTYPE html>{}", page)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn render_vfs_path(vfs: &Vfs<F>, path: &Path, is_root: bool) -> HtmlContent<'static> {
|
||||
let is_file = vfs.debug_is_file(path);
|
||||
|
||||
let (note, children) = if is_file {
|
||||
(HtmlContent::None, Vec::new())
|
||||
} else {
|
||||
let (is_exhaustive, mut children) = vfs.debug_children(path).unwrap();
|
||||
|
||||
// Sort files above directories, then sort how Path does after that.
|
||||
children.sort_unstable_by(|a, b| {
|
||||
let a_is_file = vfs.debug_is_file(a);
|
||||
let b_is_file = vfs.debug_is_file(b);
|
||||
|
||||
b_is_file.cmp(&a_is_file).then_with(|| a.cmp(b))
|
||||
});
|
||||
|
||||
let children: Vec<_> = children
|
||||
.into_iter()
|
||||
.map(|child| Self::render_vfs_path(vfs, &child, false))
|
||||
.collect();
|
||||
|
||||
let note = if is_exhaustive {
|
||||
HtmlContent::None
|
||||
} else {
|
||||
html!({ " (not enumerated)" })
|
||||
};
|
||||
|
||||
(note, children)
|
||||
};
|
||||
|
||||
// For root entries, we want the full path to contextualize the path.
|
||||
let mut name = if is_root {
|
||||
path.to_str().unwrap().to_owned()
|
||||
} else {
|
||||
path.file_name().unwrap().to_str().unwrap().to_owned()
|
||||
};
|
||||
|
||||
// Directories should end with `/` in the UI to mark them.
|
||||
if !is_file && !name.ends_with('/') && !name.ends_with('\\') {
|
||||
name.push('/');
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="vfs-entry">
|
||||
<div>
|
||||
<span class="vfs-entry-name">{ name }</span>
|
||||
<span class="vfs-entry-note">{ note }</span>
|
||||
</div>
|
||||
<div class="vfs-entry-children">
|
||||
{ Fragment::new(children) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn instance(tree: &RojoTree, id: RbxId) -> HtmlContent<'_> {
|
||||
let instance = tree.get_instance(id).unwrap();
|
||||
let children_list: Vec<_> = instance
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# [vfs]
|
||||
Name pending. Implementation of a virtual filesystem with a configurable backend and file watching.
|
||||
|
||||
## License
|
||||
[vfs] is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||
Reference in New Issue
Block a user