mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 22:25:26 +00:00
Compare commits
47 Commits
v0.6.0-alp
...
v6.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
def99a9e4d | ||
|
|
1214fc8b0d | ||
|
|
5a5b1268d3 | ||
|
|
6a1fffd1ce | ||
|
|
571ef3060a | ||
|
|
3cf82e112f | ||
|
|
9b459c20d6 | ||
|
|
5c85cd27c3 | ||
|
|
4bf73c7a8a | ||
|
|
62e51b7535 | ||
|
|
729a7f0053 | ||
|
|
03c297190d | ||
|
|
9c790eddd7 | ||
|
|
8ebe7e332b | ||
|
|
f43777e37e | ||
|
|
691a8fcdeb | ||
|
|
69c0e8d70e | ||
|
|
330c92c9a8 | ||
|
|
cf0ff60d31 | ||
|
|
9e9cf5dd1f | ||
|
|
5768d8e4a4 | ||
|
|
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 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -1,6 +1,9 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: [push]
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: ["*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -13,6 +16,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
run: rustup default ${{ matrix.rust_version }}
|
run: rustup default ${{ matrix.rust_version }}
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -10,6 +10,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Build release binary
|
- name: Build release binary
|
||||||
run: cargo build --verbose --locked --release
|
run: cargo build --verbose --locked --release
|
||||||
@@ -25,6 +27,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
@@ -47,6 +51,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose --release
|
run: cargo build --locked --verbose --release
|
||||||
|
|||||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -9,7 +9,4 @@
|
|||||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||||
[submodule "plugin/modules/t"]
|
[submodule "plugin/modules/t"]
|
||||||
path = plugin/modules/t
|
path = plugin/modules/t
|
||||||
url = https://github.com/osyrisrblx/t.git
|
url = https://github.com/osyrisrblx/t.git
|
||||||
[submodule "plugin/modules/rbx-dom"]
|
|
||||||
path = plugin/modules/rbx-dom
|
|
||||||
url = http://github.com/rojo-rbx/rbx-dom
|
|
||||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,6 +1,20 @@
|
|||||||
# Rojo Changelog
|
# Rojo Changelog
|
||||||
|
|
||||||
## Unreleased Changes for 0.6.x
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## [6.0.0 Release Candidate 1](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.1) (March 29, 2020)
|
||||||
|
This release jumped from 0.6.0 to 6.0.0. Rojo has been in use in production for many users for quite a long times, and so 6.0 is a more accurate reflection of Rojo's version than a pre-1.0 version.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
* Added support for turning `.json` files into `ModuleScript` instances ([#308](https://github.com/rojo-rbx/rojo/pull/308))
|
||||||
|
* Added `rojo plugin install` and `rojo plugin uninstall` to allow Rojo to manage its Roblox Studio plugin. ([#304](https://github.com/rojo-rbx/rojo/pull/304))
|
||||||
|
* Class names no longer need to be specified for Roblox services in Rojo projects. ([#210](https://github.com/rojo-rbx/rojo/pull/210))
|
||||||
|
* The server half of **experimental** two-way sync is now enabled by default.
|
||||||
|
* Increased default logging verbosity in commands like `rojo build`.
|
||||||
|
* Rojo now requires a project file again, just like 0.5.4.
|
||||||
|
|
||||||
## [0.6.0 Alpha 3](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.3) (March 13, 2020)
|
## [0.6.0 Alpha 3](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.3) (March 13, 2020)
|
||||||
* Added `--watch` argument to `rojo build`. ([#284](https://github.com/rojo-rbx/rojo/pull/284))
|
* Added `--watch` argument to `rojo build`. ([#284](https://github.com/rojo-rbx/rojo/pull/284))
|
||||||
|
|||||||
@@ -33,15 +33,13 @@ Please file issues and we'll try to help figure out what the best way forward is
|
|||||||
## Pushing a Rojo Release
|
## Pushing a Rojo Release
|
||||||
The Rojo release process is pretty manual right now. If you need to do it, here's how:
|
The Rojo release process is pretty manual right now. If you need to do it, here's how:
|
||||||
|
|
||||||
1. Bump server version in [`server/Cargo.toml`](server/Cargo.toml)
|
1. Bump server version in [`Cargo.toml`](Cargo.toml)
|
||||||
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
|
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
|
||||||
3. Run `cargo test` to update `Cargo.lock` and double-check tests
|
3. Run `cargo test` to update `Cargo.lock` and double-check tests
|
||||||
4. Update [`CHANGELOG.md`](CHANGELOG.md)
|
4. Update [`CHANGELOG.md`](CHANGELOG.md)
|
||||||
5. Commit!
|
5. Commit!
|
||||||
* `git add . && git commit -m "Release vX.Y.Z"`
|
* `git add . && git commit -m "Release vX.Y.Z"`
|
||||||
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
|
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
|
||||||
7. Build Windows release build of CLI
|
|
||||||
* `cargo build --release`
|
|
||||||
7. Publish the CLI
|
7. Publish the CLI
|
||||||
* `cargo publish`
|
* `cargo publish`
|
||||||
8. Build and upload the plugin
|
8. Build and upload the plugin
|
||||||
@@ -52,4 +50,5 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
|
|||||||
10. Copy GitHub release content from previous release
|
10. Copy GitHub release content from previous release
|
||||||
* Update the leading text with a summary about the release
|
* Update the leading text with a summary about the release
|
||||||
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
|
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
|
||||||
* Write a small summary of each major feature
|
* Write a small summary of each major feature
|
||||||
|
* Attach release artifacts from GitHub Actions for each platform
|
||||||
2236
Cargo.lock
generated
2236
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.6.0-alpha.3"
|
version = "6.0.0-rc.1"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "Enables professional-grade development tools for Roblox developers"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -11,7 +11,6 @@ readme = "README.md"
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
"/plugin/**",
|
|
||||||
"/test-projects/**",
|
"/test-projects/**",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -27,9 +26,6 @@ default = []
|
|||||||
# Turn on support for specifying glob ignore path rules in the project format.
|
# Turn on support for specifying glob ignore path rules in the project format.
|
||||||
unstable_glob_ignore_paths = []
|
unstable_glob_ignore_paths = []
|
||||||
|
|
||||||
# Turn on the server half of Rojo's unstable two-way sync feature.
|
|
||||||
unstable_two_way_sync = []
|
|
||||||
|
|
||||||
# Enable this feature to live-reload assets from the web UI.
|
# Enable this feature to live-reload assets from the web UI.
|
||||||
dev_live_assets = []
|
dev_live_assets = []
|
||||||
|
|
||||||
@@ -61,12 +57,15 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
memofs = { version = "0.1.0", path = "memofs" }
|
memofs = { version = "0.1.2", path = "memofs" }
|
||||||
|
|
||||||
|
anyhow = "1.0.27"
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
|
bincode = "1.2.1"
|
||||||
crossbeam-channel = "0.4.0"
|
crossbeam-channel = "0.4.0"
|
||||||
csv = "1.1.1"
|
csv = "1.1.1"
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.7.1"
|
||||||
|
fs-err = "2.2.0"
|
||||||
futures = "0.1.29"
|
futures = "0.1.29"
|
||||||
globset = "0.4.4"
|
globset = "0.4.4"
|
||||||
humantime = "1.3.0"
|
humantime = "1.3.0"
|
||||||
@@ -85,17 +84,26 @@ regex = "1.3.1"
|
|||||||
reqwest = "0.9.20"
|
reqwest = "0.9.20"
|
||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
rlua = "0.17.0"
|
rlua = "0.17.0"
|
||||||
|
roblox_install = "0.2.2"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
snafu = "0.6.0"
|
|
||||||
structopt = "0.3.5"
|
structopt = "0.3.5"
|
||||||
termcolor = "1.0.5"
|
termcolor = "1.0.5"
|
||||||
|
thiserror = "1.0.11"
|
||||||
tokio = "0.1.22"
|
tokio = "0.1.22"
|
||||||
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.6.2"
|
winreg = "0.6.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
memofs = { version = "0.1.0", path = "memofs" }
|
||||||
|
|
||||||
|
anyhow = "1.0.27"
|
||||||
|
bincode = "1.2.1"
|
||||||
|
fs-err = "2.3.0"
|
||||||
|
maplit = "1.0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rojo-insta-ext = { path = "rojo-insta-ext" }
|
rojo-insta-ext = { path = "rojo-insta-ext" }
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
<a href="https://crates.io/crates/rojo">
|
<a href="https://crates.io/crates/rojo">
|
||||||
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
|
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://rojo.space/docs/0.5.x">
|
<a href="https://rojo.space/docs">
|
||||||
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo 0.5.x Documentation" />
|
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,11 +34,10 @@ Rojo enables:
|
|||||||
* Streaming `rbxmx` and `rbxm` models into your game in real time
|
* Streaming `rbxmx` and `rbxm` models into your game in real time
|
||||||
* Packaging and deploying your project to Roblox.com from the command line
|
* Packaging and deploying your project to Roblox.com from the command line
|
||||||
|
|
||||||
Soon, Rojo will be able to:
|
In the future, Rojo will be able to:
|
||||||
|
|
||||||
* Automatically convert your existing game to work with Rojo
|
|
||||||
* Sync instances from Roblox Studio to the filesystem
|
* Sync instances from Roblox Studio to the filesystem
|
||||||
* Automatically manage your assets on Roblox.com, like images and sounds
|
* Automatically convert your existing game to work with Rojo
|
||||||
* Import custom instances like MoonScript code
|
* Import custom instances like MoonScript code
|
||||||
|
|
||||||
## [Documentation](https://rojo.space/docs)
|
## [Documentation](https://rojo.space/docs)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"$className": "ReplicatedStorage",
|
"$className": "ReplicatedStorage",
|
||||||
|
|
||||||
"Common": {
|
"Common": {
|
||||||
"$path": "src/common"
|
"$path": "src/shared"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
74
build.rs
Normal file
74
build.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use std::{
|
||||||
|
env, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use fs_err as fs;
|
||||||
|
use fs_err::File;
|
||||||
|
use maplit::hashmap;
|
||||||
|
use memofs::VfsSnapshot;
|
||||||
|
|
||||||
|
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||||
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
|
||||||
|
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
// We can skip any TestEZ test files since they aren't necessary for
|
||||||
|
// the plugin to run.
|
||||||
|
if file_name.ends_with(".spec.lua") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
|
||||||
|
children.push((file_name, child_snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(VfsSnapshot::dir(children))
|
||||||
|
} else {
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
Ok(VfsSnapshot::file(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), anyhow::Error> {
|
||||||
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
|
||||||
|
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||||
|
|
||||||
|
let plugin_modules = plugin_root.join("modules");
|
||||||
|
|
||||||
|
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||||
|
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||||
|
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||||
|
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
||||||
|
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||||
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||||
|
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||||
|
"modules" => VfsSnapshot::dir(hashmap! {
|
||||||
|
"roact" => VfsSnapshot::dir(hashmap! {
|
||||||
|
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
|
||||||
|
}),
|
||||||
|
"promise" => VfsSnapshot::dir(hashmap! {
|
||||||
|
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
|
||||||
|
}),
|
||||||
|
"t" => VfsSnapshot::dir(hashmap! {
|
||||||
|
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
|
let out_file = File::create(&out_path)?;
|
||||||
|
|
||||||
|
bincode::serialize_into(out_file, &snapshot)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -2,5 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## 0.1.1 (2020-03-18)
|
||||||
|
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
|
||||||
|
|
||||||
## 0.1.0 (2020-03-10)
|
## 0.1.0 (2020-03-10)
|
||||||
* Initial release
|
* Initial release
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -11,5 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notify = "4.0.15"
|
|
||||||
crossbeam-channel = "0.4.0"
|
crossbeam-channel = "0.4.0"
|
||||||
|
fs-err = "2.3.0"
|
||||||
|
notify = "4.0.15"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
/// A slice of a tree of files. Can be loaded into an
|
/// A slice of a tree of files. Can be loaded into an
|
||||||
/// [`InMemoryFs`](struct.InMemoryFs.html).
|
/// [`InMemoryFs`](struct.InMemoryFs.html).
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum VfsSnapshot {
|
pub enum VfsSnapshot {
|
||||||
File {
|
File {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::fs;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
@@ -55,15 +54,15 @@ impl StdBackend {
|
|||||||
|
|
||||||
impl VfsBackend for StdBackend {
|
impl VfsBackend for StdBackend {
|
||||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||||
fs::read(path)
|
fs_err::read(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||||
fs::write(path, data)
|
fs_err::write(path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
let entries: Result<Vec<_>, _> = fs::read_dir(path)?.collect();
|
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||||
let mut entries = entries?;
|
let mut entries = entries?;
|
||||||
|
|
||||||
entries.sort_by_cached_key(|entry| entry.file_name());
|
entries.sort_by_cached_key(|entry| entry.file_name());
|
||||||
@@ -78,15 +77,15 @@ impl VfsBackend for StdBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
fs::remove_file(path)
|
fs_err::remove_file(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||||
fs::remove_dir_all(path)
|
fs_err::remove_dir_all(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||||
let inner = fs::metadata(path)?;
|
let inner = fs_err::metadata(path)?;
|
||||||
|
|
||||||
Ok(Metadata {
|
Ok(Metadata {
|
||||||
is_file: inner.is_file(),
|
is_file: inner.is_file(),
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"Fmt": {
|
"Fmt": {
|
||||||
"$path": "fmt"
|
"$path": "fmt"
|
||||||
},
|
},
|
||||||
|
"RbxDom": {
|
||||||
|
"$path": "rbx_dom_lua"
|
||||||
|
},
|
||||||
"Roact": {
|
"Roact": {
|
||||||
"$path": "modules/roact/src"
|
"$path": "modules/roact/src"
|
||||||
},
|
},
|
||||||
@@ -22,9 +25,6 @@
|
|||||||
},
|
},
|
||||||
"t": {
|
"t": {
|
||||||
"$path": "modules/t/lib"
|
"$path": "modules/t/lib"
|
||||||
},
|
|
||||||
"RbxDom": {
|
|
||||||
"$path": "modules/rbx-dom/rbx_dom_lua/src"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Submodule plugin/modules/rbx-dom deleted from 5bca08fec3
44
plugin/rbx_dom_lua/.luacheckrc
Normal file
44
plugin/rbx_dom_lua/.luacheckrc
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
stds.roblox = {
|
||||||
|
read_globals = {
|
||||||
|
game = {
|
||||||
|
other_fields = true,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Roblox globals
|
||||||
|
"script",
|
||||||
|
|
||||||
|
-- Extra functions
|
||||||
|
"tick", "warn",
|
||||||
|
"wait", "typeof",
|
||||||
|
|
||||||
|
-- Types
|
||||||
|
"CFrame",
|
||||||
|
"Color3",
|
||||||
|
"Enum",
|
||||||
|
"Instance",
|
||||||
|
"NumberRange",
|
||||||
|
"Rect",
|
||||||
|
"UDim", "UDim2",
|
||||||
|
"Vector2", "Vector3",
|
||||||
|
"Vector2int16", "Vector3int16",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stds.testez = {
|
||||||
|
read_globals = {
|
||||||
|
"describe",
|
||||||
|
"it", "itFOCUS", "itSKIP",
|
||||||
|
"FOCUS", "SKIP", "HACK_NO_XPCALL",
|
||||||
|
"expect",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore = {
|
||||||
|
"212", -- unused arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
std = "lua51+roblox"
|
||||||
|
|
||||||
|
files["**/*.spec.lua"] = {
|
||||||
|
std = "+testez",
|
||||||
|
}
|
||||||
2
plugin/rbx_dom_lua/README.md
Normal file
2
plugin/rbx_dom_lua/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# rbx\_dom\_lua
|
||||||
|
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx\_dom\_weak and friends.
|
||||||
6
plugin/rbx_dom_lua/default.project.json
Normal file
6
plugin/rbx_dom_lua/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rbx_dom_lua",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
242
plugin/rbx_dom_lua/src/EncodedValue.lua
Normal file
242
plugin/rbx_dom_lua/src/EncodedValue.lua
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
local base64 = require(script.Parent.base64)
|
||||||
|
|
||||||
|
local function identity(...)
|
||||||
|
return ...
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unpackDecoder(f)
|
||||||
|
return function(value)
|
||||||
|
return f(unpack(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function serializeFloat(value)
|
||||||
|
-- TODO: Figure out a better way to serialize infinity and NaN, neither of
|
||||||
|
-- which fit into JSON.
|
||||||
|
if value == math.huge or value == -math.huge then
|
||||||
|
return 999999999 * math.sign(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local encoders
|
||||||
|
encoders = {
|
||||||
|
Bool = identity,
|
||||||
|
Content = identity,
|
||||||
|
Float32 = serializeFloat,
|
||||||
|
Float64 = serializeFloat,
|
||||||
|
Int32 = identity,
|
||||||
|
Int64 = identity,
|
||||||
|
String = identity,
|
||||||
|
|
||||||
|
BinaryString = base64.encode,
|
||||||
|
SharedString = base64.encode,
|
||||||
|
|
||||||
|
BrickColor = function(value)
|
||||||
|
return value.Number
|
||||||
|
end,
|
||||||
|
|
||||||
|
CFrame = function(value)
|
||||||
|
return {value:GetComponents()}
|
||||||
|
end,
|
||||||
|
Color3 = function(value)
|
||||||
|
return {value.r, value.g, value.b}
|
||||||
|
end,
|
||||||
|
NumberRange = function(value)
|
||||||
|
return {value.Min, value.Max}
|
||||||
|
end,
|
||||||
|
NumberSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = {
|
||||||
|
Time = keypoint.Time,
|
||||||
|
Value = keypoint.Value,
|
||||||
|
Envelope = keypoint.Envelope,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Keypoints = keypoints,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
ColorSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = {
|
||||||
|
Time = keypoint.Time,
|
||||||
|
Color = encoders.Color3(keypoint.Value),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Keypoints = keypoints,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Rect = function(value)
|
||||||
|
return {
|
||||||
|
Min = {value.Min.X, value.Min.Y},
|
||||||
|
Max = {value.Max.X, value.Max.Y},
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
UDim = function(value)
|
||||||
|
return {value.Scale, value.Offset}
|
||||||
|
end,
|
||||||
|
UDim2 = function(value)
|
||||||
|
return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset}
|
||||||
|
end,
|
||||||
|
Vector2 = function(value)
|
||||||
|
return {
|
||||||
|
serializeFloat(value.X),
|
||||||
|
serializeFloat(value.Y),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Vector2int16 = function(value)
|
||||||
|
return {value.X, value.Y}
|
||||||
|
end,
|
||||||
|
Vector3 = function(value)
|
||||||
|
return {
|
||||||
|
serializeFloat(value.X),
|
||||||
|
serializeFloat(value.Y),
|
||||||
|
serializeFloat(value.Z),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Vector3int16 = function(value)
|
||||||
|
return {value.X, value.Y, value.Z}
|
||||||
|
end,
|
||||||
|
|
||||||
|
PhysicalProperties = function(value)
|
||||||
|
if value == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return {
|
||||||
|
Density = value.Density,
|
||||||
|
Friction = value.Friction,
|
||||||
|
Elasticity = value.Elasticity,
|
||||||
|
FrictionWeight = value.FrictionWeight,
|
||||||
|
ElasticityWeight = value.ElasticityWeight,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
Ref = function(value)
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local decoders = {
|
||||||
|
Bool = identity,
|
||||||
|
Content = identity,
|
||||||
|
Enum = identity,
|
||||||
|
Float32 = identity,
|
||||||
|
Float64 = identity,
|
||||||
|
Int32 = identity,
|
||||||
|
Int64 = identity,
|
||||||
|
String = identity,
|
||||||
|
|
||||||
|
BinaryString = base64.decode,
|
||||||
|
SharedString = base64.decode,
|
||||||
|
|
||||||
|
BrickColor = BrickColor.new,
|
||||||
|
|
||||||
|
CFrame = unpackDecoder(CFrame.new),
|
||||||
|
Color3 = unpackDecoder(Color3.new),
|
||||||
|
Color3uint8 = unpackDecoder(Color3.fromRGB),
|
||||||
|
NumberRange = unpackDecoder(NumberRange.new),
|
||||||
|
UDim = unpackDecoder(UDim.new),
|
||||||
|
UDim2 = unpackDecoder(UDim2.new),
|
||||||
|
Vector2 = unpackDecoder(Vector2.new),
|
||||||
|
Vector2int16 = unpackDecoder(Vector2int16.new),
|
||||||
|
Vector3 = unpackDecoder(Vector3.new),
|
||||||
|
Vector3int16 = unpackDecoder(Vector3int16.new),
|
||||||
|
|
||||||
|
Rect = function(value)
|
||||||
|
return Rect.new(value.Min[1], value.Min[2], value.Max[1], value.Max[2])
|
||||||
|
end,
|
||||||
|
|
||||||
|
NumberSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = NumberSequenceKeypoint.new(
|
||||||
|
keypoint.Time,
|
||||||
|
keypoint.Value,
|
||||||
|
keypoint.Envelope
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return NumberSequence.new(keypoints)
|
||||||
|
end,
|
||||||
|
|
||||||
|
ColorSequence = function(value)
|
||||||
|
local keypoints = {}
|
||||||
|
|
||||||
|
for index, keypoint in ipairs(value.Keypoints) do
|
||||||
|
keypoints[index] = ColorSequenceKeypoint.new(
|
||||||
|
keypoint.Time,
|
||||||
|
Color3.new(unpack(keypoint.Color))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ColorSequence.new(keypoints)
|
||||||
|
end,
|
||||||
|
|
||||||
|
PhysicalProperties = function(properties)
|
||||||
|
if properties == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return PhysicalProperties.new(
|
||||||
|
properties.Density,
|
||||||
|
properties.Friction,
|
||||||
|
properties.Elasticity,
|
||||||
|
properties.FrictionWeight,
|
||||||
|
properties.ElasticityWeight
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
Ref = function()
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local EncodedValue = {}
|
||||||
|
|
||||||
|
function EncodedValue.decode(encodedValue)
|
||||||
|
local decoder = decoders[encodedValue.Type]
|
||||||
|
if decoder ~= nil then
|
||||||
|
return true, decoder(encodedValue.Value)
|
||||||
|
end
|
||||||
|
|
||||||
|
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncodedValue.encode(rbxValue, propertyType)
|
||||||
|
assert(propertyType ~= nil, "Property type descriptor is required")
|
||||||
|
|
||||||
|
if propertyType.type == "Data" then
|
||||||
|
local encoder = encoders[propertyType.name]
|
||||||
|
|
||||||
|
if encoder == nil then
|
||||||
|
return false, ("Missing encoder for property type %q"):format(propertyType.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if encoder ~= nil then
|
||||||
|
return true, {
|
||||||
|
Type = propertyType.name,
|
||||||
|
Value = encoder(rbxValue),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif propertyType.type == "Enum" then
|
||||||
|
return true, {
|
||||||
|
Type = "Enum",
|
||||||
|
Value = rbxValue.Value,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return false, ("Unknown property descriptor type %q"):format(tostring(propertyType.type))
|
||||||
|
end
|
||||||
|
|
||||||
|
return EncodedValue
|
||||||
127
plugin/rbx_dom_lua/src/EncodedValue.spec.lua
Normal file
127
plugin/rbx_dom_lua/src/EncodedValue.spec.lua
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
return function()
|
||||||
|
local RbxDom = require(script.Parent)
|
||||||
|
local EncodedValue = require(script.Parent.EncodedValue)
|
||||||
|
|
||||||
|
it("should decode Rect values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "Rect",
|
||||||
|
Value = {
|
||||||
|
Min = {1, 2},
|
||||||
|
Max = {3, 4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = Rect.new(1, 2, 3, 4)
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should decode ColorSequence values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "ColorSequence",
|
||||||
|
Value = {
|
||||||
|
Keypoints = {
|
||||||
|
{
|
||||||
|
Time = 0,
|
||||||
|
Color = { 0.12, 0.34, 0.56 },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Time = 1,
|
||||||
|
Color = { 0.13, 0.33, 0.37 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0, Color3.new(0.12, 0.34, 0.56)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0.13, 0.33, 0.37)),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should decode NumberSequence values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "NumberSequence",
|
||||||
|
Value = {
|
||||||
|
Keypoints = {
|
||||||
|
{
|
||||||
|
Time = 0,
|
||||||
|
Value = 0.5,
|
||||||
|
Envelope = 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Time = 1,
|
||||||
|
Value = 0.5,
|
||||||
|
Envelope = 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = NumberSequence.new({
|
||||||
|
NumberSequenceKeypoint.new(0, 0.5, 0),
|
||||||
|
NumberSequenceKeypoint.new(1, 0.5, 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should decode PhysicalProperties values", function()
|
||||||
|
local input = {
|
||||||
|
Type = "PhysicalProperties",
|
||||||
|
Value = {
|
||||||
|
Density = 0.1,
|
||||||
|
Friction = 0.2,
|
||||||
|
Elasticity = 0.3,
|
||||||
|
FrictionWeight = 0.4,
|
||||||
|
ElasticityWeight = 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local output = PhysicalProperties.new(
|
||||||
|
0.1,
|
||||||
|
0.2,
|
||||||
|
0.3,
|
||||||
|
0.4,
|
||||||
|
0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
local ok, decoded = EncodedValue.decode(input)
|
||||||
|
assert(ok, decoded)
|
||||||
|
expect(decoded).to.equal(output)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- This part of rbx_dom_lua needs some work still.
|
||||||
|
itSKIP("should encode Rect values", function()
|
||||||
|
local input = Rect.new(10, 20, 30, 40)
|
||||||
|
|
||||||
|
local output = {
|
||||||
|
Type = "Rect",
|
||||||
|
Value = {
|
||||||
|
Min = {10, 20},
|
||||||
|
Max = {30, 40},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local descriptor = RbxDom.findCanonicalPropertyDescriptor("ImageLabel", "SliceCenter")
|
||||||
|
local ok, encoded = EncodedValue.encode(input, descriptor)
|
||||||
|
|
||||||
|
assert(ok, encoded)
|
||||||
|
expect(encoded.Type).to.equal(output.Type)
|
||||||
|
expect(encoded.Value.Min[1]).to.equal(output.Value.Min[1])
|
||||||
|
expect(encoded.Value.Min[2]).to.equal(output.Value.Min[2])
|
||||||
|
expect(encoded.Value.Max[1]).to.equal(output.Value.Max[1])
|
||||||
|
expect(encoded.Value.Max[2]).to.equal(output.Value.Max[2])
|
||||||
|
end)
|
||||||
|
end
|
||||||
28
plugin/rbx_dom_lua/src/Error.lua
Normal file
28
plugin/rbx_dom_lua/src/Error.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
local Error = {}
|
||||||
|
Error.__index = Error
|
||||||
|
|
||||||
|
Error.Kind = {
|
||||||
|
UnknownProperty = "UnknownProperty",
|
||||||
|
PropertyNotReadable = "PropertyNotReadable",
|
||||||
|
PropertyNotWritable = "PropertyNotWritable",
|
||||||
|
Roblox = "Roblox",
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(Error.Kind, {
|
||||||
|
__index = function(_, key)
|
||||||
|
error(("%q is not a valid member of Error.Kind"):format(tostring(key)), 2)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
function Error.new(kind, extra)
|
||||||
|
return setmetatable({
|
||||||
|
kind = kind,
|
||||||
|
extra = extra,
|
||||||
|
}, Error)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error:__tostring()
|
||||||
|
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
|
||||||
|
end
|
||||||
|
|
||||||
|
return Error
|
||||||
80
plugin/rbx_dom_lua/src/PropertyDescriptor.lua
Normal file
80
plugin/rbx_dom_lua/src/PropertyDescriptor.lua
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
local Error = require(script.Parent.Error)
|
||||||
|
local customProperties = require(script.Parent.customProperties)
|
||||||
|
|
||||||
|
-- A wrapper around a property descriptor from the reflection database with some
|
||||||
|
-- extra convenience methods.
|
||||||
|
--
|
||||||
|
-- The aim of this API is to facilitate looking up a property once, then reading
|
||||||
|
-- from it or writing to it multiple times. It's also useful when a consumer
|
||||||
|
-- wants to check additional constraints on the property before trying to use
|
||||||
|
-- it, like scriptability.
|
||||||
|
local PropertyDescriptor = {}
|
||||||
|
PropertyDescriptor.__index = PropertyDescriptor
|
||||||
|
|
||||||
|
local function get(container, key)
|
||||||
|
return container[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set(container, key, value)
|
||||||
|
container[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
function PropertyDescriptor.fromRaw(data, className, propertyName)
|
||||||
|
return setmetatable({
|
||||||
|
scriptability = data.scriptability,
|
||||||
|
className = className,
|
||||||
|
name = propertyName,
|
||||||
|
}, PropertyDescriptor)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PropertyDescriptor:read(instance)
|
||||||
|
if self.scriptability == "ReadWrite" or self.scriptability == "Read" then
|
||||||
|
local success, value = xpcall(get, debug.traceback, instance, self.name)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return success, value
|
||||||
|
else
|
||||||
|
return false, Error.new(Error.Kind.Roblox, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "Custom" then
|
||||||
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
|
return interface.read(instance, self.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "None" or self.scriptability == "Write" then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
|
error(("Internal error: unexpected value of 'scriptability': %s"):format(tostring(self.scriptability)), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PropertyDescriptor:write(instance, value)
|
||||||
|
if self.scriptability == "ReadWrite" or self.scriptability == "Write" then
|
||||||
|
local success, err = xpcall(set, debug.traceback, instance, self.name, value)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return success
|
||||||
|
else
|
||||||
|
return false, Error.new(Error.Kind.Roblox, err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "Custom" then
|
||||||
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
|
return interface.write(instance, self.name, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.scriptability == "None" or self.scriptability == "Read" then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return PropertyDescriptor
|
||||||
18757
plugin/rbx_dom_lua/src/ReflectionDatabase/classes.lua
Normal file
18757
plugin/rbx_dom_lua/src/ReflectionDatabase/classes.lua
Normal file
File diff suppressed because it is too large
Load Diff
3
plugin/rbx_dom_lua/src/ReflectionDatabase/init.lua
Normal file
3
plugin/rbx_dom_lua/src/ReflectionDatabase/init.lua
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
return {
|
||||||
|
classes = require(script.classes)
|
||||||
|
}
|
||||||
139
plugin/rbx_dom_lua/src/base64.lua
Normal file
139
plugin/rbx_dom_lua/src/base64.lua
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
-- Thanks to Tiffany352 for this base64 implementation!
|
||||||
|
|
||||||
|
local floor = math.floor
|
||||||
|
local char = string.char
|
||||||
|
|
||||||
|
local function encodeBase64(str)
|
||||||
|
local out = {}
|
||||||
|
local nOut = 0
|
||||||
|
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
local strLen = #str
|
||||||
|
|
||||||
|
-- 3 octets become 4 hextets
|
||||||
|
for i = 1, strLen - 2, 3 do
|
||||||
|
local b1, b2, b3 = str:byte(i, i + 3)
|
||||||
|
local word = b3 + b2 * 256 + b1 * 256 * 256
|
||||||
|
|
||||||
|
local h4 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h3 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h2 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h1 = word % 64 + 1
|
||||||
|
|
||||||
|
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||||
|
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||||
|
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||||
|
out[nOut + 4] = alphabet:sub(h4, h4)
|
||||||
|
nOut = nOut + 4
|
||||||
|
end
|
||||||
|
|
||||||
|
local remainder = strLen % 3
|
||||||
|
|
||||||
|
if remainder == 2 then
|
||||||
|
-- 16 input bits -> 3 hextets (2 full, 1 partial)
|
||||||
|
local b1, b2 = str:byte(-2, -1)
|
||||||
|
-- partial is 4 bits long, leaving 2 bits of zero padding ->
|
||||||
|
-- offset = 4
|
||||||
|
local word = b2 * 4 + b1 * 4 * 256
|
||||||
|
|
||||||
|
local h3 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h2 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h1 = word % 64 + 1
|
||||||
|
|
||||||
|
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||||
|
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||||
|
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||||
|
out[nOut + 4] = "="
|
||||||
|
elseif remainder == 1 then
|
||||||
|
-- 8 input bits -> 2 hextets (2 full, 1 partial)
|
||||||
|
local b1 = str:byte(-1, -1)
|
||||||
|
-- partial is 2 bits long, leaving 4 bits of zero padding ->
|
||||||
|
-- offset = 16
|
||||||
|
local word = b1 * 16
|
||||||
|
|
||||||
|
local h2 = word % 64 + 1
|
||||||
|
word = floor(word / 64)
|
||||||
|
local h1 = word % 64 + 1
|
||||||
|
|
||||||
|
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||||
|
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||||
|
out[nOut + 3] = "="
|
||||||
|
out[nOut + 4] = "="
|
||||||
|
end
|
||||||
|
-- if the remainder is 0, then no work is needed
|
||||||
|
|
||||||
|
return table.concat(out, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function decodeBase64(str)
|
||||||
|
local out = {}
|
||||||
|
local nOut = 0
|
||||||
|
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
local strLen = #str
|
||||||
|
local acc = 0
|
||||||
|
local nAcc = 0
|
||||||
|
|
||||||
|
local alphabetLut = {}
|
||||||
|
for i = 1, #alphabet do
|
||||||
|
alphabetLut[alphabet:sub(i, i)] = i - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4 hextets become 3 octets
|
||||||
|
for i = 1, strLen do
|
||||||
|
local ch = str:sub(i, i)
|
||||||
|
local byte = alphabetLut[ch]
|
||||||
|
if byte then
|
||||||
|
acc = acc * 64 + byte
|
||||||
|
nAcc = nAcc + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if nAcc == 4 then
|
||||||
|
local b3 = acc % 256
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b2 = acc % 256
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b1 = acc % 256
|
||||||
|
|
||||||
|
out[nOut + 1] = char(b1)
|
||||||
|
out[nOut + 2] = char(b2)
|
||||||
|
out[nOut + 3] = char(b3)
|
||||||
|
nOut = nOut + 3
|
||||||
|
nAcc = 0
|
||||||
|
acc = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if nAcc == 3 then
|
||||||
|
-- 3 hextets -> 16 bit output
|
||||||
|
acc = acc * 64
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b2 = acc % 256
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b1 = acc % 256
|
||||||
|
|
||||||
|
out[nOut + 1] = char(b1)
|
||||||
|
out[nOut + 2] = char(b2)
|
||||||
|
elseif nAcc == 2 then
|
||||||
|
-- 2 hextets -> 8 bit output
|
||||||
|
acc = acc * 64
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
acc = acc * 64
|
||||||
|
acc = floor(acc / 256)
|
||||||
|
local b1 = acc % 256
|
||||||
|
|
||||||
|
out[nOut + 1] = char(b1)
|
||||||
|
elseif nAcc == 1 then
|
||||||
|
error("Base64 has invalid length")
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
decode = decodeBase64,
|
||||||
|
encode = encodeBase64,
|
||||||
|
}
|
||||||
29
plugin/rbx_dom_lua/src/base64.spec.lua
Normal file
29
plugin/rbx_dom_lua/src/base64.spec.lua
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
return function()
|
||||||
|
local base64 = require(script.Parent.base64)
|
||||||
|
|
||||||
|
it("should encode and decode", function()
|
||||||
|
local function try(str, expected)
|
||||||
|
local encoded = base64.encode(str)
|
||||||
|
expect(encoded).to.equal(expected)
|
||||||
|
expect(base64.decode(encoded)).to.equal(str)
|
||||||
|
end
|
||||||
|
|
||||||
|
try("Man", "TWFu")
|
||||||
|
try("Ma", "TWE=")
|
||||||
|
try("M", "TQ==")
|
||||||
|
try("ManM", "TWFuTQ==")
|
||||||
|
try(
|
||||||
|
[[Man is distinguished, not only by his reason, but by this ]]..
|
||||||
|
[[singular passion from other animals, which is a lust of the ]]..
|
||||||
|
[[mind, that by a perseverance of delight in the continued and ]]..
|
||||||
|
[[indefatigable generation of knowledge, exceeds the short ]]..
|
||||||
|
[[vehemence of any carnal pleasure.]],
|
||||||
|
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
|
||||||
|
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
|
||||||
|
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
|
||||||
|
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
|
||||||
|
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
|
||||||
|
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
47
plugin/rbx_dom_lua/src/customProperties.lua
Normal file
47
plugin/rbx_dom_lua/src/customProperties.lua
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
local CollectionService = game:GetService("CollectionService")
|
||||||
|
|
||||||
|
-- Defines how to read and write properties that aren't directly scriptable.
|
||||||
|
--
|
||||||
|
-- The reflection database refers to these as having scriptability = "Custom"
|
||||||
|
return {
|
||||||
|
Instance = {
|
||||||
|
Tags = {
|
||||||
|
read = function(instance, key)
|
||||||
|
local tagList = CollectionService:GetTags(instance)
|
||||||
|
|
||||||
|
return true, table.concat(tagList, "\0")
|
||||||
|
end,
|
||||||
|
write = function(instance, key, value)
|
||||||
|
local existingTags = CollectionService:GetTags(instance)
|
||||||
|
|
||||||
|
local unseenTags = {}
|
||||||
|
for _, tag in ipairs(existingTags) do
|
||||||
|
unseenTags[tag] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local tagList = string.split(value, "\0")
|
||||||
|
for _, tag in ipairs(tagList) do
|
||||||
|
unseenTags[tag] = nil
|
||||||
|
CollectionService:AddTag(instance, tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
for tag in pairs(unseenTags) do
|
||||||
|
CollectionService:RemoveTag(instance, tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LocalizationTable = {
|
||||||
|
Contents = {
|
||||||
|
read = function(instance, key)
|
||||||
|
return true, instance:GetContents()
|
||||||
|
end,
|
||||||
|
write = function(instance, key, value)
|
||||||
|
instance:SetContents(value)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
67
plugin/rbx_dom_lua/src/init.lua
Normal file
67
plugin/rbx_dom_lua/src/init.lua
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
local ReflectionDatabase = require(script.ReflectionDatabase)
|
||||||
|
local Error = require(script.Error)
|
||||||
|
local PropertyDescriptor = require(script.PropertyDescriptor)
|
||||||
|
|
||||||
|
local function findCanonicalPropertyDescriptor(className, propertyName)
|
||||||
|
local currentClassName = className
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local currentClass = ReflectionDatabase.classes[currentClassName]
|
||||||
|
|
||||||
|
if currentClass == nil then
|
||||||
|
return currentClass
|
||||||
|
end
|
||||||
|
|
||||||
|
local propertyData = currentClass.properties[propertyName]
|
||||||
|
if propertyData ~= nil then
|
||||||
|
if propertyData.isCanonical then
|
||||||
|
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
|
||||||
|
end
|
||||||
|
|
||||||
|
if propertyData.canonicalName ~= nil then
|
||||||
|
return PropertyDescriptor.fromRaw(
|
||||||
|
currentClass.properties[propertyData.canonicalName],
|
||||||
|
currentClassName,
|
||||||
|
propertyData.canonicalName)
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
currentClassName = currentClass.superclass
|
||||||
|
until currentClassName == nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readProperty(instance, propertyName)
|
||||||
|
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|
||||||
|
if descriptor == nil then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, propertyName)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.UnknownProperty, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
|
return descriptor:read(instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function writeProperty(instance, propertyName, value)
|
||||||
|
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|
||||||
|
if descriptor == nil then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, propertyName)
|
||||||
|
|
||||||
|
return false, Error.new(Error.Kind.UnknownProperty, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
|
return descriptor:write(instance, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
readProperty = readProperty,
|
||||||
|
writeProperty = writeProperty,
|
||||||
|
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
|
||||||
|
Error = Error,
|
||||||
|
EncodedValue = require(script.EncodedValue),
|
||||||
|
}
|
||||||
7
plugin/rbx_dom_lua/src/init.spec.lua
Normal file
7
plugin/rbx_dom_lua/src/init.spec.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
return function()
|
||||||
|
local RbxDom = require(script.Parent)
|
||||||
|
|
||||||
|
it("should load", function()
|
||||||
|
expect(RbxDom).to.be.ok()
|
||||||
|
end)
|
||||||
|
end
|
||||||
35
plugin/rbx_dom_lua/test-place.project.json
Normal file
35
plugin/rbx_dom_lua/test-place.project.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "rbx_dom_lua test place",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
|
||||||
|
"RbxDom": {
|
||||||
|
"$path": "src"
|
||||||
|
},
|
||||||
|
"TestEZ": {
|
||||||
|
"$path": "modules/testez/lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ServerScriptService": {
|
||||||
|
"$className": "ServerScriptService",
|
||||||
|
|
||||||
|
"Run Tests": {
|
||||||
|
"$path": "test.server.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Players": {
|
||||||
|
"$className": "Players",
|
||||||
|
"$properties": {
|
||||||
|
"CharacterAutoLoads": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HttpService": {
|
||||||
|
"$className": "HttpService",
|
||||||
|
"$properties": {
|
||||||
|
"HttpEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
plugin/rbx_dom_lua/test.server.lua
Normal file
7
plugin/rbx_dom_lua/test.server.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
local LIB_ROOT = ReplicatedStorage.RbxDom
|
||||||
|
|
||||||
|
local TestEZ = require(ReplicatedStorage.TestEZ)
|
||||||
|
|
||||||
|
TestEZ.TestBootstrap:run({LIB_ROOT})
|
||||||
@@ -233,4 +233,19 @@ function ApiContext:retrieveMessages()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ApiContext:open(id)
|
||||||
|
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
||||||
|
|
||||||
|
return Http.post(url, "")
|
||||||
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.json)
|
||||||
|
:andThen(function(body)
|
||||||
|
if body.sessionId ~= self.__sessionId then
|
||||||
|
return Promise.reject("Server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
return ApiContext
|
return ApiContext
|
||||||
@@ -13,11 +13,11 @@ local Version = require(Plugin.Version)
|
|||||||
local preloadAssets = require(Plugin.preloadAssets)
|
local preloadAssets = require(Plugin.preloadAssets)
|
||||||
local strict = require(Plugin.strict)
|
local strict = require(Plugin.strict)
|
||||||
|
|
||||||
local Theme = require(Plugin.Components.Theme)
|
|
||||||
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
||||||
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
|
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
|
||||||
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
||||||
local ErrorPanel = require(Plugin.Components.ErrorPanel)
|
local ErrorPanel = require(Plugin.Components.ErrorPanel)
|
||||||
|
local SettingsPanel = require(Plugin.Components.SettingsPanel)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ local AppStatus = strict("AppStatus", {
|
|||||||
Connecting = "Connecting",
|
Connecting = "Connecting",
|
||||||
Connected = "Connected",
|
Connected = "Connected",
|
||||||
Error = "Error",
|
Error = "Error",
|
||||||
|
Settings = "Settings",
|
||||||
})
|
})
|
||||||
|
|
||||||
local App = Roact.Component:extend("App")
|
local App = Roact.Component:extend("App")
|
||||||
@@ -74,10 +75,7 @@ function App:init()
|
|||||||
|
|
||||||
self.signals = {}
|
self.signals = {}
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
|
self.displayedVersion = Version.display(Config.version)
|
||||||
self.displayedVersion = DevSettings:isEnabled()
|
|
||||||
and Config.codename
|
|
||||||
or Version.display(Config.version)
|
|
||||||
|
|
||||||
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
|
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
|
||||||
|
|
||||||
@@ -109,12 +107,14 @@ function App:init()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:startSession(address, port)
|
function App:startSession(address, port, sessionOptions)
|
||||||
Log.trace("Starting new session")
|
Log.trace("Starting new session")
|
||||||
|
|
||||||
local baseUrl = ("http://%s:%s"):format(address, port)
|
local baseUrl = ("http://%s:%s"):format(address, port)
|
||||||
self.serveSession = ServeSession.new({
|
self.serveSession = ServeSession.new({
|
||||||
apiContext = ApiContext.new(baseUrl),
|
apiContext = ApiContext.new(baseUrl),
|
||||||
|
openScriptsExternally = sessionOptions.openScriptsExternally,
|
||||||
|
twoWaySync = sessionOptions.twoWaySync,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.serveSession:onStatusChanged(function(status, details)
|
self.serveSession:onStatusChanged(function(status, details)
|
||||||
@@ -155,8 +155,13 @@ function App:render()
|
|||||||
if self.state.appStatus == AppStatus.NotStarted then
|
if self.state.appStatus == AppStatus.NotStarted then
|
||||||
children = {
|
children = {
|
||||||
ConnectPanel = e(ConnectPanel, {
|
ConnectPanel = e(ConnectPanel, {
|
||||||
startSession = function(address, port)
|
startSession = function(address, port, settings)
|
||||||
self:startSession(address, port)
|
self:startSession(address, port, settings)
|
||||||
|
end,
|
||||||
|
openSettings = function()
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Settings,
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
cancel = function()
|
cancel = function()
|
||||||
Log.trace("Canceling session configuration")
|
Log.trace("Canceling session configuration")
|
||||||
@@ -169,7 +174,7 @@ function App:render()
|
|||||||
}
|
}
|
||||||
elseif self.state.appStatus == AppStatus.Connecting then
|
elseif self.state.appStatus == AppStatus.Connecting then
|
||||||
children = {
|
children = {
|
||||||
ConnectingPanel = Roact.createElement(ConnectingPanel),
|
ConnectingPanel = e(ConnectingPanel),
|
||||||
}
|
}
|
||||||
elseif self.state.appStatus == AppStatus.Connected then
|
elseif self.state.appStatus == AppStatus.Connected then
|
||||||
children = {
|
children = {
|
||||||
@@ -187,9 +192,19 @@ function App:render()
|
|||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
elseif self.state.appStatus == AppStatus.Settings then
|
||||||
|
children = {
|
||||||
|
e(SettingsPanel, {
|
||||||
|
back = function()
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.NotStarted,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}
|
||||||
elseif self.state.appStatus == AppStatus.Error then
|
elseif self.state.appStatus == AppStatus.Error then
|
||||||
children = {
|
children = {
|
||||||
ErrorPanel = Roact.createElement(ErrorPanel, {
|
ErrorPanel = e(ErrorPanel, {
|
||||||
errorMessage = self.state.errorMessage,
|
errorMessage = self.state.errorMessage,
|
||||||
onDismiss = function()
|
onDismiss = function()
|
||||||
self:setState({
|
self:setState({
|
||||||
@@ -200,11 +215,9 @@ function App:render()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return Roact.createElement(Theme.StudioProvider, nil, {
|
return e(Roact.Portal, {
|
||||||
UI = Roact.createElement(Roact.Portal, {
|
target = self.dockWidget,
|
||||||
target = self.dockWidget,
|
}, children)
|
||||||
}, children),
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:didMount()
|
function App:didMount()
|
||||||
|
|||||||
39
plugin/src/Components/Checkbox.lua
Normal file
39
plugin/src/Components/Checkbox.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local function Checkbox(props)
|
||||||
|
local checked = props.checked
|
||||||
|
local layoutOrder = props.layoutOrder
|
||||||
|
local onChange = props.onChange
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e("ImageButton", {
|
||||||
|
LayoutOrder = layoutOrder,
|
||||||
|
Size = UDim2.new(0, 20, 0, 20),
|
||||||
|
BorderSizePixel = 2,
|
||||||
|
BorderColor3 = theme.Text2,
|
||||||
|
BackgroundColor3 = theme.Background2,
|
||||||
|
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
onChange(not checked)
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
Indicator = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 18, 0, 18),
|
||||||
|
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.Brand1,
|
||||||
|
BackgroundTransparency = checked and 0 or 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Checkbox
|
||||||
@@ -11,6 +11,7 @@ local FitList = require(Plugin.Components.FitList)
|
|||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
local FormButton = require(Plugin.Components.FormButton)
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
local FormTextInput = require(Plugin.Components.FormTextInput)
|
local FormTextInput = require(Plugin.Components.FormTextInput)
|
||||||
|
local PluginSettings = require(Plugin.Components.PluginSettings)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -25,138 +26,157 @@ end
|
|||||||
|
|
||||||
function ConnectPanel:render()
|
function ConnectPanel:render()
|
||||||
local startSession = self.props.startSession
|
local startSession = self.props.startSession
|
||||||
|
local openSettings = self.props.openSettings
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e(Panel, nil, {
|
return PluginSettings.with(function(settings)
|
||||||
Layout = e("UIListLayout", {
|
return e(Panel, nil, {
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
Layout = e("UIListLayout", {
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
}),
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
}),
|
||||||
|
|
||||||
Inputs = e(FitList, {
|
Inputs = e(FitList, {
|
||||||
containerProps = {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
LayoutOrder = 1,
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
FillDirection = Enum.FillDirection.Horizontal,
|
|
||||||
Padding = UDim.new(0, 8),
|
|
||||||
},
|
|
||||||
paddingProps = {
|
|
||||||
PaddingTop = UDim.new(0, 20),
|
|
||||||
PaddingBottom = UDim.new(0, 10),
|
|
||||||
PaddingLeft = UDim.new(0, 24),
|
|
||||||
PaddingRight = UDim.new(0, 24),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Address = e(FitList, {
|
|
||||||
containerProps = {
|
containerProps = {
|
||||||
LayoutOrder = 1,
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 1,
|
||||||
},
|
},
|
||||||
layoutProps = {
|
layoutProps = {
|
||||||
Padding = UDim.new(0, 4),
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
Padding = UDim.new(0, 8),
|
||||||
|
},
|
||||||
|
paddingProps = {
|
||||||
|
PaddingTop = UDim.new(0, 20),
|
||||||
|
PaddingBottom = UDim.new(0, 10),
|
||||||
|
PaddingLeft = UDim.new(0, 24),
|
||||||
|
PaddingRight = UDim.new(0, 24),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
Label = e(FitText, {
|
Address = e(FitList, {
|
||||||
Kind = "TextLabel",
|
containerProps = {
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
},
|
||||||
Font = theme.TitleFont,
|
layoutProps = {
|
||||||
TextSize = 20,
|
Padding = UDim.new(0, 4),
|
||||||
Text = "Address",
|
},
|
||||||
TextColor3 = theme.Text1,
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.TitleFont,
|
||||||
|
TextSize = 20,
|
||||||
|
Text = "Address",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(FormTextInput, {
|
||||||
|
layoutOrder = 2,
|
||||||
|
width = UDim.new(0, 220),
|
||||||
|
value = self.state.address,
|
||||||
|
placeholderValue = Config.defaultHost,
|
||||||
|
onValueChange = function(newValue)
|
||||||
|
self:setState({
|
||||||
|
address = newValue,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Input = e(FormTextInput, {
|
Port = e(FitList, {
|
||||||
layoutOrder = 2,
|
containerProps = {
|
||||||
width = UDim.new(0, 220),
|
LayoutOrder = 2,
|
||||||
value = self.state.address,
|
BackgroundTransparency = 1,
|
||||||
placeholderValue = Config.defaultHost,
|
},
|
||||||
onValueChange = function(newValue)
|
layoutProps = {
|
||||||
self:setState({
|
Padding = UDim.new(0, 4),
|
||||||
address = newValue,
|
},
|
||||||
})
|
}, {
|
||||||
end,
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.TitleFont,
|
||||||
|
TextSize = 20,
|
||||||
|
Text = "Port",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(FormTextInput, {
|
||||||
|
layoutOrder = 2,
|
||||||
|
width = UDim.new(0, 80),
|
||||||
|
value = self.state.port,
|
||||||
|
placeholderValue = Config.defaultPort,
|
||||||
|
onValueChange = function(newValue)
|
||||||
|
self:setState({
|
||||||
|
port = newValue,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Port = e(FitList, {
|
Buttons = e(FitList, {
|
||||||
|
fitAxes = "Y",
|
||||||
containerProps = {
|
containerProps = {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
},
|
},
|
||||||
layoutProps = {
|
layoutProps = {
|
||||||
Padding = UDim.new(0, 4),
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
Padding = UDim.new(0, 8),
|
||||||
|
},
|
||||||
|
paddingProps = {
|
||||||
|
PaddingTop = UDim.new(0, 0),
|
||||||
|
PaddingBottom = UDim.new(0, 20),
|
||||||
|
PaddingLeft = UDim.new(0, 24),
|
||||||
|
PaddingRight = UDim.new(0, 24),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
Label = e(FitText, {
|
e(FormButton, {
|
||||||
Kind = "TextLabel",
|
layoutOrder = 1,
|
||||||
LayoutOrder = 1,
|
text = "Settings",
|
||||||
BackgroundTransparency = 1,
|
secondary = true,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
onClick = function()
|
||||||
Font = theme.TitleFont,
|
if openSettings ~= nil then
|
||||||
TextSize = 20,
|
openSettings()
|
||||||
Text = "Port",
|
end
|
||||||
TextColor3 = theme.Text1,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Input = e(FormTextInput, {
|
e(FormButton, {
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
width = UDim.new(0, 80),
|
text = "Connect",
|
||||||
value = self.state.port,
|
onClick = function()
|
||||||
placeholderValue = Config.defaultPort,
|
if startSession ~= nil then
|
||||||
onValueChange = function(newValue)
|
local address = self.state.address
|
||||||
self:setState({
|
if address:len() == 0 then
|
||||||
port = newValue,
|
address = Config.defaultHost
|
||||||
})
|
end
|
||||||
|
|
||||||
|
local port = self.state.port
|
||||||
|
if port:len() == 0 then
|
||||||
|
port = Config.defaultPort
|
||||||
|
end
|
||||||
|
|
||||||
|
local sessionOptions = {
|
||||||
|
openScriptsExternally = settings:get("openScriptsExternally"),
|
||||||
|
twoWaySync = settings:get("twoWaySync"),
|
||||||
|
}
|
||||||
|
|
||||||
|
startSession(address, port, sessionOptions)
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
})
|
||||||
|
end)
|
||||||
Buttons = e(FitList, {
|
|
||||||
fitAxes = "Y",
|
|
||||||
containerProps = {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
LayoutOrder = 2,
|
|
||||||
Size = UDim2.new(1, 0, 0, 0),
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
FillDirection = Enum.FillDirection.Horizontal,
|
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
|
||||||
Padding = UDim.new(0, 8),
|
|
||||||
},
|
|
||||||
paddingProps = {
|
|
||||||
PaddingTop = UDim.new(0, 0),
|
|
||||||
PaddingBottom = UDim.new(0, 20),
|
|
||||||
PaddingLeft = UDim.new(0, 24),
|
|
||||||
PaddingRight = UDim.new(0, 24),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
e(FormButton, {
|
|
||||||
layoutOrder = 2,
|
|
||||||
text = "Connect",
|
|
||||||
onClick = function()
|
|
||||||
if startSession ~= nil then
|
|
||||||
local address = self.state.address
|
|
||||||
if address:len() == 0 then
|
|
||||||
address = Config.defaultHost
|
|
||||||
end
|
|
||||||
|
|
||||||
local port = self.state.port
|
|
||||||
if port:len() == 0 then
|
|
||||||
port = Config.defaultPort
|
|
||||||
end
|
|
||||||
|
|
||||||
startSession(address, port)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
121
plugin/src/Components/PluginSettings.lua
Normal file
121
plugin/src/Components/PluginSettings.lua
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
--[[
|
||||||
|
Persistent plugin settings that can be accessed via Roact context.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
|
local defaultSettings = {
|
||||||
|
openScriptsExternally = false,
|
||||||
|
twoWaySync = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local Settings = {}
|
||||||
|
Settings.__index = Settings
|
||||||
|
|
||||||
|
function Settings.fromPlugin(plugin)
|
||||||
|
local values = {}
|
||||||
|
|
||||||
|
for name, defaultValue in pairs(defaultSettings) do
|
||||||
|
local savedValue = plugin:GetSetting("Rojo_" .. name)
|
||||||
|
|
||||||
|
if savedValue == nil then
|
||||||
|
plugin:SetSetting("Rojo_" .. name, defaultValue)
|
||||||
|
values[name] = defaultValue
|
||||||
|
else
|
||||||
|
values[name] = savedValue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({
|
||||||
|
__values = values,
|
||||||
|
__plugin = plugin,
|
||||||
|
__updateListeners = {},
|
||||||
|
}, Settings)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings:get(name)
|
||||||
|
if defaultSettings[name] == nil then
|
||||||
|
error("Invalid setings name " .. tostring(name), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.__values[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings:set(name, value)
|
||||||
|
self.__plugin:SetSetting("Rojo_" .. name, value)
|
||||||
|
self.__values[name] = value
|
||||||
|
|
||||||
|
for callback in pairs(self.__updateListeners) do
|
||||||
|
callback(name, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Settings:onUpdate(newCallback)
|
||||||
|
local newListeners = {}
|
||||||
|
for callback in pairs(self.__updateListeners) do
|
||||||
|
newListeners[callback] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
newListeners[newCallback] = true
|
||||||
|
self.__updateListeners = newListeners
|
||||||
|
|
||||||
|
return function()
|
||||||
|
local newListeners = {}
|
||||||
|
for callback in pairs(self.__updateListeners) do
|
||||||
|
if callback ~= newCallback then
|
||||||
|
newListeners[callback] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.__updateListeners = newListeners
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Context = Roact.createContext(nil)
|
||||||
|
|
||||||
|
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||||
|
|
||||||
|
function StudioProvider:init()
|
||||||
|
self.settings = Settings.fromPlugin(self.props.plugin)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StudioProvider:render()
|
||||||
|
return Roact.createElement(Context.Provider, {
|
||||||
|
value = self.settings,
|
||||||
|
}, self.props[Roact.Children])
|
||||||
|
end
|
||||||
|
|
||||||
|
local InternalConsumer = Roact.Component:extend("InternalConsumer")
|
||||||
|
|
||||||
|
function InternalConsumer:render()
|
||||||
|
return self.props.render(self.props.settings)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InternalConsumer:didMount()
|
||||||
|
self.disconnect = self.props.settings:onUpdate(function()
|
||||||
|
-- Trigger a dummy state update to update the settings consumer.
|
||||||
|
self:setState({})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InternalConsumer:willUnmount()
|
||||||
|
self.disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function with(callback)
|
||||||
|
return Roact.createElement(Context.Consumer, {
|
||||||
|
render = function(settings)
|
||||||
|
return Roact.createElement(InternalConsumer, {
|
||||||
|
settings = settings,
|
||||||
|
render = callback,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
StudioProvider = StudioProvider,
|
||||||
|
with = with,
|
||||||
|
}
|
||||||
119
plugin/src/Components/SettingsPanel.lua
Normal file
119
plugin/src/Components/SettingsPanel.lua
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||||
|
|
||||||
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
|
local Checkbox = require(Plugin.Components.Checkbox)
|
||||||
|
local FitList = require(Plugin.Components.FitList)
|
||||||
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
|
local Panel = require(Plugin.Components.Panel)
|
||||||
|
local PluginSettings = require(Plugin.Components.PluginSettings)
|
||||||
|
local Theme = require(Plugin.Components.Theme)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local SettingsPanel = Roact.Component:extend("SettingsPanel")
|
||||||
|
|
||||||
|
function SettingsPanel:render()
|
||||||
|
local back = self.props.back
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return PluginSettings.with(function(settings)
|
||||||
|
return e(Panel, nil, {
|
||||||
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 16),
|
||||||
|
}),
|
||||||
|
|
||||||
|
OpenScriptsExternally = e(FitList, {
|
||||||
|
containerProps = {
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
layoutProps = {
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.MainFont,
|
||||||
|
TextSize = 16,
|
||||||
|
Text = "Open Scripts Externally",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Padding = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(Checkbox, {
|
||||||
|
layoutOrder = 3,
|
||||||
|
checked = settings:get("openScriptsExternally"),
|
||||||
|
onChange = function(newValue)
|
||||||
|
settings:set("openScriptsExternally", not settings:get("openScriptsExternally"))
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
TwoWaySync = e(FitList, {
|
||||||
|
containerProps = {
|
||||||
|
LayoutOrder = 2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
layoutProps = {
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Label = e(FitText, {
|
||||||
|
Kind = "TextLabel",
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = theme.MainFont,
|
||||||
|
TextSize = 16,
|
||||||
|
Text = "Two-Way Sync (Experimental!)",
|
||||||
|
TextColor3 = theme.Text1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Padding = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = e(Checkbox, {
|
||||||
|
layoutOrder = 3,
|
||||||
|
checked = settings:get("twoWaySync"),
|
||||||
|
onChange = function(newValue)
|
||||||
|
settings:set("twoWaySync", not settings:get("twoWaySync"))
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
BackButton = e(FormButton, {
|
||||||
|
layoutOrder = 4,
|
||||||
|
text = "Okay",
|
||||||
|
secondary = true,
|
||||||
|
onClick = function()
|
||||||
|
back()
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return SettingsPanel
|
||||||
@@ -5,8 +5,8 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
|||||||
return strict("Config", {
|
return strict("Config", {
|
||||||
isDevBuild = isDevBuild,
|
isDevBuild = isDevBuild,
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = {0, 6, 0, "-alpha.3"},
|
version = {6, 0, 0, "-rc.1"},
|
||||||
expectedServerVersionString = "0.6.0 or newer",
|
expectedServerVersionString = "6.0 or newer",
|
||||||
protocolVersion = 3,
|
protocolVersion = 3,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
defaultPort = 34872,
|
defaultPort = 34872,
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ local VALUES = {
|
|||||||
[Environment.Test] = true,
|
[Environment.Test] = true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UnstableTwoWaySync = {
|
|
||||||
type = "BoolValue",
|
|
||||||
values = {
|
|
||||||
[Environment.User] = false,
|
|
||||||
[Environment.Dev] = false,
|
|
||||||
[Environment.Test] = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
|
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
|
||||||
@@ -140,10 +132,6 @@ function DevSettings:shouldTypecheck()
|
|||||||
return getValue("TypecheckingEnabled")
|
return getValue("TypecheckingEnabled")
|
||||||
end
|
end
|
||||||
|
|
||||||
function DevSettings:twoWaySyncEnabled()
|
|
||||||
return getValue("UnstableTwoWaySync")
|
|
||||||
end
|
|
||||||
|
|
||||||
function _G.ROJO_DEV_CREATE()
|
function _G.ROJO_DEV_CREATE()
|
||||||
DevSettings:createDevSettings()
|
DevSettings:createDevSettings()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,9 +11,22 @@ InstanceMap.__index = InstanceMap
|
|||||||
|
|
||||||
function InstanceMap.new(onInstanceChanged)
|
function InstanceMap.new(onInstanceChanged)
|
||||||
local self = {
|
local self = {
|
||||||
|
-- A map from IDs to instances.
|
||||||
fromIds = {},
|
fromIds = {},
|
||||||
|
|
||||||
|
-- A map from instances to IDs.
|
||||||
fromInstances = {},
|
fromInstances = {},
|
||||||
|
|
||||||
|
-- A set of all instances that updates should be paused for. This set
|
||||||
|
-- should generally be empty, and will be filled by pauseInstance
|
||||||
|
-- temporarily.
|
||||||
|
pausedUpdateInstances = {},
|
||||||
|
|
||||||
|
-- A map from instances to a signal or list of signals connected to it.
|
||||||
instancesToSignal = {},
|
instancesToSignal = {},
|
||||||
|
|
||||||
|
-- Callback that's invoked whenever an instance is changed and it was
|
||||||
|
-- not paused.
|
||||||
onInstanceChanged = onInstanceChanged,
|
onInstanceChanged = onInstanceChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +131,32 @@ function InstanceMap:destroyId(id)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Pause updates for an instance momentarily and invoke a callback.
|
||||||
|
|
||||||
|
If the callback throws an error, InstanceMap will still be kept in a
|
||||||
|
consistent state.
|
||||||
|
]]
|
||||||
|
function InstanceMap:pauseInstance(instance, callback)
|
||||||
|
local id = self.fromInstances[instance]
|
||||||
|
|
||||||
|
-- If we don't know about this instance, ignore it and do not invoke the
|
||||||
|
-- callback.
|
||||||
|
if id == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.pausedUpdateInstances[instance] = true
|
||||||
|
local success, result = xpcall(callback, debug.traceback)
|
||||||
|
self.pausedUpdateInstances[instance] = false
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return result
|
||||||
|
else
|
||||||
|
error(result, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function InstanceMap:__connectSignals(instance)
|
function InstanceMap:__connectSignals(instance)
|
||||||
-- ValueBase instances have an overriden version of the Changed signal that
|
-- ValueBase instances have an overriden version of the Changed signal that
|
||||||
-- only detects changes to their Value property.
|
-- only detects changes to their Value property.
|
||||||
@@ -150,9 +189,15 @@ end
|
|||||||
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
|
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
|
||||||
Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
|
Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
|
||||||
|
|
||||||
if self.onInstanceChanged ~= nil then
|
if self.pausedUpdateInstances[instance] then
|
||||||
self.onInstanceChanged(instance, propertyName)
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if self.onInstanceChanged == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.onInstanceChanged(instance, propertyName)
|
||||||
end
|
end
|
||||||
|
|
||||||
function InstanceMap:__disconnectSignals(instance)
|
function InstanceMap:__disconnectSignals(instance)
|
||||||
|
|||||||
25
plugin/src/PatchSet.lua
Normal file
25
plugin/src/PatchSet.lua
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
--[[
|
||||||
|
Methods to operate on either a patch created by the hydrate method, or a
|
||||||
|
patch returned from the API.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local t = require(script.Parent.Parent.t)
|
||||||
|
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
local PatchSet = {}
|
||||||
|
|
||||||
|
PatchSet.validate = t.interface({
|
||||||
|
removed = t.array(t.union(Types.RbxId, t.Instance)),
|
||||||
|
added = t.map(Types.RbxId, Types.ApiInstance),
|
||||||
|
updated = t.array(Types.ApiInstanceUpdate),
|
||||||
|
})
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Invert the given PatchSet using the given instance map.
|
||||||
|
]]
|
||||||
|
function PatchSet.invert(patchSet, instanceMap)
|
||||||
|
error("not yet implemented", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchSet
|
||||||
@@ -5,24 +5,12 @@
|
|||||||
|
|
||||||
local RbxDom = require(script.Parent.Parent.RbxDom)
|
local RbxDom = require(script.Parent.Parent.RbxDom)
|
||||||
local t = require(script.Parent.Parent.t)
|
local t = require(script.Parent.Parent.t)
|
||||||
local Log = require(script.Parent.Parent.Log)
|
|
||||||
|
|
||||||
local Types = require(script.Parent.Types)
|
local Types = require(script.Parent.Types)
|
||||||
local invariant = require(script.Parent.invariant)
|
local invariant = require(script.Parent.invariant)
|
||||||
local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
|
local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
|
||||||
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
|
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
|
||||||
|
local PatchSet = require(script.Parent.PatchSet)
|
||||||
--[[
|
|
||||||
This interface represents either a patch created by the hydrate method, or a
|
|
||||||
patch returned from the API.
|
|
||||||
|
|
||||||
This type should be a subset of Types.ApiInstanceUpdate.
|
|
||||||
]]
|
|
||||||
local IPatch = t.interface({
|
|
||||||
removed = t.array(t.union(Types.RbxId, t.Instance)),
|
|
||||||
added = t.map(Types.RbxId, Types.ApiInstance),
|
|
||||||
updated = t.array(Types.ApiInstanceUpdate),
|
|
||||||
})
|
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Attempt to safely set the parent of an instance.
|
Attempt to safely set the parent of an instance.
|
||||||
@@ -86,7 +74,7 @@ end
|
|||||||
editable by scripts.
|
editable by scripts.
|
||||||
]]
|
]]
|
||||||
local applyPatchSchema = Types.ifEnabled(t.tuple(
|
local applyPatchSchema = Types.ifEnabled(t.tuple(
|
||||||
IPatch
|
PatchSet.validate
|
||||||
))
|
))
|
||||||
function Reconciler:applyPatch(patch)
|
function Reconciler:applyPatch(patch)
|
||||||
assert(applyPatchSchema(patch))
|
assert(applyPatchSchema(patch))
|
||||||
@@ -287,7 +275,7 @@ local hydrateSchema = Types.ifEnabled(t.tuple(
|
|||||||
t.map(Types.RbxId, Types.VirtualInstance),
|
t.map(Types.RbxId, Types.VirtualInstance),
|
||||||
Types.RbxId,
|
Types.RbxId,
|
||||||
t.Instance,
|
t.Instance,
|
||||||
IPatch
|
PatchSet.validate
|
||||||
))
|
))
|
||||||
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
|
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
|
||||||
assert(hydrateSchema(apiInstances, id, instance, hydratePatch))
|
assert(hydrateSchema(apiInstances, id, instance, hydratePatch))
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
local StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
local Log = require(script.Parent.Parent.Log)
|
local Log = require(script.Parent.Parent.Log)
|
||||||
local Fmt = require(script.Parent.Parent.Fmt)
|
local Fmt = require(script.Parent.Parent.Fmt)
|
||||||
local t = require(script.Parent.Parent.t)
|
local t = require(script.Parent.Parent.t)
|
||||||
|
|
||||||
local DevSettings = require(script.Parent.DevSettings)
|
|
||||||
local InstanceMap = require(script.Parent.InstanceMap)
|
local InstanceMap = require(script.Parent.InstanceMap)
|
||||||
local Reconciler = require(script.Parent.Reconciler)
|
local Reconciler = require(script.Parent.Reconciler)
|
||||||
local strict = require(script.Parent.strict)
|
local strict = require(script.Parent.strict)
|
||||||
@@ -43,6 +44,8 @@ ServeSession.Status = Status
|
|||||||
|
|
||||||
local validateServeOptions = t.strictInterface({
|
local validateServeOptions = t.strictInterface({
|
||||||
apiContext = t.table,
|
apiContext = t.table,
|
||||||
|
openScriptsExternally = t.boolean,
|
||||||
|
twoWaySync = t.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
function ServeSession.new(options)
|
function ServeSession.new(options)
|
||||||
@@ -57,12 +60,28 @@ function ServeSession.new(options)
|
|||||||
local instanceMap = InstanceMap.new(onInstanceChanged)
|
local instanceMap = InstanceMap.new(onInstanceChanged)
|
||||||
local reconciler = Reconciler.new(instanceMap)
|
local reconciler = Reconciler.new(instanceMap)
|
||||||
|
|
||||||
|
local connections = {}
|
||||||
|
|
||||||
|
local connection = StudioService
|
||||||
|
:GetPropertyChangedSignal("ActiveScript")
|
||||||
|
:Connect(function()
|
||||||
|
local activeScript = StudioService.ActiveScript
|
||||||
|
|
||||||
|
if activeScript ~= nil then
|
||||||
|
self:__onActiveScriptChanged(activeScript)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
table.insert(connections, connection)
|
||||||
|
|
||||||
self = {
|
self = {
|
||||||
__status = Status.NotStarted,
|
__status = Status.NotStarted,
|
||||||
__apiContext = options.apiContext,
|
__apiContext = options.apiContext,
|
||||||
|
__openScriptsExternally = options.openScriptsExternally,
|
||||||
|
__twoWaySync = options.twoWaySync,
|
||||||
__reconciler = reconciler,
|
__reconciler = reconciler,
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
__statusChangedCallback = nil,
|
__statusChangedCallback = nil,
|
||||||
|
__connections = connections,
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(self, ServeSession)
|
setmetatable(self, ServeSession)
|
||||||
@@ -108,8 +127,39 @@ function ServeSession:stop()
|
|||||||
self:__stopInternal()
|
self:__stopInternal()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ServeSession:__onActiveScriptChanged(activeScript)
|
||||||
|
if not self.__openScriptsExternally then
|
||||||
|
Log.trace("Not opening script {} because feature not enabled.", activeScript)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.__status ~= Status.Connected then
|
||||||
|
Log.trace("Not opening script {} because session is not connected.", activeScript)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local scriptId = self.__instanceMap.fromInstances[activeScript]
|
||||||
|
if scriptId == nil then
|
||||||
|
Log.trace("Not opening script {} because it is not known by Rojo.", activeScript)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.debug("Trying to open script {} externally...", activeScript)
|
||||||
|
|
||||||
|
-- Force-close the script inside Studio
|
||||||
|
local existingParent = activeScript.Parent
|
||||||
|
activeScript.Parent = nil
|
||||||
|
activeScript.Parent = existingParent
|
||||||
|
|
||||||
|
-- Notify the Rojo server to open this script
|
||||||
|
self.__apiContext:open(scriptId)
|
||||||
|
end
|
||||||
|
|
||||||
function ServeSession:__onInstanceChanged(instance, propertyName)
|
function ServeSession:__onInstanceChanged(instance, propertyName)
|
||||||
if not DevSettings:twoWaySyncEnabled() then
|
if not self.__twoWaySync then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -200,6 +250,11 @@ function ServeSession:__stopInternal(err)
|
|||||||
self:__setStatus(Status.Disconnected, err)
|
self:__setStatus(Status.Disconnected, err)
|
||||||
self.__apiContext:disconnect()
|
self.__apiContext:disconnect()
|
||||||
self.__instanceMap:stop()
|
self.__instanceMap:stop()
|
||||||
|
|
||||||
|
for _, connection in ipairs(self.__connections) do
|
||||||
|
connection:Disconnect()
|
||||||
|
end
|
||||||
|
self.__connections = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
function ServeSession:__setStatus(status, detail)
|
function ServeSession:__setStatus(status, detail)
|
||||||
|
|||||||
53
plugin/src/createSignal.lua
Normal file
53
plugin/src/createSignal.lua
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
--[[
|
||||||
|
Create a new signal that can be connected to, disconnected from, and fired.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
local signal = createSignal()
|
||||||
|
local disconnect = signal:connect(function(...)
|
||||||
|
print("fired:", ...)
|
||||||
|
end)
|
||||||
|
|
||||||
|
signal:fire("a", "b", "c")
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
Avoids mutating listeners list directly to prevent iterator invalidation if
|
||||||
|
a listener is disconnected while the signal is firing.
|
||||||
|
]]
|
||||||
|
local function createSignal()
|
||||||
|
local listeners = {}
|
||||||
|
|
||||||
|
local function connect(newListener)
|
||||||
|
local nextListeners = {}
|
||||||
|
for listener in pairs(listeners) do
|
||||||
|
nextListeners[listener] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
nextListeners[newListener] = true
|
||||||
|
listeners = nextListeners
|
||||||
|
|
||||||
|
return function()
|
||||||
|
local nextListeners = {}
|
||||||
|
for listener in pairs(listeners) do
|
||||||
|
if listener ~= newListener then
|
||||||
|
nextListeners[listener] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
listeners = nextListeners
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fire(...)
|
||||||
|
for listener in pairs(listeners) do
|
||||||
|
listener(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect = connect,
|
||||||
|
fire = fire,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return createSignal
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
local RbxDom = require(script.Parent.Parent.RbxDom)
|
local RbxDom = require(script.Parent.Parent.RbxDom)
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Attempts to set a property on the given instance.
|
Attempts to read a property from the given instance.
|
||||||
]]
|
]]
|
||||||
local function getCanonincalProperty(instance, propertyName)
|
local function getCanonincalProperty(instance, propertyName)
|
||||||
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|||||||
@@ -14,9 +14,17 @@ local Roact = require(script.Parent.Roact)
|
|||||||
|
|
||||||
local Config = require(script.Config)
|
local Config = require(script.Config)
|
||||||
local App = require(script.Components.App)
|
local App = require(script.Components.App)
|
||||||
|
local Theme = require(script.Components.Theme)
|
||||||
|
local PluginSettings = require(script.Components.PluginSettings)
|
||||||
|
|
||||||
local app = Roact.createElement(App, {
|
local app = Roact.createElement(Theme.StudioProvider, nil, {
|
||||||
plugin = plugin,
|
Roact.createElement(PluginSettings.StudioProvider, {
|
||||||
|
plugin = plugin,
|
||||||
|
}, {
|
||||||
|
RojoUI = Roact.createElement(App, {
|
||||||
|
plugin = plugin,
|
||||||
|
}),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
local tree = Roact.mount(app, nil, "Rojo UI")
|
local tree = Roact.mount(app, nil, "Rojo UI")
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: rojo-test/src/build_test.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="DataModel" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">infer-service-name</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="HttpService" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">HttpService</string>
|
||||||
|
<bool name="HttpEnabled">true</bool>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
<Item class="ReplicatedStorage" referent="2">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">ReplicatedStorage</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="ModuleScript" referent="3">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Main</string>
|
||||||
|
<string name="Source">-- hello, from main</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
source: rojo-test/src/build_test.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="DataModel" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">infer-service-name</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="StarterPlayer" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">StarterPlayer</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="StarterCharacterScripts" referent="2">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">StarterCharacterScripts</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
<Item class="StarterPlayerScripts" referent="3">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">StarterPlayerScripts</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
source: rojo-test/src/build_test.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="ModuleScript" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">json_as_lua</string>
|
||||||
|
<string name="Source">return {
|
||||||
|
["1invalidident"] = "nice",
|
||||||
|
array = {1, 2, 3},
|
||||||
|
["false"] = false,
|
||||||
|
float = 1234.5452,
|
||||||
|
int = 1234,
|
||||||
|
null = nil,
|
||||||
|
object = {
|
||||||
|
hello = "world",
|
||||||
|
},
|
||||||
|
["true"] = true,
|
||||||
|
}</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
6
rojo-test/build-tests/deep_nesting/default.project.json
Normal file
6
rojo-test/build-tests/deep_nesting/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "deep_nesting",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "infer-service-name",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"Main": {
|
||||||
|
"$path": "main.lua"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HttpService": {
|
||||||
|
"$properties": {
|
||||||
|
"HttpEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rojo-test/build-tests/infer_service_name/main.lua
Normal file
1
rojo-test/build-tests/infer_service_name/main.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- hello, from main
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "infer-service-name",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
|
||||||
|
"StarterPlayer": {
|
||||||
|
"StarterPlayerScripts": {},
|
||||||
|
"StarterCharacterScripts": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rojo-test/build-tests/infer_starter_player/main.lua
Normal file
1
rojo-test/build-tests/infer_starter_player/main.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- hello, from main
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "init_with_children",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/build-tests/json_as_lua/default.project.json
Normal file
6
rojo-test/build-tests/json_as_lua/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "json_as_lua",
|
||||||
|
"tree": {
|
||||||
|
"$path": "make-me-a-script.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
rojo-test/build-tests/json_as_lua/make-me-a-script.json
Normal file
12
rojo-test/build-tests/json_as_lua/make-me-a-script.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"array": [1, 2, 3],
|
||||||
|
"object": {
|
||||||
|
"hello": "world"
|
||||||
|
},
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
"null": null,
|
||||||
|
"int": 1234,
|
||||||
|
"float": 1234.5452,
|
||||||
|
"1invalidident": "nice"
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
This is a bare text file with no project.
|
|
||||||
6
rojo-test/build-tests/rbxmx_ref/default.project.json
Normal file
6
rojo-test/build-tests/rbxmx_ref/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rbxmx_ref",
|
||||||
|
"tree": {
|
||||||
|
"$path": "model.rbxmx"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/add_folder/default.project.json
Normal file
6
rojo-test/serve-tests/add_folder/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "add_folder",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/edit_init/default.project.json
Normal file
6
rojo-test/serve-tests/edit_init/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "edit_init",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
Hello, world!
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "move_folder_of_stuff",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/remove_file/default.project.json
Normal file
6
rojo-test/serve-tests/remove_file/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "remove_file",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rojo-test/serve-tests/scripts/default.project.json
Normal file
6
rojo-test/serve-tests/scripts/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "scripts",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,16 +28,19 @@ gen_build_tests! {
|
|||||||
csv_in_folder,
|
csv_in_folder,
|
||||||
deep_nesting,
|
deep_nesting,
|
||||||
gitkeep,
|
gitkeep,
|
||||||
|
infer_service_name,
|
||||||
|
infer_starter_player,
|
||||||
init_meta_class_name,
|
init_meta_class_name,
|
||||||
init_meta_properties,
|
init_meta_properties,
|
||||||
init_with_children,
|
init_with_children,
|
||||||
|
json_as_lua,
|
||||||
json_model_in_folder,
|
json_model_in_folder,
|
||||||
json_model_legacy_name,
|
json_model_legacy_name,
|
||||||
module_in_folder,
|
module_in_folder,
|
||||||
module_init,
|
module_init,
|
||||||
plain_gitkeep,
|
|
||||||
rbxm_in_folder,
|
rbxm_in_folder,
|
||||||
rbxmx_in_folder,
|
rbxmx_in_folder,
|
||||||
|
rbxmx_ref,
|
||||||
script_meta_disabled,
|
script_meta_disabled,
|
||||||
server_in_folder,
|
server_in_folder,
|
||||||
server_init,
|
server_init,
|
||||||
@@ -52,16 +55,6 @@ gen_build_tests! {
|
|||||||
ignore_glob_spec,
|
ignore_glob_spec,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_plain_txt() {
|
|
||||||
run_build_test("plain.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_rbxmx_ref() {
|
|
||||||
run_build_test("rbxmx_ref.rbxmx");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_build_test(test_name: &str) {
|
fn run_build_test(test_name: &str) {
|
||||||
let build_test_path = get_build_tests_path();
|
let build_test_path = get_build_tests_path();
|
||||||
let working_dir = get_working_dir_path();
|
let working_dir = get_working_dir_path();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ fn scripts() {
|
|||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
read_response.intern_and_redact(&mut redactions, root_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::write(session.path().join("foo.lua"), "Updated foo!").unwrap();
|
fs::write(session.path().join("src/foo.lua"), "Updated foo!").unwrap();
|
||||||
|
|
||||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||||
assert_yaml_snapshot!(
|
assert_yaml_snapshot!(
|
||||||
@@ -51,26 +51,6 @@ fn scripts() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn just_txt() {
|
|
||||||
run_serve_test("just_txt.txt", |session, mut redactions| {
|
|
||||||
let info = session.get_api_rojo().unwrap();
|
|
||||||
let root_id = info.root_instance_id;
|
|
||||||
|
|
||||||
assert_yaml_snapshot!("just_txt_info", redactions.redacted_yaml(info));
|
|
||||||
|
|
||||||
let read_response = session.get_api_read(root_id).unwrap();
|
|
||||||
assert_yaml_snapshot!(
|
|
||||||
"just_txt_all",
|
|
||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::write(session.path(), "Changed content!").unwrap();
|
|
||||||
|
|
||||||
// TODO: Directly served files currently don't trigger changed events!
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_folder() {
|
fn add_folder() {
|
||||||
run_serve_test("add_folder", |session, mut redactions| {
|
run_serve_test("add_folder", |session, mut redactions| {
|
||||||
@@ -85,7 +65,7 @@ fn add_folder() {
|
|||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
read_response.intern_and_redact(&mut redactions, root_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::create_dir(session.path().join("my-new-folder")).unwrap();
|
fs::create_dir(session.path().join("src/my-new-folder")).unwrap();
|
||||||
|
|
||||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||||
assert_yaml_snapshot!(
|
assert_yaml_snapshot!(
|
||||||
@@ -115,7 +95,7 @@ fn remove_file() {
|
|||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
read_response.intern_and_redact(&mut redactions, root_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::remove_file(session.path().join("hello.txt")).unwrap();
|
fs::remove_file(session.path().join("src/hello.txt")).unwrap();
|
||||||
|
|
||||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||||
assert_yaml_snapshot!(
|
assert_yaml_snapshot!(
|
||||||
@@ -145,7 +125,7 @@ fn edit_init() {
|
|||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
read_response.intern_and_redact(&mut redactions, root_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::write(session.path().join("init.lua"), b"-- Edited contents").unwrap();
|
fs::write(session.path().join("src/init.lua"), b"-- Edited contents").unwrap();
|
||||||
|
|
||||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||||
assert_yaml_snapshot!(
|
assert_yaml_snapshot!(
|
||||||
@@ -191,7 +171,7 @@ fn move_folder_of_stuff() {
|
|||||||
|
|
||||||
// We're hoping that this rename gets picked up as one event. This test
|
// We're hoping that this rename gets picked up as one event. This test
|
||||||
// will fail otherwise.
|
// will fail otherwise.
|
||||||
fs::rename(stuff_path, session.path().join("new-stuff")).unwrap();
|
fs::rename(stuff_path, session.path().join("src/new-stuff")).unwrap();
|
||||||
|
|
||||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||||
assert_yaml_snapshot!(
|
assert_yaml_snapshot!(
|
||||||
|
|||||||
20
src/bin.rs
20
src/bin.rs
@@ -3,15 +3,16 @@ use std::{env, error::Error, panic, process};
|
|||||||
use backtrace::Backtrace;
|
use backtrace::Backtrace;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use librojo::cli::{self, Options, Subcommand};
|
use librojo::cli::{self, GlobalOptions, Options, Subcommand};
|
||||||
|
|
||||||
fn run(subcommand: Subcommand) -> Result<(), Box<dyn Error>> {
|
fn run(global: GlobalOptions, subcommand: Subcommand) -> Result<(), Box<dyn Error>> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
Subcommand::Init(init_options) => cli::init(init_options)?,
|
Subcommand::Init(init_options) => cli::init(init_options)?,
|
||||||
Subcommand::Serve(serve_options) => cli::serve(serve_options)?,
|
Subcommand::Serve(serve_options) => cli::serve(global, serve_options)?,
|
||||||
Subcommand::Build(build_options) => cli::build(build_options)?,
|
Subcommand::Build(build_options) => cli::build(build_options)?,
|
||||||
Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
|
Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
|
||||||
Subcommand::Doc => cli::doc()?,
|
Subcommand::Doc => cli::doc()?,
|
||||||
|
Subcommand::Plugin(plugin_options) => cli::plugin(plugin_options)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -22,7 +23,7 @@ fn main() {
|
|||||||
// PanicInfo's payload is usually a &'static str or String.
|
// PanicInfo's payload is usually a &'static str or String.
|
||||||
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload
|
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload
|
||||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||||
Some(message) => message.to_string(),
|
Some(&message) => message.to_string(),
|
||||||
None => match panic_info.payload().downcast_ref::<String>() {
|
None => match panic_info.payload().downcast_ref::<String>() {
|
||||||
Some(message) => message.clone(),
|
Some(message) => message.clone(),
|
||||||
None => "<no message>".to_string(),
|
None => "<no message>".to_string(),
|
||||||
@@ -63,10 +64,10 @@ fn main() {
|
|||||||
|
|
||||||
let options = Options::from_args();
|
let options = Options::from_args();
|
||||||
|
|
||||||
let log_filter = match options.verbosity {
|
let log_filter = match options.global.verbosity {
|
||||||
0 => "warn",
|
0 => "info",
|
||||||
1 => "warn,librojo=info",
|
1 => "info,librojo=debug",
|
||||||
2 => "warn,librojo=trace",
|
2 => "info,librojo=trace",
|
||||||
_ => "trace",
|
_ => "trace",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,9 +78,10 @@ fn main() {
|
|||||||
.format_timestamp(None)
|
.format_timestamp(None)
|
||||||
// Indent following lines equal to the log level label, like `[ERROR] `
|
// Indent following lines equal to the log level label, like `[ERROR] `
|
||||||
.format_indent(Some(8))
|
.format_indent(Some(8))
|
||||||
|
.write_style(options.global.color.into())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
if let Err(err) = run(options.subcommand) {
|
if let Err(err) = run(options.global, options.subcommand) {
|
||||||
log::error!("{}", err);
|
log::error!("{}", err);
|
||||||
|
|
||||||
let mut current_err: &dyn Error = &*err;
|
let mut current_err: &dyn Error = &*err;
|
||||||
|
|||||||
@@ -63,15 +63,6 @@ impl ChangeProcessor {
|
|||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
log::trace!("ChangeProcessor thread started");
|
log::trace!("ChangeProcessor thread started");
|
||||||
|
|
||||||
#[allow(
|
|
||||||
// Crossbeam's select macro generates code that Clippy doesn't like,
|
|
||||||
// and Clippy blames us for it.
|
|
||||||
clippy::drop_copy,
|
|
||||||
|
|
||||||
// Crossbeam uses 0 as *const _ and Clippy doesn't like that either,
|
|
||||||
// but this isn't our fault.
|
|
||||||
clippy::zero_ptr,
|
|
||||||
)]
|
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
recv(vfs_receiver) -> event => {
|
recv(vfs_receiver) -> event => {
|
||||||
@@ -187,7 +178,7 @@ impl JobThreadContext {
|
|||||||
if let Some(instigating_source) = &instance.metadata().instigating_source {
|
if let Some(instigating_source) = &instance.metadata().instigating_source {
|
||||||
match instigating_source {
|
match instigating_source {
|
||||||
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
|
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
|
||||||
InstigatingSource::ProjectNode(_, _, _) => {
|
InstigatingSource::ProjectNode(_, _, _, _) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Cannot remove instance {}, it's from a project file",
|
"Cannot remove instance {}, it's from a project file",
|
||||||
id
|
id
|
||||||
@@ -235,7 +226,7 @@ impl JobThreadContext {
|
|||||||
log::warn!("Cannot change Source to non-string value.");
|
log::warn!("Cannot change Source to non-string value.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InstigatingSource::ProjectNode(_, _, _) => {
|
InstigatingSource::ProjectNode(_, _, _, _) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Cannot remove instance {}, it's from a project file",
|
"Cannot remove instance {}, it's from a project file",
|
||||||
id
|
id
|
||||||
@@ -272,12 +263,11 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
|
|||||||
let instigating_source = match &metadata.instigating_source {
|
let instigating_source = match &metadata.instigating_source {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => {
|
None => {
|
||||||
log::warn!(
|
log::error!(
|
||||||
"Instance {} did not have an instigating source, but was considered for an update.",
|
"Instance {} did not have an instigating source, but was considered for an update.",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
log::warn!("This is a Rojo bug. Please file an issue!");
|
log::error!("This is a bug. Please file an issue!");
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -285,44 +275,50 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
|
|||||||
// How we process a file change event depends on what created this
|
// How we process a file change event depends on what created this
|
||||||
// file/folder in the first place.
|
// file/folder in the first place.
|
||||||
let applied_patch_set = match instigating_source {
|
let applied_patch_set = match instigating_source {
|
||||||
InstigatingSource::Path(path) => {
|
InstigatingSource::Path(path) => match vfs.metadata(path).with_not_found() {
|
||||||
let maybe_meta = vfs.metadata(path).with_not_found().unwrap();
|
Ok(Some(_)) => {
|
||||||
|
// 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.
|
||||||
|
|
||||||
match maybe_meta {
|
let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) {
|
||||||
Some(_meta) => {
|
Ok(Some(snapshot)) => snapshot,
|
||||||
// Our instance was previously created from a path and
|
Ok(None) => {
|
||||||
// that path still exists. We can generate a snapshot
|
log::error!(
|
||||||
// starting at that path and use it as the source for
|
"Snapshot did not return an instance from path {}",
|
||||||
// our patch.
|
path.display()
|
||||||
|
);
|
||||||
|
log::error!("This may be a bug!");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Snapshot error: {}", ErrorDisplay(err));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) {
|
let patch_set = compute_patch_set(&snapshot, &tree, id);
|
||||||
Ok(maybe_snapshot) => {
|
apply_patch_set(tree, patch_set)
|
||||||
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)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Our instance was previously created from a path, but
|
|
||||||
// that path no longer exists.
|
|
||||||
//
|
|
||||||
// We associate deleting the instigating file for an
|
|
||||||
// instance with deleting that instance.
|
|
||||||
|
|
||||||
let mut patch_set = PatchSet::new();
|
|
||||||
patch_set.removed_instances.push(id);
|
|
||||||
|
|
||||||
apply_patch_set(tree, patch_set)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Ok(None) => {
|
||||||
InstigatingSource::ProjectNode(project_path, instance_name, project_node) => {
|
// Our instance was previously created from a path, but that
|
||||||
|
// path no longer exists.
|
||||||
|
//
|
||||||
|
// We associate deleting the instigating file for an
|
||||||
|
// instance with deleting that instance.
|
||||||
|
|
||||||
|
let mut patch_set = PatchSet::new();
|
||||||
|
patch_set.removed_instances.push(id);
|
||||||
|
|
||||||
|
apply_patch_set(tree, patch_set)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Error processing filesystem change: {}", ErrorDisplay(err));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
InstigatingSource::ProjectNode(project_path, instance_name, project_node, parent_class) => {
|
||||||
// This instance is the direct subject of a project node. Since
|
// This instance is the direct subject of a project node. Since
|
||||||
// there might be information associated with our instance from
|
// there might be information associated with our instance from
|
||||||
// the project file, we snapshot the entire project node again.
|
// the project file, we snapshot the entire project node again.
|
||||||
@@ -333,12 +329,18 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
|
|||||||
instance_name,
|
instance_name,
|
||||||
project_node,
|
project_node,
|
||||||
&vfs,
|
&vfs,
|
||||||
|
parent_class.as_ref().map(|name| name.as_str()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = match snapshot_result {
|
let snapshot = match snapshot_result {
|
||||||
Ok(maybe_snapshot) => maybe_snapshot.expect("snapshot did not return an instance"),
|
Ok(Some(snapshot)) => snapshot,
|
||||||
|
Ok(None) => {
|
||||||
|
log::error!("Snapshot did not return an instance from a project node.");
|
||||||
|
log::error!("This is a bug!");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Snapshot error: {}", ErrorDisplay(err));
|
log::error!("{}", ErrorDisplay(err));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, BufWriter, Write},
|
io::{BufWriter, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use snafu::{ResultExt, Snafu};
|
use thiserror::Error;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{cli::BuildCommand, serve_session::ServeSession, snapshot::RojoTree};
|
||||||
cli::BuildCommand, project::ProjectError, serve_session::ServeSession, snapshot::RojoTree,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum OutputKind {
|
enum OutputKind {
|
||||||
@@ -31,50 +29,22 @@ fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Error)]
|
||||||
pub struct BuildError(Error);
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
enum Error {
|
enum Error {
|
||||||
#[snafu(display("Could not detect what kind of file to create"))]
|
#[error("Could not detect what kind of file to build. Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.")]
|
||||||
UnknownOutputKind,
|
UnknownOutputKind,
|
||||||
|
|
||||||
#[snafu(display("{}", source))]
|
|
||||||
Io { source: io::Error },
|
|
||||||
|
|
||||||
#[snafu(display("{}", source))]
|
|
||||||
XmlModelEncode { source: rbx_xml::EncodeError },
|
|
||||||
|
|
||||||
#[snafu(display("Binary model error: {:?}", source))]
|
|
||||||
BinaryModelEncode {
|
|
||||||
#[snafu(source(false))]
|
|
||||||
source: rbx_binary::EncodeError,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("{}", source))]
|
|
||||||
Project { source: ProjectError },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rbx_binary::EncodeError> for Error {
|
|
||||||
fn from(source: rbx_binary::EncodeError) -> Self {
|
|
||||||
Error::BinaryModelEncode { source }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xml_encode_config() -> rbx_xml::EncodeOptions {
|
fn xml_encode_config() -> rbx_xml::EncodeOptions {
|
||||||
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
|
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(options: BuildCommand) -> Result<(), BuildError> {
|
pub fn build(options: BuildCommand) -> Result<(), anyhow::Error> {
|
||||||
Ok(build_inner(options)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_inner(options: BuildCommand) -> Result<(), Error> {
|
|
||||||
log::trace!("Constructing in-memory filesystem");
|
log::trace!("Constructing in-memory filesystem");
|
||||||
|
|
||||||
let vfs = Vfs::new_default();
|
let vfs = Vfs::new_default();
|
||||||
|
|
||||||
let session = ServeSession::new(vfs, &options.absolute_project());
|
let session = ServeSession::new(vfs, &options.absolute_project())?;
|
||||||
let mut cursor = session.message_queue().cursor();
|
let mut cursor = session.message_queue().cursor();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -98,14 +68,14 @@ fn build_inner(options: BuildCommand) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
|
fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Error> {
|
||||||
let output_kind = detect_output_kind(&options).ok_or(Error::UnknownOutputKind)?;
|
let output_kind = detect_output_kind(&options).ok_or(Error::UnknownOutputKind)?;
|
||||||
log::debug!("Hoping to generate file of type {:?}", output_kind);
|
log::debug!("Hoping to generate file of type {:?}", output_kind);
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
log::trace!("Opening output file for write");
|
log::trace!("Opening output file for write");
|
||||||
let file = File::create(&options.output).context(Io)?;
|
let file = File::create(&options.output)?;
|
||||||
let mut file = BufWriter::new(file);
|
let mut file = BufWriter::new(file);
|
||||||
|
|
||||||
match output_kind {
|
match output_kind {
|
||||||
@@ -113,8 +83,7 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
|
|||||||
// Model files include the root instance of the tree and all its
|
// Model files include the root instance of the tree and all its
|
||||||
// descendants.
|
// descendants.
|
||||||
|
|
||||||
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())
|
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
|
||||||
.context(XmlModelEncode)?;
|
|
||||||
}
|
}
|
||||||
OutputKind::Rbxlx => {
|
OutputKind::Rbxlx => {
|
||||||
// Place files don't contain an entry for the DataModel, but our
|
// Place files don't contain an entry for the DataModel, but our
|
||||||
@@ -123,8 +92,7 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
|
|||||||
let root_instance = tree.get_instance(root_id).unwrap();
|
let root_instance = tree.get_instance(root_id).unwrap();
|
||||||
let top_level_ids = root_instance.children();
|
let top_level_ids = root_instance.children();
|
||||||
|
|
||||||
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())
|
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
|
||||||
.context(XmlModelEncode)?;
|
|
||||||
}
|
}
|
||||||
OutputKind::Rbxm => {
|
OutputKind::Rbxm => {
|
||||||
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
|
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
|
||||||
@@ -141,7 +109,14 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file.flush().context(Io)?;
|
file.flush()?;
|
||||||
|
|
||||||
|
let filename = options
|
||||||
|
.output
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("<invalid utf-8>");
|
||||||
|
log::info!("Built project to {}", filename);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,4 @@
|
|||||||
use opener::{open, OpenError};
|
pub fn doc() -> Result<(), anyhow::Error> {
|
||||||
use snafu::Snafu;
|
opener::open("https://rojo.space/docs")?;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub struct DocError(Error);
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
enum Error {
|
|
||||||
Open { source: OpenError },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OpenError> for Error {
|
|
||||||
fn from(source: OpenError) -> Self {
|
|
||||||
Error::Open { source }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn doc() -> Result<(), DocError> {
|
|
||||||
doc_inner()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn doc_inner() -> Result<(), Error> {
|
|
||||||
open("https://rojo.space/docs")?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
|
|
||||||
use snafu::Snafu;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::cli::{InitCommand, InitKind};
|
use crate::cli::{InitCommand, InitKind};
|
||||||
|
|
||||||
@@ -20,32 +20,16 @@ static PLACE_PROJECT: &str =
|
|||||||
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
|
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
|
||||||
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
|
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Error)]
|
||||||
pub struct InitError(Error);
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
enum Error {
|
enum Error {
|
||||||
#[snafu(display("A project file named default.project.json already exists in this folder"))]
|
#[error("A project file named default.project.json already exists in this folder")]
|
||||||
AlreadyExists,
|
AlreadyExists,
|
||||||
|
|
||||||
#[snafu(display("git init failed"))]
|
#[error("git init failed")]
|
||||||
GitInit,
|
GitInit,
|
||||||
|
|
||||||
#[snafu(display("I/O error"))]
|
|
||||||
Io { source: io::Error },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
pub fn init(options: InitCommand) -> Result<(), anyhow::Error> {
|
||||||
fn from(source: io::Error) -> Self {
|
|
||||||
Self::Io { source }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(options: InitCommand) -> Result<(), InitError> {
|
|
||||||
Ok(init_inner(options)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_inner(options: InitCommand) -> Result<(), Error> {
|
|
||||||
let base_path = options.absolute_path();
|
let base_path = options.absolute_path();
|
||||||
fs::create_dir_all(&base_path)?;
|
fs::create_dir_all(&base_path)?;
|
||||||
|
|
||||||
@@ -65,7 +49,7 @@ fn init_inner(options: InitCommand) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> {
|
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
|
||||||
eprintln!("Creating new place project '{}'", project_params.name);
|
eprintln!("Creating new place project '{}'", project_params.name);
|
||||||
|
|
||||||
let project_file = project_params.render_template(PLACE_PROJECT);
|
let project_file = project_params.render_template(PLACE_PROJECT);
|
||||||
@@ -109,7 +93,7 @@ fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), Err
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), Error> {
|
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> {
|
||||||
eprintln!("Creating new model project '{}'", project_params.name);
|
eprintln!("Creating new model project '{}'", project_params.name);
|
||||||
|
|
||||||
let project_file = project_params.render_template(MODEL_PROJECT);
|
let project_file = project_params.render_template(MODEL_PROJECT);
|
||||||
@@ -147,14 +131,14 @@ impl ProjectParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to initialize a Git repository if necessary, and create .gitignore.
|
/// Attempt to initialize a Git repository if necessary, and create .gitignore.
|
||||||
fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), Error> {
|
fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
|
||||||
if should_git_init(path) {
|
if should_git_init(path) {
|
||||||
log::debug!("Initializing Git repository...");
|
log::debug!("Initializing Git repository...");
|
||||||
|
|
||||||
let status = Command::new("git").arg("init").current_dir(path).status()?;
|
let status = Command::new("git").arg("init").current_dir(path).status()?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(Error::GitInit);
|
return Err(Error::GitInit.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +170,7 @@ fn should_git_init(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write a file if it does not exist yet, otherwise, leave it alone.
|
/// Write a file if it does not exist yet, otherwise, leave it alone.
|
||||||
fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), Error> {
|
fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> {
|
||||||
let file_res = OpenOptions::new().write(true).create_new(true).open(path);
|
let file_res = OpenOptions::new().write(true).create_new(true).open(path);
|
||||||
|
|
||||||
let mut file = match file_res {
|
let mut file = match file_res {
|
||||||
@@ -205,7 +189,7 @@ fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Try to create a project file and fail if it already exists.
|
/// Try to create a project file and fail if it already exists.
|
||||||
fn try_create_project(base_path: &Path, contents: &str) -> Result<(), Error> {
|
fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Error> {
|
||||||
let project_path = base_path.join("default.project.json");
|
let project_path = base_path.join("default.project.json");
|
||||||
|
|
||||||
let file_res = OpenOptions::new()
|
let file_res = OpenOptions::new()
|
||||||
@@ -217,7 +201,7 @@ fn try_create_project(base_path: &Path, contents: &str) -> Result<(), Error> {
|
|||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return match err.kind() {
|
return match err.kind() {
|
||||||
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists),
|
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists.into()),
|
||||||
_ => Err(err.into()),
|
_ => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
mod build;
|
mod build;
|
||||||
mod doc;
|
mod doc;
|
||||||
mod init;
|
mod init;
|
||||||
|
mod plugin;
|
||||||
mod serve;
|
mod serve;
|
||||||
mod upload;
|
mod upload;
|
||||||
|
|
||||||
@@ -16,10 +17,12 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use self::build::*;
|
pub use self::build::*;
|
||||||
pub use self::doc::*;
|
pub use self::doc::*;
|
||||||
pub use self::init::*;
|
pub use self::init::*;
|
||||||
|
pub use self::plugin::*;
|
||||||
pub use self::serve::*;
|
pub use self::serve::*;
|
||||||
pub use self::upload::*;
|
pub use self::upload::*;
|
||||||
|
|
||||||
@@ -27,16 +30,73 @@ pub use self::upload::*;
|
|||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
#[structopt(name = "Rojo", about, author)]
|
#[structopt(name = "Rojo", about, author)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
/// Sets verbosity level. Can be specified multiple times.
|
#[structopt(flatten)]
|
||||||
#[structopt(long = "verbose", short, global(true), parse(from_occurrences))]
|
pub global: GlobalOptions,
|
||||||
pub verbosity: u8,
|
|
||||||
|
|
||||||
/// Subcommand to run in this invocation.
|
/// Subcommand to run in this invocation.
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
pub subcommand: Subcommand,
|
pub subcommand: Subcommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All of Rojo's subcommands.
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub struct GlobalOptions {
|
||||||
|
/// Sets verbosity level. Can be specified multiple times.
|
||||||
|
#[structopt(long("verbose"), short, global(true), parse(from_occurrences))]
|
||||||
|
pub verbosity: u8,
|
||||||
|
|
||||||
|
/// Set color behavior. Valid values are auto, always, and never.
|
||||||
|
#[structopt(long("color"), global(true), default_value("auto"))]
|
||||||
|
pub color: ColorChoice,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ColorChoice {
|
||||||
|
Auto,
|
||||||
|
Always,
|
||||||
|
Never,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ColorChoice {
|
||||||
|
type Err = ColorChoiceParseError;
|
||||||
|
|
||||||
|
fn from_str(source: &str) -> Result<Self, Self::Err> {
|
||||||
|
match source {
|
||||||
|
"auto" => Ok(ColorChoice::Auto),
|
||||||
|
"always" => Ok(ColorChoice::Always),
|
||||||
|
"never" => Ok(ColorChoice::Never),
|
||||||
|
_ => Err(ColorChoiceParseError {
|
||||||
|
attempted: source.to_owned(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ColorChoice> for termcolor::ColorChoice {
|
||||||
|
fn from(value: ColorChoice) -> Self {
|
||||||
|
match value {
|
||||||
|
ColorChoice::Auto => termcolor::ColorChoice::Auto,
|
||||||
|
ColorChoice::Always => termcolor::ColorChoice::Always,
|
||||||
|
ColorChoice::Never => termcolor::ColorChoice::Never,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ColorChoice> for env_logger::WriteStyle {
|
||||||
|
fn from(value: ColorChoice) -> Self {
|
||||||
|
match value {
|
||||||
|
ColorChoice::Auto => env_logger::WriteStyle::Auto,
|
||||||
|
ColorChoice::Always => env_logger::WriteStyle::Always,
|
||||||
|
ColorChoice::Never => env_logger::WriteStyle::Never,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("Invalid color choice '{attempted}'. Valid values are: auto, always, never")]
|
||||||
|
pub struct ColorChoiceParseError {
|
||||||
|
attempted: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
pub enum Subcommand {
|
pub enum Subcommand {
|
||||||
/// Creates a new Rojo project.
|
/// Creates a new Rojo project.
|
||||||
@@ -53,6 +113,9 @@ pub enum Subcommand {
|
|||||||
|
|
||||||
/// Open Rojo's documentation in your browser.
|
/// Open Rojo's documentation in your browser.
|
||||||
Doc,
|
Doc,
|
||||||
|
|
||||||
|
/// Manages Rojo's Roblox Studio plugin.
|
||||||
|
Plugin(PluginCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes a new Rojo project.
|
/// Initializes a new Rojo project.
|
||||||
@@ -229,3 +292,21 @@ fn resolve_path(path: &Path) -> Cow<'_, Path> {
|
|||||||
Cow::Owned(env::current_dir().unwrap().join(path))
|
Cow::Owned(env::current_dir().unwrap().join(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub enum PluginSubcommand {
|
||||||
|
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is
|
||||||
|
/// already installed, installing it again will overwrite the current plugin
|
||||||
|
/// file.
|
||||||
|
Install,
|
||||||
|
|
||||||
|
/// Removes the plugin if it is installed.
|
||||||
|
Uninstall,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install Rojo's plugin.
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
pub struct PluginCommand {
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
subcommand: PluginSubcommand,
|
||||||
|
}
|
||||||
|
|||||||
70
src/cli/plugin.rs
Normal file
70
src/cli/plugin.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::BufWriter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
|
||||||
|
use roblox_install::RobloxStudio;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{PluginCommand, PluginSubcommand},
|
||||||
|
serve_session::ServeSession,
|
||||||
|
};
|
||||||
|
|
||||||
|
static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.bincode"));
|
||||||
|
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
|
||||||
|
|
||||||
|
pub fn plugin(options: PluginCommand) -> Result<()> {
|
||||||
|
match options.subcommand {
|
||||||
|
PluginSubcommand::Install => install_plugin(),
|
||||||
|
PluginSubcommand::Uninstall => uninstall_plugin(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_plugin() -> Result<()> {
|
||||||
|
let plugin_snapshot: VfsSnapshot = bincode::deserialize(PLUGIN_BINCODE)
|
||||||
|
.expect("Rojo's plugin was not properly packed into Rojo's binary");
|
||||||
|
|
||||||
|
let studio = RobloxStudio::locate()?;
|
||||||
|
|
||||||
|
let plugins_folder_path = studio.plugins_path();
|
||||||
|
|
||||||
|
if !plugins_folder_path.exists() {
|
||||||
|
log::debug!("Creating Roblox Studio plugins folder");
|
||||||
|
fs::create_dir(plugins_folder_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut in_memory_fs = InMemoryFs::new();
|
||||||
|
in_memory_fs.load_snapshot("plugin", plugin_snapshot)?;
|
||||||
|
|
||||||
|
let vfs = Vfs::new(in_memory_fs);
|
||||||
|
let session = ServeSession::new(vfs, "plugin")?;
|
||||||
|
|
||||||
|
let plugin_path = plugins_folder_path.join(PLUGIN_FILE_NAME);
|
||||||
|
log::debug!("Writing plugin to {}", plugin_path.display());
|
||||||
|
|
||||||
|
let mut file = BufWriter::new(File::create(plugin_path)?);
|
||||||
|
|
||||||
|
let tree = session.tree();
|
||||||
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
|
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninstall_plugin() -> Result<()> {
|
||||||
|
let studio = RobloxStudio::locate()?;
|
||||||
|
|
||||||
|
let plugin_path = studio.plugins_path().join(PLUGIN_FILE_NAME);
|
||||||
|
|
||||||
|
if plugin_path.exists() {
|
||||||
|
log::debug!("Removing existing plugin from {}", plugin_path.display());
|
||||||
|
fs::remove_file(plugin_path)?;
|
||||||
|
} else {
|
||||||
|
log::debug!("Plugin not installed at {}", plugin_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -3,28 +3,22 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use snafu::Snafu;
|
|
||||||
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
||||||
|
|
||||||
use crate::{cli::ServeCommand, serve_session::ServeSession, web::LiveServer};
|
use crate::{
|
||||||
|
cli::{GlobalOptions, ServeCommand},
|
||||||
|
serve_session::ServeSession,
|
||||||
|
web::LiveServer,
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_PORT: u16 = 34872;
|
const DEFAULT_PORT: u16 = 34872;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
|
||||||
pub struct ServeError(Error);
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
enum Error {}
|
|
||||||
|
|
||||||
pub fn serve(options: ServeCommand) -> Result<(), ServeError> {
|
|
||||||
Ok(serve_inner(options)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serve_inner(options: ServeCommand) -> Result<(), Error> {
|
|
||||||
let vfs = Vfs::new_default();
|
let vfs = Vfs::new_default();
|
||||||
|
|
||||||
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project()));
|
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project())?);
|
||||||
|
|
||||||
let port = options
|
let port = options
|
||||||
.port
|
.port
|
||||||
@@ -33,14 +27,14 @@ fn serve_inner(options: ServeCommand) -> Result<(), Error> {
|
|||||||
|
|
||||||
let server = LiveServer::new(session);
|
let server = LiveServer::new(session);
|
||||||
|
|
||||||
let _ = show_start_message(port);
|
let _ = show_start_message(port, global.color.into());
|
||||||
server.start(port);
|
server.start(port);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_start_message(port: u16) -> io::Result<()> {
|
fn show_start_message(port: u16, color: ColorChoice) -> io::Result<()> {
|
||||||
let writer = BufferWriter::stdout(ColorChoice::Auto);
|
let writer = BufferWriter::stdout(color);
|
||||||
let mut buffer = writer.buffer();
|
let mut buffer = writer.buffer();
|
||||||
|
|
||||||
writeln!(&mut buffer, "Rojo server listening:")?;
|
writeln!(&mut buffer, "Rojo server listening:")?;
|
||||||
|
|||||||
@@ -1,45 +1,30 @@
|
|||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use reqwest::header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT};
|
use reqwest::header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT};
|
||||||
use snafu::{ResultExt, Snafu};
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{auth_cookie::get_auth_cookie, cli::UploadCommand, common_setup};
|
use crate::{auth_cookie::get_auth_cookie, cli::UploadCommand, serve_session::ServeSession};
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Error)]
|
||||||
pub struct UploadError(Error);
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
enum Error {
|
enum Error {
|
||||||
#[snafu(display(
|
#[error("Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
|
||||||
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
|
|
||||||
))]
|
|
||||||
NeedAuthCookie,
|
NeedAuthCookie,
|
||||||
|
|
||||||
#[snafu(display("XML model file encode error: {}", source))]
|
#[error("The Roblox API returned an unexpected error: {body}")]
|
||||||
XmlModel { source: rbx_xml::EncodeError },
|
|
||||||
|
|
||||||
#[snafu(display("HTTP error: {}", source))]
|
|
||||||
Http { source: reqwest::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Roblox API error: {}", body))]
|
|
||||||
RobloxApi { body: String },
|
RobloxApi { body: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload(options: UploadCommand) -> Result<(), UploadError> {
|
pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
|
||||||
Ok(upload_inner(options)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upload_inner(options: UploadCommand) -> Result<(), Error> {
|
|
||||||
let cookie = options
|
let cookie = options
|
||||||
.cookie
|
.cookie
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(get_auth_cookie)
|
.or_else(get_auth_cookie)
|
||||||
.ok_or(Error::NeedAuthCookie)?;
|
.ok_or(Error::NeedAuthCookie)?;
|
||||||
|
|
||||||
log::trace!("Constructing in-memory filesystem");
|
|
||||||
let vfs = Vfs::new_default();
|
let vfs = Vfs::new_default();
|
||||||
|
|
||||||
let (_maybe_project, tree) = common_setup::start(&options.absolute_project(), &vfs);
|
let session = ServeSession::new(vfs, &options.absolute_project())?;
|
||||||
|
|
||||||
|
let tree = session.tree();
|
||||||
let inner_tree = tree.inner();
|
let inner_tree = tree.inner();
|
||||||
let root_id = inner_tree.get_root_id();
|
let root_id = inner_tree.get_root_id();
|
||||||
let root_instance = inner_tree.get_instance(root_id).unwrap();
|
let root_instance = inner_tree.get_instance(root_id).unwrap();
|
||||||
@@ -55,7 +40,7 @@ fn upload_inner(options: UploadCommand) -> Result<(), Error> {
|
|||||||
let config = rbx_xml::EncodeOptions::new()
|
let config = rbx_xml::EncodeOptions::new()
|
||||||
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
|
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
|
||||||
|
|
||||||
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config).context(XmlModel)?;
|
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config)?;
|
||||||
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
|
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
|
||||||
@@ -72,13 +57,13 @@ fn upload_inner(options: UploadCommand) -> Result<(), Error> {
|
|||||||
.header(CONTENT_TYPE, "application/xml")
|
.header(CONTENT_TYPE, "application/xml")
|
||||||
.header(ACCEPT, "application/json")
|
.header(ACCEPT, "application/json")
|
||||||
.body(buffer)
|
.body(buffer)
|
||||||
.send()
|
.send()?;
|
||||||
.context(Http)?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(Error::RobloxApi {
|
return Err(Error::RobloxApi {
|
||||||
body: response.text().context(Http)?,
|
body: response.text()?,
|
||||||
});
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
//! Initialization routines that are used by more than one Rojo command or
|
|
||||||
//! utility.
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use memofs::Vfs;
|
|
||||||
use rbx_dom_weak::RbxInstanceProperties;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
project::Project,
|
|
||||||
snapshot::{
|
|
||||||
apply_patch_set, compute_patch_set, InstanceContext, InstancePropertiesWithMeta,
|
|
||||||
PathIgnoreRule, RojoTree,
|
|
||||||
},
|
|
||||||
snapshot_middleware::snapshot_from_vfs,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn start(fuzzy_project_path: &Path, vfs: &Vfs) -> (Option<Project>, RojoTree) {
|
|
||||||
log::trace!("Loading project file from {}", fuzzy_project_path.display());
|
|
||||||
let maybe_project = Project::load_fuzzy(fuzzy_project_path).expect("TODO: Project load failed");
|
|
||||||
|
|
||||||
log::trace!("Constructing initial tree");
|
|
||||||
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
|
|
||||||
properties: RbxInstanceProperties {
|
|
||||||
name: "ROOT".to_owned(),
|
|
||||||
class_name: "Folder".to_owned(),
|
|
||||||
properties: Default::default(),
|
|
||||||
},
|
|
||||||
metadata: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let root_id = tree.get_root_id();
|
|
||||||
|
|
||||||
let mut instance_context = InstanceContext::default();
|
|
||||||
|
|
||||||
if let Some(project) = &maybe_project {
|
|
||||||
let rules = project.glob_ignore_paths.iter().map(|glob| PathIgnoreRule {
|
|
||||||
glob: glob.clone(),
|
|
||||||
base_path: project.folder_location().to_path_buf(),
|
|
||||||
});
|
|
||||||
|
|
||||||
instance_context.add_path_ignore_rules(rules);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("Generating snapshot of instances from VFS");
|
|
||||||
let snapshot = snapshot_from_vfs(&instance_context, vfs, &fuzzy_project_path)
|
|
||||||
.expect("snapshot failed")
|
|
||||||
.expect("snapshot did not return an instance");
|
|
||||||
|
|
||||||
log::trace!("Computing patch set");
|
|
||||||
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
|
|
||||||
|
|
||||||
log::trace!("Applying patch set");
|
|
||||||
apply_patch_set(&mut tree, patch_set);
|
|
||||||
|
|
||||||
(maybe_project, tree)
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,9 @@ mod tree_view;
|
|||||||
|
|
||||||
mod auth_cookie;
|
mod auth_cookie;
|
||||||
mod change_processor;
|
mod change_processor;
|
||||||
mod common_setup;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod glob;
|
mod glob;
|
||||||
|
mod lua_ast;
|
||||||
mod message_queue;
|
mod message_queue;
|
||||||
mod multimap;
|
mod multimap;
|
||||||
mod path_serializer;
|
mod path_serializer;
|
||||||
|
|||||||
279
src/lua_ast.rs
Normal file
279
src/lua_ast.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
//! Defines module for defining a small Lua AST for simple codegen.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::{self, Write},
|
||||||
|
num::FpCategory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Trait that helps turn a type into an equivalent Lua snippet.
|
||||||
|
///
|
||||||
|
/// Designed to be similar to the `Display` trait from Rust's std.
|
||||||
|
trait FmtLua {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result;
|
||||||
|
|
||||||
|
/// Used to override how this type will appear when used as a table key.
|
||||||
|
/// Some types, like strings, can have a shorter representation as a table
|
||||||
|
/// key than the default, safe approach.
|
||||||
|
fn fmt_table_key(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
write!(output, "[")?;
|
||||||
|
self.fmt_lua(output)?;
|
||||||
|
write!(output, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum Statement {
|
||||||
|
Return(Expression),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for Statement {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Return(literal) => {
|
||||||
|
write!(output, "return ")?;
|
||||||
|
literal.fmt_lua(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Statement {
|
||||||
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let mut stream = LuaStream::new(output);
|
||||||
|
FmtLua::fmt_lua(self, &mut stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum Expression {
|
||||||
|
Nil,
|
||||||
|
Bool(bool),
|
||||||
|
Number(f64),
|
||||||
|
String(String),
|
||||||
|
Table(Table),
|
||||||
|
|
||||||
|
/// Arrays are not technically distinct from other tables in Lua, but this
|
||||||
|
/// representation is more convenient.
|
||||||
|
Array(Vec<Expression>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expression {
|
||||||
|
pub fn table(entries: Vec<(Expression, Expression)>) -> Self {
|
||||||
|
Self::Table(Table { entries })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for Expression {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Nil => write!(output, "nil"),
|
||||||
|
Self::Bool(inner) => inner.fmt_lua(output),
|
||||||
|
Self::Number(inner) => inner.fmt_lua(output),
|
||||||
|
Self::String(inner) => inner.fmt_lua(output),
|
||||||
|
Self::Table(inner) => inner.fmt_lua(output),
|
||||||
|
Self::Array(inner) => inner.fmt_lua(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_table_key(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Nil => panic!("nil cannot be a table key"),
|
||||||
|
Self::Bool(inner) => inner.fmt_table_key(output),
|
||||||
|
Self::Number(inner) => inner.fmt_table_key(output),
|
||||||
|
Self::String(inner) => inner.fmt_table_key(output),
|
||||||
|
Self::Table(inner) => inner.fmt_table_key(output),
|
||||||
|
Self::Array(inner) => inner.fmt_table_key(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Expression {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'_ str> for Expression {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self::String(value.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Table> for Expression {
|
||||||
|
fn from(value: Table) -> Self {
|
||||||
|
Self::Table(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for bool {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
write!(output, "{}", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for f64 {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
match self.classify() {
|
||||||
|
FpCategory::Nan => write!(output, "0/0"),
|
||||||
|
FpCategory::Infinite => {
|
||||||
|
if self.is_sign_positive() {
|
||||||
|
write!(output, "math.huge")
|
||||||
|
} else {
|
||||||
|
write!(output, "-math.huge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => write!(output, "{}", self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for String {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
write!(output, "\"{}\"", self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_table_key(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
if is_valid_ident(self) {
|
||||||
|
write!(output, "{}", self)
|
||||||
|
} else {
|
||||||
|
write!(output, "[\"{}\"]", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for Vec<Expression> {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
write!(output, "{{")?;
|
||||||
|
|
||||||
|
for (index, value) in self.iter().enumerate() {
|
||||||
|
value.fmt_lua(output)?;
|
||||||
|
|
||||||
|
if index < self.len() - 1 {
|
||||||
|
write!(output, ", ")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(output, "}}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Table {
|
||||||
|
pub entries: Vec<(Expression, Expression)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtLua for Table {
|
||||||
|
fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result {
|
||||||
|
writeln!(output, "{{")?;
|
||||||
|
output.indent();
|
||||||
|
|
||||||
|
for (key, value) in &self.entries {
|
||||||
|
key.fmt_table_key(output)?;
|
||||||
|
write!(output, " = ")?;
|
||||||
|
value.fmt_lua(output)?;
|
||||||
|
writeln!(output, ",")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.unindent();
|
||||||
|
write!(output, "}}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_ident_char_start(value: char) -> bool {
|
||||||
|
value.is_ascii_alphabetic() || value == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_ident_char(value: char) -> bool {
|
||||||
|
value.is_ascii_alphanumeric() || value == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_keyword(value: &str) -> bool {
|
||||||
|
match value {
|
||||||
|
"and" | "break" | "do" | "else" | "elseif" | "end" | "false" | "for" | "function"
|
||||||
|
| "if" | "in" | "local" | "nil" | "not" | "or" | "repeat" | "return" | "then" | "true"
|
||||||
|
| "until" | "while" => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells whether the given string is a valid Lua identifier.
|
||||||
|
fn is_valid_ident(value: &str) -> bool {
|
||||||
|
if is_keyword(value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chars = value.chars();
|
||||||
|
|
||||||
|
match chars.next() {
|
||||||
|
Some(first) => {
|
||||||
|
if !is_valid_ident_char_start(first) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
chars.all(is_valid_ident_char)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a `fmt::Write` with additional tracking to do pretty-printing of Lua.
|
||||||
|
///
|
||||||
|
/// Behaves similarly to `fmt::Formatter`. This trait's relationship to `LuaFmt`
|
||||||
|
/// is very similar to `Formatter`'s relationship to `Display`.
|
||||||
|
struct LuaStream<'a> {
|
||||||
|
indent_level: usize,
|
||||||
|
is_start_of_line: bool,
|
||||||
|
inner: &'a mut (dyn fmt::Write + 'a),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Write for LuaStream<'_> {
|
||||||
|
/// Method to support the `write!` and `writeln!` macros. Instead of using a
|
||||||
|
/// trait directly, these macros just call `write_str` on their first
|
||||||
|
/// argument.
|
||||||
|
///
|
||||||
|
/// This method is also available on `io::Write` and `fmt::Write`.
|
||||||
|
fn write_str(&mut self, value: &str) -> fmt::Result {
|
||||||
|
let mut is_first_line = true;
|
||||||
|
|
||||||
|
for line in value.split('\n') {
|
||||||
|
if is_first_line {
|
||||||
|
is_first_line = false;
|
||||||
|
} else {
|
||||||
|
self.line()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !line.is_empty() {
|
||||||
|
if self.is_start_of_line {
|
||||||
|
self.is_start_of_line = false;
|
||||||
|
let indentation = "\t".repeat(self.indent_level);
|
||||||
|
self.inner.write_str(&indentation)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.write_str(line)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LuaStream<'a> {
|
||||||
|
fn new(inner: &'a mut (dyn fmt::Write + 'a)) -> Self {
|
||||||
|
LuaStream {
|
||||||
|
indent_level: 0,
|
||||||
|
is_start_of_line: true,
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indent(&mut self) {
|
||||||
|
self.indent_level += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unindent(&mut self) {
|
||||||
|
assert!(self.indent_level > 0);
|
||||||
|
self.indent_level -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line(&mut self) -> fmt::Result {
|
||||||
|
self.is_start_of_line = true;
|
||||||
|
self.inner.write_str("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,22 +6,26 @@ use std::{
|
|||||||
|
|
||||||
use rbx_dom_weak::UnresolvedRbxValue;
|
use rbx_dom_weak::UnresolvedRbxValue;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::{ResultExt, Snafu};
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::glob::Glob;
|
use crate::glob::Glob;
|
||||||
|
|
||||||
static PROJECT_FILENAME: &str = "default.project.json";
|
static PROJECT_FILENAME: &str = "default.project.json";
|
||||||
|
|
||||||
/// Error type returned by any function that handles projects.
|
/// Error type returned by any function that handles projects.
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Error)]
|
||||||
pub struct ProjectError(Error);
|
#[error(transparent)]
|
||||||
|
pub struct ProjectError(#[from] Error);
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Error)]
|
||||||
enum Error {
|
enum Error {
|
||||||
/// A general IO error occurred.
|
#[error(transparent)]
|
||||||
Io { source: io::Error, path: PathBuf },
|
Io {
|
||||||
|
#[from]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
/// An error with JSON parsing occurred.
|
#[error("Error parsing Rojo project in path {}", .path.display())]
|
||||||
Json {
|
Json {
|
||||||
source: serde_json::Error,
|
source: serde_json::Error,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -125,14 +129,14 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_exact(project_file_location: &Path) -> Result<Self, ProjectError> {
|
fn load_exact(project_file_location: &Path) -> Result<Self, Error> {
|
||||||
let contents = fs::read_to_string(project_file_location).context(Io {
|
let contents = fs::read_to_string(project_file_location)?;
|
||||||
path: project_file_location,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut project: Project = serde_json::from_str(&contents).context(Json {
|
let mut project: Project =
|
||||||
path: project_file_location,
|
serde_json::from_str(&contents).map_err(|source| Error::Json {
|
||||||
})?;
|
source,
|
||||||
|
path: project_file_location.to_owned(),
|
||||||
|
})?;
|
||||||
|
|
||||||
project.file_location = project_file_location.to_path_buf();
|
project.file_location = project_file_location.to_path_buf();
|
||||||
project.check_compatibility();
|
project.check_compatibility();
|
||||||
@@ -140,10 +144,6 @@ impl Project {
|
|||||||
Ok(project)
|
Ok(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), ProjectError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if there are any compatibility issues with this project file and
|
/// Checks if there are any compatibility issues with this project file and
|
||||||
/// warns the user if there are any.
|
/// warns the user if there are any.
|
||||||
fn check_compatibility(&self) {
|
fn check_compatibility(&self) {
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex, MutexGuard},
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
|
use rbx_dom_weak::RbxInstanceProperties;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
change_processor::ChangeProcessor,
|
change_processor::ChangeProcessor,
|
||||||
common_setup,
|
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
project::Project,
|
project::{Project, ProjectError},
|
||||||
session_id::SessionId,
|
session_id::SessionId,
|
||||||
snapshot::{AppliedPatchSet, PatchSet, RojoTree},
|
snapshot::{
|
||||||
|
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext,
|
||||||
|
InstancePropertiesWithMeta, PatchSet, PathIgnoreRule, RojoTree,
|
||||||
|
},
|
||||||
|
snapshot_middleware::{snapshot_from_vfs, SnapshotError},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Contains all of the state for a Rojo serve session.
|
/// Contains all of the state for a Rojo serve session.
|
||||||
@@ -43,15 +48,12 @@ pub struct ServeSession {
|
|||||||
/// diagnostics.
|
/// diagnostics.
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
|
|
||||||
/// The root project for the serve session, if there was one defined.
|
/// The root project for the serve session.
|
||||||
///
|
///
|
||||||
/// This will be defined if a folder with a `default.project.json` file was
|
/// This will be defined if a folder with a `default.project.json` file was
|
||||||
/// used for starting the serve session, or if the user specified a full
|
/// used for starting the serve session, or if the user specified a full
|
||||||
/// path to a `.project.json` file.
|
/// path to a `.project.json` file.
|
||||||
///
|
root_project: Project,
|
||||||
/// If `root_project` is None, values from the project should be treated as
|
|
||||||
/// their defaults.
|
|
||||||
root_project: Option<Project>,
|
|
||||||
|
|
||||||
/// A randomly generated ID for this serve session. It's used to ensure that
|
/// A randomly generated ID for this serve session. It's used to ensure that
|
||||||
/// a client doesn't begin connecting to a different server part way through
|
/// a client doesn't begin connecting to a different server part way through
|
||||||
@@ -82,24 +84,57 @@ pub struct ServeSession {
|
|||||||
tree_mutation_sender: Sender<PatchSet>,
|
tree_mutation_sender: Sender<PatchSet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 ServeSession {
|
impl ServeSession {
|
||||||
/// Start a new serve session from the given in-memory filesystem and start
|
/// Start a new serve session from the given in-memory filesystem and start
|
||||||
/// path.
|
/// path.
|
||||||
///
|
///
|
||||||
/// The project file is expected to be loaded out-of-band since it's
|
/// The project file is expected to be loaded out-of-band since it's
|
||||||
/// currently loaded from the filesystem directly instead of through the
|
/// currently loaded from the filesystem directly instead of through the
|
||||||
/// in-memory filesystem layer.
|
/// in-memory filesystem layer.
|
||||||
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Self {
|
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> {
|
||||||
let start_path = start_path.as_ref();
|
let start_path = start_path.as_ref();
|
||||||
|
|
||||||
log::trace!("Starting new ServeSession at path {}", start_path.display(),);
|
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let (root_project, tree) = common_setup::start(start_path, &vfs);
|
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
||||||
|
|
||||||
|
log::debug!("Loading project file from {}", start_path.display());
|
||||||
|
let root_project =
|
||||||
|
Project::load_fuzzy(start_path)?.ok_or_else(|| ServeSessionError::NoProjectFound {
|
||||||
|
path: start_path.to_owned(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let rules = root_project
|
||||||
|
.glob_ignore_paths
|
||||||
|
.iter()
|
||||||
|
.map(|glob| PathIgnoreRule {
|
||||||
|
glob: glob.clone(),
|
||||||
|
base_path: root_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 did not return an instance");
|
||||||
|
|
||||||
|
log::trace!("Computing initial patch set");
|
||||||
|
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
|
||||||
|
|
||||||
|
log::trace!("Applying initial patch set");
|
||||||
|
apply_patch_set(&mut tree, patch_set);
|
||||||
|
|
||||||
let session_id = SessionId::new();
|
let session_id = SessionId::new();
|
||||||
let message_queue = MessageQueue::new();
|
let message_queue = MessageQueue::new();
|
||||||
@@ -118,7 +153,7 @@ impl ServeSession {
|
|||||||
tree_mutation_receiver,
|
tree_mutation_receiver,
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
change_processor,
|
change_processor,
|
||||||
start_time,
|
start_time,
|
||||||
session_id,
|
session_id,
|
||||||
@@ -127,11 +162,9 @@ impl ServeSession {
|
|||||||
message_queue,
|
message_queue,
|
||||||
tree_mutation_sender,
|
tree_mutation_sender,
|
||||||
vfs,
|
vfs,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl ServeSession {
|
|
||||||
pub fn tree_handle(&self) -> Arc<Mutex<RojoTree>> {
|
pub fn tree_handle(&self) -> Arc<Mutex<RojoTree>> {
|
||||||
Arc::clone(&self.tree)
|
Arc::clone(&self.tree)
|
||||||
}
|
}
|
||||||
@@ -144,6 +177,7 @@ impl ServeSession {
|
|||||||
self.tree_mutation_sender.clone()
|
self.tree_mutation_sender.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn vfs(&self) -> &Vfs {
|
pub fn vfs(&self) -> &Vfs {
|
||||||
&self.vfs
|
&self.vfs
|
||||||
}
|
}
|
||||||
@@ -156,16 +190,12 @@ impl ServeSession {
|
|||||||
self.session_id
|
self.session_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_name(&self) -> Option<&str> {
|
pub fn project_name(&self) -> &str {
|
||||||
self.root_project
|
&self.root_project.name
|
||||||
.as_ref()
|
|
||||||
.map(|project| project.name.as_str())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_port(&self) -> Option<u16> {
|
pub fn project_port(&self) -> Option<u16> {
|
||||||
self.root_project
|
self.root_project.serve_port
|
||||||
.as_ref()
|
|
||||||
.and_then(|project| project.serve_port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_time(&self) -> Instant {
|
pub fn start_time(&self) -> Instant {
|
||||||
@@ -173,217 +203,28 @@ impl ServeSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> {
|
pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> {
|
||||||
self.root_project
|
self.root_project.serve_place_ids.as_ref()
|
||||||
.as_ref()
|
|
||||||
.and_then(|project| project.serve_place_ids.as_ref())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This module is named to trick Insta into naming the resulting snapshots
|
#[derive(Debug, Error)]
|
||||||
/// correctly.
|
pub enum ServeSessionError {
|
||||||
///
|
#[error(
|
||||||
/// See https://github.com/mitsuhiko/insta/issues/78
|
"Rojo requires a project file, but no project file was found in path {}\n\
|
||||||
#[cfg(test)]
|
See https://rojo.space/docs/ for guides and documentation.",
|
||||||
mod serve_session {
|
.path.display()
|
||||||
use super::*;
|
)]
|
||||||
|
NoProjectFound { path: PathBuf },
|
||||||
|
|
||||||
use std::{path::PathBuf, time::Duration};
|
#[error(transparent)]
|
||||||
|
Project {
|
||||||
|
#[from]
|
||||||
|
source: ProjectError,
|
||||||
|
},
|
||||||
|
|
||||||
use maplit::hashmap;
|
#[error(transparent)]
|
||||||
use memofs::{InMemoryFs, VfsEvent, VfsSnapshot};
|
Snapshot {
|
||||||
use rojo_insta_ext::RedactionMap;
|
#[from]
|
||||||
use tokio::{runtime::Runtime, timer::Timeout};
|
source: SnapshotError,
|
||||||
|
},
|
||||||
use crate::tree_view::view_tree;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn just_folder() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/foo", VfsSnapshot::empty_dir())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let session = ServeSession::new(vfs, "/foo");
|
|
||||||
|
|
||||||
let mut rm = RedactionMap::new();
|
|
||||||
insta::assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn project_with_folder() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot(
|
|
||||||
"/foo",
|
|
||||||
VfsSnapshot::dir(hashmap! {
|
|
||||||
"default.project.json" => VfsSnapshot::file(r#"
|
|
||||||
{
|
|
||||||
"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();
|
|
||||||
insta::assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn script_with_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);
|
|
||||||
|
|
||||||
let session = ServeSession::new(vfs, "/root");
|
|
||||||
|
|
||||||
let mut rm = RedactionMap::new();
|
|
||||||
insta::assert_yaml_snapshot!(view_tree(&session.tree(), &mut rm));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn change_txt_file() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("Hello!"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs.clone());
|
|
||||||
|
|
||||||
let session = ServeSession::new(vfs, "/foo.txt");
|
|
||||||
|
|
||||||
let mut rm = RedactionMap::new();
|
|
||||||
insta::assert_yaml_snapshot!(
|
|
||||||
"change_txt_file_before",
|
|
||||||
view_tree(&session.tree(), &mut rm)
|
|
||||||
);
|
|
||||||
|
|
||||||
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("World!"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let receiver = session.message_queue().subscribe_any();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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 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",
|
|
||||||
|
|
||||||
"Child": {
|
|
||||||
"$path": "file.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#),
|
|
||||||
"file.txt" => VfsSnapshot::file("initial content"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs.clone());
|
|
||||||
|
|
||||||
let session = ServeSession::new(vfs, "/foo");
|
|
||||||
|
|
||||||
let mut rm = RedactionMap::new();
|
|
||||||
insta::assert_yaml_snapshot!(
|
|
||||||
"change_file_in_project_before",
|
|
||||||
view_tree(&session.tree(), &mut rm)
|
|
||||||
);
|
|
||||||
|
|
||||||
imfs.load_snapshot("/foo/file.txt", VfsSnapshot::file("Changed!"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let receiver = session.message_queue().subscribe_any();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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 rm)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ pub enum InstigatingSource {
|
|||||||
#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf,
|
#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf,
|
||||||
String,
|
String,
|
||||||
ProjectNode,
|
ProjectNode,
|
||||||
|
Option<String>,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +171,13 @@ impl fmt::Debug for InstigatingSource {
|
|||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()),
|
InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()),
|
||||||
InstigatingSource::ProjectNode(path, name, node) => write!(
|
InstigatingSource::ProjectNode(path, name, node, parent_class) => write!(
|
||||||
formatter,
|
formatter,
|
||||||
"ProjectNode({}: {:?}) from path {}",
|
"ProjectNode({}: {:?}) from path {} and parent class {:?}",
|
||||||
name,
|
name,
|
||||||
node,
|
node,
|
||||||
path.display()
|
path.display(),
|
||||||
|
parent_class,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,68 @@
|
|||||||
use std::{error::Error, fmt, io, path::PathBuf};
|
use std::{io, path::PathBuf};
|
||||||
|
|
||||||
use snafu::Snafu;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub struct SnapshotError {
|
pub enum SnapshotError {
|
||||||
detail: SnapshotErrorDetail,
|
#[error("file name had malformed Unicode")]
|
||||||
path: Option<PathBuf>,
|
FileNameBadUnicode { path: PathBuf },
|
||||||
|
|
||||||
|
#[error("file had malformed Unicode contents at path {}", .path.display())]
|
||||||
|
FileContentsBadUnicode {
|
||||||
|
source: std::str::Utf8Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("malformed project file at path {}", .path.display())]
|
||||||
|
MalformedProject {
|
||||||
|
source: serde_json::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("malformed .model.json file at path {}", .path.display())]
|
||||||
|
MalformedModelJson {
|
||||||
|
source: serde_json::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("malformed .meta.json file at path {}", .path.display())]
|
||||||
|
MalformedMetaJson {
|
||||||
|
source: serde_json::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("malformed JSON at path {}", .path.display())]
|
||||||
|
MalformedJson {
|
||||||
|
source: serde_json::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Io {
|
||||||
|
#[from]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SnapshotError {
|
impl SnapshotError {
|
||||||
pub fn new(detail: SnapshotErrorDetail, path: Option<impl Into<PathBuf>>) -> Self {
|
|
||||||
Self {
|
|
||||||
detail,
|
|
||||||
path: path.map(Into::into),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn wrap(source: impl Into<SnapshotErrorDetail>, path: impl Into<PathBuf>) -> Self {
|
|
||||||
Self {
|
|
||||||
detail: source.into(),
|
|
||||||
path: Some(path.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn file_did_not_exist(path: impl Into<PathBuf>) -> Self {
|
|
||||||
Self {
|
|
||||||
detail: SnapshotErrorDetail::FileDidNotExist,
|
|
||||||
path: Some(path.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> Self {
|
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> Self {
|
||||||
Self {
|
Self::FileNameBadUnicode { path: path.into() }
|
||||||
detail: SnapshotErrorDetail::FileNameBadUnicode,
|
|
||||||
path: Some(path.into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn file_contents_bad_unicode(
|
pub(crate) fn file_contents_bad_unicode(
|
||||||
source: std::str::Utf8Error,
|
source: std::str::Utf8Error,
|
||||||
path: impl Into<PathBuf>,
|
path: impl Into<PathBuf>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self::FileContentsBadUnicode {
|
||||||
detail: SnapshotErrorDetail::FileContentsBadUnicode { source },
|
source,
|
||||||
path: Some(path.into()),
|
path: path.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn malformed_project(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
|
pub(crate) fn malformed_project(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
|
||||||
Self {
|
Self::MalformedProject {
|
||||||
detail: SnapshotErrorDetail::MalformedProject { source },
|
source,
|
||||||
path: Some(path.into()),
|
path: path.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,82 +70,23 @@ impl SnapshotError {
|
|||||||
source: serde_json::Error,
|
source: serde_json::Error,
|
||||||
path: impl Into<PathBuf>,
|
path: impl Into<PathBuf>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self::MalformedModelJson {
|
||||||
detail: SnapshotErrorDetail::MalformedModelJson { source },
|
source,
|
||||||
path: Some(path.into()),
|
path: path.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn malformed_meta_json(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
|
pub(crate) fn malformed_meta_json(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
|
||||||
Self {
|
Self::MalformedMetaJson {
|
||||||
detail: SnapshotErrorDetail::MalformedMetaJson { source },
|
source,
|
||||||
path: Some(path.into()),
|
path: path.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn malformed_json(source: serde_json::Error, path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self::MalformedJson {
|
||||||
|
source,
|
||||||
|
path: path.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for SnapshotError {
|
|
||||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
||||||
self.detail.source()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for SnapshotError {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match &self.path {
|
|
||||||
Some(path) => write!(formatter, "{} in path {}", self.detail, path.display()),
|
|
||||||
None => write!(formatter, "{}", self.detail),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for SnapshotError {
|
|
||||||
fn from(inner: io::Error) -> Self {
|
|
||||||
Self::new(inner.into(), Option::<PathBuf>::None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rlua::Error> for SnapshotError {
|
|
||||||
fn from(error: rlua::Error) -> Self {
|
|
||||||
Self::new(error.into(), Option::<PathBuf>::None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
|
||||||
pub enum SnapshotErrorDetail {
|
|
||||||
#[snafu(display("I/O error"))]
|
|
||||||
IoError { source: io::Error },
|
|
||||||
|
|
||||||
#[snafu(display("Lua error"))]
|
|
||||||
Lua { source: rlua::Error },
|
|
||||||
|
|
||||||
#[snafu(display("file did not exist"))]
|
|
||||||
FileDidNotExist,
|
|
||||||
|
|
||||||
#[snafu(display("file name had malformed Unicode"))]
|
|
||||||
FileNameBadUnicode,
|
|
||||||
|
|
||||||
#[snafu(display("file had malformed Unicode contents"))]
|
|
||||||
FileContentsBadUnicode { source: std::str::Utf8Error },
|
|
||||||
|
|
||||||
#[snafu(display("malformed project file"))]
|
|
||||||
MalformedProject { source: serde_json::Error },
|
|
||||||
|
|
||||||
#[snafu(display("malformed .model.json file"))]
|
|
||||||
MalformedModelJson { source: serde_json::Error },
|
|
||||||
|
|
||||||
#[snafu(display("malformed .meta.json file"))]
|
|
||||||
MalformedMetaJson { source: serde_json::Error },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for SnapshotErrorDetail {
|
|
||||||
fn from(source: io::Error) -> Self {
|
|
||||||
SnapshotErrorDetail::IoError { source }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rlua::Error> for SnapshotErrorDetail {
|
|
||||||
fn from(source: rlua::Error) -> Self {
|
|
||||||
SnapshotErrorDetail::Lua { source }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
142
src/snapshot_middleware/json.rs
Normal file
142
src/snapshot_middleware/json.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use maplit::hashmap;
|
||||||
|
use memofs::{IoResultExt, Vfs};
|
||||||
|
use rbx_dom_weak::RbxValue;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
lua_ast::{Expression, Statement},
|
||||||
|
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
error::SnapshotError,
|
||||||
|
meta_file::AdjacentMetadata,
|
||||||
|
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||||
|
util::match_file_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Catch-all middleware for snapshots on JSON files that aren't used for other
|
||||||
|
/// features, like Rojo projects, JSON models, or meta files.
|
||||||
|
pub struct SnapshotJson;
|
||||||
|
|
||||||
|
impl SnapshotMiddleware for SnapshotJson {
|
||||||
|
fn from_vfs(context: &InstanceContext, vfs: &Vfs, path: &Path) -> SnapshotInstanceResult {
|
||||||
|
let meta = vfs.metadata(path)?;
|
||||||
|
|
||||||
|
if meta.is_dir() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This middleware should not need to know about the .meta.json
|
||||||
|
// middleware. Should there be a way to signal "I'm not returning an
|
||||||
|
// instance and no one should"?
|
||||||
|
if match_file_name(path, ".meta.json").is_some() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance_name = match match_file_name(path, ".json") {
|
||||||
|
Some(name) => name,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = vfs.read(path)?;
|
||||||
|
|
||||||
|
let value: serde_json::Value = serde_json::from_slice(&contents)
|
||||||
|
.map_err(|err| SnapshotError::malformed_json(err, path))?;
|
||||||
|
|
||||||
|
let as_lua = json_to_lua(value).to_string();
|
||||||
|
|
||||||
|
let properties = hashmap! {
|
||||||
|
"Source".to_owned() => RbxValue::String {
|
||||||
|
value: as_lua,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
|
||||||
|
|
||||||
|
let mut snapshot = InstanceSnapshot::new()
|
||||||
|
.name(instance_name)
|
||||||
|
.class_name("ModuleScript")
|
||||||
|
.properties(properties)
|
||||||
|
.metadata(
|
||||||
|
InstanceMetadata::new()
|
||||||
|
.instigating_source(path)
|
||||||
|
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()])
|
||||||
|
.context(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
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 json_to_lua(value: serde_json::Value) -> Statement {
|
||||||
|
Statement::Return(json_to_lua_value(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_to_lua_value(value: serde_json::Value) -> Expression {
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Value::Null => Expression::Nil,
|
||||||
|
Value::Bool(value) => Expression::Bool(value),
|
||||||
|
Value::Number(value) => Expression::Number(value.as_f64().unwrap()),
|
||||||
|
Value::String(value) => Expression::String(value),
|
||||||
|
Value::Array(values) => {
|
||||||
|
Expression::Array(values.into_iter().map(json_to_lua_value).collect())
|
||||||
|
}
|
||||||
|
Value::Object(values) => Expression::table(
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, value)| (key.into(), json_to_lua_value(value)))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use memofs::{InMemoryFs, VfsSnapshot};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_from_vfs() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/foo.json",
|
||||||
|
VfsSnapshot::file(
|
||||||
|
r#"{
|
||||||
|
"array": [1, 2, 3],
|
||||||
|
"object": {
|
||||||
|
"hello": "world"
|
||||||
|
},
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
"null": null,
|
||||||
|
"int": 1234,
|
||||||
|
"float": 1234.5452,
|
||||||
|
"1invalidident": "nice"
|
||||||
|
}"#,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut vfs = Vfs::new(imfs.clone());
|
||||||
|
|
||||||
|
let instance_snapshot = SnapshotJson::from_vfs(
|
||||||
|
&InstanceContext::default(),
|
||||||
|
&mut vfs,
|
||||||
|
Path::new("/foo.json"),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
mod csv;
|
mod csv;
|
||||||
mod dir;
|
mod dir;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod json;
|
||||||
mod json_model;
|
mod json_model;
|
||||||
mod lua;
|
mod lua;
|
||||||
mod meta_file;
|
mod meta_file;
|
||||||
@@ -17,20 +18,27 @@ mod rbxmx;
|
|||||||
mod txt;
|
mod txt;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use self::error::*;
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
|
|
||||||
use self::middleware::{SnapshotInstanceResult, SnapshotMiddleware};
|
|
||||||
use self::{
|
|
||||||
csv::SnapshotCsv, dir::SnapshotDir, json_model::SnapshotJsonModel, lua::SnapshotLua,
|
|
||||||
project::SnapshotProject, rbxlx::SnapshotRbxlx, rbxm::SnapshotRbxm, rbxmx::SnapshotRbxmx,
|
|
||||||
txt::SnapshotTxt,
|
|
||||||
};
|
|
||||||
use crate::snapshot::InstanceContext;
|
use crate::snapshot::InstanceContext;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
csv::SnapshotCsv,
|
||||||
|
dir::SnapshotDir,
|
||||||
|
json::SnapshotJson,
|
||||||
|
json_model::SnapshotJsonModel,
|
||||||
|
lua::SnapshotLua,
|
||||||
|
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
|
||||||
|
project::SnapshotProject,
|
||||||
|
rbxlx::SnapshotRbxlx,
|
||||||
|
rbxm::SnapshotRbxm,
|
||||||
|
rbxmx::SnapshotRbxmx,
|
||||||
|
txt::SnapshotTxt,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use self::error::*;
|
||||||
pub use self::project::snapshot_project_node;
|
pub use self::project::snapshot_project_node;
|
||||||
|
|
||||||
macro_rules! middlewares {
|
macro_rules! middlewares {
|
||||||
@@ -65,5 +73,6 @@ middlewares! {
|
|||||||
SnapshotLua,
|
SnapshotLua,
|
||||||
SnapshotCsv,
|
SnapshotCsv,
|
||||||
SnapshotTxt,
|
SnapshotTxt,
|
||||||
|
SnapshotJson,
|
||||||
SnapshotDir,
|
SnapshotDir,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{borrow::Cow, collections::HashMap, path::Path};
|
use std::{borrow::Cow, collections::HashMap, path::Path};
|
||||||
|
|
||||||
use memofs::{IoResultExt, Vfs};
|
use memofs::{IoResultExt, Vfs};
|
||||||
use rbx_reflection::try_resolve_value;
|
use rbx_reflection::{get_class_descriptor, try_resolve_value};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
project::{Project, ProjectNode},
|
project::{Project, ProjectNode},
|
||||||
@@ -62,6 +62,7 @@ impl SnapshotMiddleware for SnapshotProject {
|
|||||||
&project.name,
|
&project.name,
|
||||||
&project.tree,
|
&project.tree,
|
||||||
vfs,
|
vfs,
|
||||||
|
None,
|
||||||
)?
|
)?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ pub fn snapshot_project_node(
|
|||||||
instance_name: &str,
|
instance_name: &str,
|
||||||
node: &ProjectNode,
|
node: &ProjectNode,
|
||||||
vfs: &Vfs,
|
vfs: &Vfs,
|
||||||
|
parent_class: Option<&str>,
|
||||||
) -> SnapshotInstanceResult {
|
) -> SnapshotInstanceResult {
|
||||||
let name = Cow::Owned(instance_name.to_owned());
|
let name = Cow::Owned(instance_name.to_owned());
|
||||||
let mut class_name = node
|
let mut class_name = node
|
||||||
@@ -158,13 +160,43 @@ pub fn snapshot_project_node(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let class_name = class_name
|
let class_name = class_name
|
||||||
|
.or_else(|| {
|
||||||
|
// If className wasn't defined from another source, we may be able
|
||||||
|
// to infer one.
|
||||||
|
|
||||||
|
let parent_class = parent_class?;
|
||||||
|
|
||||||
|
if parent_class == "DataModel" {
|
||||||
|
// Members of DataModel with names that match known services are
|
||||||
|
// probably supposed to be those services.
|
||||||
|
|
||||||
|
let descriptor = get_class_descriptor(&name)?;
|
||||||
|
|
||||||
|
if descriptor.is_service() {
|
||||||
|
return Some(name.clone());
|
||||||
|
}
|
||||||
|
} else if parent_class == "StarterPlayer" {
|
||||||
|
// StarterPlayer has two special members with their own classes.
|
||||||
|
|
||||||
|
if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" {
|
||||||
|
return Some(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
// TODO: Turn this into an error object.
|
// TODO: Turn this into an error object.
|
||||||
.expect("$className or $path must be specified");
|
.expect("$className or $path must be specified");
|
||||||
|
|
||||||
for (child_name, child_project_node) in &node.children {
|
for (child_name, child_project_node) in &node.children {
|
||||||
if let Some(child) =
|
if let Some(child) = snapshot_project_node(
|
||||||
snapshot_project_node(context, project_folder, child_name, child_project_node, vfs)?
|
context,
|
||||||
{
|
project_folder,
|
||||||
|
child_name,
|
||||||
|
child_project_node,
|
||||||
|
vfs,
|
||||||
|
Some(&class_name),
|
||||||
|
)? {
|
||||||
children.push(child);
|
children.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,6 +226,7 @@ pub fn snapshot_project_node(
|
|||||||
project_folder.to_path_buf(),
|
project_folder.to_path_buf(),
|
||||||
instance_name.to_string(),
|
instance_name.to_string(),
|
||||||
node.clone(),
|
node.clone(),
|
||||||
|
parent_class.map(|name| name.to_owned()),
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(Some(InstanceSnapshot {
|
Ok(Some(InstanceSnapshot {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user