mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 13:21:38 +00:00
Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api
This commit is contained in:
commit
2e0f3dc011
42 changed files with 1054 additions and 426 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -23,3 +23,6 @@
|
||||||
# Macos file system junk
|
# Macos file system junk
|
||||||
._*
|
._*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|
||||||
|
# JetBrains IDEs
|
||||||
|
/.idea/
|
||||||
|
|
|
||||||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -32,6 +32,21 @@ Making a new release? Simply add the new header with the version and date undern
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
* Added headless API for Studio companion plugins. ([#639])
|
* Added headless API for Studio companion plugins. ([#639])
|
||||||
|
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
||||||
|
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
||||||
|
* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201])
|
||||||
|
* Fixed a bug where the notification timeout thread would fail to cancel on unmount ([#1211])
|
||||||
|
* Added a "Forget" option to the sync reminder notification to avoid being reminded for that place in the future ([#1215])
|
||||||
|
|
||||||
|
[#639]: https://github.com/rojo-rbx/rojo/pull/639
|
||||||
|
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
|
||||||
|
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
|
||||||
|
[#1201]: https://github.com/rojo-rbx/rojo/pull/1201
|
||||||
|
[#1211]: https://github.com/rojo-rbx/rojo/pull/1211
|
||||||
|
[#1215]: https://github.com/rojo-rbx/rojo/pull/1215
|
||||||
|
|
||||||
|
## [7.7.0-rc.1] (November 27th, 2025)
|
||||||
|
|
||||||
* Fixed a bug where passing `--skip-git` to `rojo init` would still create a file named `gitignore.txt` ([#1172])
|
* Fixed a bug where passing `--skip-git` to `rojo init` would still create a file named `gitignore.txt` ([#1172])
|
||||||
* A new command `rojo syncback` has been added. It can be used as `rojo syncback [path to project] --input [path to file]`. ([#937])
|
* A new command `rojo syncback` has been added. It can be used as `rojo syncback [path to project] --input [path to file]`. ([#937])
|
||||||
This command takes a Roblox file and pulls Instances out of it and places them in the correct position in the provided project.
|
This command takes a Roblox file and pulls Instances out of it and places them in the correct position in the provided project.
|
||||||
|
|
@ -66,14 +81,11 @@ Making a new release? Simply add the new header with the version and date undern
|
||||||
- `syncCurrentCamera` is a toggle for whether to sync back the Workspace's CurrentCamera. Defaults to `false`.
|
- `syncCurrentCamera` is a toggle for whether to sync back the Workspace's CurrentCamera. Defaults to `false`.
|
||||||
- `syncUnscriptable` is a toggle for whether to sync back properties that cannot be set by the Roblox Studio plugin. Defaults to `true`.
|
- `syncUnscriptable` is a toggle for whether to sync back properties that cannot be set by the Roblox Studio plugin. Defaults to `true`.
|
||||||
|
|
||||||
If you are used to the `UpliftGames` version of this feature, there are a few notable differences:
|
|
||||||
- `syncUnscriptable` defaults to `true` instead of `false`
|
|
||||||
- `ignoreTrees` doesn't require the root of the project's name in it.
|
|
||||||
* Fixed bugs and improved performance & UX for the script diff viewer ([#994])
|
* Fixed bugs and improved performance & UX for the script diff viewer ([#994])
|
||||||
* Rebuilt the internal communication between the server and plugin to use [websockets](https://devforum.roblox.com/t/websockets-support-in-studio-is-now-available/4021932/1) instead of [long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling) ([#1142])
|
* Rebuilt the internal communication between the server and plugin to use [websockets](https://devforum.roblox.com/t/websockets-support-in-studio-is-now-available/4021932/1) instead of [long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling) ([#1142])
|
||||||
* Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159])
|
* Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159])
|
||||||
|
|
||||||
[#639]: https://github.com/rojo-rbx/rojo/pull/639
|
[7.7.0-rc.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.7.0-rc.1
|
||||||
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
||||||
[#994]: https://github.com/rojo-rbx/rojo/pull/994
|
[#994]: https://github.com/rojo-rbx/rojo/pull/994
|
||||||
[#1142]: https://github.com/rojo-rbx/rojo/pull/1142
|
[#1142]: https://github.com/rojo-rbx/rojo/pull/1142
|
||||||
|
|
|
||||||
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -1313,12 +1313,13 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"notify",
|
"notify",
|
||||||
"serde",
|
"serde",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1792,9 +1793,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_binary"
|
name = "rbx_binary"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d419f67c8012bf83569086e1208c541478b3b8e4f523deaa0b80d723fb5ef22"
|
checksum = "95e2b4a187679aa3d169ed50ed5eedbf26383459fec83bf1232c2934b35b24de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1810,9 +1811,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_dom_weak"
|
name = "rbx_dom_weak"
|
||||||
version = "4.0.0"
|
version = "4.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc74878a4a801afc8014b14ede4b38015a13de5d29ab0095d5ed284a744253f6"
|
checksum = "a7a5c48c2605913fbb1986bceb3e18ef9f12eadedb7edd62bf9fb03447b57c46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"rbx_types",
|
"rbx_types",
|
||||||
|
|
@ -1822,9 +1823,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_reflection"
|
name = "rbx_reflection"
|
||||||
version = "6.0.0"
|
version = "6.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "565dd3430991f35443fa6d23cc239fade2110c5089deb6bae5de77c400df4fd2"
|
checksum = "84f635e79d5d710c82e9049faa57d32945e76a6b041280dc6274f732c0dd78dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rbx_types",
|
"rbx_types",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1833,9 +1834,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_reflection_database"
|
name = "rbx_reflection_database"
|
||||||
version = "2.0.1+roblox-697"
|
version = "2.0.2+roblox-700"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d69035a14b103c5a9b8bc6a61d30f4ee6f2608afdee137dae09b26037dba5dc8"
|
checksum = "de2753b896d08d74316d8b89fbeb2470ebd3986404ebba82fa85fcc0330955cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1846,9 +1847,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_types"
|
name = "rbx_types"
|
||||||
version = "3.0.0"
|
version = "3.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03220ffce2bd06ad04f77a003cb807f2e5b2a18e97623066a5ac735a978398af"
|
checksum = "de3b89eefdd71f5e2a25543b1ead9f6ea2ed3fdfba8b397cbdb0de053eb2463e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
|
|
@ -1861,9 +1862,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_xml"
|
name = "rbx_xml"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be6c302cefe9c92ed09bcbb075cd24379271de135b0af331409a64c2ea3646ee"
|
checksum = "e0cbaf53b44c9cc0fad1e5dc8ac63fb32fa0ecaa26d32b269cebe4dca8b7b4de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
|
|
@ -2037,7 +2038,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.7.0-prealpha"
|
version = "7.7.0-rc.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
|
|
|
||||||
18
Cargo.toml
18
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.7.0-prealpha"
|
version = "7.7.0-rc.1"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
authors = [
|
authors = [
|
||||||
"Lucien Greathouse <me@lpghatguy.com>",
|
"Lucien Greathouse <me@lpghatguy.com>",
|
||||||
|
|
@ -46,20 +46,22 @@ name = "build"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
memofs = { version = "0.3.1", path = "crates/memofs" }
|
||||||
|
|
||||||
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
||||||
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [
|
||||||
|
# "unstable_text_format",
|
||||||
|
# ] }
|
||||||
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
||||||
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
||||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||||
|
|
||||||
rbx_binary = { version = "2.0.0", features = ["unstable_text_format"] }
|
rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] }
|
||||||
rbx_dom_weak = "4.0.0"
|
rbx_dom_weak = "4.1.0"
|
||||||
rbx_reflection = "6.0.0"
|
rbx_reflection = "6.1.0"
|
||||||
rbx_reflection_database = "2.0.1"
|
rbx_reflection_database = "2.0.2"
|
||||||
rbx_xml = "2.0.0"
|
rbx_xml = "2.0.1"
|
||||||
|
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.80"
|
||||||
backtrace = "0.3.69"
|
backtrace = "0.3.69"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
# memofs Changelog
|
# memofs Changelog
|
||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
* Added `Vfs::canonicalize`. [#1201]
|
||||||
|
|
||||||
|
## 0.3.1 (2025-11-27)
|
||||||
* Added `Vfs::exists`. [#1169]
|
* Added `Vfs::exists`. [#1169]
|
||||||
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
|
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Lucien Greathouse <me@lpghatguy.com>",
|
"Lucien Greathouse <me@lpghatguy.com>",
|
||||||
"Micah Reid <git@dekkonot.com>",
|
"Micah Reid <git@dekkonot.com>",
|
||||||
|
|
@ -19,3 +19,6 @@ crossbeam-channel = "0.5.12"
|
||||||
fs-err = "2.11.0"
|
fs-err = "2.11.0"
|
||||||
notify = "4.0.17"
|
notify = "4.0.17"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.10.1"
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,33 @@ impl VfsBackend for InMemoryFs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
|
||||||
|
// in MemoFS. The current implementation will error if no prepended absolute path
|
||||||
|
// is found. It really only normalizes paths within the provided path's context.
|
||||||
|
// Example: "/Users/username/project/../other/file.txt" ->
|
||||||
|
// "/Users/username/other/file.txt"
|
||||||
|
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
|
||||||
|
// This is not very robust. We should implement proper path normalization here or otherwise
|
||||||
|
// warn if we are missing context and can not fully canonicalize the path correctly.
|
||||||
|
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for component in path.components() {
|
||||||
|
match component {
|
||||||
|
std::path::Component::ParentDir => {
|
||||||
|
normalized.pop();
|
||||||
|
}
|
||||||
|
std::path::Component::CurDir => {}
|
||||||
|
_ => normalized.push(component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner = self.inner.lock().unwrap();
|
||||||
|
match inner.entries.get(&normalized) {
|
||||||
|
Some(_) => Ok(normalized),
|
||||||
|
None => not_found(&normalized),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||||
|
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||||
|
|
@ -225,6 +226,11 @@ impl VfsInner {
|
||||||
self.backend.metadata(path)
|
self.backend.metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.backend.canonicalize(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
self.backend.event_receiver()
|
self.backend.event_receiver()
|
||||||
}
|
}
|
||||||
|
|
@ -413,6 +419,19 @@ impl Vfs {
|
||||||
self.inner.lock().unwrap().metadata(path)
|
self.inner.lock().unwrap().metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize a path via the underlying backend.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
|
||||||
|
/// resolved against the backend's current working directory (if applicable) and errors are
|
||||||
|
/// surfaced directly from the backend.
|
||||||
|
///
|
||||||
|
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
|
||||||
|
#[inline]
|
||||||
|
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.lock().unwrap().canonicalize(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
|
|
@ -540,6 +559,13 @@ impl VfsLock<'_> {
|
||||||
self.inner.metadata(path)
|
self.inner.metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize a path via the underlying backend.
|
||||||
|
#[inline]
|
||||||
|
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.canonicalize(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
|
|
@ -555,7 +581,9 @@ impl VfsLock<'_> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{InMemoryFs, Vfs, VfsSnapshot};
|
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot};
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// https://github.com/rojo-rbx/rojo/issues/899
|
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -571,4 +599,62 @@ mod test {
|
||||||
"bar\nfoo\n\n"
|
"bar\nfoo\n\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://github.com/rojo-rbx/rojo/issues/1200
|
||||||
|
#[test]
|
||||||
|
fn canonicalize_in_memory_success() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||||
|
|
||||||
|
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
|
||||||
|
PathBuf::from("/test/file.txt")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
contents.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonicalize_in_memory_missing_errors() {
|
||||||
|
let imfs = InMemoryFs::new();
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
|
let err = vfs.canonicalize("test").unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonicalize_std_backend_success() {
|
||||||
|
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let file_path = dir.path().join("file.txt");
|
||||||
|
fs_err::write(&file_path, contents.to_string()).unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(StdBackend::new());
|
||||||
|
let canonicalized = vfs.canonicalize(&file_path).unwrap();
|
||||||
|
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
vfs.read_to_string(&canonicalized).unwrap().to_string(),
|
||||||
|
contents.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonicalize_std_backend_missing_errors() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let file_path = dir.path().join("test");
|
||||||
|
|
||||||
|
let vfs = Vfs::new(StdBackend::new());
|
||||||
|
let err = vfs.canonicalize(&file_path).unwrap_err();
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||||
|
|
||||||
|
|
@ -31,17 +31,11 @@ impl VfsBackend for NoopBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
|
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
|
|
@ -56,6 +50,10 @@ impl VfsBackend for NoopBackend {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
|
||||||
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
crossbeam_channel::never()
|
crossbeam_channel::never()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,10 @@ impl VfsBackend for StdBackend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||||
|
fs_err::canonicalize(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
self.watcher_receiver.clone()
|
self.watcher_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
7.7.0-prealpha
|
7.7.0-rc.1
|
||||||
|
|
@ -1,139 +1,10 @@
|
||||||
-- Thanks to Tiffany352 for this base64 implementation!
|
local EncodingService = game:GetService("EncodingService")
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
decode = decodeBase64,
|
decode = function(input: string)
|
||||||
encode = encodeBase64,
|
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
|
||||||
|
end,
|
||||||
|
encode = function(input: string)
|
||||||
|
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,4 +208,30 @@ return {
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
StyleRule = {
|
||||||
|
PropertiesSerialize = {
|
||||||
|
read = function(instance: StyleRule)
|
||||||
|
return true, instance:GetProperties()
|
||||||
|
end,
|
||||||
|
write = function(instance: StyleRule, _, value: { [any]: any })
|
||||||
|
if typeof(value) ~= "table" then
|
||||||
|
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||||
|
end
|
||||||
|
|
||||||
|
local existing = instance:GetProperties()
|
||||||
|
|
||||||
|
for itemName, itemValue in pairs(value) do
|
||||||
|
instance:SetProperty(itemName, itemValue)
|
||||||
|
end
|
||||||
|
|
||||||
|
for existingItemName in pairs(existing) do
|
||||||
|
if value[existingItemName] == nil then
|
||||||
|
instance:SetProperty(existingItemName, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -290,31 +290,39 @@ function ApiContext:open(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:serialize(ids: { string })
|
function ApiContext:serialize(ids: { string })
|
||||||
local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
||||||
|
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||||
|
|
||||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
return Http.post(url, request_body)
|
||||||
if body.sessionId ~= self.__sessionId then
|
:andThen(rejectFailedRequests)
|
||||||
return Promise.reject("Server changed ID")
|
:andThen(Http.Response.json)
|
||||||
end
|
:andThen(function(response_body)
|
||||||
|
if response_body.sessionId ~= self.__sessionId then
|
||||||
|
return Promise.reject("Server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
assert(validateApiSerialize(body))
|
assert(validateApiSerialize(response_body))
|
||||||
|
|
||||||
return body
|
return response_body
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:refPatch(ids: { string })
|
function ApiContext:refPatch(ids: { string })
|
||||||
local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
||||||
|
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||||
|
|
||||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
return Http.post(url, request_body)
|
||||||
if body.sessionId ~= self.__sessionId then
|
:andThen(rejectFailedRequests)
|
||||||
return Promise.reject("Server changed ID")
|
:andThen(Http.Response.json)
|
||||||
end
|
:andThen(function(response_body)
|
||||||
|
if response_body.sessionId ~= self.__sessionId then
|
||||||
|
return Promise.reject("Server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
assert(validateApiRefPatch(body))
|
assert(validateApiRefPatch(response_body))
|
||||||
|
|
||||||
return body
|
return response_body
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ApiContext
|
return ApiContext
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,15 @@ local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotifi
|
||||||
function FullscreenNotification:init()
|
function FullscreenNotification:init()
|
||||||
self.transparency, self.setTransparency = Roact.createBinding(0)
|
self.transparency, self.setTransparency = Roact.createBinding(0)
|
||||||
self.lifetime = self.props.timeout
|
self.lifetime = self.props.timeout
|
||||||
|
self.dismissed = false
|
||||||
end
|
end
|
||||||
|
|
||||||
function FullscreenNotification:dismiss()
|
function FullscreenNotification:dismiss()
|
||||||
|
if self.dismissed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.dismissed = true
|
||||||
|
|
||||||
if self.props.onClose then
|
if self.props.onClose then
|
||||||
self.props.onClose()
|
self.props.onClose()
|
||||||
end
|
end
|
||||||
|
|
@ -59,7 +65,7 @@ function FullscreenNotification:didMount()
|
||||||
end
|
end
|
||||||
|
|
||||||
function FullscreenNotification:willUnmount()
|
function FullscreenNotification:willUnmount()
|
||||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
if self.timeout and coroutine.status(self.timeout) == "suspended" then
|
||||||
task.cancel(self.timeout)
|
task.cancel(self.timeout)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ function Notification:init()
|
||||||
self.binding = bindingUtil.fromMotor(self.motor)
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
|
|
||||||
self.lifetime = self.props.timeout
|
self.lifetime = self.props.timeout
|
||||||
|
self.dismissed = false
|
||||||
|
|
||||||
self.motor:onStep(function(value)
|
self.motor:onStep(function(value)
|
||||||
if value <= 0 and self.props.onClose then
|
if value <= 0 and self.props.onClose then
|
||||||
|
|
@ -35,6 +36,11 @@ function Notification:init()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:dismiss()
|
function Notification:dismiss()
|
||||||
|
if self.dismissed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.dismissed = true
|
||||||
|
|
||||||
self.motor:setGoal(Flipper.Spring.new(0, {
|
self.motor:setGoal(Flipper.Spring.new(0, {
|
||||||
frequency = 5,
|
frequency = 5,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
|
|
@ -76,7 +82,7 @@ function Notification:didMount()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:willUnmount()
|
function Notification:willUnmount()
|
||||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
if self.timeout and coroutine.status(self.timeout) == "suspended" then
|
||||||
task.cancel(self.timeout)
|
task.cancel(self.timeout)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,19 @@ function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
||||||
Settings:set("priorEndpoints", priorSyncInfos)
|
Settings:set("priorEndpoints", priorSyncInfos)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function App:forgetPriorSyncInfo()
|
||||||
|
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||||
|
if not priorSyncInfos then
|
||||||
|
priorSyncInfos = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = tostring(game.PlaceId)
|
||||||
|
priorSyncInfos[id] = nil
|
||||||
|
Log.trace("Erased last used endpoint for {}", game.PlaceId)
|
||||||
|
|
||||||
|
Settings:set("priorEndpoints", priorSyncInfos)
|
||||||
|
end
|
||||||
|
|
||||||
function App:getHostAndPort(): (string, string)
|
function App:getHostAndPort(): (string, string)
|
||||||
local host = self.host:getValue()
|
local host = self.host:getValue()
|
||||||
local port = self.port:getValue()
|
local port = self.port:getValue()
|
||||||
|
|
@ -578,7 +591,8 @@ function App:checkSyncReminder()
|
||||||
self:findActiveServer()
|
self:findActiveServer()
|
||||||
:andThen(function(serverInfo, host, port)
|
:andThen(function(serverInfo, host, port)
|
||||||
self:sendSyncReminder(
|
self:sendSyncReminder(
|
||||||
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
|
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`,
|
||||||
|
{ "Connect", "Dismiss" }
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
:catch(function()
|
:catch(function()
|
||||||
|
|
@ -589,7 +603,8 @@ function App:checkSyncReminder()
|
||||||
|
|
||||||
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
||||||
self:sendSyncReminder(
|
self:sendSyncReminder(
|
||||||
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
|
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`,
|
||||||
|
{ "Connect", "Forget", "Dismiss" }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
@ -629,12 +644,16 @@ function App:stopSyncReminderPolling()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:sendSyncReminder(message: string)
|
function App:sendSyncReminder(message: string, shownActions: { string })
|
||||||
local syncReminderMode = Settings:get("syncReminderMode")
|
local syncReminderMode = Settings:get("syncReminderMode")
|
||||||
if syncReminderMode == "None" then
|
if syncReminderMode == "None" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local connectIndex = table.find(shownActions, "Connect")
|
||||||
|
local forgetIndex = table.find(shownActions, "Forget")
|
||||||
|
local dismissIndex = table.find(shownActions, "Dismiss")
|
||||||
|
|
||||||
self.dismissSyncReminder = self:addNotification({
|
self.dismissSyncReminder = self:addNotification({
|
||||||
text = message,
|
text = message,
|
||||||
timeout = 120,
|
timeout = 120,
|
||||||
|
|
@ -643,24 +662,39 @@ function App:sendSyncReminder(message: string)
|
||||||
self.dismissSyncReminder = nil
|
self.dismissSyncReminder = nil
|
||||||
end,
|
end,
|
||||||
actions = {
|
actions = {
|
||||||
Connect = {
|
Connect = if connectIndex
|
||||||
text = "Connect",
|
then {
|
||||||
style = "Solid",
|
text = "Connect",
|
||||||
layoutOrder = 1,
|
style = "Solid",
|
||||||
onClick = function()
|
layoutOrder = connectIndex,
|
||||||
self:startSession()
|
onClick = function()
|
||||||
end,
|
self:startSession()
|
||||||
},
|
end,
|
||||||
Dismiss = {
|
}
|
||||||
text = "Dismiss",
|
else nil,
|
||||||
style = "Bordered",
|
Forget = if forgetIndex
|
||||||
layoutOrder = 2,
|
then {
|
||||||
onClick = function()
|
text = "Forget",
|
||||||
-- If the user dismisses the reminder,
|
style = "Bordered",
|
||||||
-- then we don't need to remind them again
|
layoutOrder = forgetIndex,
|
||||||
self:stopSyncReminderPolling()
|
onClick = function()
|
||||||
end,
|
-- The user doesn't want to be reminded again about this sync
|
||||||
},
|
self:forgetPriorSyncInfo()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
else nil,
|
||||||
|
Dismiss = if dismissIndex
|
||||||
|
then {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = dismissIndex,
|
||||||
|
onClick = function()
|
||||||
|
-- If the user dismisses the reminder,
|
||||||
|
-- then we don't need to remind them again
|
||||||
|
self:stopSyncReminderPolling()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
else nil,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
source: tests/rojo_test/syncback_util.rs
|
||||||
|
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||||
|
---
|
||||||
|
Writing src/Pointer1.model.json
|
||||||
|
Writing src/Pointer2.model.json
|
||||||
|
Writing src/Pointer3.model.json
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
source: tests/tests/syncback.rs
|
||||||
|
expression: src/Pointer1.model.json
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"className": "ObjectValue"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
source: tests/tests/syncback.rs
|
||||||
|
expression: src/Pointer2.model.json
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"className": "ObjectValue"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
source: tests/tests/syncback.rs
|
||||||
|
expression: src/Pointer3.model.json
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"className": "ObjectValue"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "ref_properties_pruned",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ServerScriptService": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
rojo-test/syncback-tests/ref_properties_pruned/input.rbxl
Normal file
BIN
rojo-test/syncback-tests/ref_properties_pruned/input.rbxl
Normal file
Binary file not shown.
|
|
@ -1,12 +1,12 @@
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
||||||
use jod_thread::JoinHandle;
|
use jod_thread::JoinHandle;
|
||||||
use memofs::{IoResultExt, Vfs, VfsEvent};
|
use memofs::{IoResultExt, Vfs, VfsEvent};
|
||||||
use rbx_dom_weak::types::{Ref, Variant};
|
use rbx_dom_weak::types::{Ref, Variant};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
|
|
@ -114,6 +114,49 @@ struct JobThreadContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JobThreadContext {
|
impl JobThreadContext {
|
||||||
|
/// Computes and applies patches to the DOM for a given file path.
|
||||||
|
///
|
||||||
|
/// This function finds the nearest ancestor to the given path that has associated instances
|
||||||
|
/// in the tree.
|
||||||
|
/// It then computes and applies changes for each affected instance ID and
|
||||||
|
/// returns a vector of applied patch sets.
|
||||||
|
fn apply_patches(&self, path: PathBuf) -> Vec<AppliedPatchSet> {
|
||||||
|
let mut tree = self.tree.lock().unwrap();
|
||||||
|
let mut applied_patches = Vec::new();
|
||||||
|
|
||||||
|
// Find the nearest ancestor to this path that has
|
||||||
|
// associated instances in the tree. This helps make sure
|
||||||
|
// that we handle additions correctly, especially if we
|
||||||
|
// receive events for descendants of a large tree being
|
||||||
|
// created all at once.
|
||||||
|
let mut current_path = path.as_path();
|
||||||
|
let affected_ids = loop {
|
||||||
|
let ids = tree.get_ids_at_path(current_path);
|
||||||
|
|
||||||
|
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
||||||
|
|
||||||
|
if !ids.is_empty() {
|
||||||
|
break ids.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("Trying parent path...");
|
||||||
|
match current_path.parent() {
|
||||||
|
Some(parent) => current_path = parent,
|
||||||
|
None => break Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for id in affected_ids {
|
||||||
|
if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) {
|
||||||
|
if !patch.is_empty() {
|
||||||
|
applied_patches.push(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applied_patches
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_vfs_event(&self, event: VfsEvent) {
|
fn handle_vfs_event(&self, event: VfsEvent) {
|
||||||
log::trace!("Vfs event: {:?}", event);
|
log::trace!("Vfs event: {:?}", event);
|
||||||
|
|
||||||
|
|
@ -125,41 +168,16 @@ impl JobThreadContext {
|
||||||
// For a given VFS event, we might have many changes to different parts
|
// For a given VFS event, we might have many changes to different parts
|
||||||
// of the tree. Calculate and apply all of these changes.
|
// of the tree. Calculate and apply all of these changes.
|
||||||
let applied_patches = match event {
|
let applied_patches = match event {
|
||||||
VfsEvent::Create(path) | VfsEvent::Remove(path) | VfsEvent::Write(path) => {
|
VfsEvent::Create(path) | VfsEvent::Write(path) => {
|
||||||
let mut tree = self.tree.lock().unwrap();
|
self.apply_patches(self.vfs.canonicalize(&path).unwrap())
|
||||||
let mut applied_patches = Vec::new();
|
}
|
||||||
|
VfsEvent::Remove(path) => {
|
||||||
// Find the nearest ancestor to this path that has
|
// MemoFS does not track parent removals yet, so we can canonicalize
|
||||||
// associated instances in the tree. This helps make sure
|
// the parent path safely and then append the removed path's file name.
|
||||||
// that we handle additions correctly, especially if we
|
let parent = path.parent().unwrap();
|
||||||
// receive events for descendants of a large tree being
|
let file_name = path.file_name().unwrap();
|
||||||
// created all at once.
|
let parent_normalized = self.vfs.canonicalize(parent).unwrap();
|
||||||
let mut current_path = path.as_path();
|
self.apply_patches(parent_normalized.join(file_name))
|
||||||
let affected_ids = loop {
|
|
||||||
let ids = tree.get_ids_at_path(current_path);
|
|
||||||
|
|
||||||
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
|
||||||
|
|
||||||
if !ids.is_empty() {
|
|
||||||
break ids.to_vec();
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("Trying parent path...");
|
|
||||||
match current_path.parent() {
|
|
||||||
Some(parent) => current_path = parent,
|
|
||||||
None => break Vec::new(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for id in affected_ids {
|
|
||||||
if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) {
|
|
||||||
if !patch.is_empty() {
|
|
||||||
applied_patches.push(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applied_patches
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("Unhandled VFS event: {:?}", event);
|
log::warn!("Unhandled VFS event: {:?}", event);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ pub fn snapshot_csv(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()]),
|
.relevant_paths(vec![vfs.canonicalize(path)?]),
|
||||||
);
|
);
|
||||||
|
|
||||||
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
||||||
|
|
|
||||||
|
|
@ -62,18 +62,19 @@ pub fn snapshot_dir_no_meta(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let normalized_path = vfs.canonicalize(path)?;
|
||||||
let relevant_paths = vec![
|
let relevant_paths = vec![
|
||||||
path.to_path_buf(),
|
normalized_path.clone(),
|
||||||
// TODO: We shouldn't need to know about Lua existing in this
|
// TODO: We shouldn't need to know about Lua existing in this
|
||||||
// middleware. Should we figure out a way for that function to add
|
// middleware. Should we figure out a way for that function to add
|
||||||
// relevant paths to this middleware?
|
// relevant paths to this middleware?
|
||||||
path.join("init.lua"),
|
normalized_path.join("init.lua"),
|
||||||
path.join("init.luau"),
|
normalized_path.join("init.luau"),
|
||||||
path.join("init.server.lua"),
|
normalized_path.join("init.server.lua"),
|
||||||
path.join("init.server.luau"),
|
normalized_path.join("init.server.luau"),
|
||||||
path.join("init.client.lua"),
|
normalized_path.join("init.client.lua"),
|
||||||
path.join("init.client.luau"),
|
normalized_path.join("init.client.luau"),
|
||||||
path.join("init.csv"),
|
normalized_path.join("init.csv"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot::new()
|
let snapshot = InstanceSnapshot::new()
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub fn snapshot_json(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ pub fn snapshot_json_model(
|
||||||
snapshot.metadata = snapshot
|
snapshot.metadata = snapshot
|
||||||
.metadata
|
.metadata
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context)
|
.context(context)
|
||||||
.specified_id(id)
|
.specified_id(id)
|
||||||
.schema(schema);
|
.schema(schema);
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ pub fn snapshot_lua(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ pub fn snapshot_rbxm(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub fn snapshot_rbxmx(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub fn snapshot_toml(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ pub fn snapshot_txt(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ pub fn snapshot_yaml(
|
||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![path.to_path_buf()])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ use rbx_dom_weak::{
|
||||||
ustr, Instance, Ustr, WeakDom,
|
ustr, Instance, Ustr, WeakDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{multimap::MultiMap, REF_ID_ATTRIBUTE_NAME, REF_POINTER_ATTRIBUTE_PREFIX};
|
use crate::{
|
||||||
|
multimap::MultiMap, syncback::snapshot::inst_path, REF_ID_ATTRIBUTE_NAME,
|
||||||
|
REF_POINTER_ATTRIBUTE_PREFIX,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct RefLinks {
|
pub struct RefLinks {
|
||||||
/// A map of referents to each of their Ref properties.
|
/// A map of referents to each of their Ref properties.
|
||||||
|
|
@ -50,6 +53,22 @@ pub fn collect_referents(dom: &WeakDom) -> RefLinks {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let target = match dom.get_by_ref(*prop_value) {
|
||||||
|
Some(inst) => inst,
|
||||||
|
None => {
|
||||||
|
// Properties that are Some but point to nothing may as
|
||||||
|
// well be `nil`. Roblox and us never produce these values
|
||||||
|
// but syncback prunes trees without adjusting ref
|
||||||
|
// properties for performance reasons.
|
||||||
|
log::warn!(
|
||||||
|
"Property {}.{} will be `nil` on the disk because the actual value is not being included in syncback",
|
||||||
|
inst_path(dom, inst_ref),
|
||||||
|
prop_name
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
links.insert(
|
links.insert(
|
||||||
inst_ref,
|
inst_ref,
|
||||||
RefLink {
|
RefLink {
|
||||||
|
|
@ -58,10 +77,6 @@ pub fn collect_referents(dom: &WeakDom) -> RefLinks {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let target = dom
|
|
||||||
.get_by_ref(*prop_value)
|
|
||||||
.expect("Refs in DOM should point to valid Instances");
|
|
||||||
|
|
||||||
// 1. Check if target has an ID
|
// 1. Check if target has an ID
|
||||||
if let Some(id) = get_existing_id(target) {
|
if let Some(id) = get_existing_id(target) {
|
||||||
// If it does, we need to check whether that ID is a duplicate
|
// If it does, we need to check whether that ID is a duplicate
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
|
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
|
||||||
//! JSON.
|
//! JSON.
|
||||||
|
|
||||||
use std::{
|
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc};
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fs,
|
|
||||||
path::PathBuf,
|
|
||||||
str::FromStr,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use futures::{sink::SinkExt, stream::StreamExt};
|
use futures::{sink::SinkExt, stream::StreamExt};
|
||||||
use hyper::{body, Body, Method, Request, Response, StatusCode};
|
use hyper::{body, Body, Method, Request, Response, StatusCode};
|
||||||
|
|
@ -30,7 +24,10 @@ use crate::{
|
||||||
},
|
},
|
||||||
util::{json, json_ok},
|
util::{json, json_ok},
|
||||||
},
|
},
|
||||||
web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse},
|
web_api::{
|
||||||
|
BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest,
|
||||||
|
SerializeResponse,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>) -> Response<Body> {
|
pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>) -> Response<Body> {
|
||||||
|
|
@ -53,12 +50,8 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(&Method::GET, path) if path.starts_with("/api/serialize/") => {
|
(&Method::POST, "/api/serialize") => service.handle_api_serialize(request).await,
|
||||||
service.handle_api_serialize(request).await
|
(&Method::POST, "/api/ref-patch") => service.handle_api_ref_patch(request).await,
|
||||||
}
|
|
||||||
(&Method::GET, path) if path.starts_with("/api/ref-patch/") => {
|
|
||||||
service.handle_api_ref_patch(request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
(&Method::POST, path) if path.starts_with("/api/open/") => {
|
(&Method::POST, path) if path.starts_with("/api/open/") => {
|
||||||
service.handle_api_open(request).await
|
service.handle_api_open(request).await
|
||||||
|
|
@ -229,22 +222,30 @@ impl ApiService {
|
||||||
/// that correspond to the requested Instances. These values have their
|
/// that correspond to the requested Instances. These values have their
|
||||||
/// `Value` property set to point to the requested Instance.
|
/// `Value` property set to point to the requested Instance.
|
||||||
async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
|
async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
|
||||||
let argument = &request.uri().path()["/api/serialize/".len()..];
|
let session_id = self.serve_session.session_id();
|
||||||
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
|
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||||
|
|
||||||
let requested_ids = match requested_ids {
|
let request: SerializeRequest = match json::from_slice(&body) {
|
||||||
Ok(ids) => ids,
|
Ok(request) => request,
|
||||||
Err(_) => {
|
Err(err) => {
|
||||||
return json(
|
return json(
|
||||||
ErrorResponse::bad_request("Malformed ID list"),
|
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if request.session_id != session_id {
|
||||||
|
return json(
|
||||||
|
ErrorResponse::bad_request("Wrong session ID"),
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder"));
|
let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder"));
|
||||||
|
|
||||||
let tree = self.serve_session.tree();
|
let tree = self.serve_session.tree();
|
||||||
for id in &requested_ids {
|
for id in &request.ids {
|
||||||
if let Some(instance) = tree.get_instance(*id) {
|
if let Some(instance) = tree.get_instance(*id) {
|
||||||
let clone = response_dom.insert(
|
let clone = response_dom.insert(
|
||||||
Ref::none(),
|
Ref::none(),
|
||||||
|
|
@ -290,20 +291,26 @@ impl ApiService {
|
||||||
/// and referent properties need to be updated after the serialize
|
/// and referent properties need to be updated after the serialize
|
||||||
/// endpoint is used.
|
/// endpoint is used.
|
||||||
async fn handle_api_ref_patch(self, request: Request<Body>) -> Response<Body> {
|
async fn handle_api_ref_patch(self, request: Request<Body>) -> Response<Body> {
|
||||||
let argument = &request.uri().path()["/api/ref-patch/".len()..];
|
let session_id = self.serve_session.session_id();
|
||||||
let requested_ids: Result<HashSet<Ref>, _> =
|
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||||
argument.split(',').map(Ref::from_str).collect();
|
|
||||||
|
|
||||||
let requested_ids = match requested_ids {
|
let request: RefPatchRequest = match json::from_slice(&body) {
|
||||||
Ok(ids) => ids,
|
Ok(request) => request,
|
||||||
Err(_) => {
|
Err(err) => {
|
||||||
return json(
|
return json(
|
||||||
ErrorResponse::bad_request("Malformed ID list"),
|
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if request.session_id != session_id {
|
||||||
|
return json(
|
||||||
|
ErrorResponse::bad_request("Wrong session ID"),
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
|
let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
|
||||||
|
|
||||||
let tree = self.serve_session.tree();
|
let tree = self.serve_session.tree();
|
||||||
|
|
@ -312,7 +319,7 @@ impl ApiService {
|
||||||
let Variant::Ref(prop_value) = prop_value else {
|
let Variant::Ref(prop_value) = prop_value else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(target_id) = requested_ids.get(prop_value) {
|
if let Some(target_id) = request.ids.get(prop_value) {
|
||||||
let instance_id = instance.id();
|
let instance_id = instance.id();
|
||||||
let update =
|
let update =
|
||||||
instance_updates
|
instance_updates
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,13 @@ pub struct OpenResponse {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SerializeRequest {
|
||||||
|
pub session_id: SessionId,
|
||||||
|
pub ids: Vec<Ref>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SerializeResponse {
|
pub struct SerializeResponse {
|
||||||
|
|
@ -269,6 +276,13 @@ impl BufferEncode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RefPatchRequest {
|
||||||
|
pub session_id: SessionId,
|
||||||
|
pub ids: HashSet<Ref>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RefPatchResponse<'a> {
|
pub struct RefPatchResponse<'a> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write as _,
|
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
|
|
@ -13,8 +12,12 @@ use rbx_dom_weak::types::Ref;
|
||||||
|
|
||||||
use tempfile::{tempdir, TempDir};
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
use librojo::web_api::{
|
use librojo::{
|
||||||
ReadResponse, SerializeResponse, ServerInfoResponse, SocketPacket, SocketPacketType,
|
web_api::{
|
||||||
|
ReadResponse, SerializeRequest, SerializeResponse, ServerInfoResponse, SocketPacket,
|
||||||
|
SocketPacketType,
|
||||||
|
},
|
||||||
|
SessionId,
|
||||||
};
|
};
|
||||||
use rojo_insta_ext::RedactionMap;
|
use rojo_insta_ext::RedactionMap;
|
||||||
|
|
||||||
|
|
@ -226,16 +229,19 @@ impl TestServeSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_api_serialize(&self, ids: &[Ref]) -> Result<SerializeResponse, reqwest::Error> {
|
pub fn get_api_serialize(
|
||||||
let mut id_list = String::with_capacity(ids.len() * 33);
|
&self,
|
||||||
for id in ids {
|
ids: &[Ref],
|
||||||
write!(id_list, "{id},").unwrap();
|
session_id: SessionId,
|
||||||
}
|
) -> Result<SerializeResponse, reqwest::Error> {
|
||||||
id_list.pop();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let url = format!("http://localhost:{}/api/serialize", self.port);
|
||||||
|
let body = serde_json::to_string(&SerializeRequest {
|
||||||
|
session_id,
|
||||||
|
ids: ids.to_vec(),
|
||||||
|
});
|
||||||
|
|
||||||
let url = format!("http://localhost:{}/api/serialize/{}", self.port, id_list);
|
client.post(url).body((body).unwrap()).send()?.json()
|
||||||
|
|
||||||
reqwest::blocking::get(url)?.json()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -646,7 +646,7 @@ fn meshpart_with_id() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let serialize_response = session
|
let serialize_response = session
|
||||||
.get_api_serialize(&[*meshpart, *objectvalue])
|
.get_api_serialize(&[*meshpart, *objectvalue], info.session_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// We don't assert a snapshot on the SerializeResponse because the model includes the
|
// We don't assert a snapshot on the SerializeResponse because the model includes the
|
||||||
|
|
@ -673,7 +673,9 @@ fn forced_parent() {
|
||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
read_response.intern_and_redact(&mut redactions, root_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
let serialize_response = session.get_api_serialize(&[root_id]).unwrap();
|
let serialize_response = session
|
||||||
|
.get_api_serialize(&[root_id], info.session_id)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(serialize_response.session_id, info.session_id);
|
assert_eq!(serialize_response.session_id, info.session_id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,13 @@ syncback_tests! {
|
||||||
ref_properties_conflict => ["src/Pointer_2.model.json", "src/Target_2.model.json"],
|
ref_properties_conflict => ["src/Pointer_2.model.json", "src/Target_2.model.json"],
|
||||||
// Ensures that having multiple pointers that are aimed at the same target doesn't trigger ref rewrites.
|
// Ensures that having multiple pointers that are aimed at the same target doesn't trigger ref rewrites.
|
||||||
ref_properties_duplicate => [],
|
ref_properties_duplicate => [],
|
||||||
|
// Ensures that ref properties that point to nothing after the prune both
|
||||||
|
// do not leave any trace of themselves
|
||||||
|
ref_properties_pruned => ["src/Pointer1.model.json", "src/Pointer2.model.json", "src/Pointer3.model.json"],
|
||||||
// Ensures that the old middleware is respected during syncback
|
// Ensures that the old middleware is respected during syncback
|
||||||
respect_old_middleware => ["default.project.json", "src/model_json.model.json", "src/rbxm.rbxm", "src/rbxmx.rbxmx"],
|
respect_old_middleware => ["default.project.json", "src/model_json.model.json", "src/rbxm.rbxm", "src/rbxmx.rbxmx"],
|
||||||
|
// Ensures that the `$schema` field roundtrips with syncback
|
||||||
|
schema_roundtrip => ["default.project.json", "src/model.model.json", "src/init/init.meta.json", "src/adjacent.meta.json"],
|
||||||
// Ensures that StringValues inside project files are written to the
|
// Ensures that StringValues inside project files are written to the
|
||||||
// project file, but only if they don't have `$path` set
|
// project file, but only if they don't have `$path` set
|
||||||
string_value_project => ["default.project.json"],
|
string_value_project => ["default.project.json"],
|
||||||
|
|
@ -81,5 +86,4 @@ syncback_tests! {
|
||||||
sync_rules => ["src/module.modulescript", "src/text.text"],
|
sync_rules => ["src/module.modulescript", "src/text.text"],
|
||||||
// Ensures that the `syncUnscriptable` setting works
|
// Ensures that the `syncUnscriptable` setting works
|
||||||
unscriptable_properties => ["default.project.json"],
|
unscriptable_properties => ["default.project.json"],
|
||||||
schema_roundtrip => ["default.project.json", "src/model.model.json", "src/init/init.meta.json", "src/adjacent.meta.json"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue