From 03410ced6d7244e8284fb17cdc7ae4ce5e701a1e Mon Sep 17 00:00:00 2001 From: quaywinn <62822174+gaymeowing@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:07:19 -0500 Subject: [PATCH 01/11] Use buffer for ClassIcon EditableImages (#1149) --- plugin/src/App/Components/ClassIcon.lua | 70 +++++++++++++++------ plugin/src/App/Components/EditableImage.lua | 5 +- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/plugin/src/App/Components/ClassIcon.lua b/plugin/src/App/Components/ClassIcon.lua index c3e46e0d..43f1ed2b 100644 --- a/plugin/src/App/Components/ClassIcon.lua +++ b/plugin/src/App/Components/ClassIcon.lua @@ -1,6 +1,11 @@ local StudioService = game:GetService("StudioService") local AssetService = game:GetService("AssetService") +type CachedImageInfo = { + pixels: buffer, + size: Vector2, +} + local Rojo = script:FindFirstAncestor("Rojo") local Plugin = Rojo.Plugin local Packages = Rojo.Packages @@ -11,44 +16,71 @@ local e = Roact.createElement local EditableImage = require(Plugin.App.Components.EditableImage) -local imageCache = {} -local function getImageSizeAndPixels(image) - if not imageCache[image] then - local editableImage = AssetService:CreateEditableImageAsync(image) +local imageCache: { [string]: CachedImageInfo } = {} + +local function cloneBuffer(b: buffer): buffer + local newBuffer = buffer.create(buffer.len(b)) + buffer.copy(newBuffer, 0, b) + return newBuffer +end + +local function getImageSizeAndPixels(image: string): (Vector2, buffer) + local cachedImage = imageCache[image] + + if not cachedImage then + local editableImage = AssetService:CreateEditableImageAsync(Content.fromUri(image)) + local size = editableImage.Size + local pixels = editableImage:ReadPixelsBuffer(Vector2.zero, size) imageCache[image] = { - Size = editableImage.Size, - Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size), + pixels = pixels, + size = size, } + + return size, cloneBuffer(pixels) end - return imageCache[image].Size, table.clone(imageCache[image].Pixels) + return cachedImage.size, cloneBuffer(cachedImage.pixels) end local function getRecoloredClassIcon(className, color) local iconProps = StudioService:GetClassIcon(className) if iconProps and color then - local success, editableImageSize, editableImagePixels = pcall(function() - local size, pixels = getImageSizeAndPixels(iconProps.Image) + --stylua: ignore + local success, editableImageSize, editableImagePixels = pcall(function(_iconProps: { [any]: any }, _color: Color3): (Vector2, buffer) + local size, pixels = getImageSizeAndPixels(_iconProps.Image) + local pixelsLen = buffer.len(pixels) local minVal, maxVal = math.huge, -math.huge - for i = 1, #pixels, 4 do - if pixels[i + 3] == 0 then + + for i = 0, pixelsLen, 4 do + if buffer.readu8(pixels, i + 3) == 0 then continue end - local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2]) + local pixelVal = math.max( + buffer.readu8(pixels, i), + buffer.readu8(pixels, i + 1), + buffer.readu8(pixels, i + 2) + ) minVal = math.min(minVal, pixelVal) maxVal = math.max(maxVal, pixelVal) end - local hue, sat, val = color:ToHSV() - for i = 1, #pixels, 4 do - if pixels[i + 3] == 0 then + local hue, sat, val = _color:ToHSV() + + for i = 0, pixelsLen, 4 do + if buffer.readu8(pixels, i + 3) == 0 then continue end + local gIndex = i + 1 + local bIndex = i + 2 - local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2]) + local pixelVal = math.max( + buffer.readu8(pixels, i), + buffer.readu8(pixels, gIndex), + buffer.readu8(pixels, bIndex) + ) local newVal = val if minVal < maxVal then -- Remap minVal - maxVal to val*0.9 - val @@ -56,10 +88,12 @@ local function getRecoloredClassIcon(className, color) end local newPixelColor = Color3.fromHSV(hue, sat, newVal) - pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B + buffer.writeu8(pixels, i, newPixelColor.R) + buffer.writeu8(pixels, gIndex, newPixelColor.G) + buffer.writeu8(pixels, bIndex, newPixelColor.B) end return size, pixels - end) + end, iconProps, color) if success then iconProps.EditableImagePixels = editableImagePixels iconProps.EditableImageSize = editableImageSize diff --git a/plugin/src/App/Components/EditableImage.lua b/plugin/src/App/Components/EditableImage.lua index 501a37ee..cf7dcd7d 100644 --- a/plugin/src/App/Components/EditableImage.lua +++ b/plugin/src/App/Components/EditableImage.lua @@ -12,7 +12,8 @@ function EditableImage:init() end function EditableImage:writePixels() - local image = self.ref.current + local image = self.ref.current :: EditableImage + if not image then return end @@ -20,7 +21,7 @@ function EditableImage:writePixels() return end - image:WritePixels(Vector2.zero, self.props.size, self.props.pixels) + image:WritePixelsBuffer(Vector2.zero, self.props.size, self.props.pixels) end function EditableImage:render() From ea70d8929138b443b071ad81a425da116cbc0654 Mon Sep 17 00:00:00 2001 From: Micah Date: Tue, 18 Nov 2025 18:47:43 -0800 Subject: [PATCH 02/11] Support .jsonc extension for all JSON files (#1159) --- CHANGELOG.md | 4 + src/project.rs | 72 ++++++--- src/serve_session.rs | 11 +- src/snapshot_middleware/csv.rs | 92 +++++++++-- src/snapshot_middleware/dir.rs | 22 +-- src/snapshot_middleware/json.rs | 49 +++++- src/snapshot_middleware/lua.rs | 57 ++++--- src/snapshot_middleware/meta_file.rs | 150 +++++++++++++++++- src/snapshot_middleware/mod.rs | 13 +- ...t_middleware__csv__test__csv_from_vfs.snap | 2 +- ...pshot_middleware__csv__test__csv_init.snap | 29 ++++ ...leware__csv__test__csv_init_with_meta.snap | 29 ++++ ..._middleware__csv__test__csv_with_meta.snap | 2 +- ...t_middleware__dir__test__empty_folder.snap | 4 +- ...ddleware__dir__test__folder_in_folder.snap | 7 +- ...leware__json__test__instance_from_vfs.snap | 2 +- ...middleware__json__test__with_metadata.snap | 22 +++ ...are__lua__test__class_client_from_vfs.snap | 2 +- ...are__lua__test__class_module_from_vfs.snap | 2 +- ...re__lua__test__class_module_with_meta.snap | 2 +- ...are__lua__test__class_script_disabled.snap | 2 +- ...re__lua__test__class_script_with_meta.snap | 2 +- ...are__lua__test__class_server_from_vfs.snap | 2 +- ...ware__lua__test__init_module_from_vfs.snap | 15 +- ..._test__init_module_from_vfs_with_meta.snap | 29 ++++ ...re__lua__test__plugin_module_from_vfs.snap | 1 + ...lua__test__runcontext_client_from_vfs.snap | 2 +- ...lua__test__runcontext_module_from_vfs.snap | 2 +- ...ua__test__runcontext_module_with_meta.snap | 2 +- ...lua__test__runcontext_script_disabled.snap | 2 +- ...ua__test__runcontext_script_with_meta.snap | 2 +- ...lua__test__runcontext_server_from_vfs.snap | 2 +- ...__meta_file__test__adjacent_read_json.snap | 17 ++ ..._meta_file__test__adjacent_read_jsonc.snap | 17 ++ ..._meta_file__test__directory_read_json.snap | 17 ++ ...meta_file__test__directory_read_jsonc.snap | 17 ++ ...oject__test__project_with_path_to_txt.snap | 2 +- ...leware__toml__test__instance_from_vfs.snap | 2 +- ...middleware__toml__test__with_metadata.snap | 22 +++ ...dleware__txt__test__instance_from_vfs.snap | 2 +- ..._middleware__txt__test__with_metadata.snap | 22 +++ ...leware__yaml__test__instance_from_vfs.snap | 1 + ...middleware__yaml__test__with_metadata.snap | 22 +++ src/snapshot_middleware/toml.rs | 49 +++++- src/snapshot_middleware/txt.rs | 39 ++++- src/snapshot_middleware/yaml.rs | 47 +++++- 46 files changed, 759 insertions(+), 153 deletions(-) create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 97dd23bf..0cc2991b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159]) + +[#1159]: https://github.com/rojo-rbx/rojo/pull/1159 + ## [7.6.1] (November 6th, 2025) * Fixed a bug where the last sync timestamp was not updating correctly in the plugin ([#1132]) diff --git a/src/project.rs b/src/project.rs index 83c1b1bb..a3bb00df 100644 --- a/src/project.rs +++ b/src/project.rs @@ -13,7 +13,8 @@ use thiserror::Error; use crate::{glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule}; -static PROJECT_FILENAME: &str = "default.project.json"; +/// Represents 'default' project names that act as `init` files +pub static DEFAULT_PROJECT_NAMES: [&str; 2] = ["default.project.json", "default.project.jsonc"]; /// Error type returned by any function that handles projects. #[derive(Debug, Error)] @@ -131,7 +132,7 @@ impl Project { pub fn is_project_file(path: &Path) -> bool { path.file_name() .and_then(|name| name.to_str()) - .map(|name| name.ends_with(".project.json")) + .map(|name| name.ends_with(".project.json") || name.ends_with(".project.jsonc")) .unwrap_or(false) } @@ -149,18 +150,19 @@ impl Project { None } } else { - let child_path = path.join(PROJECT_FILENAME); - let child_meta = fs::metadata(&child_path).ok()?; + for filename in DEFAULT_PROJECT_NAMES { + let child_path = path.join(filename); + let child_meta = fs::metadata(&child_path).ok()?; - if child_meta.is_file() { - Some(child_path) - } else { - // This is a folder with the same name as a Rojo default project - // file. - // - // That's pretty weird, but we can roll with it. - None + if child_meta.is_file() { + return Some(child_path); + } } + // This is a folder with the same name as a Rojo default project + // file. + // + // That's pretty weird, but we can roll with it. + None } } @@ -181,16 +183,20 @@ impl Project { // If you're editing this to be generic, make sure you also alter the // snapshot middleware to support generic init paths. - if file_name == PROJECT_FILENAME { - let folder_name = self.folder_location().file_name().and_then(OsStr::to_str); - if let Some(folder_name) = folder_name { - self.name = Some(folder_name.to_string()); - } else { - return Err(Error::FolderNameInvalid { - path: self.file_location.clone(), - }); + for default_file_name in DEFAULT_PROJECT_NAMES { + if file_name == default_file_name { + let folder_name = self.folder_location().file_name().and_then(OsStr::to_str); + if let Some(folder_name) = folder_name { + self.name = Some(folder_name.to_string()); + return Ok(()); + } else { + return Err(Error::FolderNameInvalid { + path: self.file_location.clone(), + }); + } } - } else if let Some(fallback) = fallback { + } + if let Some(fallback) = fallback { self.name = Some(fallback.to_string()); } else { // As of the time of writing (July 10, 2024) there is no way for @@ -257,6 +263,10 @@ impl Project { project_file_location: &Path, fallback_name: Option<&str>, ) -> Result { + log::debug!( + "Loading project file from {}", + project_file_location.display() + ); let project_path = project_file_location.to_path_buf(); let contents = vfs.read(&project_path).map_err(|e| match e.kind() { io::ErrorKind::NotFound => Error::NoProjectFound { @@ -272,6 +282,24 @@ impl Project { )?) } + pub(crate) fn load_initial_project(vfs: &Vfs, path: &Path) -> Result { + if Self::is_project_file(path) { + Self::load_exact(vfs, path, None) + } else { + // Check for default projects. + for default_project_name in DEFAULT_PROJECT_NAMES { + let project_path = path.join(default_project_name); + if project_path.exists() { + return Self::load_exact(vfs, &project_path, None); + } + } + Err(Error::NoProjectFound { + path: path.to_path_buf(), + } + .into()) + } + } + /// Checks if there are any compatibility issues with this project file and /// warns the user if there are any. fn check_compatibility(&self) { @@ -530,7 +558,7 @@ mod test { let project = Project::load_from_slice( project_json.as_bytes(), - PathBuf::from("/test/default.project.json"), + PathBuf::from("/test/default.project.jsonc"), None, ) .expect("Failed to parse project with JSONC features"); diff --git a/src/serve_session.rs b/src/serve_session.rs index 4f585018..cb3e6f5b 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, collections::HashSet, io, net::IpAddr, @@ -101,15 +100,7 @@ impl ServeSession { log::trace!("Starting new ServeSession at path {}", start_path.display()); - let project_path = if Project::is_project_file(start_path) { - Cow::Borrowed(start_path) - } else { - Cow::Owned(start_path.join("default.project.json")) - }; - - log::debug!("Loading project file from {}", project_path.display()); - - let root_project = Project::load_exact(&vfs, &project_path, None)?; + let root_project = Project::load_initial_project(&vfs, start_path)?; let mut tree = RojoTree::new(InstanceSnapshot::new()); diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs index 96c45e91..e81aa1c3 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -1,24 +1,23 @@ use std::{collections::BTreeMap, path::Path}; use anyhow::Context; -use memofs::{IoResultExt, Vfs}; +use memofs::Vfs; use rbx_dom_weak::ustr; use serde::Serialize; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; - -use super::{ - dir::{dir_meta, snapshot_dir_no_meta}, - meta_file::AdjacentMetadata, +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + snapshot_middleware::meta_file::DirectoryMetadata, }; +use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata}; + pub fn snapshot_csv( _context: &InstanceContext, vfs: &Vfs, path: &Path, name: &str, ) -> anyhow::Result> { - let meta_path = path.with_file_name(format!("{}.meta.json", name)); let contents = vfs.read(path)?; let table_contents = convert_localization_csv(&contents).with_context(|| { @@ -35,13 +34,10 @@ pub fn snapshot_csv( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]), + .relevant_paths(vec![path.to_path_buf()]), ); - 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)?; - } + AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; Ok(Some(snapshot)) } @@ -75,9 +71,7 @@ pub fn snapshot_csv_init( init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; - if let Some(mut meta) = dir_meta(vfs, folder_path)? { - meta.apply_all(&mut init_snapshot)?; - } + DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?; Ok(Some(init_snapshot)) } @@ -223,4 +217,72 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, insta::assert_yaml_snapshot!(instance_snapshot); } + + #[test] + fn csv_init() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/root", + VfsSnapshot::dir([( + "init.csv", + VfsSnapshot::file( + r#" +Key,Source,Context,Example,es +Ack,Ack!,,An exclamation of despair,¡Ay!"#, + ), + )]), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + + let instance_snapshot = snapshot_csv_init( + &InstanceContext::with_emit_legacy_scripts(Some(true)), + &vfs, + Path::new("/root/init.csv"), + ) + .unwrap() + .unwrap(); + + insta::with_settings!({ sort_maps => true }, { + insta::assert_yaml_snapshot!(instance_snapshot); + }); + } + + #[test] + fn csv_init_with_meta() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/root", + VfsSnapshot::dir([ + ( + "init.csv", + VfsSnapshot::file( + r#" +Key,Source,Context,Example,es +Ack,Ack!,,An exclamation of despair,¡Ay!"#, + ), + ), + ( + "init.meta.json", + VfsSnapshot::file(r#"{"id": "manually specified"}"#), + ), + ]), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + + let instance_snapshot = snapshot_csv_init( + &InstanceContext::with_emit_legacy_scripts(Some(true)), + &vfs, + Path::new("/root/init.csv"), + ) + .unwrap() + .unwrap(); + + insta::with_settings!({ sort_maps => true }, { + insta::assert_yaml_snapshot!(instance_snapshot); + }); + } } diff --git a/src/snapshot_middleware/dir.rs b/src/snapshot_middleware/dir.rs index 5cfab8d5..18959cd5 100644 --- a/src/snapshot_middleware/dir.rs +++ b/src/snapshot_middleware/dir.rs @@ -1,6 +1,6 @@ use std::path::Path; -use memofs::{DirEntry, IoResultExt, Vfs}; +use memofs::{DirEntry, Vfs}; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; @@ -16,26 +16,11 @@ pub fn snapshot_dir( None => return Ok(None), }; - if let Some(mut meta) = dir_meta(vfs, path)? { - meta.apply_all(&mut snapshot)?; - } + DirectoryMetadata::read_and_apply_all(vfs, path, &mut snapshot)?; Ok(Some(snapshot)) } -/// Retrieves the meta file that should be applied for this directory, if it -/// exists. -pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result> { - let meta_path = path.join("init.meta.json"); - - if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? { - let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?; - Ok(Some(metadata)) - } else { - Ok(None) - } -} - /// Snapshot a directory without applying meta files; useful for if the /// directory's ClassName will change before metadata should be applied. For /// example, this can happen if the directory contains an `init.client.lua` @@ -73,11 +58,8 @@ pub fn snapshot_dir_no_meta( .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", path.display()))? .to_string(); - let meta_path = path.join("init.meta.json"); - let relevant_paths = vec![ path.to_path_buf(), - meta_path, // TODO: We shouldn't need to know about Lua existing in this // middleware. Should we figure out a way for that function to add // relevant paths to this middleware? diff --git a/src/snapshot_middleware/json.rs b/src/snapshot_middleware/json.rs index ead6adf8..8cd029f5 100644 --- a/src/snapshot_middleware/json.rs +++ b/src/snapshot_middleware/json.rs @@ -1,6 +1,6 @@ use std::path::Path; -use memofs::{IoResultExt, Vfs}; +use memofs::Vfs; use rbx_dom_weak::ustr; use crate::{ @@ -25,8 +25,6 @@ pub fn snapshot_json( let as_lua = json_to_lua(value).to_string(); - let meta_path = path.with_file_name(format!("{}.meta.json", name)); - let mut snapshot = InstanceSnapshot::new() .name(name) .class_name("ModuleScript") @@ -34,14 +32,11 @@ pub fn snapshot_json( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]) + .relevant_paths(vec![path.to_path_buf()]) .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)?; - } + AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; Ok(Some(snapshot)) } @@ -111,4 +106,42 @@ mod test { insta::assert_yaml_snapshot!(instance_snapshot); } + + #[test] + fn with_metadata() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/foo.json", + VfsSnapshot::file( + r#"{ + "array": [1, 2, 3], + "int": 1234, + "float": 1234.5452, + }"#, + ), + ) + .unwrap(); + imfs.load_snapshot( + "/foo.meta.json", + VfsSnapshot::file( + r#"{ + "id": "manually specified" + }"#, + ), + ) + .unwrap(); + + let vfs = Vfs::new(imfs.clone()); + + let instance_snapshot = snapshot_json( + &InstanceContext::default(), + &vfs, + Path::new("/foo.json"), + "foo", + ) + .unwrap() + .unwrap(); + + insta::assert_yaml_snapshot!(instance_snapshot); + } } diff --git a/src/snapshot_middleware/lua.rs b/src/snapshot_middleware/lua.rs index c4174cf0..32638d7f 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -1,14 +1,11 @@ use std::{path::Path, str}; -use memofs::{IoResultExt, Vfs}; +use memofs::Vfs; use rbx_dom_weak::{types::Enum, ustr, HashMapExt as _, UstrMap}; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; -use super::{ - dir::{dir_meta, snapshot_dir_no_meta}, - meta_file::AdjacentMetadata, -}; +use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata, meta_file::DirectoryMetadata}; #[derive(Debug)] pub enum ScriptType { @@ -73,8 +70,6 @@ pub fn snapshot_lua( ); } - let meta_path = path.with_file_name(format!("{}.meta.json", name)); - let mut snapshot = InstanceSnapshot::new() .name(name) .class_name(class_name) @@ -82,14 +77,11 @@ pub fn snapshot_lua( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]) + .relevant_paths(vec![path.to_path_buf()]) .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)?; - } + AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; Ok(Some(snapshot)) } @@ -126,9 +118,7 @@ pub fn snapshot_lua_init( init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; - if let Some(mut meta) = dir_meta(vfs, folder_path)? { - meta.apply_all(&mut init_snapshot)?; - } + DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?; Ok(Some(init_snapshot)) } @@ -300,7 +290,6 @@ mod test { }); } - #[ignore = "init.lua functionality has moved to the root snapshot function"] #[test] fn init_module_from_vfs() { let mut imfs = InMemoryFs::new(); @@ -312,11 +301,41 @@ mod test { let vfs = Vfs::new(imfs); - let instance_snapshot = snapshot_lua( + let instance_snapshot = snapshot_lua_init( &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, - Path::new("/root"), - "root", + Path::new("/root/init.lua"), + ScriptType::Module, + ) + .unwrap() + .unwrap(); + + insta::with_settings!({ sort_maps => true }, { + insta::assert_yaml_snapshot!(instance_snapshot); + }); + } + + #[test] + fn init_module_from_vfs_with_meta() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/root", + VfsSnapshot::dir([ + ("init.lua", VfsSnapshot::file("Hello!")), + ( + "init.meta.json", + VfsSnapshot::file(r#"{"id": "manually specified"}"#), + ), + ]), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + + let instance_snapshot = snapshot_lua_init( + &InstanceContext::with_emit_legacy_scripts(Some(true)), + &vfs, + Path::new("/root/init.lua"), ScriptType::Module, ) .unwrap() diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs index f07e015c..59acc8d5 100644 --- a/src/snapshot_middleware/meta_file.rs +++ b/src/snapshot_middleware/meta_file.rs @@ -1,6 +1,10 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use anyhow::{format_err, Context}; +use memofs::{IoResultExt as _, Vfs}; use rbx_dom_weak::{types::Attributes, Ustr, UstrMap}; use serde::{Deserialize, Serialize}; @@ -33,7 +37,38 @@ pub struct AdjacentMetadata { } impl AdjacentMetadata { - pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result { + /// Attempts to read a meta file for the provided path and name, and if + /// one exists applies it. + /// + /// Also inserts the potential metadata paths into the snapshot's relevant + /// paths for convenience purposes. + pub fn read_and_apply_all( + vfs: &Vfs, + path: &Path, + name: &str, + snapshot: &mut InstanceSnapshot, + ) -> anyhow::Result<()> { + let meta_path_json = path.with_file_name(format!("{name}.meta.json")); + let meta_path_jsonc = path.with_file_name(format!("{name}.meta.jsonc")); + + if let Some(meta_contents) = vfs.read(&meta_path_json).with_not_found()? { + let mut metadata = Self::from_slice(&meta_contents, meta_path_json.clone())?; + metadata.apply_all(snapshot)?; + } + + if let Some(meta_contents) = vfs.read(&meta_path_jsonc).with_not_found()? { + let mut metadata = Self::from_slice(&meta_contents, meta_path_json.clone())?; + metadata.apply_all(snapshot)?; + } + + // Rather than pushing these in the snapshot middleware, we can just do it here. + snapshot.metadata.relevant_paths.push(meta_path_json); + snapshot.metadata.relevant_paths.push(meta_path_jsonc); + + Ok(()) + } + + fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result { let mut meta: Self = json::from_slice_with_context(slice, || { format!( "File contained malformed .meta.json data: {}", @@ -130,7 +165,37 @@ pub struct DirectoryMetadata { } impl DirectoryMetadata { - pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result { + /// Attempts to read an `init.meta`` file for the provided path, and if + /// one exists applies it. + /// + /// Also inserts the potential metadata paths into the snapshot's relevant + /// paths for convenience purposes. + pub fn read_and_apply_all( + vfs: &Vfs, + path: &Path, + snapshot: &mut InstanceSnapshot, + ) -> anyhow::Result<()> { + let meta_path_json = path.join("init.meta.json"); + let meta_path_jsonc = path.join("init.meta.jsonc"); + + if let Some(meta_contents) = vfs.read(&meta_path_json).with_not_found()? { + let mut metadata = Self::from_slice(&meta_contents, meta_path_json.clone())?; + metadata.apply_all(snapshot)?; + } + + if let Some(meta_contents) = vfs.read(&meta_path_jsonc).with_not_found()? { + let mut metadata = Self::from_slice(&meta_contents, meta_path_jsonc.clone())?; + metadata.apply_all(snapshot)?; + } + + // Rather than pushing these in the snapshot middleware, we can just do it here. + snapshot.metadata.relevant_paths.push(meta_path_json); + snapshot.metadata.relevant_paths.push(meta_path_jsonc); + + Ok(()) + } + + fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result { let mut meta: Self = json::from_slice_with_context(slice, || { format!( "File contained malformed init.meta.json data: {}", @@ -211,3 +276,82 @@ impl DirectoryMetadata { Ok(()) } } + +#[cfg(test)] +mod test { + use memofs::{InMemoryFs, VfsSnapshot}; + + use super::*; + + #[test] + fn adjacent_read_json() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/foo/bar.meta.json", + VfsSnapshot::file(r#"{"id": "manually specified"}"#), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + let path = Path::new("/foo/bar.rojo"); + let mut snapshot = InstanceSnapshot::new(); + + AdjacentMetadata::read_and_apply_all(&vfs, path, "bar", &mut snapshot).unwrap(); + + insta::assert_yaml_snapshot!(snapshot); + } + + #[test] + fn adjacent_read_jsonc() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/foo/bar.meta.jsonc", + VfsSnapshot::file(r#"{"id": "manually specified"}"#), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + let path = Path::new("/foo/bar.rojo"); + let mut snapshot = InstanceSnapshot::new(); + + AdjacentMetadata::read_and_apply_all(&vfs, path, "bar", &mut snapshot).unwrap(); + + insta::assert_yaml_snapshot!(snapshot); + } + + #[test] + fn directory_read_json() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/foo/init.meta.json", + VfsSnapshot::file(r#"{"id": "manually specified"}"#), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + let path = Path::new("/foo/"); + let mut snapshot = InstanceSnapshot::new(); + + DirectoryMetadata::read_and_apply_all(&vfs, path, &mut snapshot).unwrap(); + + insta::assert_yaml_snapshot!(snapshot); + } + + #[test] + fn directory_read_jsonc() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "/foo/init.meta.jsonc", + VfsSnapshot::file(r#"{"id": "manually specified"}"#), + ) + .unwrap(); + + let vfs = Vfs::new(imfs); + let path = Path::new("/foo/"); + let mut snapshot = InstanceSnapshot::new(); + + DirectoryMetadata::read_and_apply_all(&vfs, path, &mut snapshot).unwrap(); + + insta::assert_yaml_snapshot!(snapshot); + } +} diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs index 3d2fbde5..30c5d13b 100644 --- a/src/snapshot_middleware/mod.rs +++ b/src/snapshot_middleware/mod.rs @@ -28,8 +28,8 @@ use anyhow::Context; use memofs::{IoResultExt, Vfs}; use serde::{Deserialize, Serialize}; -use crate::glob::Glob; use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule}; +use crate::{glob::Glob, project::DEFAULT_PROJECT_NAMES}; use self::{ csv::{snapshot_csv, snapshot_csv_init}, @@ -122,9 +122,11 @@ pub fn snapshot_from_vfs( fn get_init_path>(vfs: &Vfs, dir: P) -> anyhow::Result> { let path = dir.as_ref(); - let project_path = path.join("default.project.json"); - if vfs.metadata(&project_path).with_not_found()?.is_some() { - return Ok(Some(project_path)); + for default_project_name in DEFAULT_PROJECT_NAMES { + let project_path = path.join(default_project_name); + if vfs.metadata(&project_path).with_not_found()?.is_some() { + return Ok(Some(project_path)); + } } let init_path = path.join("init.luau"); @@ -312,8 +314,11 @@ pub fn default_sync_rules() -> &'static [SyncRule] { sync_rule!("*.plugin.luau", PluginScript, ".plugin.luau"), sync_rule!("*.{lua,luau}", ModuleScript), sync_rule!("*.project.json", Project, ".project.json"), + sync_rule!("*.project.jsonc", Project, ".project.jsonc"), sync_rule!("*.model.json", JsonModel, ".model.json"), + sync_rule!("*.model.jsonc", JsonModel, ".model.jsonc"), sync_rule!("*.json", Json, ".json", "*.meta.json"), + sync_rule!("*.jsonc", Json, ".jsonc", "*.meta.jsonc"), sync_rule!("*.toml", Toml), sync_rule!("*.csv", Csv), sync_rule!("*.txt", Text), diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap index 9b92ec7d..577f0f56 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.csv - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Contents: String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]" children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap new file mode 100644 index 00000000..39abe8ed --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap @@ -0,0 +1,29 @@ +--- +source: src/snapshot_middleware/csv.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: /root + relevant_paths: + - /root + - /root/init.lua + - /root/init.luau + - /root/init.server.lua + - /root/init.server.luau + - /root/init.client.lua + - /root/init.client.luau + - /root/init.csv + - /root/init.meta.json + - /root/init.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: ~ +name: root +class_name: LocalizationTable +properties: + Contents: + String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]" +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap new file mode 100644 index 00000000..75bf31bc --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap @@ -0,0 +1,29 @@ +--- +source: src/snapshot_middleware/csv.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: /root + relevant_paths: + - /root + - /root/init.lua + - /root/init.luau + - /root/init.server.lua + - /root/init.server.luau + - /root/init.client.lua + - /root/init.client.luau + - /root/init.csv + - /root/init.meta.json + - /root/init.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: root +class_name: LocalizationTable +properties: + Contents: + String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]" +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap index 7c2d9f6c..ede33710 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.csv - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Contents: String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]" children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap index ab27bc67..6a182d33 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap @@ -9,7 +9,6 @@ metadata: Path: /foo relevant_paths: - /foo - - /foo/init.meta.json - /foo/init.lua - /foo/init.luau - /foo/init.server.lua @@ -17,6 +16,8 @@ metadata: - /foo/init.client.lua - /foo/init.client.luau - /foo/init.csv + - /foo/init.meta.json + - /foo/init.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -24,4 +25,3 @@ name: foo class_name: Folder properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap index 4a24bdb1..3999f376 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap @@ -9,7 +9,6 @@ metadata: Path: /foo relevant_paths: - /foo - - /foo/init.meta.json - /foo/init.lua - /foo/init.luau - /foo/init.server.lua @@ -17,6 +16,8 @@ metadata: - /foo/init.client.lua - /foo/init.client.luau - /foo/init.csv + - /foo/init.meta.json + - /foo/init.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -31,7 +32,6 @@ children: Path: /foo/Child relevant_paths: - /foo/Child - - /foo/Child/init.meta.json - /foo/Child/init.lua - /foo/Child/init.luau - /foo/Child/init.server.lua @@ -39,6 +39,8 @@ children: - /foo/Child/init.client.lua - /foo/Child/init.client.luau - /foo/Child/init.csv + - /foo/Child/init.meta.json + - /foo/Child/init.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -46,4 +48,3 @@ children: class_name: Folder properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap index b41503ad..69ae3c26 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.json - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: "return {\n\t[\"1invalidident\"] = \"nice\",\n\tarray = {1, 2, 3},\n\t[\"false\"] = false,\n\tfloat = 1234.5452,\n\tint = 1234,\n\tnull = nil,\n\tobject = {\n\t\thello = \"world\",\n\t},\n\t[\"true\"] = true,\n}" children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap new file mode 100644 index 00000000..580b0b38 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap @@ -0,0 +1,22 @@ +--- +source: src/snapshot_middleware/json.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: /foo.json + relevant_paths: + - /foo.json + - /foo.meta.json + - /foo.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: foo +class_name: ModuleScript +properties: + Source: + String: "return {\n\tarray = {1, 2, 3},\n\tfloat = 1234.5452,\n\tint = 1234,\n}" +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap index 92321572..49445676 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.client.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap index 53c7ee33..d0a134da 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap index 1b7fde09..ad129df9 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap index d0038e4d..4b870d05 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /bar.server.lua - /bar.meta.json + - /bar.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -23,4 +24,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap index a5f53240..0dfb278a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -21,4 +22,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap index 4ad99cf1..5007e665 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -21,4 +22,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap index 0077f4c0..8a24827b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap @@ -2,23 +2,28 @@ source: src/snapshot_middleware/lua.rs expression: instance_snapshot --- -snapshot_id: ~ +snapshot_id: "00000000000000000000000000000000" metadata: ignore_unknown_instances: false instigating_source: Path: /root relevant_paths: - /root - - /root/init.meta.json - /root/init.lua + - /root/init.luau - /root/init.server.lua + - /root/init.server.luau - /root/init.client.lua + - /root/init.client.luau + - /root/init.csv + - /root/init.meta.json + - /root/init.meta.jsonc context: - script_type: Class + emit_legacy_scripts: true + specified_id: ~ name: root class_name: ModuleScript properties: Source: - Type: String - Value: Hello! + String: Hello! children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap new file mode 100644 index 00000000..ea560ad1 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap @@ -0,0 +1,29 @@ +--- +source: src/snapshot_middleware/lua.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: /root + relevant_paths: + - /root + - /root/init.lua + - /root/init.luau + - /root/init.server.lua + - /root/init.server.luau + - /root/init.client.lua + - /root/init.client.luau + - /root/init.csv + - /root/init.meta.json + - /root/init.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: root +class_name: ModuleScript +properties: + Source: + String: Hello! +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap index 8f88f38e..5f21eb7f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.plugin.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap index e9704b1c..507e25d6 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.client.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ @@ -21,4 +22,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap index 6f5909c4..89ce9626 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap index ff8c51ec..b869da66 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap index c7404363..15523090 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /bar.server.lua - /bar.meta.json + - /bar.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ @@ -23,4 +24,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap index 512c3991..182e0fd3 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ @@ -21,4 +22,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap index 4295f6ba..dc22b34a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ @@ -21,4 +22,3 @@ properties: Source: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap new file mode 100644 index 00000000..d9577829 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap @@ -0,0 +1,17 @@ +--- +source: src/snapshot_middleware/meta_file.rs +expression: snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + relevant_paths: + - /foo/bar.meta.json + - /foo/bar.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: DEFAULT +class_name: DEFAULT +properties: {} +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap new file mode 100644 index 00000000..d9577829 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap @@ -0,0 +1,17 @@ +--- +source: src/snapshot_middleware/meta_file.rs +expression: snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + relevant_paths: + - /foo/bar.meta.json + - /foo/bar.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: DEFAULT +class_name: DEFAULT +properties: {} +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap new file mode 100644 index 00000000..4d2a9e55 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap @@ -0,0 +1,17 @@ +--- +source: src/snapshot_middleware/meta_file.rs +expression: snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + relevant_paths: + - /foo/init.meta.json + - /foo/init.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: DEFAULT +class_name: DEFAULT +properties: {} +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap new file mode 100644 index 00000000..4d2a9e55 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap @@ -0,0 +1,17 @@ +--- +source: src/snapshot_middleware/meta_file.rs +expression: snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + relevant_paths: + - /foo/init.meta.json + - /foo/init.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: DEFAULT +class_name: DEFAULT +properties: {} +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap index e723e6d2..75320b53 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo/other.txt - /foo/other.meta.json + - /foo/other.meta.jsonc - /foo/default.project.json context: emit_legacy_scripts: true @@ -20,4 +21,3 @@ properties: Value: String: "Hello, world!" children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap index 71a14153..e21e9414 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.toml - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Source: String: "return {\n\t[\"1invalidident\"] = \"nice\",\n\tarray = {1, 2, 3},\n\tdates = {\n\t\tlocaldate = \"1979-05-27\",\n\t\tlocaldatetime = \"1979-05-27T07:32:00\",\n\t\tlocaltime = \"00:32:00.999999\",\n\t\toffset1 = \"1979-05-27T00:32:00.999999-07:00\",\n\t\toffset2 = \"1979-05-27T07:32:00Z\",\n\t},\n\t[\"false\"] = false,\n\tfloat = 1234.5452,\n\tint = 1234,\n\tobject = {\n\t\thello = \"world\",\n\t},\n\t[\"true\"] = true,\n}" children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap new file mode 100644 index 00000000..4dd7cfc3 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap @@ -0,0 +1,22 @@ +--- +source: src/snapshot_middleware/toml.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: foo.toml + relevant_paths: + - foo.toml + - foo.meta.json + - foo.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: foo +class_name: ModuleScript +properties: + Source: + String: "return {\n\tarray = {1, 2, 3},\n\tbool = false,\n\tint = 123,\n}" +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap index 54253eb1..bd877a12 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.txt - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ @@ -19,4 +20,3 @@ properties: Value: String: Hello there! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap new file mode 100644 index 00000000..c99e8910 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap @@ -0,0 +1,22 @@ +--- +source: src/snapshot_middleware/txt.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: /foo.txt + relevant_paths: + - /foo.txt + - /foo.meta.json + - /foo.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: foo +class_name: StringValue +properties: + Value: + String: Hello there! +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap index 86e8a375..33d2e1a4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap @@ -10,6 +10,7 @@ metadata: relevant_paths: - /foo.yaml - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap new file mode 100644 index 00000000..4d04eac5 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap @@ -0,0 +1,22 @@ +--- +source: src/snapshot_middleware/yaml.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: foo.yaml + relevant_paths: + - foo.yaml + - foo.meta.json + - foo.meta.jsonc + context: + emit_legacy_scripts: true + specified_id: manually specified +name: foo +class_name: ModuleScript +properties: + Source: + String: "return {\n\tvalue = 1234,\n}" +children: [] diff --git a/src/snapshot_middleware/toml.rs b/src/snapshot_middleware/toml.rs index 9b44862e..7262cfa9 100644 --- a/src/snapshot_middleware/toml.rs +++ b/src/snapshot_middleware/toml.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::Context; -use memofs::{IoResultExt, Vfs}; +use memofs::Vfs; use rbx_dom_weak::ustr; use crate::{ @@ -24,8 +24,6 @@ pub fn snapshot_toml( let as_lua = toml_to_lua(value).to_string(); - let meta_path = path.with_file_name(format!("{}.meta.json", name)); - let mut snapshot = InstanceSnapshot::new() .name(name) .class_name("ModuleScript") @@ -33,14 +31,11 @@ pub fn snapshot_toml( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]) + .relevant_paths(vec![path.to_path_buf()]) .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)?; - } + AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; Ok(Some(snapshot)) } @@ -117,4 +112,42 @@ mod test { insta::assert_yaml_snapshot!(instance_snapshot); } + + #[test] + fn with_metadata() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "foo.toml", + VfsSnapshot::file( + r#" + array = [1, 2, 3] + int = 123 + bool = false + "#, + ), + ) + .unwrap(); + imfs.load_snapshot( + "foo.meta.json", + VfsSnapshot::file( + r#"{ + "id": "manually specified" + }"#, + ), + ) + .unwrap(); + + let vfs = Vfs::new(imfs.clone()); + + let instance_snapshot = snapshot_toml( + &InstanceContext::default(), + &vfs, + Path::new("foo.toml"), + "foo", + ) + .unwrap() + .unwrap(); + + insta::assert_yaml_snapshot!(instance_snapshot); + } } diff --git a/src/snapshot_middleware/txt.rs b/src/snapshot_middleware/txt.rs index 745280fb..761ba671 100644 --- a/src/snapshot_middleware/txt.rs +++ b/src/snapshot_middleware/txt.rs @@ -1,6 +1,6 @@ use std::{path::Path, str}; -use memofs::{IoResultExt, Vfs}; +use memofs::Vfs; use rbx_dom_weak::ustr; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; @@ -16,8 +16,6 @@ pub fn snapshot_txt( let contents = vfs.read_to_string(path)?; let contents_str = contents.as_str(); - let meta_path = path.with_file_name(format!("{}.meta.json", name)); - let mut snapshot = InstanceSnapshot::new() .name(name) .class_name("StringValue") @@ -25,14 +23,11 @@ pub fn snapshot_txt( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]) + .relevant_paths(vec![path.to_path_buf()]) .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)?; - } + AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; Ok(Some(snapshot)) } @@ -62,4 +57,32 @@ mod test { insta::assert_yaml_snapshot!(instance_snapshot); } + + #[test] + fn with_metadata() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot("/foo.txt", VfsSnapshot::file("Hello there!")) + .unwrap(); + imfs.load_snapshot( + "/foo.meta.json", + VfsSnapshot::file( + r#"{ + "id": "manually specified" + }"#, + ), + ) + .unwrap(); + let vfs = Vfs::new(imfs.clone()); + + let instance_snapshot = snapshot_txt( + &InstanceContext::default(), + &vfs, + Path::new("/foo.txt"), + "foo", + ) + .unwrap() + .unwrap(); + + insta::assert_yaml_snapshot!(instance_snapshot); + } } diff --git a/src/snapshot_middleware/yaml.rs b/src/snapshot_middleware/yaml.rs index 346e4386..c8132d49 100644 --- a/src/snapshot_middleware/yaml.rs +++ b/src/snapshot_middleware/yaml.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::Context as _; -use memofs::{IoResultExt, Vfs}; +use memofs::Vfs; use rbx_dom_weak::ustr; use yaml_rust2::{Yaml, YamlLoader}; @@ -30,8 +30,6 @@ pub fn snapshot_yaml( let as_lua = Statement::Return(yaml_to_luau(value)?); - let meta_path = path.with_file_name(format!("{}.meta.json", name)); - let mut snapshot = InstanceSnapshot::new() .name(name) .class_name("ModuleScript") @@ -39,14 +37,11 @@ pub fn snapshot_yaml( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]) + .relevant_paths(vec![path.to_path_buf()]) .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)?; - } + AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; Ok(Some(snapshot)) } @@ -231,4 +226,40 @@ value: 9007199254740993 .unwrap() .unwrap(); } + + #[test] + fn with_metadata() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot( + "foo.yaml", + VfsSnapshot::file( + r#" + value: 1234 + "#, + ), + ) + .unwrap(); + imfs.load_snapshot( + "foo.meta.json", + VfsSnapshot::file( + r#"{ + "id": "manually specified", + }"#, + ), + ) + .unwrap(); + + let vfs = Vfs::new(imfs.clone()); + + let instance_snapshot = snapshot_yaml( + &InstanceContext::default(), + &vfs, + Path::new("foo.yaml"), + "foo", + ) + .unwrap() + .unwrap(); + + insta::assert_yaml_snapshot!(instance_snapshot); + } } From 31ec216a95ccae1080ca1211e0370a72f3748e81 Mon Sep 17 00:00:00 2001 From: quaywinn <62822174+gaymeowing@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:49:52 -0500 Subject: [PATCH 03/11] Remove pairs() and ipairs() (#1150) --- plugin/fmt/init.lua | 4 ++-- plugin/src/App/bindingUtil.lua | 2 +- plugin/src/Assets.lua | 2 +- plugin/src/ChangeBatcher/createPatchSet.lua | 2 +- plugin/src/ChangeBatcher/encodePatchUpdate.lua | 2 +- plugin/src/Dictionary.lua | 2 +- plugin/src/InstanceMap.lua | 6 +++--- plugin/src/Reconciler/diff.lua | 4 ++-- plugin/src/Reconciler/diff.spec.lua | 2 +- plugin/src/Reconciler/hydrate.lua | 2 +- plugin/src/Reconciler/reify.lua | 2 +- plugin/src/Reconciler/reify.spec.lua | 2 +- plugin/src/preloadAssets.lua | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/plugin/fmt/init.lua b/plugin/fmt/init.lua index 7ef24d8e..403538f4 100644 --- a/plugin/fmt/init.lua +++ b/plugin/fmt/init.lua @@ -25,7 +25,7 @@ local function defaultTableDebug(buffer, input) buffer:writeRaw("{") - for key, value in pairs(input) do + for key, value in input do buffer:write("[{:?}] = {:?}", key, value) if next(input, key) ~= nil then @@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input) buffer:writeLineRaw("{") buffer:indent() - for key, value in pairs(input) do + for key, value in input do buffer:writeLine("[{:?}] = {:#?},", key, value) end diff --git a/plugin/src/App/bindingUtil.lua b/plugin/src/App/bindingUtil.lua index 3797871e..60d3876f 100644 --- a/plugin/src/App/bindingUtil.lua +++ b/plugin/src/App/bindingUtil.lua @@ -44,7 +44,7 @@ end local function blendAlpha(alphaValues) local alpha = 0 - for _, value in pairs(alphaValues) do + for _, value in alphaValues do alpha = alpha + (1 - alpha) * value end diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index e48c1217..c2c168ee 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -74,7 +74,7 @@ local Assets = { local function guardForTypos(name, map) strict(name, map) - for key, child in pairs(map) do + for key, child in map do if type(child) == "table" then guardForTypos(("%s.%s"):format(name, key), child) end diff --git a/plugin/src/ChangeBatcher/createPatchSet.lua b/plugin/src/ChangeBatcher/createPatchSet.lua index d418bb4d..5502d0d6 100644 --- a/plugin/src/ChangeBatcher/createPatchSet.lua +++ b/plugin/src/ChangeBatcher/createPatchSet.lua @@ -15,7 +15,7 @@ local encodePatchUpdate = require(script.Parent.encodePatchUpdate) return function(instanceMap, propertyChanges) local patch = PatchSet.newEmpty() - for instance, properties in pairs(propertyChanges) do + for instance, properties in propertyChanges do local instanceId = instanceMap.fromInstances[instance] if instanceId == nil then diff --git a/plugin/src/ChangeBatcher/encodePatchUpdate.lua b/plugin/src/ChangeBatcher/encodePatchUpdate.lua index 36888a88..5076fea2 100644 --- a/plugin/src/ChangeBatcher/encodePatchUpdate.lua +++ b/plugin/src/ChangeBatcher/encodePatchUpdate.lua @@ -10,7 +10,7 @@ return function(instance, instanceId, properties) changedProperties = {}, } - for propertyName in pairs(properties) do + for propertyName in properties do if propertyName == "Name" then update.changedName = instance.Name else diff --git a/plugin/src/Dictionary.lua b/plugin/src/Dictionary.lua index 93d4ad91..09b878a6 100644 --- a/plugin/src/Dictionary.lua +++ b/plugin/src/Dictionary.lua @@ -14,7 +14,7 @@ local function merge(...) local source = select(i, ...) if source ~= nil then - for key, value in pairs(source) do + for key, value in source do if value == None then output[key] = nil else diff --git a/plugin/src/InstanceMap.lua b/plugin/src/InstanceMap.lua index 76754fef..250f41e6 100644 --- a/plugin/src/InstanceMap.lua +++ b/plugin/src/InstanceMap.lua @@ -63,7 +63,7 @@ function InstanceMap:__fmtDebug(output) -- Collect all of the entries in the InstanceMap and sort them by their -- label, which helps make our output deterministic. local entries = {} - for id, instance in pairs(self.fromIds) do + for id, instance in self.fromIds do local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName) table.insert(entries, { id, label }) @@ -73,7 +73,7 @@ function InstanceMap:__fmtDebug(output) return a[2] < b[2] end) - for _, entry in ipairs(entries) do + for _, entry in entries do output:writeLine("{}: {}", entry[1], entry[2]) end @@ -227,7 +227,7 @@ function InstanceMap:__disconnectSignals(instance) -- around the extra table. ValueBase objects force us to use multiple -- signals to emulate the Instance.Changed event, however. if typeof(signals) == "table" then - for _, signal in ipairs(signals) do + for _, signal in signals do signal:Disconnect() end else diff --git a/plugin/src/Reconciler/diff.lua b/plugin/src/Reconciler/diff.lua index 4a1d9361..fb4c79d0 100644 --- a/plugin/src/Reconciler/diff.lua +++ b/plugin/src/Reconciler/diff.lua @@ -38,13 +38,13 @@ local function trueEquals(a, b): boolean -- For tables, try recursive deep equality if typeA == "table" and typeB == "table" then local checkedKeys = {} - for key, value in pairs(a) do + for key, value in a do checkedKeys[key] = true if not trueEquals(value, b[key]) then return false end end - for key, value in pairs(b) do + for key, value in b do if checkedKeys[key] then continue end diff --git a/plugin/src/Reconciler/diff.spec.lua b/plugin/src/Reconciler/diff.spec.lua index 451540b3..69279ef5 100644 --- a/plugin/src/Reconciler/diff.spec.lua +++ b/plugin/src/Reconciler/diff.spec.lua @@ -14,7 +14,7 @@ return function() local function size(dict) local len = 0 - for _ in pairs(dict) do + for _ in dict do len = len + 1 end diff --git a/plugin/src/Reconciler/hydrate.lua b/plugin/src/Reconciler/hydrate.lua index 353e7366..d7798160 100644 --- a/plugin/src/Reconciler/hydrate.lua +++ b/plugin/src/Reconciler/hydrate.lua @@ -26,7 +26,7 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance) for _, childId in ipairs(virtualInstance.Children) do local virtualChild = virtualInstances[childId] - for childIndex, childInstance in ipairs(existingChildren) do + for childIndex, childInstance in existingChildren do if not isExistingChildVisited[childIndex] then -- We guard accessing Name and ClassName in order to avoid -- tripping over children of DataModel that Rojo won't have diff --git a/plugin/src/Reconciler/reify.lua b/plugin/src/Reconciler/reify.lua index 586dd342..de7725ce 100644 --- a/plugin/src/Reconciler/reify.lua +++ b/plugin/src/Reconciler/reify.lua @@ -126,7 +126,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch) }) end - for _, entry in ipairs(deferredRefs) do + for _, entry in deferredRefs do local _, refId = next(entry.virtualValue) if refId == nil then diff --git a/plugin/src/Reconciler/reify.spec.lua b/plugin/src/Reconciler/reify.spec.lua index 3c8750fa..20f363d7 100644 --- a/plugin/src/Reconciler/reify.spec.lua +++ b/plugin/src/Reconciler/reify.spec.lua @@ -12,7 +12,7 @@ return function() local function size(dict) local len = 0 - for _ in pairs(dict) do + for _ in dict do len = len + 1 end diff --git a/plugin/src/preloadAssets.lua b/plugin/src/preloadAssets.lua index 1cf9b2d8..47c6d40b 100644 --- a/plugin/src/preloadAssets.lua +++ b/plugin/src/preloadAssets.lua @@ -9,7 +9,7 @@ local gatherAssetUrlsRecursive function gatherAssetUrlsRecursive(currentTable, currentUrls) currentUrls = currentUrls or {} - for _, value in pairs(currentTable) do + for _, value in currentTable do if typeof(value) == "string" then table.insert(currentUrls, value) elseif typeof(value) == "table" then From 071b6e7e23874e23e43d8eccf745748ad1ce12e4 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 18 Nov 2025 20:26:44 -0800 Subject: [PATCH 04/11] Improved string diff viewer (#994) --- CHANGELOG.md | 2 + plugin/Packages/Highlighter | 2 +- plugin/src/App/Components/CodeLabel.lua | 66 --- .../StringDiffVisualizer/StringDiff.lua | 369 +++++++++++++-- .../Components/StringDiffVisualizer/init.lua | 444 ++++++++++++++---- plugin/src/App/Components/VirtualScroller.lua | 15 +- plugin/src/App/StatusPages/Confirming.lua | 14 +- plugin/src/App/StatusPages/Connected.lua | 14 +- 8 files changed, 705 insertions(+), 221 deletions(-) delete mode 100644 plugin/src/App/Components/CodeLabel.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc2991b..6581dff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,10 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* Fixed bugs and improved performance & UX for the script diff viewer ([#994]) * Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159]) +[#994]: https://github.com/rojo-rbx/rojo/pull/994 [#1159]: https://github.com/rojo-rbx/rojo/pull/1159 ## [7.6.1] (November 6th, 2025) diff --git a/plugin/Packages/Highlighter b/plugin/Packages/Highlighter index e0d06144..c12c488d 160000 --- a/plugin/Packages/Highlighter +++ b/plugin/Packages/Highlighter @@ -1 +1 @@ -Subproject commit e0d061449ea5c4452ef77008b5197ae4d3d77621 +Subproject commit c12c488dadfcf6e7b66a14d107b74e751d08060c diff --git a/plugin/src/App/Components/CodeLabel.lua b/plugin/src/App/Components/CodeLabel.lua deleted file mode 100644 index 1951106b..00000000 --- a/plugin/src/App/Components/CodeLabel.lua +++ /dev/null @@ -1,66 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin -local Packages = Rojo.Packages - -local Roact = require(Packages.Roact) -local Highlighter = require(Packages.Highlighter) -Highlighter.matchStudioSettings() - -local e = Roact.createElement - -local Theme = require(Plugin.App.Theme) - -local CodeLabel = Roact.PureComponent:extend("CodeLabel") - -function CodeLabel:init() - self.labelRef = Roact.createRef() - self.highlightsRef = Roact.createRef() -end - -function CodeLabel:didMount() - Highlighter.highlight({ - textObject = self.labelRef:getValue(), - }) - self:updateHighlights() -end - -function CodeLabel:didUpdate() - self:updateHighlights() -end - -function CodeLabel:updateHighlights() - local highlights = self.highlightsRef:getValue() - if not highlights then - return - end - - for _, lineLabel in highlights:GetChildren() do - local lineNum = tonumber(string.match(lineLabel.Name, "%d+") or "0") - lineLabel.BackgroundColor3 = self.props.lineBackground - lineLabel.BorderSizePixel = 0 - lineLabel.BackgroundTransparency = if self.props.markedLines[lineNum] then 0.25 else 1 - end -end - -function CodeLabel:render() - return Theme.with(function(theme) - return e("TextLabel", { - Size = self.props.size, - Position = self.props.position, - Text = self.props.text, - BackgroundTransparency = 1, - FontFace = theme.Font.Code, - TextSize = theme.TextSize.Code, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - TextColor3 = Color3.fromRGB(255, 255, 255), - [Roact.Ref] = self.labelRef, - }, { - SyntaxHighlights = e("Folder", { - [Roact.Ref] = self.highlightsRef, - }), - }) - end) -end - -return CodeLabel diff --git a/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua b/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua index 6632c006..0b3a0f4a 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua @@ -1,3 +1,4 @@ +--!strict --[[ Based on DiffMatchPatch by Neil Fraser. https://github.com/google/diff-match-patch @@ -67,8 +68,187 @@ function StringDiff.findDiffs(text1: string, text2: string): Diffs end -- Cleanup the diff + diffs = StringDiff._cleanupSemantic(diffs) diffs = StringDiff._reorderAndMerge(diffs) + -- Remove any empty diffs + local cursor = 1 + while cursor and diffs[cursor] do + if diffs[cursor].value == "" then + table.remove(diffs, cursor) + else + cursor += 1 + end + end + + return diffs +end + +function StringDiff._computeDiff(text1: string, text2: string): Diffs + -- Assumes that the prefix and suffix have already been trimmed off + -- and shortcut returns have been made so these texts must be different + + local text1Length, text2Length = #text1, #text2 + + if text1Length == 0 then + -- It's simply inserting all of text2 into text1 + return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } } + end + + if text2Length == 0 then + -- It's simply deleting all of text1 + return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } } + end + + local longText = if text1Length > text2Length then text1 else text2 + local shortText = if text1Length > text2Length then text2 else text1 + local shortTextLength = #shortText + + -- Shortcut if the shorter string exists entirely inside the longer one + local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true) + if indexOf ~= nil then + local diffs = { + { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) }, + { actionType = StringDiff.ActionTypes.Equal, value = shortText }, + { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) }, + } + -- Swap insertions for deletions if diff is reversed + if text1Length > text2Length then + diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete + end + return diffs + end + + if shortTextLength == 1 then + -- Single character string + -- After the previous shortcut, the character can't be an equality + return { + { actionType = StringDiff.ActionTypes.Delete, value = text1 }, + { actionType = StringDiff.ActionTypes.Insert, value = text2 }, + } + end + + return StringDiff._bisect(text1, text2) +end + +function StringDiff._cleanupSemantic(diffs: Diffs): Diffs + -- Reduce the number of edits by eliminating semantically trivial equalities. + local changes = false + local equalities = {} -- Stack of indices where equalities are found. + local equalitiesLength = 0 -- Keeping our own length var is faster. + local lastEquality: string? = nil + -- Always equal to diffs[equalities[equalitiesLength]].value + local pointer = 1 -- Index of current position. + -- Number of characters that changed prior to the equality. + local length_insertions1 = 0 + local length_deletions1 = 0 + -- Number of characters that changed after the equality. + local length_insertions2 = 0 + local length_deletions2 = 0 + + while diffs[pointer] do + if diffs[pointer].actionType == StringDiff.ActionTypes.Equal then -- Equality found. + equalitiesLength = equalitiesLength + 1 + equalities[equalitiesLength] = pointer + length_insertions1 = length_insertions2 + length_deletions1 = length_deletions2 + length_insertions2 = 0 + length_deletions2 = 0 + lastEquality = diffs[pointer].value + else -- An insertion or deletion. + if diffs[pointer].actionType == StringDiff.ActionTypes.Insert then + length_insertions2 = length_insertions2 + #diffs[pointer].value + else + length_deletions2 = length_deletions2 + #diffs[pointer].value + end + -- Eliminate an equality that is smaller or equal to the edits on both + -- sides of it. + if + lastEquality + and (#lastEquality <= math.max(length_insertions1, length_deletions1)) + and (#lastEquality <= math.max(length_insertions2, length_deletions2)) + then + -- Duplicate record. + table.insert( + diffs, + equalities[equalitiesLength], + { actionType = StringDiff.ActionTypes.Delete, value = lastEquality } + ) + -- Change second copy to insert. + diffs[equalities[equalitiesLength] + 1].actionType = StringDiff.ActionTypes.Insert + -- Throw away the equality we just deleted. + equalitiesLength = equalitiesLength - 1 + -- Throw away the previous equality (it needs to be reevaluated). + equalitiesLength = equalitiesLength - 1 + pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 + length_insertions1, length_deletions1 = 0, 0 -- Reset the counters. + length_insertions2, length_deletions2 = 0, 0 + lastEquality = nil + changes = true + end + end + pointer = pointer + 1 + end + + -- Normalize the diff. + if changes then + StringDiff._reorderAndMerge(diffs) + end + StringDiff._cleanupSemanticLossless(diffs) + + -- Find any overlaps between deletions and insertions. + -- e.g: abcxxxxxxdef + -- -> abcxxxdef + -- e.g: xxxabcdefxxx + -- -> defxxxabc + -- Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 2 + while diffs[pointer] do + if + diffs[pointer - 1].actionType == StringDiff.ActionTypes.Delete + and diffs[pointer].actionType == StringDiff.ActionTypes.Insert + then + local deletion = diffs[pointer - 1].value + local insertion = diffs[pointer].value + local overlap_length1 = StringDiff._commonOverlap(deletion, insertion) + local overlap_length2 = StringDiff._commonOverlap(insertion, deletion) + if overlap_length1 >= overlap_length2 then + if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then + -- Overlap found. Insert an equality and trim the surrounding edits. + table.insert( + diffs, + pointer, + { actionType = StringDiff.ActionTypes.Equal, value = string.sub(insertion, 1, overlap_length1) } + ) + diffs[pointer - 1].value = string.sub(deletion, 1, #deletion - overlap_length1) + diffs[pointer + 1].value = string.sub(insertion, overlap_length1 + 1) + pointer = pointer + 1 + end + else + if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then + -- Reverse overlap found. + -- Insert an equality and swap and trim the surrounding edits. + table.insert( + diffs, + pointer, + { actionType = StringDiff.ActionTypes.Equal, value = string.sub(deletion, 1, overlap_length2) } + ) + diffs[pointer - 1] = { + actionType = StringDiff.ActionTypes.Insert, + value = string.sub(insertion, 1, #insertion - overlap_length2), + } + diffs[pointer + 1] = { + actionType = StringDiff.ActionTypes.Delete, + value = string.sub(deletion, overlap_length2 + 1), + } + pointer = pointer + 1 + end + end + pointer = pointer + 1 + end + pointer = pointer + 1 + end + return diffs end @@ -124,51 +304,164 @@ function StringDiff._sharedSuffix(text1: string, text2: string): number return pointerMid end -function StringDiff._computeDiff(text1: string, text2: string): Diffs - -- Assumes that the prefix and suffix have already been trimmed off - -- and shortcut returns have been made so these texts must be different +function StringDiff._commonOverlap(text1: string, text2: string): number + -- Determine if the suffix of one string is the prefix of another. - local text1Length, text2Length = #text1, #text2 - - if text1Length == 0 then - -- It's simply inserting all of text2 into text1 - return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } } + -- Cache the text lengths to prevent multiple calls. + local text1_length = #text1 + local text2_length = #text2 + -- Eliminate the null case. + if text1_length == 0 or text2_length == 0 then + return 0 + end + -- Truncate the longer string. + if text1_length > text2_length then + text1 = string.sub(text1, text1_length - text2_length + 1) + elseif text1_length < text2_length then + text2 = string.sub(text2, 1, text1_length) + end + local text_length = math.min(text1_length, text2_length) + -- Quick check for the worst case. + if text1 == text2 then + return text_length end - if text2Length == 0 then - -- It's simply deleting all of text1 - return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } } - end - - local longText = if text1Length > text2Length then text1 else text2 - local shortText = if text1Length > text2Length then text2 else text1 - local shortTextLength = #shortText - - -- Shortcut if the shorter string exists entirely inside the longer one - local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true) - if indexOf ~= nil then - local diffs = { - { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) }, - { actionType = StringDiff.ActionTypes.Equal, value = shortText }, - { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) }, - } - -- Swap insertions for deletions if diff is reversed - if text1Length > text2Length then - diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete + -- Start by looking for a single character match + -- and increase length until no match is found. + -- Performance analysis: https://neil.fraser.name/news/2010/11/04/ + local best = 0 + local length = 1 + while true do + local pattern = string.sub(text1, text_length - length + 1) + local found = string.find(text2, pattern, 1, true) + if found == nil then + return best end - return diffs + length = length + found - 1 + if found == 1 or string.sub(text1, text_length - length + 1) == string.sub(text2, 1, length) then + best = length + length = length + 1 + end + end +end + +function StringDiff._cleanupSemanticScore(one: string, two: string): number + -- Given two strings, compute a score representing whether the internal + -- boundary falls on logical boundaries. + -- Scores range from 6 (best) to 0 (worst). + + if (#one == 0) or (#two == 0) then + -- Edges are the best. + return 6 end - if shortTextLength == 1 then - -- Single character string - -- After the previous shortcut, the character can't be an equality - return { - { actionType = StringDiff.ActionTypes.Delete, value = text1 }, - { actionType = StringDiff.ActionTypes.Insert, value = text2 }, - } - end + -- Each port of this function behaves slightly differently due to + -- subtle differences in each language's definition of things like + -- 'whitespace'. Since this function's purpose is largely cosmetic, + -- the choice has been made to use each language's native features + -- rather than force total conformity. + local char1 = string.sub(one, -1) + local char2 = string.sub(two, 1, 1) + local nonAlphaNumeric1 = string.match(char1, "%W") + local nonAlphaNumeric2 = string.match(char2, "%W") + local whitespace1 = nonAlphaNumeric1 and string.match(char1, "%s") + local whitespace2 = nonAlphaNumeric2 and string.match(char2, "%s") + local lineBreak1 = whitespace1 and string.match(char1, "%c") + local lineBreak2 = whitespace2 and string.match(char2, "%c") + local blankLine1 = lineBreak1 and string.match(one, "\n\r?\n$") + local blankLine2 = lineBreak2 and string.match(two, "^\r?\n\r?\n") - return StringDiff._bisect(text1, text2) + if blankLine1 or blankLine2 then + -- Five points for blank lines. + return 5 + elseif lineBreak1 or lineBreak2 then + -- Four points for line breaks + -- DEVIATION: Prefer to start on a line break instead of end on it + return if lineBreak1 then 4 else 4.5 + elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then + -- Three points for end of sentences. + return 3 + elseif whitespace1 or whitespace2 then + -- Two points for whitespace. + return 2 + elseif nonAlphaNumeric1 or nonAlphaNumeric2 then + -- One point for non-alphanumeric. + return 1 + end + return 0 +end + +function StringDiff._cleanupSemanticLossless(diffs: Diffs) + -- Look for single edits surrounded on both sides by equalities + -- which can be shifted sideways to align the edit to a word boundary. + -- e.g: The cat came. -> The cat came. + + local pointer = 2 + -- Intentionally ignore the first and last element (don't need checking). + while diffs[pointer + 1] do + local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] + if + (prevDiff.actionType == StringDiff.ActionTypes.Equal) + and (nextDiff.actionType == StringDiff.ActionTypes.Equal) + then + -- This is a single edit surrounded by equalities. + local diff = diffs[pointer] + + local equality1 = prevDiff.value + local edit = diff.value + local equality2 = nextDiff.value + + -- First, shift the edit as far left as possible. + local commonOffset = StringDiff._sharedSuffix(equality1, edit) + if commonOffset > 0 then + local commonString = string.sub(edit, -commonOffset) + equality1 = string.sub(equality1, 1, -commonOffset - 1) + edit = commonString .. string.sub(edit, 1, -commonOffset - 1) + equality2 = commonString .. equality2 + end + + -- Second, step character by character right, looking for the best fit. + local bestEquality1 = equality1 + local bestEdit = edit + local bestEquality2 = equality2 + local bestScore = StringDiff._cleanupSemanticScore(equality1, edit) + + StringDiff._cleanupSemanticScore(edit, equality2) + + while string.byte(edit, 1) == string.byte(equality2, 1) do + equality1 = equality1 .. string.sub(edit, 1, 1) + edit = string.sub(edit, 2) .. string.sub(equality2, 1, 1) + equality2 = string.sub(equality2, 2) + local score = StringDiff._cleanupSemanticScore(equality1, edit) + + StringDiff._cleanupSemanticScore(edit, equality2) + -- The > (rather than >=) encourages leading rather than trailing whitespace on edits. + -- I just think it looks better for indentation changes to start the line, + -- since then indenting several lines all have aligned diffs at the start + if score > bestScore then + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + end + end + if prevDiff.value ~= bestEquality1 then + -- We have an improvement, save it back to the diff. + if #bestEquality1 > 0 then + diffs[pointer - 1].value = bestEquality1 + else + table.remove(diffs, pointer - 1) + pointer = pointer - 1 + end + diffs[pointer].value = bestEdit + if #bestEquality2 > 0 then + diffs[pointer + 1].value = bestEquality2 + else + table.remove(diffs, pointer + 1) + pointer = pointer - 1 + end + end + end + pointer = pointer + 1 + end end function StringDiff._bisect(text1: string, text2: string): Diffs diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index e8e0d024..6802383c 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -5,15 +5,15 @@ local Packages = Rojo.Packages local Roact = require(Packages.Roact) local Log = require(Packages.Log) local Highlighter = require(Packages.Highlighter) +Highlighter.matchStudioSettings() local StringDiff = require(script:FindFirstChild("StringDiff")) local Timer = require(Plugin.Timer) local Theme = require(Plugin.App.Theme) local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync) -local CodeLabel = require(Plugin.App.Components.CodeLabel) local BorderedContainer = require(Plugin.App.Components.BorderedContainer) -local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) +local VirtualScroller = require(Plugin.App.Components.VirtualScroller) local e = Roact.createElement @@ -21,26 +21,29 @@ local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer") function StringDiffVisualizer:init() self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0)) - self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.updateEvent = Instance.new("BindableEvent") + self.lineHeight, self.setLineHeight = Roact.createBinding(15) + self.canvasPosition, self.setCanvasPosition = Roact.createBinding(Vector2.zero) + self.windowWidth, self.setWindowWidth = Roact.createBinding(math.huge) -- Ensure that the script background is up to date with the current theme self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() - task.defer(function() - -- Defer to allow Highlighter to process the theme change first + -- Delay to allow Highlighter to process the theme change first + task.delay(1 / 20, function() self:updateScriptBackground() + self:updateDiffs() + -- Rerender the virtual list elements + self.updateEvent:Fire() end) end) self:updateScriptBackground() - - self:setState({ - add = {}, - remove = {}, - }) + self:updateDiffs() end function StringDiffVisualizer:willUnmount() self.themeChangedConnection:Disconnect() + self.updateEvent:Destroy() end function StringDiffVisualizer:updateScriptBackground() @@ -51,96 +54,188 @@ function StringDiffVisualizer:updateScriptBackground() end function StringDiffVisualizer:didUpdate(previousProps) - if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then - local add, remove = self:calculateDiffLines() - self:setState({ - add = add, - remove = remove, - }) + if + previousProps.currentString ~= self.props.currentString + or previousProps.incomingString ~= self.props.incomingString + then + self:updateDiffs() end end -function StringDiffVisualizer:calculateContentSize(theme) - local oldString, newString = self.props.oldString, self.props.newString - - local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge) - local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge) - - self.setContentSize( - Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) - ) -end - -function StringDiffVisualizer:calculateDiffLines() - Timer.start("StringDiffVisualizer:calculateDiffLines") - local oldString, newString = self.props.oldString, self.props.newString +function StringDiffVisualizer:updateDiffs() + Timer.start("StringDiffVisualizer:updateDiffs") + local currentString, incomingString = self.props.currentString, self.props.incomingString -- Diff the two texts local startClock = os.clock() - local diffs = StringDiff.findDiffs(oldString, newString) + local diffs = + StringDiff.findDiffs((string.gsub(currentString, "\t", " ")), (string.gsub(incomingString, "\t", " "))) local stopClock = os.clock() Log.trace( "Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections", - #oldString, - #newString, + #currentString, + #incomingString, math.round((stopClock - startClock) * 1000 * 1000), #diffs ) - -- Determine which lines to highlight - local add, remove = {}, {} + -- Build the rich text lines + local currentRichTextLines = Highlighter.buildRichTextLines({ + src = currentString, + }) + local incomingRichTextLines = Highlighter.buildRichTextLines({ + src = incomingString, + }) - local oldLineNum, newLineNum = 1, 1 + local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines) + + -- Find the diff locations + local currentDiffs, incomingDiffs = {}, {} + local firstDiffLineNum = 0 + + local currentLineNum, incomingLineNum = 1, 1 + local currentIdx, incomingIdx = 1, 1 for _, diff in diffs do local actionType, text = diff.actionType, diff.value - local lines = select(2, string.gsub(text, "\n", "\n")) + local lineCount = select(2, string.gsub(text, "\n", "\n")) + local lines = string.split(text, "\n") if actionType == StringDiff.ActionTypes.Equal then - oldLineNum += lines - newLineNum += lines - elseif actionType == StringDiff.ActionTypes.Insert then - if lines > 0 then - local textLines = string.split(text, "\n") - for i, textLine in textLines do - if string.match(textLine, "%S") then - add[newLineNum + i - 1] = true - end - end + if lineCount > 0 then + -- Jump cursor ahead to last line + currentLineNum += lineCount + incomingLineNum += lineCount + currentIdx = #lines[#lines] + incomingIdx = #lines[#lines] else - if string.match(text, "%S") then - add[newLineNum] = true - end + -- Move along this line + currentIdx += #text + incomingIdx += #text + end + + continue + end + + if actionType == StringDiff.ActionTypes.Insert then + if firstDiffLineNum == 0 then + firstDiffLineNum = incomingLineNum + end + + for i, lineText in lines do + if i > 1 then + -- Move to next line + incomingLineNum += 1 + incomingIdx = 0 + end + if not incomingDiffs[incomingLineNum] then + incomingDiffs[incomingLineNum] = {} + end + -- Mark these characters on this line + table.insert(incomingDiffs[incomingLineNum], { + start = incomingIdx, + stop = incomingIdx + #lineText, + }) + incomingIdx += #lineText end - newLineNum += lines elseif actionType == StringDiff.ActionTypes.Delete then - if lines > 0 then - local textLines = string.split(text, "\n") - for i, textLine in textLines do - if string.match(textLine, "%S") then - remove[oldLineNum + i - 1] = true - end - end - else - if string.match(text, "%S") then - remove[oldLineNum] = true - end + if firstDiffLineNum == 0 then + firstDiffLineNum = currentLineNum + end + + for i, lineText in lines do + if i > 1 then + -- Move to next line + currentLineNum += 1 + currentIdx = 0 + end + if not currentDiffs[currentLineNum] then + currentDiffs[currentLineNum] = {} + end + -- Mark these characters on this line + table.insert(currentDiffs[currentLineNum], { + start = currentIdx, + stop = currentIdx + #lineText, + }) + currentIdx += #lineText end - oldLineNum += lines else Log.warn("Unknown diff action: {} {}", actionType, text) end end Timer.stop() - return add, remove + + self:setState({ + maxLines = maxLines, + currentRichTextLines = currentRichTextLines, + incomingRichTextLines = incomingRichTextLines, + currentDiffs = currentDiffs, + incomingDiffs = incomingDiffs, + }) + + -- Scroll to the first diff line + task.defer(self.setCanvasPosition, Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16))) end function StringDiffVisualizer:render() - local oldString, newString = self.props.oldString, self.props.newString + local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs + local currentRichTextLines, incomingRichTextLines = + self.state.currentRichTextLines, self.state.incomingRichTextLines + local maxLines = self.state.maxLines return Theme.with(function(theme) - self:calculateContentSize(theme) + self.setLineHeight(theme.TextSize.Code) + + -- Calculate the width of the canvas + -- (One line at a time to avoid the char limit of getTextBoundsAsync) + local canvasWidth = 0 + for i = 1, maxLines do + local currentLine = currentRichTextLines[i] + if currentLine and string.find(currentLine, "%S") then + local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) + if bounds.X > canvasWidth then + canvasWidth = bounds.X + end + end + local incomingLine = incomingRichTextLines[i] + if incomingLine and string.find(incomingLine, "%S") then + local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) + if bounds.X > canvasWidth then + canvasWidth = bounds.X + end + end + end + + local lineNumberWidth = + getTextBoundsAsync(tostring(maxLines), theme.Font.Code, theme.TextSize.Body, math.huge, true).X + + canvasWidth += lineNumberWidth + 12 + + local removalScrollMarkers = {} + local insertionScrollMarkers = {} + for lineNum in currentDiffs do + table.insert( + removalScrollMarkers, + e("Frame", { + Size = UDim2.fromScale(0.5, 1 / maxLines), + Position = UDim2.fromScale(0, (lineNum - 1) / maxLines), + BorderSizePixel = 0, + BackgroundColor3 = theme.Diff.Background.Remove, + }) + ) + end + for lineNum in incomingDiffs do + table.insert( + insertionScrollMarkers, + e("Frame", { + Size = UDim2.fromScale(0.5, 1 / maxLines), + Position = UDim2.fromScale(0.5, (lineNum - 1) / maxLines), + BorderSizePixel = 0, + BackgroundColor3 = theme.Diff.Background.Add, + }) + ) + end return e(BorderedContainer, { size = self.props.size, @@ -159,43 +254,196 @@ function StringDiffVisualizer:render() CornerRadius = UDim.new(0, 5), }), }), - Separator = e("Frame", { - Size = UDim2.new(0, 2, 1, 0), - Position = UDim2.new(0.5, 0, 0, 0), - AnchorPoint = Vector2.new(0.5, 0), - BorderSizePixel = 0, - BackgroundColor3 = theme.BorderedContainer.BorderColor, - BackgroundTransparency = 0.5, - }), - Old = e(ScrollingFrame, { - position = UDim2.new(0, 2, 0, 2), - size = UDim2.new(0.5, -7, 1, -4), - scrollingDirection = Enum.ScrollingDirection.XY, - transparency = self.props.transparency, - contentSize = self.contentSize, + Main = e("Frame", { + Size = UDim2.new(1, -10, 1, -2), + Position = UDim2.new(0, 2, 0, 2), + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = function(rbx) + self.setWindowWidth(rbx.AbsoluteSize.X * 0.5 - 10) + end, }, { - Source = e(CodeLabel, { - size = UDim2.new(1, 0, 1, 0), + Separator = e("Frame", { + Size = UDim2.new(0, 2, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.BorderedContainer.BorderColor, + BackgroundTransparency = 0.5, + }), + Current = e(VirtualScroller, { position = UDim2.new(0, 0, 0, 0), - text = oldString, - lineBackground = theme.Diff.Background.Remove, - markedLines = self.state.remove, + size = UDim2.new(0.5, -1, 1, 0), + transparency = self.props.transparency, + count = maxLines, + updateEvent = self.updateEvent.Event, + canvasWidth = canvasWidth, + canvasPosition = self.canvasPosition, + onCanvasPositionChanged = self.setCanvasPosition, + render = function(i) + local lineDiffs = currentDiffs[i] + local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) + + -- Show diff markers over the specific changed characters + if lineDiffs then + local charWidth = math.round(theme.TextSize.Code * 0.5) + for diffIdx, diff in lineDiffs do + local start, stop = diff.start, diff.stop + diffFrames[diffIdx] = e("Frame", { + Size = if #lineDiffs == 1 + and start == 0 + and stop == 0 + then UDim2.fromScale(1, 1) + else UDim2.new( + 0, + math.max(charWidth * (stop - start), charWidth * 0.4), + 1, + 0 + ), + Position = UDim2.fromOffset(charWidth * start, 0), + BackgroundColor3 = theme.Diff.Background.Remove, + BackgroundTransparency = 0.85, + BorderSizePixel = 0, + ZIndex = -1, + }) + end + end + + return Roact.createFragment({ + LineNumber = e("TextLabel", { + Size = UDim2.new(0, lineNumberWidth + 8, 1, 0), + Text = i, + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0.9, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Body, + TextColor3 = if lineDiffs then theme.Diff.Background.Remove else theme.SubTextColor, + TextXAlignment = Enum.TextXAlignment.Right, + }, { + Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }), + }), + Content = e("Frame", { + Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0), + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + BackgroundColor3 = theme.Diff.Background.Remove, + BackgroundTransparency = if lineDiffs then 0.95 else 1, + BorderSizePixel = 0, + }, { + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = currentRichTextLines[i] or "", + RichText = true, + BackgroundTransparency = 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), + }), + }) + end, + getHeightBinding = function() + return self.lineHeight + end, + }), + Incoming = e(VirtualScroller, { + position = UDim2.new(0.5, 1, 0, 0), + size = UDim2.new(0.5, -1, 1, 0), + transparency = self.props.transparency, + count = maxLines, + updateEvent = self.updateEvent.Event, + canvasWidth = canvasWidth, + canvasPosition = self.canvasPosition, + onCanvasPositionChanged = self.setCanvasPosition, + render = function(i) + local lineDiffs = incomingDiffs[i] + local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) + + -- Show diff markers over the specific changed characters + if lineDiffs then + local charWidth = math.round(theme.TextSize.Code * 0.5) + for diffIdx, diff in lineDiffs do + local start, stop = diff.start, diff.stop + diffFrames[diffIdx] = e("Frame", { + Size = if #lineDiffs == 1 + and start == 0 + and stop == 0 + then UDim2.fromScale(1, 1) + else UDim2.new( + 0, + math.max(charWidth * (stop - start), charWidth * 0.4), + 1, + 0 + ), + Position = UDim2.fromOffset(charWidth * start, 0), + BackgroundColor3 = theme.Diff.Background.Add, + BackgroundTransparency = 0.85, + BorderSizePixel = 0, + ZIndex = -1, + }) + end + end + + return Roact.createFragment({ + LineNumber = e("TextLabel", { + Size = UDim2.new(0, lineNumberWidth + 8, 1, 0), + Text = i, + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0.9, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Body, + TextColor3 = if lineDiffs then theme.Diff.Background.Add else theme.SubTextColor, + TextXAlignment = Enum.TextXAlignment.Right, + }, { + Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }), + }), + Content = e("Frame", { + Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0), + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + BackgroundColor3 = theme.Diff.Background.Add, + BackgroundTransparency = if lineDiffs then 0.95 else 1, + BorderSizePixel = 0, + }, { + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = incomingRichTextLines[i] or "", + RichText = true, + BackgroundColor3 = theme.Diff.Background.Add, + BackgroundTransparency = 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), + }), + }) + end, + getHeightBinding = function() + return self.lineHeight + end, }), }), - New = e(ScrollingFrame, { - position = UDim2.new(0.5, 5, 0, 2), - size = UDim2.new(0.5, -7, 1, -4), - scrollingDirection = Enum.ScrollingDirection.XY, - transparency = self.props.transparency, - contentSize = self.contentSize, + ScrollMarkers = e("Frame", { + Size = self.windowWidth:map(function(windowWidth) + return UDim2.new(0, 8, 1, -4 - (if canvasWidth > windowWidth then 10 else 0)) + end), + Position = UDim2.new(1, -2, 0, 2), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, }, { - Source = e(CodeLabel, { - size = UDim2.new(1, 0, 1, 0), - position = UDim2.new(0, 0, 0, 0), - text = newString, - lineBackground = theme.Diff.Background.Add, - markedLines = self.state.add, - }), + insertions = Roact.createFragment(insertionScrollMarkers), + removals = Roact.createFragment(removalScrollMarkers), }), }) end) diff --git a/plugin/src/App/Components/VirtualScroller.lua b/plugin/src/App/Components/VirtualScroller.lua index 466da8a4..63d966ca 100644 --- a/plugin/src/App/Components/VirtualScroller.lua +++ b/plugin/src/App/Components/VirtualScroller.lua @@ -15,8 +15,10 @@ local VirtualScroller = Roact.Component:extend("VirtualScroller") function VirtualScroller:init() self.scrollFrameRef = Roact.createRef() self:setState({ - WindowSize = Vector2.new(), - CanvasPosition = Vector2.new(), + WindowSize = Vector2.zero, + CanvasPosition = if self.props.canvasPosition + then self.props.canvasPosition:getValue() or Vector2.zero + else Vector2.zero, }) self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0) @@ -41,6 +43,10 @@ function VirtualScroller:didMount() local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition") self.canvasPositionChanged = canvasPositionSignal:Connect(function() + if self.props.onCanvasPositionChanged then + pcall(self.props.onCanvasPositionChanged, rbx.CanvasPosition) + end + if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then self:setState({ CanvasPosition = rbx.CanvasPosition }) self:refresh() @@ -134,8 +140,9 @@ function VirtualScroller:render() BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor, BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor, CanvasSize = self.totalCanvas:map(function(s) - return UDim2.fromOffset(0, s) + return UDim2.fromOffset(props.canvasWidth or 0, s) end), + CanvasPosition = self.props.canvasPosition, ScrollBarThickness = 9, ScrollBarImageColor3 = theme.ScrollBarColor, ScrollBarImageTransparency = props.transparency:map(function(value) @@ -146,7 +153,7 @@ function VirtualScroller:render() BottomImage = Assets.Images.ScrollBar.Bottom, ElasticBehavior = Enum.ElasticBehavior.Always, - ScrollingDirection = Enum.ScrollingDirection.Y, + ScrollingDirection = Enum.ScrollingDirection.XY, VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar, [Roact.Ref] = self.scrollFrameRef, }, { diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua index a334a83c..951df0f5 100644 --- a/plugin/src/App/StatusPages/Confirming.lua +++ b/plugin/src/App/StatusPages/Confirming.lua @@ -23,8 +23,8 @@ function ConfirmingPage:init() self:setState({ showingStringDiff = false, - oldString = "", - newString = "", + currentString = "", + incomingString = "", showingTableDiff = false, oldTable = {}, newTable = {}, @@ -56,11 +56,11 @@ function ConfirmingPage:render() patchTree = self.props.patchTree, - showStringDiff = function(oldString: string, newString: string) + showStringDiff = function(currentString: string, incomingString: string) self:setState({ showingStringDiff = true, - oldString = oldString, - newString = newString, + currentString = currentString, + incomingString = incomingString, }) end, showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? }) @@ -167,8 +167,8 @@ function ConfirmingPage:render() anchorPoint = Vector2.new(0, 0), transparency = self.props.transparency, - oldString = self.state.oldString, - newString = self.state.newString, + currentString = self.state.currentString, + incomingString = self.state.incomingString, }), }), }), diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua index 1d089e28..2a489366 100644 --- a/plugin/src/App/StatusPages/Connected.lua +++ b/plugin/src/App/StatusPages/Connected.lua @@ -307,8 +307,8 @@ function ConnectedPage:init() renderChanges = false, hoveringChangeInfo = false, showingStringDiff = false, - oldString = "", - newString = "", + currentString = "", + incomingString = "", }) self.changeInfoText, self.setChangeInfoText = Roact.createBinding("") @@ -511,11 +511,11 @@ function ConnectedPage:render() patchData = self.props.patchData, patchTree = self.props.patchTree, serveSession = self.props.serveSession, - showStringDiff = function(oldString: string, newString: string) + showStringDiff = function(currentString: string, incomingString: string) self:setState({ showingStringDiff = true, - oldString = oldString, - newString = newString, + currentString = currentString, + incomingString = incomingString, }) end, showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? }) @@ -566,8 +566,8 @@ function ConnectedPage:render() anchorPoint = Vector2.new(0, 0), transparency = self.props.transparency, - oldString = self.state.oldString, - newString = self.state.newString, + currentString = self.state.currentString, + incomingString = self.state.incomingString, }), }), }), From 9b5a07191b5bfeb66d60ce478cf34e9c7a5a9793 Mon Sep 17 00:00:00 2001 From: Micah Date: Wed, 19 Nov 2025 09:21:33 -0800 Subject: [PATCH 05/11] Implement Syncback to support converting Roblox files to a Rojo project (#937) This is a very large commit. Consider checking the linked PR for more information. --- CHANGELOG.md | 37 ++ Cargo.lock | 32 +- Cargo.toml | 6 +- crates/memofs/CHANGELOG.md | 1 + crates/memofs/src/in_memory_fs.rs | 15 + crates/memofs/src/lib.rs | 62 ++ crates/memofs/src/noop_backend.rs | 14 + crates/memofs/src/std_backend.rs | 8 + ...__syncback_util__child_but_not-stdout.snap | 6 + ..._rojo_test__syncback_util__csv-stdout.snap | 7 + ...ncback_util__duplicate_rojo_id-stdout.snap | 5 + ...back_util__ignore_paths_adding-stdout.snap | 7 + ...ncback_util__ignore_paths_init-stdout.snap | 6 + ...nore_paths_removing-src__Message.rbxm.snap | 73 +++ ...ck_util__ignore_paths_removing-stdout.snap | 5 + ...back_util__ignore_trees_adding-stdout.snap | 6 + ...ck_util__ignore_trees_removing-stdout.snap | 5 + ...yncback_util__json_middlewares-stdout.snap | 8 + ...syncback_util__nested_projects-stdout.snap | 6 + ...ck_util__nested_projects_weird-stdout.snap | 6 + ...t__syncback_util__project_init-stdout.snap | 6 + ...back_util__project_reserialize-stdout.snap | 6 + ...allback-src__ChildWithDuplicates.rbxm.snap | 73 +++ ...__syncback_util__rbxm_fallback-stdout.snap | 9 + ..._syncback_util__ref_properties-stdout.snap | 6 + ...ack_util__ref_properties_blank-stdout.snap | 7 + ..._util__ref_properties_conflict-stdout.snap | 6 + ...util__ref_properties_duplicate-stdout.snap | 5 + ...respect_old_middleware-src__rbxm.rbxm.snap | 55 ++ ...k_util__respect_old_middleware-stdout.snap | 8 + ...ack_util__string_value_project-stdout.snap | 6 + ...est__syncback_util__sync_rules-stdout.snap | 6 + ..._util__unscriptable_properties-stdout.snap | 5 + ...ut_not-OnlyOneCopy__child_of_one.luau.snap | 5 + ...torage__child_replicated_storage.luau.snap | 5 + ...nd__tests__syncback__csv-src__csv.csv.snap | 6 + ...syncback__csv-src__csv_init__init.csv.snap | 6 + ...uplicate_rojo_id-container.model.json.snap | 25 + ...aths_adding-src__int_value.model.json.snap | 10 + ...ding-src__subfolder__string_value.txt.snap | 5 + ..._paths_init-src__init-file__init.luau.snap | 5 + ..._ignore_paths_init-src__non-init.luau.snap | 6 + ...es-src__dir_with_meta__init.meta.json.snap | 12 + ...iddlewares-src__model_json.model.json.snap | 10 + ...ewares-src__project_json.project.json.snap | 17 + ...__nested_projects-nested.project.json.snap | 19 + ...ack__nested_projects-string_value.txt.snap | 5 + ...weird-src__modules__ClientModule.luau.snap | 5 + ...weird-src__modules__ServerModule.luau.snap | 5 + ...syncback__project_init-src__init.luau.snap | 5 + ...t_reserialize-attribute_mismatch.luau.snap | 5 + ...ialize-property_mismatch.project.json.snap | 15 + ...ef_properties-src__pointer.model.json.snap | 10 + ...ref_properties-src__target.model.json.snap | 10 + ...perties_blank-src__pointer.model.json.snap | 10 + ...roperties_blank-src__target.meta.json.snap | 9 + ..._ref_properties_blank-src__target.txt.snap | 5 + ...es_conflict-src__Pointer_2.model.json.snap | 11 + ...ies_conflict-src__Target_2.model.json.snap | 11 + ...t_old_middleware-default.project.json.snap | 16 + ...middleware-src__model_json.model.json.snap | 10 + ...spect_old_middleware-src__rbxmx.rbxmx.snap | 16 + ...ng_value_project-default.project.json.snap | 22 + ...__sync_rules-src__module.modulescript.snap | 6 + ...__syncback__sync_rules-src__text.text.snap | 6 + ...table_properties-default.project.json.snap | 18 + .../input-project/OnlyOneCopy/.gitkeep | 0 .../input-project/ReplicatedStorage/.gitkeep | 0 .../input-project/default.project.json | 12 + .../syncback-tests/child_but_not/input.rbxl | Bin 0 -> 56708 bytes .../csv/input-project/default.project.json | 6 + .../csv/input-project/src/csv_init/init.csv | 2 + rojo-test/syncback-tests/csv/input.rbxm | Bin 0 -> 1041 bytes .../input-project/container.model.json | 13 + .../input-project/default.project.json | 11 + .../duplicate_rojo_id/input.rbxl | Bin 0 -> 53809 bytes .../input-project/default.project.json | 11 + .../input-project/src/.gitkeep | 0 .../ignore_paths_adding/input.rbxm | Bin 0 -> 3670 bytes .../input-project/default.project.json | 11 + .../input-project/src/init-file/init.luau | 1 + .../input-project/src/non-init.luau | 1 + .../ignore_paths_init/input.rbxm | Bin 0 -> 1111 bytes .../input-project/default.project.json | 14 + .../input-project/src/not_in_place.luau | 1 + .../ignore_paths_removing/input.rbxl | Bin 0 -> 45880 bytes .../input-project/default.project.json | 14 + .../input-project/src/.gitkeep | 0 .../ignore_trees_adding/input.rbxl | Bin 0 -> 53275 bytes .../input-project/default.project.json | 14 + .../input-project/src/KeepMe/.gitkeep | 0 .../ignore_trees_removing/input.rbxl | Bin 0 -> 43090 bytes .../input-project/default.project.json | 6 + .../src/dir_with_meta/init.meta.json | 3 + .../input-project/src/model_json.model.json | 3 + .../src/project_json.project.json | 6 + .../json_middlewares/input.rbxm | Bin 0 -> 1551 bytes .../expected/default.project.json | 11 + .../expected/nested.project.json | 15 + .../nested_projects/expected/string_value.txt | 1 + .../input-project/default.project.json | 11 + .../input-project/nested.project.json | 12 + .../input-project/string_value.txt | 0 .../syncback-tests/nested_projects/input.rbxl | Bin 0 -> 56532 bytes .../input-project/client-only.project.json | 9 + .../input-project/default.project.json | 16 + .../input-project/server-only.project.json | 9 + .../input-project/src/modules/.gitkeep | 0 .../nested_projects_weird/input.rbxl | Bin 0 -> 56762 bytes .../input-project/default.project.json | 11 + .../project_init/input-project/src/init.luau | 0 .../syncback-tests/project_init/input.rbxl | Bin 0 -> 56195 bytes .../expected/attribute_mismatch.luau | 1 + .../expected/default.project.json | 29 + .../expected/property_mismatch.project.json | 11 + .../input-project/attribute_mismatch.luau | 0 .../input-project/default.project.json | 15 + .../property_mismatch.project.json | 6 + .../project_reserialize/input.rbxm | Bin 0 -> 1381 bytes .../input-project/default.project.json | 6 + .../src/ChildWithDuplicates/.gitkeep | 0 .../syncback-tests/rbxm_fallback/input.rbxm | Bin 0 -> 549 bytes .../input-project/default.project.json | 9 + .../input-project/src/pointer.model.json | 3 + .../input-project/src/target.model.json | 3 + .../syncback-tests/ref_properties/input.rbxl | Bin 0 -> 56493 bytes .../input-project/default.project.json | 9 + .../input-project/src/.gitkeep | 0 .../ref_properties_blank/input.rbxl | Bin 0 -> 49473 bytes .../input-project/default.project.json | 9 + .../input-project/src/Pointer_1.model.json | 6 + .../input-project/src/Pointer_2.model.json | 6 + .../input-project/src/Target_1.model.json | 6 + .../input-project/src/Target_2.model.json | 6 + .../ref_properties_conflict/input.rbxl | Bin 0 -> 66581 bytes .../input-project/default.project.json | 6 + .../input-project/src/Pointer_1.model.json | 6 + .../input-project/src/Pointer_2.model.json | 6 + .../input-project/src/Target.model.json | 6 + .../ref_properties_duplicate/input.rbxm | Bin 0 -> 990 bytes .../input-project/default.project.json | 9 + .../input-project/src/model_json.model.json | 3 + .../input-project/src/rbxm.rbxm | Bin 0 -> 446 bytes .../input-project/src/rbxmx.rbxmx | 15 + .../respect_old_middleware/input.rbxm | Bin 0 -> 1254 bytes .../input-project/default.project.json | 12 + .../input-project/string_value.txt | 0 .../string_value_project/input.rbxm | Bin 0 -> 1033 bytes .../input-project/default.project.json | 16 + .../input-project/src/module.modulescript | 1 + .../sync_rules/input-project/src/text.text | 1 + .../syncback-tests/sync_rules/input.rbxm | Bin 0 -> 1487 bytes .../input-project/default.project.json | 9 + .../unscriptable_properties/input.rbxm | Bin 0 -> 503 bytes src/change_processor.rs | 17 +- src/cli/mod.rs | 4 + src/cli/syncback.rs | 282 +++++++++ src/lib.rs | 10 + src/path_serializer.rs | 22 +- src/project.rs | 29 +- src/resolution.rs | 96 +++- src/serve_session.rs | 4 + src/snapshot/metadata.rs | 44 +- ..._snapshot__tests__apply__add_property.snap | 1 + ...s__apply__remove_property_after_patch.snap | 1 + ...tests__apply__remove_property_initial.snap | 1 + ...tests__apply__set_name_and_class_name.snap | 1 + ...__snapshot__tests__compute__add_child.snap | 1 + src/snapshot/tree.rs | 19 + src/snapshot_middleware/csv.rs | 180 +++++- src/snapshot_middleware/dir.rs | 170 +++++- src/snapshot_middleware/json_model.rs | 95 +++- src/snapshot_middleware/lua.rs | 99 +++- src/snapshot_middleware/meta_file.rs | 246 +++++++- src/snapshot_middleware/mod.rs | 286 +++++++--- src/snapshot_middleware/project.rs | 342 ++++++++++- src/snapshot_middleware/rbxm.rs | 23 +- src/snapshot_middleware/rbxmx.rs | 34 +- ...t_middleware__csv__test__csv_from_vfs.snap | 1 + ...pshot_middleware__csv__test__csv_init.snap | 1 + ...leware__csv__test__csv_init_with_meta.snap | 1 + ..._middleware__csv__test__csv_with_meta.snap | 1 + ...t_middleware__dir__test__empty_folder.snap | 1 + ...ddleware__dir__test__folder_in_folder.snap | 2 + ...leware__json__test__instance_from_vfs.snap | 1 + ...middleware__json__test__with_metadata.snap | 1 + ...are__json_model__test__model_from_vfs.snap | 2 + ...on_model__test__model_from_vfs_legacy.snap | 2 + ...are__lua__test__class_client_from_vfs.snap | 1 + ...are__lua__test__class_module_from_vfs.snap | 1 + ...re__lua__test__class_module_with_meta.snap | 1 + ...are__lua__test__class_script_disabled.snap | 1 + ...re__lua__test__class_script_with_meta.snap | 1 + ...are__lua__test__class_server_from_vfs.snap | 1 + ...ware__lua__test__init_module_from_vfs.snap | 1 + ..._test__init_module_from_vfs_with_meta.snap | 1 + ...re__lua__test__plugin_module_from_vfs.snap | 1 + ...lua__test__runcontext_client_from_vfs.snap | 1 + ...lua__test__runcontext_module_from_vfs.snap | 1 + ...ua__test__runcontext_module_with_meta.snap | 1 + ...lua__test__runcontext_script_disabled.snap | 1 + ...ua__test__runcontext_script_with_meta.snap | 1 + ...lua__test__runcontext_server_from_vfs.snap | 1 + ...__meta_file__test__adjacent_read_json.snap | 1 + ..._meta_file__test__adjacent_read_jsonc.snap | 1 + ..._meta_file__test__directory_read_json.snap | 1 + ...meta_file__test__directory_read_jsonc.snap | 1 + ...eware__project__test__no_name_project.snap | 1 + ...oject__test__project_from_direct_file.snap | 1 + ...test__project_path_property_overrides.snap | 1 + ..._project__test__project_with_children.snap | 11 +- ...t__test__project_with_path_to_project.snap | 1 + ...ct_with_path_to_project_with_children.snap | 11 +- ...oject__test__project_with_path_to_txt.snap | 1 + ...est__project_with_resolved_properties.snap | 1 + ...t__project_with_unresolved_properties.snap | 1 + ...leware__toml__test__instance_from_vfs.snap | 1 + ...middleware__toml__test__with_metadata.snap | 1 + ...dleware__txt__test__instance_from_vfs.snap | 1 + ..._middleware__txt__test__with_metadata.snap | 1 + ...leware__yaml__test__instance_from_vfs.snap | 1 + ...middleware__yaml__test__with_metadata.snap | 1 + src/snapshot_middleware/txt.rs | 44 +- src/snapshot_middleware/util.rs | 7 + src/syncback/file_names.rs | 128 +++++ src/syncback/fs_snapshot.rs | 191 +++++++ src/syncback/hash/mod.rs | 122 ++++ src/syncback/hash/variant.rs | 212 +++++++ src/syncback/mod.rs | 534 ++++++++++++++++++ src/syncback/property_filter.rs | 111 ++++ src/syncback/ref_properties.rs | 192 +++++++ src/syncback/snapshot.rs | 259 +++++++++ src/variant_eq.rs | 191 +++++++ src/web/ui.rs | 1 + tests/rojo_test/io_util.rs | 2 + tests/rojo_test/mod.rs | 1 + tests/rojo_test/syncback_util.rs | 116 ++++ tests/tests/mod.rs | 1 + tests/tests/syncback.rs | 81 +++ 239 files changed, 5325 insertions(+), 225 deletions(-) create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap create mode 100644 rojo-test/syncback-tests/child_but_not/input-project/OnlyOneCopy/.gitkeep create mode 100644 rojo-test/syncback-tests/child_but_not/input-project/ReplicatedStorage/.gitkeep create mode 100644 rojo-test/syncback-tests/child_but_not/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/child_but_not/input.rbxl create mode 100644 rojo-test/syncback-tests/csv/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv create mode 100644 rojo-test/syncback-tests/csv/input.rbxm create mode 100644 rojo-test/syncback-tests/duplicate_rojo_id/input-project/container.model.json create mode 100644 rojo-test/syncback-tests/duplicate_rojo_id/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl create mode 100644 rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_paths_adding/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ignore_paths_adding/input.rbxm create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input.rbxm create mode 100644 rojo-test/syncback-tests/ignore_paths_removing/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_paths_removing/input-project/src/not_in_place.luau create mode 100644 rojo-test/syncback-tests/ignore_paths_removing/input.rbxl create mode 100644 rojo-test/syncback-tests/ignore_trees_adding/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_trees_adding/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ignore_trees_adding/input.rbxl create mode 100644 rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_trees_removing/input-project/src/KeepMe/.gitkeep create mode 100644 rojo-test/syncback-tests/ignore_trees_removing/input.rbxl create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input.rbxm create mode 100644 rojo-test/syncback-tests/nested_projects/expected/default.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/expected/nested.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/expected/string_value.txt create mode 100644 rojo-test/syncback-tests/nested_projects/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/input-project/nested.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/input-project/string_value.txt create mode 100644 rojo-test/syncback-tests/nested_projects/input.rbxl create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/client-only.project.json create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/server-only.project.json create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/src/modules/.gitkeep create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input.rbxl create mode 100644 rojo-test/syncback-tests/project_init/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/project_init/input-project/src/init.luau create mode 100644 rojo-test/syncback-tests/project_init/input.rbxl create mode 100644 rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau create mode 100644 rojo-test/syncback-tests/project_reserialize/expected/default.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/input-project/attribute_mismatch.luau create mode 100644 rojo-test/syncback-tests/project_reserialize/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/input-project/property_mismatch.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/input.rbxm create mode 100644 rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/rbxm_fallback/input-project/src/ChildWithDuplicates/.gitkeep create mode 100644 rojo-test/syncback-tests/rbxm_fallback/input.rbxm create mode 100644 rojo-test/syncback-tests/ref_properties/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties/input-project/src/pointer.model.json create mode 100644 rojo-test/syncback-tests/ref_properties/input-project/src/target.model.json create mode 100644 rojo-test/syncback-tests/ref_properties/input.rbxl create mode 100644 rojo-test/syncback-tests/ref_properties_blank/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_blank/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ref_properties_blank/input.rbxl create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input.rbxl create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxmx.rbxmx create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input.rbxm create mode 100644 rojo-test/syncback-tests/string_value_project/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/string_value_project/input-project/string_value.txt create mode 100644 rojo-test/syncback-tests/string_value_project/input.rbxm create mode 100644 rojo-test/syncback-tests/sync_rules/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript create mode 100644 rojo-test/syncback-tests/sync_rules/input-project/src/text.text create mode 100644 rojo-test/syncback-tests/sync_rules/input.rbxm create mode 100644 rojo-test/syncback-tests/unscriptable_properties/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/unscriptable_properties/input.rbxm create mode 100644 src/cli/syncback.rs create mode 100644 src/syncback/file_names.rs create mode 100644 src/syncback/fs_snapshot.rs create mode 100644 src/syncback/hash/mod.rs create mode 100644 src/syncback/hash/variant.rs create mode 100644 src/syncback/mod.rs create mode 100644 src/syncback/property_filter.rs create mode 100644 src/syncback/ref_properties.rs create mode 100644 src/syncback/snapshot.rs create mode 100644 src/variant_eq.rs create mode 100644 tests/rojo_test/syncback_util.rs create mode 100644 tests/tests/syncback.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6581dff8..b267ff8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,46 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* 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. + Syncback is primarily controlled by the project file. Any Instances who are either referenced in the project file or a descendant + of one that is will be placed in an appropriate location. + + In addition, a new field has been added to project files, `syncbackRules` to control how it behaves: + + ```json + { + "syncbackRules": { + "ignoreTrees": [ + "ServerStorage/ImportantSecrets", + ], + "ignorePaths": [ + "src/ServerStorage/Secrets/*" + ], + "ignoreProperties": { + "BasePart": ["Color"] + }, + "syncCurrentCamera": false, + "syncUnscriptable": true, + } + } + ``` + + A brief explanation of each field: + + - `ignoreTrees` is a list of paths in the **roblox file** that should be ignored + - `ignorePaths` is a list of paths in the **file system** that should be ignored + - `ignoreProperties` is a list of properties that won't be synced back + - `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`. + + 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]) * Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159]) +[#937]: https://github.com/rojo-rbx/rojo/pull/937 [#994]: https://github.com/rojo-rbx/rojo/pull/994 [#1159]: https://github.com/rojo-rbx/rojo/pull/1159 diff --git a/Cargo.lock b/Cargo.lock index 46b973f7..52577619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,6 +546,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -765,7 +774,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.5", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -784,12 +793,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - [[package]] name = "hashbrown" version = "0.15.4" @@ -935,12 +938,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.15.4", + "serde", ] [[package]] @@ -1635,6 +1639,7 @@ dependencies = [ "rbx_dom_weak", "rbx_reflection", "rbx_reflection_database", + "serde", "thiserror", "zstd", ] @@ -1898,6 +1903,7 @@ dependencies = [ "anyhow", "backtrace", "bincode", + "blake3", "clap 3.2.25", "criterion", "crossbeam-channel", @@ -1905,11 +1911,13 @@ dependencies = [ "data-encoding", "embed-resource", "env_logger", + "float-cmp", "fs-err", "futures", "globset", "humantime", "hyper", + "indexmap 2.10.0", "insta", "jod-thread", "jsonc-parser", @@ -2962,9 +2970,9 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", diff --git a/Cargo.toml b/Cargo.toml index 4a3b80bf..2deb184b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ memofs = { version = "0.3.0", path = "crates/memofs" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_xml = { path = "../rbx-dom/rbx_xml" } -rbx_binary = "2.0.0" +rbx_binary = { version = "2.0.0", features = ["unstable_text_format"] } rbx_dom_weak = "4.0.0" rbx_reflection = "6.0.0" rbx_reflection_database = "2.0.1" @@ -97,6 +97,10 @@ profiling = "1.0.15" yaml-rust2 = "0.10.3" data-encoding = "2.8.0" +blake3 = "1.5.0" +float-cmp = "0.9.0" +indexmap = { version = "2.10.0", features = ["serde"] } + [target.'cfg(windows)'.dependencies] winreg = "0.10.1" diff --git a/crates/memofs/CHANGELOG.md b/crates/memofs/CHANGELOG.md index 44c3aa50..16d578c3 100644 --- a/crates/memofs/CHANGELOG.md +++ b/crates/memofs/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.3.0 (2024-03-15) * Changed `StdBackend` file watching component to use minimal recursive watches. [#830] * Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854] +* Added `create_dir` and `create_dir_all` to allow creating directories. [#830]: https://github.com/rojo-rbx/rojo/pull/830 [#854]: https://github.com/rojo-rbx/rojo/pull/854 diff --git a/crates/memofs/src/in_memory_fs.rs b/crates/memofs/src/in_memory_fs.rs index 7bc654af..acca08c4 100644 --- a/crates/memofs/src/in_memory_fs.rs +++ b/crates/memofs/src/in_memory_fs.rs @@ -176,6 +176,21 @@ impl VfsBackend for InMemoryFs { } } + fn create_dir(&mut self, path: &Path) -> io::Result<()> { + let mut inner = self.inner.lock().unwrap(); + inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir()) + } + + fn create_dir_all(&mut self, path: &Path) -> io::Result<()> { + let mut inner = self.inner.lock().unwrap(); + let mut path_buf = path.to_path_buf(); + while let Some(parent) = path_buf.parent() { + inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?; + path_buf.pop(); + } + inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir()) + } + fn remove_file(&mut self, path: &Path) -> io::Result<()> { let mut inner = self.inner.lock().unwrap(); diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index 7a348ff6..028a413b 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -71,6 +71,8 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static { fn read(&mut self, path: &Path) -> io::Result>; fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>; fn read_dir(&mut self, path: &Path) -> io::Result; + fn create_dir(&mut self, path: &Path) -> io::Result<()>; + fn create_dir_all(&mut self, path: &Path) -> io::Result<()>; fn metadata(&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<()>; @@ -190,6 +192,16 @@ impl VfsInner { Ok(dir) } + fn create_dir>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.backend.create_dir(path) + } + + fn create_dir_all>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.backend.create_dir_all(path) + } + fn remove_file>(&mut self, path: P) -> io::Result<()> { let path = path.as_ref(); let _ = self.backend.unwatch(path); @@ -326,6 +338,31 @@ impl Vfs { self.inner.lock().unwrap().read_dir(path) } + /// Creates a directory at the provided location. + /// + /// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir]. + /// Similiar to that function, this function will fail if the parent of the + /// path does not exist. + /// + /// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html + #[inline] + pub fn create_dir>(&self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.lock().unwrap().create_dir(path) + } + + /// Creates a directory at the provided location, recursively creating + /// all parent components if they are missing. + /// + /// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all]. + /// + /// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html + #[inline] + pub fn create_dir_all>(&self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.lock().unwrap().create_dir_all(path) + } + /// Remove a file. /// /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. @@ -428,6 +465,31 @@ impl VfsLock<'_> { self.inner.read_dir(path) } + /// Creates a directory at the provided location. + /// + /// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir]. + /// Similiar to that function, this function will fail if the parent of the + /// path does not exist. + /// + /// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html + #[inline] + pub fn create_dir>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.create_dir(path) + } + + /// Creates a directory at the provided location, recursively creating + /// all parent components if they are missing. + /// + /// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all]. + /// + /// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html + #[inline] + pub fn create_dir_all>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.create_dir_all(path) + } + /// Remove a file. /// /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. diff --git a/crates/memofs/src/noop_backend.rs b/crates/memofs/src/noop_backend.rs index 31a2cca9..18f6f809 100644 --- a/crates/memofs/src/noop_backend.rs +++ b/crates/memofs/src/noop_backend.rs @@ -26,6 +26,20 @@ impl VfsBackend for NoopBackend { Err(io::Error::other("NoopBackend doesn't do anything")) } + fn create_dir(&mut self, _path: &Path) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::Other, + "NoopBackend doesn't do anything", + )) + } + + fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::Other, + "NoopBackend doesn't do anything", + )) + } + fn remove_file(&mut self, _path: &Path) -> io::Result<()> { Err(io::Error::other("NoopBackend doesn't do anything")) } diff --git a/crates/memofs/src/std_backend.rs b/crates/memofs/src/std_backend.rs index 54c142dc..d39fb179 100644 --- a/crates/memofs/src/std_backend.rs +++ b/crates/memofs/src/std_backend.rs @@ -78,6 +78,14 @@ impl VfsBackend for StdBackend { }) } + fn create_dir(&mut self, path: &Path) -> io::Result<()> { + fs_err::create_dir(path) + } + + fn create_dir_all(&mut self, path: &Path) -> io::Result<()> { + fs_err::create_dir_all(path) + } + fn remove_file(&mut self, path: &Path) -> io::Result<()> { fs_err::remove_file(path) } diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap new file mode 100644 index 00000000..7495dc1f --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing OnlyOneCopy/child_of_one.luau +Writing ReplicatedStorage/child_replicated_storage.luau diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap new file mode 100644 index 00000000..61fc2288 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/csv.csv +Writing src/csv_init/init.csv +Writing src/csv_init diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap new file mode 100644 index 00000000..11b5f72c --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing container.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap new file mode 100644 index 00000000..3fc6b6b0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/int_value.model.json +Writing src/subfolder/string_value.txt +Writing src/subfolder diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap new file mode 100644 index 00000000..6222321f --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/non-init.luau +Writing src/init-file diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap new file mode 100644 index 00000000..6f480488 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap @@ -0,0 +1,73 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: src/Message.rbxm +--- +num_types: 1 +num_instances: 1 +chunks: + - Inst: + type_id: 0 + type_name: Message + object_format: 0 + referents: + - 0 + - Prop: + type_id: 0 + prop_name: AttributesSerialize + prop_type: String + values: + - "" + - Prop: + type_id: 0 + prop_name: Capabilities + prop_type: SecurityCapabilities + values: + - 0 + - Prop: + type_id: 0 + prop_name: HistoryId + prop_type: UniqueId + values: + - "00000000000000000000000000000000" + - Prop: + type_id: 0 + prop_name: Name + prop_type: String + values: + - Message + - Prop: + type_id: 0 + prop_name: DefinesCapabilities + prop_type: Bool + values: + - false + - Prop: + type_id: 0 + prop_name: SourceAssetId + prop_type: Int64 + values: + - -1 + - Prop: + type_id: 0 + prop_name: Tags + prop_type: String + values: + - "" + - Prop: + type_id: 0 + prop_name: Text + prop_type: String + values: + - This message should be written to the disk. + - Prop: + type_id: 0 + prop_name: UniqueId + prop_type: UniqueId + values: + - 2030b7775c30713f085b9d4800005a8b + - Prnt: + version: 0 + links: + - - 0 + - -1 + - End diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap new file mode 100644 index 00000000..7449e6b1 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/Message.rbxm diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap new file mode 100644 index 00000000..f1f8c02c --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/IncludeMe/.gitkeep +Writing src/IncludeMe diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap new file mode 100644 index 00000000..b6b9aaa0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- + diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap new file mode 100644 index 00000000..f6e609d1 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap @@ -0,0 +1,8 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/dir_with_meta/init.meta.json +Writing src/model_json.model.json +Writing src/project_json.project.json +Writing src/dir_with_meta diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap new file mode 100644 index 00000000..eab1bd54 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing nested.project.json +Writing string_value.txt diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap new file mode 100644 index 00000000..1f97ad7e --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/modules/ClientModule.luau +Writing src/modules/ServerModule.luau diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap new file mode 100644 index 00000000..409b4d57 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/init.luau +Writing src diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap new file mode 100644 index 00000000..d6e4238a --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing attribute_mismatch.luau +Writing property_mismatch.project.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap new file mode 100644 index 00000000..7fe594f1 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap @@ -0,0 +1,73 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: src/ChildWithDuplicates.rbxm +--- +num_types: 1 +num_instances: 3 +chunks: + - Inst: + type_id: 0 + type_name: Folder + object_format: 0 + referents: + - 0 + - 1 + - 2 + - Prop: + type_id: 0 + prop_name: AttributesSerialize + prop_type: String + values: + - "" + - "" + - "" + - Prop: + type_id: 0 + prop_name: Capabilities + prop_type: SecurityCapabilities + values: + - 0 + - 0 + - 0 + - Prop: + type_id: 0 + prop_name: Name + prop_type: String + values: + - DuplicateChild + - DuplicateChild + - ChildWithDuplicates + - Prop: + type_id: 0 + prop_name: DefinesCapabilities + prop_type: Bool + values: + - false + - false + - false + - Prop: + type_id: 0 + prop_name: SourceAssetId + prop_type: Int64 + values: + - -1 + - -1 + - -1 + - Prop: + type_id: 0 + prop_name: Tags + prop_type: String + values: + - "" + - "" + - "" + - Prnt: + version: 0 + links: + - - 0 + - 2 + - - 1 + - 2 + - - 2 + - -1 + - End diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap new file mode 100644 index 00000000..ecf33548 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap @@ -0,0 +1,9 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/ChildWithDuplicates.rbxm +Writing src/ChildWithoutDuplicates/Child/.gitkeep +Writing src/ChildWithoutDuplicates +Writing src/ChildWithoutDuplicates/Child +Removing src/ChildWithDuplicates diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap new file mode 100644 index 00000000..774fe9d6 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/pointer.model.json +Writing src/target.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap new file mode 100644 index 00000000..a2cadfb2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/pointer.model.json +Writing src/target.meta.json +Writing src/target.txt diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap new file mode 100644 index 00000000..0942314d --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/Pointer_2.model.json +Writing src/Target_2.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap new file mode 100644 index 00000000..b6b9aaa0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- + diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap new file mode 100644 index 00000000..0c748692 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap @@ -0,0 +1,55 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: src/rbxm.rbxm +--- +num_types: 1 +num_instances: 1 +chunks: + - Inst: + type_id: 0 + type_name: Folder + object_format: 0 + referents: + - 0 + - Prop: + type_id: 0 + prop_name: AttributesSerialize + prop_type: String + values: + - "" + - Prop: + type_id: 0 + prop_name: Capabilities + prop_type: SecurityCapabilities + values: + - 0 + - Prop: + type_id: 0 + prop_name: Name + prop_type: String + values: + - rbxm + - Prop: + type_id: 0 + prop_name: DefinesCapabilities + prop_type: Bool + values: + - false + - Prop: + type_id: 0 + prop_name: SourceAssetId + prop_type: Int64 + values: + - -1 + - Prop: + type_id: 0 + prop_name: Tags + prop_type: String + values: + - rbxmx + - Prnt: + version: 0 + links: + - - 0 + - -1 + - End diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap new file mode 100644 index 00000000..05dbd817 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap @@ -0,0 +1,8 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json +Writing src/model_json.model.json +Writing src/rbxm.rbxm +Writing src/rbxmx.rbxmx diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap new file mode 100644 index 00000000..fc49a162 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json +Writing string_value.txt diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap new file mode 100644 index 00000000..00045825 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/module.modulescript +Writing src/text.text diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap new file mode 100644 index 00000000..3ef76951 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap new file mode 100644 index 00000000..b37618ce --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: OnlyOneCopy/child_of_one.luau +--- +-- this should be in OnlyOneCopy/child_of_one diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap new file mode 100644 index 00000000..8d7939df --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: ReplicatedStorage/child_replicated_storage.luau +--- +-- -- this should be in child_replicated_storage diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap new file mode 100644 index 00000000..313f4c7e --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/csv.csv +--- +Key,Source,Context,Example,es +Ack,Ack!,,An exclamation of despair,¡Ay! diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap new file mode 100644 index 00000000..22266002 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/csv_init/init.csv +--- +Key,Source,Context,Example,en +Rojo,Rojo,,Rojo is a really cool program,Red diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap new file mode 100644 index 00000000..b15329a2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap @@ -0,0 +1,25 @@ +--- +source: tests/tests/syncback.rs +expression: container.model.json +--- +{ + "className": "Folder", + "children": [ + { + "name": "value_1", + "className": "ObjectValue", + "attributes": { + "Rojo_Id": "value_1", + "Rojo_Target_Value": "value_1" + } + }, + { + "name": "value_2", + "className": "ObjectValue", + "attributes": { + "Rojo_Id": "72bc28150ada2e6206442ee300004084", + "Rojo_Target_Value": "72bc28150ada2e6206442ee300004084" + } + } + ] +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap new file mode 100644 index 00000000..85e75bdc --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/int_value.model.json +--- +{ + "className": "IntValue", + "properties": { + "Value": 1337.0 + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap new file mode 100644 index 00000000..869aefb8 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/subfolder/string_value.txt +--- +This memorial dedicated to Club Penguin. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap new file mode 100644 index 00000000..48375888 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/init-file/init.luau +--- +-- This file SHOULD NOT be updated diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap new file mode 100644 index 00000000..343aed6f --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/non-init.luau +--- +-- This module SHOULD be updated +-- This text is new. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap new file mode 100644 index 00000000..8c56c92c --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests/syncback.rs +expression: src/dir_with_meta/init.meta.json +--- +{ + "properties": { + "Tags": [ + "This tag on dir_with_meta" + ] + }, + "className": "Configuration" +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap new file mode 100644 index 00000000..7e9aacf9 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/model_json.model.json +--- +{ + "className": "StringValue", + "properties": { + "Value": "This text is model_json" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap new file mode 100644 index 00000000..00bba2f3 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap @@ -0,0 +1,17 @@ +--- +source: tests/tests/syncback.rs +expression: src/project_json.project.json +--- +{ + "name": "project_json", + "tree": { + "$className": "Color3Value", + "$properties": { + "Value": [ + 1337.0, + -1337.0, + 1337.0 + ] + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap new file mode 100644 index 00000000..5a2ebb8d --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap @@ -0,0 +1,19 @@ +--- +source: tests/tests/syncback.rs +expression: nested.project.json +--- +{ + "name": "Nested", + "tree": { + "$className": "Configuration", + "BoolValue": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + }, + "StringValue": { + "$path": "string_value.txt" + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap new file mode 100644 index 00000000..7ed1790d --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: string_value.txt +--- +Nested project string value :-) diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap new file mode 100644 index 00000000..e070784a --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/modules/ClientModule.luau +--- +-- Client module diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap new file mode 100644 index 00000000..cae56ba2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/modules/ServerModule.luau +--- +-- Server module diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap new file mode 100644 index 00000000..6c7428fb --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/init.luau +--- +-- Project init script diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap new file mode 100644 index 00000000..b59d4f60 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: attribute_mismatch.luau +--- +-- This script is a part of project_reserialize diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap new file mode 100644 index 00000000..a06e7f36 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap @@ -0,0 +1,15 @@ +--- +source: tests/tests/syncback.rs +expression: property_mismatch.project.json +--- +{ + "name": "property_mismatch", + "tree": { + "$className": "BrickColorValue", + "$properties": { + "Value": { + "BrickColor": 345 + } + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap new file mode 100644 index 00000000..3108d5c0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/pointer.model.json +--- +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "test referent id" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap new file mode 100644 index 00000000..a3aa961b --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/target.model.json +--- +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "test referent id" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap new file mode 100644 index 00000000..76186e98 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/pointer.model.json +--- +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "62e89c49e4f800c20629c71b00003fc0" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap new file mode 100644 index 00000000..83d77426 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap @@ -0,0 +1,9 @@ +--- +source: tests/tests/syncback.rs +expression: src/target.meta.json +--- +{ + "attributes": { + "Rojo_Id": "62e89c49e4f800c20629c71b00003fc0" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap new file mode 100644 index 00000000..3d35ac47 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/target.txt +--- +This is a target. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap new file mode 100644 index 00000000..555c1851 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap @@ -0,0 +1,11 @@ +--- +source: tests/tests/syncback.rs +assertion_line: 28 +expression: src/Pointer_2.model.json +--- +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "0bd3e9e11879191708a2bc4a00000c20" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap new file mode 100644 index 00000000..2314c3d3 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap @@ -0,0 +1,11 @@ +--- +source: tests/tests/syncback.rs +assertion_line: 28 +expression: src/Target_2.model.json +--- +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "0bd3e9e11879191708a2bc4a00000c20" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap new file mode 100644 index 00000000..315941cd --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap @@ -0,0 +1,16 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "respect_old_middleware", + "tree": { + "project_node": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + }, + "$path": "src" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap new file mode 100644 index 00000000..2c77ad07 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/model_json.model.json +--- +{ + "className": "StringValue", + "properties": { + "Value": "This should be a .model.json file" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap new file mode 100644 index 00000000..14c517cc --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap @@ -0,0 +1,16 @@ +--- +source: tests/tests/syncback.rs +expression: src/rbxmx.rbxmx +--- + + + + rbxmx + + 0 + false + -1 + cmJ4bXg= + + + diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap new file mode 100644 index 00000000..4e62a0c8 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap @@ -0,0 +1,22 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "string_value_project", + "tree": { + "$className": "Folder", + "inside_project_file": { + "$className": "StringValue", + "$properties": { + "Value": "imgettingverytiredofwritingthesetests2" + } + }, + "on_file_system": { + "$attributes": { + "imgettingverytiredofwritingthesetests": "person299 was ahead of his time" + }, + "$path": "string_value.txt" + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap new file mode 100644 index 00000000..7d01aac4 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/module.modulescript +--- +-- This should be a in the file 'module.modulescript'. It should be updated to have a second line. +-- This is the second line in 'module.modulescript'. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap new file mode 100644 index 00000000..dc196fc9 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/text.text +--- +-- This should be a in the file 'text.text'. It should be updated to have a second line. +-- This is the second line in 'text.text'. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap new file mode 100644 index 00000000..236064e3 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap @@ -0,0 +1,18 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "unscriptable_properties", + "tree": { + "$className": "BinaryStringValue", + "$properties": { + "Value": { + "BinaryString": "Rojo/is/cool" + } + } + }, + "syncbackRules": { + "syncUnscriptable": true + } +} diff --git a/rojo-test/syncback-tests/child_but_not/input-project/OnlyOneCopy/.gitkeep b/rojo-test/syncback-tests/child_but_not/input-project/OnlyOneCopy/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/child_but_not/input-project/ReplicatedStorage/.gitkeep b/rojo-test/syncback-tests/child_but_not/input-project/ReplicatedStorage/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/child_but_not/input-project/default.project.json b/rojo-test/syncback-tests/child_but_not/input-project/default.project.json new file mode 100644 index 00000000..d23f524c --- /dev/null +++ b/rojo-test/syncback-tests/child_but_not/input-project/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "child_but_not", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "ReplicatedStorage", + "OnlyOneCopy": { + "$path": "OnlyOneCopy" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/child_but_not/input.rbxl b/rojo-test/syncback-tests/child_but_not/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..6a10886db3acbf943ca33c19dd6e908078fed37c GIT binary patch literal 56708 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 anO4Jp&R&`{e-OPXFFw=quQ&YLFaKX(lP%Z) literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/csv/input-project/default.project.json b/rojo-test/syncback-tests/csv/input-project/default.project.json new file mode 100644 index 00000000..da44ea35 --- /dev/null +++ b/rojo-test/syncback-tests/csv/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "csv", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv b/rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv new file mode 100644 index 00000000..24244d6c --- /dev/null +++ b/rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv @@ -0,0 +1,2 @@ +Key,Source,Context,Example,en +,,,, diff --git a/rojo-test/syncback-tests/csv/input.rbxm b/rojo-test/syncback-tests/csv/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..d42d3ef51925f57c84cae4848079db13b7dcf8b5 GIT binary patch literal 1041 zcmaJ>&2AGh5cVz*N<^vJQYwlHn^lXb0C7S{s6tw`K}rQ7N^(O`IU6@Jan{k=TP0L| z0Nw�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 RxGB<854VF$&0y{6{|i`LvG4!@ literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json b/rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json new file mode 100644 index 00000000..01e24a7a --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "ignore_paths_adding", + "tree": { + "$path": "src" + }, + "syncbackRules": { + "ignorePaths": [ + "src/*.rbxm" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_paths_adding/input-project/src/.gitkeep b/rojo-test/syncback-tests/ignore_paths_adding/input-project/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ignore_paths_adding/input.rbxm b/rojo-test/syncback-tests/ignore_paths_adding/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..93331ba077c0b0e076b1f04ae73c0eed81dc01ca GIT binary patch literal 3670 zcmb7HTW=dh6y9}{G)+lJNG~+yvI!RXTuqhfx1)@r{`?+>X#lDF_& zp;nh+z?DO?KZWRQ6lPPl&&_Kj~k!T7&Zw>toChss~UoLV#9TdE5SnX3m;iR3(_pO ze4c>xfVo!S(K4=SIb8V93t!=ogkjlDF~1I*)i)L|k8=Nc+MH`!xDB zU?d5avzt9*1MXs?=tWZTiyJ%KSRYw)Pe0mlACz0Q)|gs`Mjz% zH^;%ldt)nI7gMq>#uT`?&3d}9eGj7~$rgs@ISqX;q`utl#>C&ao03vby^vb-|ZJ#h(CzDvXR#*mwU zAdD>6MscvFfV)Gz2p>p7>`1v@jr=~HWk75p4365P21&MnS>)Pm1Yri~RgD87Yb>hC zFcbl=*u5aoaKw;g6;KL$=aBhND2mvix^y+#^a9`^fYw}i9ovT_x1bTh292#)b(zoY zezCDE1AyGjB9EhCY$ovHu0WVWX^b4?j9mzwnwupGjKJZiTJr|)(g=)(B%8p(xC}OH zGFUZDJeYvYP^*(?`DW$n_3PhXymbD)+TZ^M{s3V>2uQLFG|nP_V&#+})XyL5i3olXT&n$;;`-a2KmYg(dW*JzmbF-eFa1j`d=@s89CC&1{GcSu!2Y>e zCR^74M_xFJSSRH5Fcv{wQfAeuZ^{Fsu4H6#^j(CUvbUV*R%f2P9$7Zca${SsL$lm1 z`Wb9I;oZEo3xHE&%eMuMd0UIzL}s{#%$kZQ1OSq_*c!s@MEXz0IkY?=PMOvr*E-F! z*-8g3xObc~W9vPw;aZBIevWJCKQ_O?0b|%Xqsu{LTIX})GfE~4&d^wsVQ?G5;V0*QrQzyqp^h6kRJUM-9}UV}$Hs;XZ$nrCfn^$Ti{?@5pA eoKz}H?tT2HSy^_E&K}tz&>eE{Rr+u8hyMW0<9;dt literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json b/rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json new file mode 100644 index 00000000..dabd1116 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "ignore_paths_init", + "tree": { + "$path": "src" + }, + "syncbackRules": { + "ignorePaths": [ + "**/init-file/*.luau" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau new file mode 100644 index 00000000..2879cb9b --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau @@ -0,0 +1 @@ +-- This file SHOULD NOT be updated diff --git a/rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau new file mode 100644 index 00000000..ddce6ce5 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau @@ -0,0 +1 @@ +-- This module SHOULD be updated diff --git a/rojo-test/syncback-tests/ignore_paths_init/input.rbxm b/rojo-test/syncback-tests/ignore_paths_init/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..1cc07ef2376214a3200669039038f2994918cf76 GIT binary patch literal 1111 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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json b/rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json new file mode 100644 index 00000000..ec17e8f7 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json @@ -0,0 +1,14 @@ +{ + "name": "ignore_trees_removing", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + }, + "syncbackRules": { + "ignoreTrees": [ + "ReplicatedStorage/KeepMe" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_trees_removing/input-project/src/KeepMe/.gitkeep b/rojo-test/syncback-tests/ignore_trees_removing/input-project/src/KeepMe/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ignore_trees_removing/input.rbxl b/rojo-test/syncback-tests/ignore_trees_removing/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..422bbf3500ea00a47578b90fe54081c3784eb059 GIT binary patch literal 43090 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ρHngkLo^P>B;MoIqLyhh1WLg5?%wz@_IGPYpvus639kp zHWa1I)=F**7?In+OSs*T$EmskPLv#8suzRvP6K=umfQSGSPmr1^2sdAbTg%AyIHSu53KJ?hU0VYE?gev80ov;Whv^pruX)|nhe?w`+>nceJ)(?w}z!`n+7E|1D3VZfRQH`IOnA)s$%eu>m3)L1abx*Vc1CUcLn=X>6{;TRICX6_)#OA)&tYL>-Zp7ERwztfU^k8x|@vbifR0zu9? za<`%eioiGdD%=%u`U@V&(pv4w+QMO-tZeX&;g5vuU0(o`Ji)(HOqI(F6 zEkfSa>_+N@MVLFld0x^`Cb~MUq@mHR%^Q;@-Ko>i5*McfBEZyit{G|1MD#bglA{g! zKivb6$Zg(rR=3h>?>$^wNUB}TMWN`e4{$NQr-))xo|l*_)qhMm(f?w`Tyg?2nS?RX ztz}Fad|Jk&IuFwmoiAq0rGCV57$bjM#x&%X6doeiq?5paGlxMyQEavg&JEet(20By zs%d|Kq`0LEF3uroX><#`nDt^gU71Hn6ga-$h9EX3)WQ&WO$dsuX;Iz+G1K(#cFlU_an1II^58{Vuxg&ww=!P z$dBTX%$=oZNDid%5Chg~u_zR!v50Q0EnsM;rz3yhC64->JU%NI=_g>}g%SEAd!#kn zogb6e2=b_+5w^?{jR2C-i1EM4YJ`q9nT@F?1t=;=!4@#06o8jV0i(4pAv^*&5d%Zd z{0TgHzC%=n@3xQ1=_Bbu2TN(r&t{8Pvo?A*q&YzHr6g~hEiyJck z6&)xxTWSq?*1$QL3*rF}rrhZ`P*dpbN8}6E6{HXF{k~O}n5d z)h?|swtx}!1$c?RoRP;*%S8$w2Aqf=y(>MhF5e+RUqxoNyb_rK$;xahD>E$uEU}sh zpr{}MTfk5eP=WT*lr6L%62N9aB>{DV*8<*oZgeJ z68T4Ymrq!IO6hyMbic{Zu)-39YKJ8;T0-wgrsH zZr~;CMm8uoDclDbU&7`p2?Zq(wEejmAToSADHka`3OJDsO=h=%J>GTb&H<>ZNXeE` zA|)VMDNUD433aR{B`7LL$rdo8lz^8=sUeSXTBPt^zzO3GF9(nQ%^L>zDlE78m#`d2 zmgO_$EN4txjpa~OV7V<|D9h0k94IL~2%HJ1SG-m$Irp_Y24hLEQ9CXON@S#G5CA7K zV(b|kM0(otPz!;yinwf1CE@~-5!d{O+lz}>E)Kin+G_r2UwtyJcf1vC(G_Q~!W@C& zB^uMip{gq7=L5BdgDl`73yjYUi zDyek_Xo=3CjS!H+1AyytnWX{RI(X7H)j*6WF)6$cIMc^WGswG5(N5z5mu-e%#Wm*) zGkW2053 zd0MNOxv8dAP*l(=TR^E+Q76)!I`9(dKPit-fs_=U09=>LNFTYz=Zz;bZ1IufdH)V& z6;aueN<;-DBdUpBR#ck#1P9fa4@Cv$+X6;pKJXIeYx@VBFrO_NG4ti^6$CAtD6t1Bn6y3!Dk zt_5wp>X~E0r~5(I57ZlaTDO?zsis>{RM0J3z=*m9yhOLOfB+}D#k_E=TYRV>Z_J>p z2*~DMA|N1H0Ugf@NUJz*2^EjtvodQ^15?5Ut`~ysB&7QyC~)m89K71!NM5`*nLi~D zaF3+$L7>=cblm={yrX_)b4^aici)h>BwnFO%;}^ctq%?l+m=k5Q$}Sv@oT=nVY~VI zM}kELr8Hhg(YzniFeiYYCWX5x2gebY6CWpNGS|I4z*5cBst+8-CfxwMijG37fB3^6 z{xI*Jpnw^QV!^-Z2K8@#95omBPB1|Vk3hhbX+j`eS`s(nPO+({{;gI-RIDKhk*pq2 zHB(UZUO}EfRqLA5zAmquxUKrHN#V33&nQ|x)Fz8tAwDpY!R2PN-RF}!%CQt{7DVzE zi+oHVumX&>bSYXq9hyQ4kHR+OKR|oQFQ9rtFqj}IJPsA}8$xBiQc+Tz4)-8r6R{;M zEUTF1(I9?itHZ`uj{NN=#*!z%>?49pzPvvqOTO1HZ}N(B)Sc*{#qxl~;U%}n2cWvq zHwtDrOx|{ZM!F+YQbR>~TB^kGiWEL%Iap98?=(*Z8^1WQ99E==^`MTD8Os%vyui^4 zij);6l#LXA22vgukeZjOv$d{9{R>xHw($kMU#!~8uA8SGnP1Dkbor{_ye6WgieKhy zEBtEu2y6-$Knjl)$!77{=j3g(wtV2oPhXFJR^ZPVA|Y!)Mt40}ju{p$I-NE5g5TQW zt^L@|-!`XkZm*4L2B`A==gxIss>u=OxHw8C15)@|N@uWBTzcf??_XVAeQ9;|BSU^p zz%+mqJ_>n5u3~aoHzUlHwyWxIuUTpJ&1s2J|8q(Gi#tkbX1lj8z3QeckSCq1{g{Ft z5>x4t*eUPys67*sNl!Dix#`NT1NmM<!mF_G{r|H?ytBDk+(yV z!ej@&98fE#CF3Tqd;0vjv*%BL>D=`5r_WrtaQ2I{&%b=({1>L*%ligASvBE7 zQ9*dNfT6-WE%^Z=g%9x;{c>dKk>=B*Ix82pBnyuGzudvmMghN;Jj5lE*WLwiBKf^? z!xvwo58>s-4!DZgZRRCn2a*;0(X80Dr4ig!6BZN|gk=jDDlE-!;3fPfaAlN73U>ic z_>J*k23D<#Sx!%Zfy&<5~8xtwg<4-l;>PrkwBq{G$t^2Jc5~6c7 zH}Zct0iGhFqXg|IWwg*n1NL*j=+ocKu}-p zyY`^sp%^m_5=HWP?w8u|u*%8jNX@AoQKl!Q!YCmrJWLY~$dwEX&8cH!yX2?1N2A&K zil7FXX0A0MzZw0?Dd)-(JKP6Tu=J>(pKbTO)~qkbmd2a8i_K?N4w~VkkAoJu z9WvGNVskKg4yd?cKtpPl_ez6Mt>ML$FfX^xyPdV(di{MFQzH)Dfx?9nubvk&rofa_ zh6&pMQyvsolpM1x$V(S}96)Lg-~s@U!ZF%~jWd;Jy>hWNUXquX<)zMe(>DWeW&=s# zPIAG!+c*ED4B59OO#D^(@#}JtKHUrKfLs=4jG#OAvVg-g{`+5k{qJ1;?svcY_nf)^ z@bCZ0@BenY*;@Ti|MkE9&!6A@?RVeU+Fbd{tFQk3Z>--vKE1HK{Lx1@e)wnq`OS|e z@812JyPy1<-~9O3DUI-&i}csVmBVgZ+EquE13JB>{k4zfn7-B+D5&zBPI_c4e>wWr zG00PLYx589r!|;z{Fc(iz-yT^b;phA%L9|3(|vwI=iTt*Q%BQJvO!Z^DT-QjS2-%o ze2#J(B*$B2bT8jflV^fVm9N+U&17U&`x;V@ya5hEwfYUons3Aga$2RFDwKH6d~{&y z>-Vn8LS|Et=u(p4TE726+hz4LaLt)U@ybyAJ=AagV4OMgJh^a^(WG`MtN^3XpR?e_W*@ z7__|)q9I@5f%l^7b4XwL;4 zPr$*|foImA5@+6!-%~>cN$p?0B`4SQ+-shE-^a`yNX3h$CBT>T@_E(~f6aBzotfS2 zF0Ti2;$K^T;P3PD{0X^8;R9q(%jddrpD}Q_o}6MBXApa*e4CN~Tz9GMx|?S0-S*eo zKq(~Yz+ls|oU<`7n#`l`70_$_4LO71@@o7RTjGQDBsh5+kLqp6L4ZA3)@#Z^F5P)& zjcEdt@@4^174K&opuAt68>VlT*XQI6?6~lglN7GK3YlfSikI~&TCQh=WIL14`U^#= zx}OvPg^HMPP4$tv7|a)o%InX!gLxw{GAt|Rk&@lF5xziEVp-*bNj52RO2=j6}W8+ z7?In+OSr9F2jGO;hvddgkeCQ;z{NM@L8|1?RAIXrhnSdII-@jvFjG zEwA6*DOr$?698$|we=ZMNWe>kqxTI#?{q^@Tl04Gw1>dM@L(`v~MsJ!)pMMyCqT0yc-=OB1Jsszud zBgQiqta!ju?o<_;BHt1FqKr7>J4q7=9xk5curH6L}g6g*r}>ZKJ0NY?Lc^TOQo zLQ+jpprG4hmW9(+nryrq+?}R}dBB+-h`Pv&GUAuB_v5L0&1{erG|<)~-Rc&7P31nx z#tb6!xzo_03af|O&_vYnj^^k#Tv_z*>9EuVZ*Zq@i>%7x-qtDkx zJM~P(wKV`$)^K%kXrKYMip#Qjm*^CbtWKRNcRpVdl4?2yMFr8@0*1;`O9ps}WFC{p zC*>lAM*%17rWIGcmg`SDo9OLO#zR$w@wSu_#skSPe*EX;l;bAC_ zQi9obE+NCJ*EExO>Yb3s1;LydDLe+vak-|KE>3l~I?ecq)@Xi(XxUsb(k2Y59Y>a8mdXSd>flDad4F*S+M)kz)M~gcJ)_u~Bz15R>mv>(HJi zfqU{*Hg82%0a|Q{=is$?+Bjmm4q%{y9r76)PFw8J4V!n%;z`Y7p3ge67ra9=_^76A z4d04i#`^V7qt5aH$r2SHg?lK3C~06%MWO?GZWbh(AANS_n%8O3*(V8U(2b(=q;LlW z3=t%bzk!{6^zEWe4WLRq#}o3HhLXbLfT{o7SK?Se{*yNHp}Xu$tHqiz&kLyXnH4#V z-fJffeNwVKB^N0?K_L&yb;*@2y~0QHLef;HHPw_2C-NSsYmy!c!FiUE++^*Nlt?hjiHfaQ(W_{bNg+^q%C=I^GFkMmke6%uGRKER5e}FDr^MmU$XllABRk7v0Chz5S7udaU$yGaJ`=kn8 zEYn5#Hm|(M+F6@+n(HpF+ik>$l%|wVOZHF6MG8LyX)68k9iTljJHk07R643 zHL2pjD>20^Fz*D#r(%2{<)S%xcZzW?d24d^rj^B6YG6KQ+>k%gXoLD=XT!&Ac|5Zt zk$K5D!WNL^pVxUhwZt6lM2Zn!vHd1k&i7nnn~F6@vqz%XeiI_;;G&7T2`TnA{3+J= zgkagQKB3udc#3|}PEt4ym#|zFW9gAH5D4NeOxN6Yt2--Sk&aiG^DQObGAjyhS4>sM z0`X{4kA*2A6UEiWvr!yvwf)Ga_dtTv6i3r;883p%{j6+(ijMkX)c4x*Yl7G@F8@e+ zsWvIk{$P20d}8PLSncEbczt|)eAkYH_tYki*BbS)Bei?SYdh~9ANzx2WB<1HUmh|O qR68CN(zCT^kJlcZnciJHV2^Ek`L!97E6$Ge;xmE2()MqC`Tqm^Z%>v0 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/default.project.json b/rojo-test/syncback-tests/json_middlewares/input-project/default.project.json new file mode 100644 index 00000000..75b25839 --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "json_middleware", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json b/rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json new file mode 100644 index 00000000..d6202957 --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json @@ -0,0 +1,3 @@ +{ + "className": "Configuration" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json b/rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json new file mode 100644 index 00000000..a095016a --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json @@ -0,0 +1,3 @@ +{ + "className": "StringValue" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json b/rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json new file mode 100644 index 00000000..ed845205 --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json @@ -0,0 +1,6 @@ +{ + "name": "project_json", + "tree": { + "$className": "Color3Value" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input.rbxm b/rojo-test/syncback-tests/json_middlewares/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..ef4d7d78032d244ccf22e05f503f754dd704df6c GIT binary patch literal 1551 zcmb7F-EPw`77yusd* zNAMfKcQTSKDnZJTj_ugz`#$HyZcntDCTi8Y{=K`?-C=Bl-H^H*Ux!iuh(?EhpWVJk zqYiPnS|)NPZ9lW>i;}4|h_yNsXW=+{PUELU!0n>FPaLR3>32VPk_m=9ge|B88Xy92 z53z!fflANiJk#7tl@@v*`U6^hOa$DvPwf`*lS*Qt3n5$I#yt{Y8)K_wIZ-~IJB7N@OL5sQ8g*1WX^FXD+TB)j4u)dj9 zh-hF{QJ~hUmz1wZ1Y8I5(yB-FfCDyJ$T&{K71zS7yM>VdyH!1GMTyE`JESB55pa9B ztj%0OL<3if0=2H-Viy<2wIMHEp6 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/nested_projects/expected/default.project.json b/rojo-test/syncback-tests/nested_projects/expected/default.project.json new file mode 100644 index 00000000..edb89e3c --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/expected/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "nested_projects", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Nested": { + "$path": "nested.project.json" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/expected/nested.project.json b/rojo-test/syncback-tests/nested_projects/expected/nested.project.json new file mode 100644 index 00000000..c71f3be9 --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/expected/nested.project.json @@ -0,0 +1,15 @@ +{ + "name": "Nested", + "tree": { + "$className": "Configuration", + "BoolValue": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + }, + "StringValue": { + "$path": "string_value.txt" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/expected/string_value.txt b/rojo-test/syncback-tests/nested_projects/expected/string_value.txt new file mode 100644 index 00000000..cb6a6af2 --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/expected/string_value.txt @@ -0,0 +1 @@ +effective cover predict pawn south \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/input-project/default.project.json b/rojo-test/syncback-tests/nested_projects/input-project/default.project.json new file mode 100644 index 00000000..edb89e3c --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "nested_projects", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Nested": { + "$path": "nested.project.json" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/input-project/nested.project.json b/rojo-test/syncback-tests/nested_projects/input-project/nested.project.json new file mode 100644 index 00000000..8cc8ad1e --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/input-project/nested.project.json @@ -0,0 +1,12 @@ +{ + "name": "Nested", + "tree": { + "$className": "Configuration", + "StringValue": { + "$path": "string_value.txt" + }, + "BoolValue": { + "$className": "BoolValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/input-project/string_value.txt b/rojo-test/syncback-tests/nested_projects/input-project/string_value.txt new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/nested_projects/input.rbxl b/rojo-test/syncback-tests/nested_projects/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..4b47117252b47471fd350a58f8cb2d54fa407a35 GIT binary patch literal 56532 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

-Szi)%32^y&Dhwz}q{v-LtJ!I$J80 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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json b/rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json new file mode 100644 index 00000000..9c390105 --- /dev/null +++ b/rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "rbxm_fallback", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/rbxm_fallback/input-project/src/ChildWithDuplicates/.gitkeep b/rojo-test/syncback-tests/rbxm_fallback/input-project/src/ChildWithDuplicates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/rbxm_fallback/input.rbxm b/rojo-test/syncback-tests/rbxm_fallback/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..37659d9445ca1600e77d08c17010072fe7a47255 GIT binary patch literal 549 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�^fKXmhih-kF1X?x+LH}PVV54~6Z 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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json new file mode 100644 index 00000000..765340a2 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "ref_properties_conflict", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json new file mode 100644 index 00000000..c39022dc --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json new file mode 100644 index 00000000..c39022dc --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json new file mode 100644 index 00000000..0e54f027 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json @@ -0,0 +1,6 @@ +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json new file mode 100644 index 00000000..0e54f027 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json @@ -0,0 +1,6 @@ +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input.rbxl b/rojo-test/syncback-tests/ref_properties_conflict/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..0aee7958e9f5afb5f50fd8776a058860bd72be7f GIT binary patch literal 66581 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 MXa^UX!PRH~A0jwdC;$Ke literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json new file mode 100644 index 00000000..968375e9 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "ref_properties_duplicate", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json new file mode 100644 index 00000000..3d078bc0 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "an ID that should not change" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json new file mode 100644 index 00000000..3d078bc0 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "an ID that should not change" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json new file mode 100644 index 00000000..8b47ff0c --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json @@ -0,0 +1,6 @@ +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "an ID that should not change" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm b/rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..f26cc34743b5ca47839e9d4c5d01c38c648ba967 GIT binary patch literal 990 zcmaiz+iuf95Qf*LrL-kNZPkK$Ko0b9Sq|c+sw$!;pdgW2iQ(4T#M5LelOy^f1-e%^$aomF*QFd?CaFTdmKXsI||*i`fge z;jy>Q*14XhYNTwdu=AlhSphYfO%@qEbkL8l3saud8_`AEj%0V0-L z!v?Et7sD$W*{?D!=uX9gANDW8FFYTyL9T)423yNoqlSg0d_YFYRDCCJSG<44Q_I5b zE%DgbS32${{x1763%c#VPNWU;i7wJ4$g~Yc6Pb+(o1m=&HproUfrFgOsT`_QSw(qs zQ#|lRh$(@JfXC+uSndg)Xsb<^Dx>^vA!A1VJDz=uG?oMAl6_BpBkvgphA!i&(Ni+m zEKUj#mzK~=B+ZyC7J13hAsggyMgbrLU6>KI@|tm; literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json b/rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json new file mode 100644 index 00000000..bac8234a --- /dev/null +++ b/rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "respect_old_middleware", + "tree": { + "$path": "src", + "project_node": { + "$className": "BoolValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json new file mode 100644 index 00000000..a095016a --- /dev/null +++ b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json @@ -0,0 +1,3 @@ +{ + "className": "StringValue" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..c29baaaf4a21a3fb63a4c23198136d5c63154794 GIT binary patch literal 446 zcmZWmZA!yH5Zx3JEmqMYqN29qe=ksk&_<|WEG8G&w4)59-LkuZ;0?T;N7)TN^QyeQ8lJ`)0A4|Z4hXzB&oxCGMd^<{>^10;fQ4BHo`-dnC3Pp%-#y5*? z{tAg89Ks$5$9kdc)FDXG_t9FU0 z1BtJNKsbW!2@BKMIi=2#pDr$;3KG;~XS_~)=s?D_Qop + true + null + nil + + + + 0 + false + rbxmx + -1 + + + + \ No newline at end of file diff --git a/rojo-test/syncback-tests/respect_old_middleware/input.rbxm b/rojo-test/syncback-tests/respect_old_middleware/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..6bee55304ff72a6fb8e8dc467c85711f33f6ffbc GIT binary patch literal 1254 zcmZ`(O>fgc5M7%zg_7`9REd-ix8wi^!T}+HRFDDUEJr@qzL65@&wVgpv-Q)kd_VzIw) z*liE9JFw1t7OSyJ+gYkVYBfzw1>Q8tL6_vLesn+{-vz0GkO z)=ohmWvMiMnW!*UKcw3YBe;zRBm(vZ#~SQ?I15KAR;iL^k4~bT&H{(>SWTt*KgwT2 zr)1r5B0ZLwCHhQ`)8SM{lGy|J_`!s|$*~UW>ns_|wlOk2iYOi7ke<3`vB|*8%H=1S^QmKT z==N4os8qT<;57nXEehCs3|NY{gGD0EjN+kdh7%Po7#dbsssf|iSiXGTv+4&eFMcqEr71TRq1ct!G+#(N3w3m1XD zYLY^^T;q8>kin@kfjQM#90em8gh9(~sl_b?$128!=nn3JSoGK%B=E#HAK2V+e+c1q Sx`$xozZyOdp67qJw|@bK<=+Yb literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/string_value_project/input-project/default.project.json b/rojo-test/syncback-tests/string_value_project/input-project/default.project.json new file mode 100644 index 00000000..13871dd2 --- /dev/null +++ b/rojo-test/syncback-tests/string_value_project/input-project/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "string_value_project", + "tree": { + "$className": "Folder", + "on_file_system": { + "$path": "string_value.txt" + }, + "inside_project_file": { + "$className": "StringValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/string_value_project/input-project/string_value.txt b/rojo-test/syncback-tests/string_value_project/input-project/string_value.txt new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/string_value_project/input.rbxm b/rojo-test/syncback-tests/string_value_project/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..7cafbe93fcf981363f9f54e587bbea5fe2bbef55 GIT binary patch literal 1033 zcmZ`(O>Yx15cMu4E&ZTsD}-nSbx;BIgwzA5QiT*~kcvbIO~f^FvXjJUcOBW@rs06N z@^6<*uP|@JCZMKIdb{hH=kfU2$*DEH*j&|q{@z;OUl$@2Lfm1N_}J*w4|(wPZy?_b z_-mKhxEja0uYEo9=7Z6xcPkwAHly=qyYq;@A2NY_4bKYmy@?~T0wn+lQCMQY$aj$0 zV79&0>F|q+Gb&m_mYWM(7oS@H&p>>SS@%4pmw9q+vRcLZ8x_l7#sTLaL&(VQD$8iK%bITkphflkT&J1Z>YH@6BSnku1Sv1ink9Dc$C@@+h~m~E5UKGmHg ze;!p4WdzV;)tzFE;f@-*`OYpmkRij!Hxa(Yi>hde&z!!;vS<9($&kEfk1xqi zy|xsY!NlrakstUGaWCY&3t`-ttTXBH$%&jOC)J2lB+WpMw3A*ZRBjAKf$LRtans*` z8vsiE!ici#r3M%JS3C~{epR8LJB;n>)ai)2V{5)r-**Q(CLFR!H^jK>rp{A>Rcjo3 z&IGR4P-Thzvo?dmf85^(P&4vMx$NA3gi-egzrSO^$oC<;%Pj70fc0*-y0WnP_$@2l z$Yfrwi2TSK$vN4xW|GQYHk2y$BNLn9RO(dDkK_d(NR)7-Z<0i%k!xKvJGe6CZGX)_ b=0F^V;r7h_pwT?zH15F+r!QxJ0B`;P$_Ud0 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/sync_rules/input-project/default.project.json b/rojo-test/syncback-tests/sync_rules/input-project/default.project.json new file mode 100644 index 00000000..c6f08c56 --- /dev/null +++ b/rojo-test/syncback-tests/sync_rules/input-project/default.project.json @@ -0,0 +1,16 @@ +{ + "name": "sync_rules", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "use": "text", + "pattern": "*.text" + }, + { + "use": "moduleScript", + "pattern": "*.modulescript" + } + ] +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript b/rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript new file mode 100644 index 00000000..ef32f608 --- /dev/null +++ b/rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript @@ -0,0 +1 @@ +-- This should be a in the file 'module.modulescript'. It should be updated to have a second line. \ No newline at end of file diff --git a/rojo-test/syncback-tests/sync_rules/input-project/src/text.text b/rojo-test/syncback-tests/sync_rules/input-project/src/text.text new file mode 100644 index 00000000..0bbe8d4f --- /dev/null +++ b/rojo-test/syncback-tests/sync_rules/input-project/src/text.text @@ -0,0 +1 @@ +-- This should be a in the file 'text.text'. It should be updated to have a second line. \ No newline at end of file diff --git a/rojo-test/syncback-tests/sync_rules/input.rbxm b/rojo-test/syncback-tests/sync_rules/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..c6f3beef754eefb373b6a9dfb6b01ff6e6b382a6 GIT binary patch literal 1487 zcmb7ETW-@p6dk882!skMgm_O-ML;4UM5O_#fRfflK_o;ZBH{}id)f@RcH|kO7Q_#H zu>iy#*Z>=00W85{18~ozu1HHkxze4N&)m6l?(wWdVc!q8^Phg@vgfjlO|g+vXRK*= zYl}3xvO7-}EE=66x3`DBbfvDvI(!t$K&xq5)ln=OtxoqWjn9z7k|Stm$=5^Q6A?oQ z7=V|lzi4IzJb^FA$(x}U`=aAUa;Q^y2JlPt@j?pEWoUGayrUx-Y(8`RSfuFb_R}YA zWX;K8$w{;ew*fE|Cb|9TqXXx~$FSQgZk44zu;W%JTY{)>U zKX+ve92Y&0y<`ajGjP^&wuD98Gqn@AFCx0QLTpYexGpT2N1G$>gmL7Gno>eHyfl^t zJWe)uq&@*NCV1D`RC~7@=L5@=@qrx^@R1&HC?CilN`WQWH2Py1ycXW*qF6K3jSC9P zd(=T$TShRT;Q`I=k;9S_fS`6HrH8TftgAGAS6;2ImF-*0#d`hj?c!2(y;7`KD|WHG zytHoDD^#YC^L%eX-W+biG&tg?!|&R*F|oxAXVaXhF(;(+%(6b!qLb&Yu`rXr)93`yNX6M(`0aTpBb0~J#LRyEvshYz=*W!M$0 zNEnGn=`Ztx3xI38Tf-Tk8))O+R7`Xo455BB s7MZ--(O>22>Sg|Ma<1Nv<583%I2?iZ`ipYmhta@wAp!cQi{E|r1+k7+V*mgE literal 0 HcmV?d00001 diff --git a/src/change_processor.rs b/src/change_processor.rs index c5978b8c..78d45acc 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -183,7 +183,7 @@ impl JobThreadContext { if let Some(instigating_source) = &instance.metadata().instigating_source { match instigating_source { InstigatingSource::Path(path) => fs::remove_file(path).unwrap(), - InstigatingSource::ProjectNode(_, _, _, _) => { + InstigatingSource::ProjectNode { .. } => { log::warn!( "Cannot remove instance {:?}, it's from a project file", id @@ -231,7 +231,7 @@ impl JobThreadContext { log::warn!("Cannot change Source to non-string value."); } } - InstigatingSource::ProjectNode(_, _, _, _) => { + InstigatingSource::ProjectNode { .. } => { log::warn!( "Cannot remove instance {:?}, it's from a project file", id @@ -317,16 +317,21 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option< } }, - InstigatingSource::ProjectNode(project_path, instance_name, project_node, parent_class) => { + InstigatingSource::ProjectNode { + path, + name, + node, + parent_class, + } => { // This instance is the direct subject of a project node. Since // there might be information associated with our instance from // the project file, we snapshot the entire project node again. let snapshot_result = snapshot_project_node( &metadata.context, - project_path, - instance_name, - project_node, + path, + name, + node, vfs, parent_class.as_ref().map(|name| name.as_str()), ); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 065128fb..7d3d87b1 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,6 +7,7 @@ mod init; mod plugin; mod serve; mod sourcemap; +mod syncback; mod upload; use std::{borrow::Cow, env, path::Path, str::FromStr}; @@ -21,6 +22,7 @@ pub use self::init::{InitCommand, InitKind}; pub use self::plugin::{PluginCommand, PluginSubcommand}; pub use self::serve::ServeCommand; pub use self::sourcemap::SourcemapCommand; +pub use self::syncback::SyncbackCommand; pub use self::upload::UploadCommand; /// Command line options that Rojo accepts, defined using the clap crate. @@ -46,6 +48,7 @@ impl Options { Subcommand::FmtProject(subcommand) => subcommand.run(), Subcommand::Doc(subcommand) => subcommand.run(), Subcommand::Plugin(subcommand) => subcommand.run(), + Subcommand::Syncback(subcommand) => subcommand.run(self.global), } } } @@ -119,6 +122,7 @@ pub enum Subcommand { FmtProject(FmtProjectCommand), Doc(DocCommand), Plugin(PluginCommand), + Syncback(SyncbackCommand), } pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> { diff --git a/src/cli/syncback.rs b/src/cli/syncback.rs new file mode 100644 index 00000000..282d55c8 --- /dev/null +++ b/src/cli/syncback.rs @@ -0,0 +1,282 @@ +use std::{ + io::{self, BufReader, Write as _}, + mem::forget, + path::{Path, PathBuf}, + time::Instant, +}; + +use anyhow::Context; +use clap::Parser; +use fs_err::File; +use memofs::Vfs; +use rbx_dom_weak::{InstanceBuilder, WeakDom}; +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; + +use crate::{ + path_serializer::display_absolute, + serve_session::ServeSession, + syncback::{syncback_loop, FsSnapshot}, +}; + +use super::{resolve_path, GlobalOptions}; + +const UNKNOWN_INPUT_KIND_ERR: &str = "Could not detect what kind of file was inputted. \ + Expected input file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx."; + +/// Performs 'syncback' for the provided project, using the `input` file +/// given. +/// +/// Syncback exists to convert Roblox files into a Rojo project automatically. +/// It uses the project.json file provided to traverse the Roblox file passed as +/// to serialize Instances to the file system in a format that Rojo understands. +/// +/// To ease programmatic use, this command pipes all normal output to stderr. +#[derive(Debug, Parser)] +pub struct SyncbackCommand { + /// Path to the project to sync back to. + #[clap(default_value = "")] + pub project: PathBuf, + + /// Path to the Roblox file to pull Instances from. + #[clap(long, short)] + pub input: PathBuf, + + /// If provided, a list all of the files and directories that will be + /// added or removed is emitted into stdout. + #[clap(long, short)] + pub list: bool, + + /// If provided, syncback will not actually write anything to the file + /// system. The command will otherwise run normally. + #[clap(long)] + pub dry_run: bool, + + /// If provided, the prompt for writing to the file system is skipped. + #[clap(long, short = 'y')] + pub non_interactive: bool, +} + +impl SyncbackCommand { + pub fn run(&self, global: GlobalOptions) -> anyhow::Result<()> { + let path_old = resolve_path(&self.project); + let path_new = resolve_path(&self.input); + + let input_kind = FileKind::from_path(&path_new).context(UNKNOWN_INPUT_KIND_ERR)?; + let dom_start_timer = Instant::now(); + let dom_new = read_dom(&path_new, input_kind)?; + log::debug!( + "Finished opening file in {:0.02}s", + dom_start_timer.elapsed().as_secs_f32() + ); + + let vfs = Vfs::new_default(); + vfs.set_watch_enabled(false); + + let project_start_timer = Instant::now(); + let session_old = ServeSession::new(vfs, path_old.clone())?; + log::debug!( + "Finished opening project in {:0.02}s", + project_start_timer.elapsed().as_secs_f32() + ); + + let mut dom_old = session_old.tree(); + + log::debug!("Old root: {}", dom_old.inner().root().class); + log::debug!("New root: {}", dom_new.root().class); + + if log::log_enabled!(log::Level::Trace) { + log::trace!("Children of old root:"); + for child in dom_old.inner().root().children() { + let inst = dom_old.get_instance(*child).unwrap(); + log::trace!("{} (class: {})", inst.name(), inst.class_name()); + } + log::trace!("Children of new root:"); + for child in dom_new.root().children() { + let inst = dom_new.get_by_ref(*child).unwrap(); + log::trace!("{} (class: {})", inst.name, inst.class); + } + } + + let syncback_timer = Instant::now(); + eprintln!("Beginning syncback..."); + let snapshot = syncback_loop( + session_old.vfs(), + &mut dom_old, + dom_new, + session_old.root_project(), + )?; + log::debug!( + "Syncback finished in {:.02}s!", + syncback_timer.elapsed().as_secs_f32() + ); + + let base_path = session_old.root_project().folder_location(); + if self.list { + list_files(&snapshot, global.color.into(), base_path)?; + } + + if !self.dry_run { + if !self.non_interactive { + eprintln!( + "Would write {} files/folders and remove {} files/folders.", + snapshot.added_paths().len(), + snapshot.removed_paths().len() + ); + eprint!("Is this okay? (Y/N): "); + io::stderr().flush()?; + let mut line = String::with_capacity(1); + io::stdin().read_line(&mut line)?; + line = line.trim().to_lowercase(); + if line != "y" { + eprintln!("Aborting due to user input!"); + return Ok(()); + } + } + eprintln!("Writing to the file system..."); + snapshot.write_to_vfs(base_path, session_old.vfs())?; + eprintln!("Finished syncback.") + } else { + eprintln!( + "Would write {} files/folders and remove {} files/folders.", + snapshot.added_paths().len(), + snapshot.removed_paths().len() + ); + eprintln!("Aborting before writing to file system due to `--dry-run`"); + } + + // It is potentially prohibitively expensive to drop a ServeSession, + // and the program is about to exit anyway so we're just going to forget + // about it. + drop(dom_old); + forget(session_old); + + Ok(()) + } +} + +fn read_dom(path: &Path, file_kind: FileKind) -> anyhow::Result { + let content = BufReader::new(File::open(path)?); + match file_kind { + FileKind::Rbxl => rbx_binary::from_reader(content).with_context(|| { + format!( + "Could not deserialize binary place file at {}", + path.display() + ) + }), + FileKind::Rbxlx => rbx_xml::from_reader(content, xml_decode_config()) + .with_context(|| format!("Could not deserialize XML place file at {}", path.display())), + FileKind::Rbxm => { + let temp_tree = rbx_binary::from_reader(content).with_context(|| { + format!( + "Could not deserialize binary place file at {}", + path.display() + ) + })?; + + process_model_dom(temp_tree) + } + FileKind::Rbxmx => { + let temp_tree = + rbx_xml::from_reader(content, xml_decode_config()).with_context(|| { + format!("Could not deserialize XML model file at {}", path.display()) + })?; + process_model_dom(temp_tree) + } + } +} + +fn process_model_dom(dom: WeakDom) -> anyhow::Result { + let temp_children = dom.root().children(); + if temp_children.len() == 1 { + let real_root = dom.get_by_ref(temp_children[0]).unwrap(); + let mut new_tree = WeakDom::new(InstanceBuilder::new(real_root.class)); + for (name, property) in &real_root.properties { + new_tree + .root_mut() + .properties + .insert(*name, property.to_owned()); + } + + let children = dom.clone_multiple_into_external(real_root.children(), &mut new_tree); + for child in children { + new_tree.transfer_within(child, new_tree.root_ref()); + } + Ok(new_tree) + } else { + anyhow::bail!( + "Rojo does not currently support models with more \ + than one Instance at the Root!" + ); + } +} + +fn xml_decode_config() -> rbx_xml::DecodeOptions<'static> { + rbx_xml::DecodeOptions::new().property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown) +} + +/// The different kinds of input that Rojo can syncback. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FileKind { + /// An XML model file. + Rbxmx, + + /// An XML place file. + Rbxlx, + + /// A binary model file. + Rbxm, + + /// A binary place file. + Rbxl, +} + +impl FileKind { + fn from_path(output: &Path) -> Option { + let extension = output.extension()?.to_str()?; + + match extension { + "rbxlx" => Some(FileKind::Rbxlx), + "rbxmx" => Some(FileKind::Rbxmx), + "rbxl" => Some(FileKind::Rbxl), + "rbxm" => Some(FileKind::Rbxm), + _ => None, + } + } +} + +fn list_files(snapshot: &FsSnapshot, color: ColorChoice, base_path: &Path) -> io::Result<()> { + let no_color = ColorSpec::new(); + let mut add_color = ColorSpec::new(); + add_color.set_fg(Some(Color::Green)); + let mut remove_color = ColorSpec::new(); + remove_color.set_fg(Some(Color::Red)); + + let writer = BufferWriter::stdout(color); + let mut buffer = writer.buffer(); + + let added = snapshot.added_paths(); + if !added.is_empty() { + buffer.set_color(&add_color)?; + for path in added { + writeln!( + &mut buffer, + "Writing {}", + display_absolute(path.strip_prefix(base_path).unwrap_or(path)) + )?; + } + } + let removed = snapshot.removed_paths(); + if !removed.is_empty() { + buffer.set_color(&remove_color)?; + for path in removed { + writeln!( + &mut buffer, + "Removing {}", + display_absolute(path.strip_prefix(base_path).unwrap_or(path)) + )?; + } + } + buffer.set_color(&no_color)?; + + writer.print(&buffer) +} diff --git a/src/lib.rs b/src/lib.rs index 864f8fdc..f1947eaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,9 +22,19 @@ mod serve_session; mod session_id; mod snapshot; mod snapshot_middleware; +mod syncback; +mod variant_eq; mod web; +// TODO: Work out what we should expose publicly + pub use project::*; pub use rojo_ref::*; pub use session_id::SessionId; +pub use snapshot::{ + InstanceContext, InstanceMetadata, InstanceSnapshot, InstanceWithMeta, InstanceWithMetaMut, + RojoDescendants, RojoTree, +}; +pub use snapshot_middleware::{snapshot_from_vfs, Middleware, ScriptType}; +pub use syncback::{syncback_loop, FsSnapshot, SyncbackData, SyncbackSnapshot}; pub use web::interface as web_api; diff --git a/src/path_serializer.rs b/src/path_serializer.rs index 29edaf82..2ddd3a4e 100644 --- a/src/path_serializer.rs +++ b/src/path_serializer.rs @@ -5,24 +5,32 @@ use std::path::Path; use serde::{ser::SerializeSeq, Serialize, Serializer}; -pub fn serialize_absolute(path: T, serializer: S) -> Result -where - S: Serializer, - T: AsRef, -{ +/// Converts the provided value into a String with all directory separators +/// converted into `/`. +pub fn display_absolute>(path: T) -> String { let as_str = path .as_ref() .as_os_str() .to_str() .expect("Invalid Unicode in file path, cannot serialize"); - let replaced = as_str.replace('\\', "/"); + as_str.replace('\\', "/") +} - serializer.serialize_str(&replaced) +/// A serializer for serde that serialize a value with all directory separators +/// converted into `/`. +pub fn serialize_absolute(path: T, serializer: S) -> Result +where + S: Serializer, + T: AsRef, +{ + serializer.serialize_str(&display_absolute(path)) } #[derive(Serialize)] struct WithAbsolute<'a>(#[serde(serialize_with = "serialize_absolute")] &'a Path); +/// A serializer for serde that serialize a list of values with all directory +/// separators converted into `/`. pub fn serialize_vec_absolute(paths: &[T], serializer: S) -> Result where S: Serializer, diff --git a/src/project.rs b/src/project.rs index a3bb00df..20c2dc9c 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, ffi::OsStr, fs, io, net::IpAddr, @@ -7,11 +7,13 @@ use std::{ }; use memofs::Vfs; -use rbx_dom_weak::{Ustr, UstrMap}; +use rbx_dom_weak::Ustr; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule}; +use crate::{ + glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule, syncback::SyncbackRules, +}; /// Represents 'default' project names that act as `init` files pub static DEFAULT_PROJECT_NAMES: [&str; 2] = ["default.project.json", "default.project.jsonc"]; @@ -114,6 +116,10 @@ pub struct Project { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub glob_ignore_paths: Vec, + /// A list of rules for syncback with this project file. + #[serde(skip_serializing_if = "Option::is_none")] + pub syncback_rules: Option, + /// A list of mappings of globs to syncing rules. If a file matches a glob, /// it will be 'transformed' into an Instance following the rule provided. /// Globs are relative to the folder the project file is in. @@ -332,12 +338,21 @@ pub enum PathNode { } impl PathNode { + /// Returns the path of the `PathNode`, without regard for if it's optional + // or not. + #[inline] pub fn path(&self) -> &Path { match self { PathNode::Required(pathbuf) => pathbuf, PathNode::Optional(OptionalPathNode { optional }) => optional, } } + + /// Returns whether this `PathNode` is optional or not. + #[inline] + pub fn is_optional(&self) -> bool { + matches!(self, PathNode::Optional(_)) + } } /// Describes an instance and its descendants in a project. @@ -367,16 +382,16 @@ pub struct ProjectNode { #[serde( rename = "$properties", default, - skip_serializing_if = "HashMap::is_empty" + skip_serializing_if = "BTreeMap::is_empty" )] - pub properties: UstrMap, + pub properties: BTreeMap, #[serde( rename = "$attributes", default, - skip_serializing_if = "HashMap::is_empty" + skip_serializing_if = "BTreeMap::is_empty" )] - pub attributes: HashMap, + pub attributes: BTreeMap, /// Defines the behavior when Rojo encounters unknown instances in Roblox /// Studio during live sync. `$ignoreUnknownInstances` should be considered diff --git a/src/resolution.rs b/src/resolution.rs index 86132c35..1e743efd 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -2,8 +2,8 @@ use std::borrow::Borrow; use anyhow::{bail, format_err}; use rbx_dom_weak::types::{ - Attributes, CFrame, Color3, Content, ContentId, Enum, Font, MaterialColors, Matrix3, Tags, - Variant, VariantType, Vector2, Vector3, + Attributes, CFrame, Color3, Content, ContentId, ContentType, Enum, Font, MaterialColors, + Matrix3, Tags, Variant, VariantType, Vector2, Vector3, }; use rbx_reflection::{DataType, PropertyDescriptor}; use serde::{Deserialize, Serialize}; @@ -37,6 +37,98 @@ impl UnresolvedValue { UnresolvedValue::Ambiguous(partial) => partial.resolve_unambiguous(), } } + + /// Creates an `UnresolvedValue` from a variant, using a class and property + /// name to potentially allow for ambiguous Enum variants. + pub fn from_variant(variant: Variant, class_name: &str, prop_name: &str) -> Self { + let descriptor = find_descriptor(class_name, prop_name); + if descriptor.is_some() { + // We can only use an ambiguous syntax if the property is known + // to the reflection database. + Self::Ambiguous(match variant { + Variant::Enum(rbx_enum) => { + if let Some(property) = descriptor { + if let DataType::Enum(enum_name) = &property.data_type { + let database = rbx_reflection_database::get().unwrap(); + if let Some(enum_descriptor) = database.enums.get(enum_name) { + for (variant_name, id) in &enum_descriptor.items { + if *id == rbx_enum.to_u32() { + return Self::Ambiguous(AmbiguousValue::String( + variant_name.to_string(), + )); + } + } + } + } + } + return Self::FullyQualified(variant); + } + Variant::Bool(bool) => AmbiguousValue::Bool(bool), + Variant::Float32(n) => AmbiguousValue::Number(n as f64), + Variant::Float64(n) => AmbiguousValue::Number(n), + Variant::Int32(n) => AmbiguousValue::Number(n as f64), + Variant::Int64(n) => AmbiguousValue::Number(n as f64), + Variant::String(str) => AmbiguousValue::String(str), + Variant::Tags(tags) => { + AmbiguousValue::StringArray(tags.iter().map(|s| s.to_string()).collect()) + } + Variant::Content(ref content) => match content.value() { + ContentType::None => AmbiguousValue::String(String::new()), + ContentType::Uri(uri) => AmbiguousValue::String(uri.clone()), + _ => return Self::FullyQualified(variant), + }, + Variant::ContentId(content) => AmbiguousValue::String(content.into_string()), + Variant::Vector2(vector) => { + AmbiguousValue::Array2([vector.x as f64, vector.y as f64]) + } + Variant::Vector3(vector) => { + AmbiguousValue::Array3([vector.x as f64, vector.y as f64, vector.z as f64]) + } + Variant::Color3(color) => { + AmbiguousValue::Array3([color.r as f64, color.g as f64, color.b as f64]) + } + Variant::CFrame(cf) => AmbiguousValue::Array12([ + cf.position.x as f64, + cf.position.y as f64, + cf.position.z as f64, + cf.orientation.x.x as f64, + cf.orientation.x.y as f64, + cf.orientation.x.z as f64, + cf.orientation.y.x as f64, + cf.orientation.y.y as f64, + cf.orientation.y.z as f64, + cf.orientation.z.x as f64, + cf.orientation.z.y as f64, + cf.orientation.z.z as f64, + ]), + Variant::Attributes(attr) => AmbiguousValue::Attributes(attr), + Variant::Font(font) => AmbiguousValue::Font(font), + Variant::MaterialColors(colors) => AmbiguousValue::MaterialColors(colors), + _ => { + return Self::FullyQualified(variant); + } + }) + } else { + Self::FullyQualified(variant) + } + } + + /// Creates an `UnresolvedValue` from a variant, only returning ambiguous + /// values if they're able to be resolved in a context-free environment. + pub fn from_variant_unambiguous(variant: Variant) -> Self { + match variant { + Variant::String(str) => Self::Ambiguous(AmbiguousValue::String(str)), + Variant::Float64(number) => Self::Ambiguous(AmbiguousValue::Number(number)), + Variant::Bool(bool) => Self::Ambiguous(AmbiguousValue::Bool(bool)), + Variant::BinaryString(bstr) => match std::str::from_utf8(bstr.as_ref()) { + Ok(_) => Self::Ambiguous(AmbiguousValue::String( + String::from_utf8(bstr.into_vec()).unwrap(), + )), + Err(_) => Self::FullyQualified(Variant::BinaryString(bstr)), + }, + _ => Self::FullyQualified(variant), + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/serve_session.rs b/src/serve_session.rs index cb3e6f5b..f50d3926 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -210,6 +210,10 @@ impl ServeSession { pub fn root_dir(&self) -> &Path { self.root_project.folder_location() } + + pub fn root_project(&self) -> &Project { + &self.root_project + } } #[derive(Debug, Error)] diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs index 20505f29..71a731ef 100644 --- a/src/snapshot/metadata.rs +++ b/src/snapshot/metadata.rs @@ -62,6 +62,10 @@ pub struct InstanceMetadata { /// Indicates the ID used for Ref properties pointing to this Instance. pub specified_id: Option, + + /// The Middleware that was used to create this Instance. Should generally + /// not be `None` except if the snapshotting process is not completed. + pub middleware: Option, } impl InstanceMetadata { @@ -72,6 +76,7 @@ impl InstanceMetadata { relevant_paths: Vec::new(), context: InstanceContext::default(), specified_id: None, + middleware: None, } } @@ -109,6 +114,13 @@ impl InstanceMetadata { ..self } } + + pub fn middleware(self, middleware: Middleware) -> Self { + Self { + middleware: Some(middleware), + ..self + } + } } impl Default for InstanceMetadata { @@ -215,22 +227,40 @@ impl PathIgnoreRule { } } +/// Represents where a particular Instance or InstanceSnapshot came from. #[derive(Clone, PartialEq, Serialize, Deserialize)] pub enum InstigatingSource { + /// The path the Instance was made from. Path(#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf), - ProjectNode( - #[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf, - String, - Box, - Option, - ), + /// The node in a Project that the Instance was made from. + ProjectNode { + #[serde(serialize_with = "path_serializer::serialize_absolute")] + path: PathBuf, + name: String, + node: ProjectNode, + parent_class: Option, + }, +} + +impl InstigatingSource { + pub fn path(&self) -> &Path { + match self { + Self::Path(path) => path.as_path(), + Self::ProjectNode { path, .. } => path.as_path(), + } + } } impl fmt::Debug for InstigatingSource { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()), - InstigatingSource::ProjectNode(path, name, node, parent_class) => write!( + InstigatingSource::ProjectNode { + name, + node, + path, + parent_class, + } => write!( formatter, "ProjectNode({}: {:?}) from path {} and parent class {:?}", name, diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap index 2f3852b5..c4390b6d 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap @@ -14,5 +14,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap index 31bdfecc..fa4d3ebc 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap @@ -12,5 +12,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap index cafc3f5e..f07da090 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap @@ -14,5 +14,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap index 359ee86e..cc582633 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap @@ -12,5 +12,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap index 9147ca9b..4c404604 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap @@ -13,6 +13,7 @@ added_instances: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: New class_name: Folder properties: {} diff --git a/src/snapshot/tree.rs b/src/snapshot/tree.rs index 76bcdc07..9e891f2c 100644 --- a/src/snapshot/tree.rs +++ b/src/snapshot/tree.rs @@ -73,6 +73,13 @@ impl RojoTree { self.inner.root_ref() } + /// Returns the root Instance of this tree. + #[inline] + pub fn root(&self) -> InstanceWithMeta<'_> { + self.get_instance(self.get_root_id()) + .expect("RojoTrees should have a root") + } + pub fn get_instance(&self, id: Ref) -> Option> { if let Some(instance) = self.inner.get_by_ref(id) { let metadata = self.metadata_map.get(&id).unwrap(); @@ -322,6 +329,10 @@ impl<'a> InstanceWithMeta<'a> { pub fn metadata(&self) -> &'a InstanceMetadata { self.metadata } + + pub fn inner(&self) -> &Instance { + self.instance + } } /// RojoTree's equivalent of `&'a mut Instance`. @@ -371,6 +382,14 @@ impl InstanceWithMetaMut<'_> { pub fn metadata(&self) -> &InstanceMetadata { self.metadata } + + pub fn inner(&self) -> &Instance { + self.instance + } + + pub fn inner_mut(&mut self) -> &mut Instance { + self.instance + } } #[cfg(test)] diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs index e81aa1c3..75a043d1 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -1,16 +1,24 @@ -use std::{collections::BTreeMap, path::Path}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + path::Path, +}; use anyhow::Context; use memofs::Vfs; -use rbx_dom_weak::ustr; -use serde::Serialize; +use rbx_dom_weak::{types::Variant, ustr}; +use serde::{Deserialize, Serialize}; use crate::{ snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, - snapshot_middleware::meta_file::DirectoryMetadata, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, }; -use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata}; +use super::{ + dir::{snapshot_dir_no_meta, syncback_dir_no_meta}, + meta_file::{AdjacentMetadata, DirectoryMetadata}, + PathExt as _, +}; pub fn snapshot_csv( _context: &InstanceContext, @@ -51,9 +59,10 @@ pub fn snapshot_csv_init( context: &InstanceContext, vfs: &Vfs, init_path: &Path, + name: &str, ) -> anyhow::Result> { let folder_path = init_path.parent().unwrap(); - let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap(); + let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path, name)?.unwrap(); if dir_snapshot.class_name != "Folder" { anyhow::bail!( @@ -70,33 +79,111 @@ pub fn snapshot_csv_init( init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; + // The directory snapshot middleware includes all possible init paths + // so we don't need to add it here. DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?; Ok(Some(init_snapshot)) } +pub fn syncback_csv<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = + if let Some(Variant::String(content)) = new_inst.properties.get(&ustr("Contents")) { + content.as_str() + } else { + anyhow::bail!("LocalizationTables must have a `Contents` property that is a String") + }; + let mut fs_snapshot = FsSnapshot::new(); + fs_snapshot.add_file(&snapshot.path, localization_to_csv(contents)?); + + let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(mut meta) = meta { + // LocalizationTables have relatively few properties that we care + // about, so shifting is fine. + meta.properties.shift_remove(&ustr("Contents")); + + if !meta.is_empty() { + let parent = snapshot.path.parent_err()?; + fs_snapshot.add_file( + parent.join(format!("{}.meta.json", new_inst.name)), + serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?, + ) + } + } + + Ok(SyncbackReturn { + fs_snapshot, + children: Vec::new(), + removed_children: Vec::new(), + }) +} + +pub fn syncback_csv_init<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = + if let Some(Variant::String(content)) = new_inst.properties.get(&ustr("Contents")) { + content.as_str() + } else { + anyhow::bail!("LocalizationTables must have a `Contents` property that is a String") + }; + + let mut dir_syncback = syncback_dir_no_meta(snapshot)?; + dir_syncback.fs_snapshot.add_file( + snapshot.path.join("init.csv"), + localization_to_csv(contents)?, + ); + + let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(mut meta) = meta { + // LocalizationTables have relatively few properties that we care + // about, so shifting is fine. + meta.properties.shift_remove(&ustr("Contents")); + if !meta.is_empty() { + dir_syncback.fs_snapshot.add_file( + snapshot.path.join("init.meta.json"), + serde_json::to_vec_pretty(&meta) + .context("could not serialize new init.meta.json")?, + ); + } + } + + Ok(dir_syncback) +} + /// Struct that holds any valid row from a Roblox CSV translation table. /// /// We manually deserialize into this table from CSV, but let serde_json handle /// serialization. -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct LocalizationEntry<'a> { #[serde(skip_serializing_if = "Option::is_none")] - key: Option<&'a str>, + key: Option>, #[serde(skip_serializing_if = "Option::is_none")] - context: Option<&'a str>, + context: Option>, + + // Roblox writes `examples` for LocalizationTable's Content property, which + // causes it to not roundtrip correctly. + // This is reported here: https://devforum.roblox.com/t/2908720. + // + // To support their mistake, we support an alias named `examples`. + #[serde(skip_serializing_if = "Option::is_none", alias = "examples")] + example: Option>, #[serde(skip_serializing_if = "Option::is_none")] - example: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - source: Option<&'a str>, + source: Option>, // We use a BTreeMap here to get deterministic output order. - values: BTreeMap<&'a str, &'a str>, + values: BTreeMap, Cow<'a, str>>, } /// Normally, we'd be able to let the csv crate construct our struct for us. @@ -130,12 +217,14 @@ fn convert_localization_csv(contents: &[u8]) -> Result { } match header { - "Key" => entry.key = Some(value), - "Source" => entry.source = Some(value), - "Context" => entry.context = Some(value), - "Example" => entry.example = Some(value), + "Key" => entry.key = Some(Cow::Borrowed(value)), + "Source" => entry.source = Some(Cow::Borrowed(value)), + "Context" => entry.context = Some(Cow::Borrowed(value)), + "Example" => entry.example = Some(Cow::Borrowed(value)), _ => { - entry.values.insert(header, value); + entry + .values + .insert(Cow::Borrowed(header), Cow::Borrowed(value)); } } } @@ -153,6 +242,57 @@ fn convert_localization_csv(contents: &[u8]) -> Result { Ok(encoded) } +/// Takes a localization table (as a string) and converts it into a CSV file. +/// +/// The CSV file is ordered, so it should be deterministic. +fn localization_to_csv(csv_contents: &str) -> anyhow::Result> { + let mut out = Vec::new(); + let mut writer = csv::Writer::from_writer(&mut out); + + let mut csv: Vec = + serde_json::from_str(csv_contents).context("cannot decode JSON from localization table")?; + + // TODO sort this better + csv.sort_by(|a, b| a.source.partial_cmp(&b.source).unwrap()); + + let mut headers = vec!["Key", "Source", "Context", "Example"]; + // We want both order and a lack of duplicates, so we use a BTreeSet. + let mut extra_headers = BTreeSet::new(); + for entry in &csv { + for lang in entry.values.keys() { + extra_headers.insert(lang.as_ref()); + } + } + headers.extend(extra_headers.iter()); + + writer + .write_record(&headers) + .context("could not write headers for localization table")?; + + let mut record: Vec<&str> = Vec::with_capacity(headers.len()); + for entry in &csv { + record.push(entry.key.as_deref().unwrap_or_default()); + record.push(entry.source.as_deref().unwrap_or_default()); + record.push(entry.context.as_deref().unwrap_or_default()); + record.push(entry.example.as_deref().unwrap_or_default()); + + let values = &entry.values; + for header in &extra_headers { + record.push(values.get(*header).map(AsRef::as_ref).unwrap_or_default()); + } + + writer + .write_record(&record) + .context("cannot write record for localization table")?; + record.clear(); + } + + // We must drop `writer` here to regain access to `out`. + drop(writer); + + Ok(out) +} + #[cfg(test)] mod test { use super::*; @@ -240,6 +380,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.csv"), + "root", ) .unwrap() .unwrap(); @@ -277,6 +418,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.csv"), + "root", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/dir.rs b/src/snapshot_middleware/dir.rs index 18959cd5..b825542d 100644 --- a/src/snapshot_middleware/dir.rs +++ b/src/snapshot_middleware/dir.rs @@ -1,17 +1,27 @@ -use std::path::Path; +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; +use anyhow::Context; use memofs::{DirEntry, Vfs}; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource}, + syncback::{hash_instance, FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; use super::{meta_file::DirectoryMetadata, snapshot_from_vfs}; +const EMPTY_DIR_KEEP_NAME: &str = ".gitkeep"; + pub fn snapshot_dir( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let mut snapshot = match snapshot_dir_no_meta(context, vfs, path)? { + let mut snapshot = match snapshot_dir_no_meta(context, vfs, path, name)? { Some(snapshot) => snapshot, None => return Ok(None), }; @@ -29,6 +39,7 @@ pub fn snapshot_dir_no_meta( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { let passes_filter_rules = |child: &DirEntry| { context @@ -51,13 +62,6 @@ pub fn snapshot_dir_no_meta( } } - let instance_name = path - .file_name() - .expect("Could not extract file name") - .to_str() - .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", path.display()))? - .to_string(); - let relevant_paths = vec![ path.to_path_buf(), // TODO: We shouldn't need to know about Lua existing in this @@ -73,7 +77,7 @@ pub fn snapshot_dir_no_meta( ]; let snapshot = InstanceSnapshot::new() - .name(instance_name) + .name(name) .class_name("Folder") .children(snapshot_children) .metadata( @@ -86,6 +90,136 @@ pub fn snapshot_dir_no_meta( Ok(Some(snapshot)) } +pub fn syncback_dir<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let mut dir_syncback = syncback_dir_no_meta(snapshot)?; + + let mut meta = DirectoryMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(meta) = &mut meta { + if new_inst.class != "Folder" { + meta.class_name = Some(new_inst.class); + } + + if !meta.is_empty() { + dir_syncback.fs_snapshot.add_file( + snapshot.path.join("init.meta.json"), + serde_json::to_vec_pretty(&meta) + .context("could not serialize new init.meta.json")?, + ); + } + } + + let metadata_empty = meta + .as_ref() + .map(DirectoryMetadata::is_empty) + .unwrap_or_default(); + if new_inst.children().is_empty() && metadata_empty { + dir_syncback + .fs_snapshot + .add_file(snapshot.path.join(EMPTY_DIR_KEEP_NAME), Vec::new()) + } + + Ok(dir_syncback) +} + +pub fn syncback_dir_no_meta<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let mut children = Vec::new(); + let mut removed_children = Vec::new(); + + // We have to enforce unique child names for the file system. + let mut child_names = HashSet::with_capacity(new_inst.children().len()); + let mut duplicate_set = HashSet::new(); + for child_ref in new_inst.children() { + let child = snapshot.get_new_instance(*child_ref).unwrap(); + if !child_names.insert(child.name.to_lowercase()) { + duplicate_set.insert(child.name.as_str()); + } + } + if !duplicate_set.is_empty() { + if duplicate_set.len() <= 25 { + anyhow::bail!( + "Instance has children with duplicate name (case may not exactly match):\n {}", + duplicate_set.into_iter().collect::>().join(", ") + ); + } + anyhow::bail!("Instance has more than 25 children with duplicate names"); + } + + if let Some(old_inst) = snapshot.old_inst() { + let mut old_child_map = HashMap::with_capacity(old_inst.children().len()); + for child in old_inst.children() { + let inst = snapshot.get_old_instance(*child).unwrap(); + old_child_map.insert(inst.name(), inst); + } + + for new_child_ref in new_inst.children() { + let new_child = snapshot.get_new_instance(*new_child_ref).unwrap(); + if let Some(old_child) = old_child_map.remove(new_child.name.as_str()) { + if old_child.metadata().relevant_paths.is_empty() { + log::debug!( + "Skipping instance {} because it doesn't exist on the disk", + old_child.name() + ); + continue; + } else if matches!( + old_child.metadata().instigating_source, + Some(InstigatingSource::ProjectNode { .. }) + ) { + log::debug!( + "Skipping instance {} because it originates in a project file", + old_child.name() + ); + continue; + } + // This child exists in both doms. Pass it on. + children.push(snapshot.with_joined_path(*new_child_ref, Some(old_child.id()))?); + } else { + // The child only exists in the the new dom + children.push(snapshot.with_joined_path(*new_child_ref, None)?); + } + } + // Any children that are in the old dom but not the new one are removed. + removed_children.extend(old_child_map.into_values()); + } else { + // There is no old instance. Just add every child. + for new_child_ref in new_inst.children() { + children.push(snapshot.with_joined_path(*new_child_ref, None)?); + } + } + let mut fs_snapshot = FsSnapshot::new(); + + if let Some(old_ref) = snapshot.old { + let new_hash = hash_instance(snapshot.project(), snapshot.new_tree(), snapshot.new) + .expect("new Instance should be hashable"); + let old_hash = hash_instance(snapshot.project(), snapshot.old_tree(), old_ref) + .expect("old Instance should be hashable"); + + if old_hash != new_hash { + fs_snapshot.add_dir(&snapshot.path); + } else { + log::debug!( + "Skipping reserializing directory {} because old and new tree hash the same", + new_inst.name + ); + } + } else { + fs_snapshot.add_dir(&snapshot.path); + } + + Ok(SyncbackReturn { + fs_snapshot, + children, + removed_children, + }) +} + #[cfg(test)] mod test { use super::*; @@ -100,9 +234,10 @@ mod test { let vfs = Vfs::new(imfs); - let instance_snapshot = snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo")) - .unwrap() - .unwrap(); + let instance_snapshot = + snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo"), "foo") + .unwrap() + .unwrap(); insta::assert_yaml_snapshot!(instance_snapshot); } @@ -118,9 +253,10 @@ mod test { let vfs = Vfs::new(imfs); - let instance_snapshot = snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo")) - .unwrap() - .unwrap(); + let instance_snapshot = + snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo"), "foo") + .unwrap() + .unwrap(); insta::assert_yaml_snapshot!(instance_snapshot); } diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs index 2b472095..a6f0148f 100644 --- a/src/snapshot_middleware/json_model.rs +++ b/src/snapshot_middleware/json_model.rs @@ -1,17 +1,19 @@ -use std::{borrow::Cow, collections::HashMap, path::Path, str}; +use std::{borrow::Cow, path::Path, str}; use anyhow::Context; +use indexmap::IndexMap; use memofs::Vfs; use rbx_dom_weak::{ - types::{Attributes, Ref}, + types::{Attributes, Ref, Variant}, HashMapExt as _, Ustr, UstrMap, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::{ json, resolution::UnresolvedValue, snapshot::{InstanceContext, InstanceSnapshot}, + syncback::{filter_properties_preallocated, FsSnapshot, SyncbackReturn, SyncbackSnapshot}, RojoRef, }; @@ -63,13 +65,86 @@ pub fn snapshot_json_model( Ok(Some(snapshot)) } -#[derive(Debug, Deserialize)] +pub fn syncback_json_model<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let mut property_buffer = Vec::with_capacity(snapshot.new_inst().properties.len()); + + let mut model = json_model_from_pair(snapshot, &mut property_buffer, snapshot.new); + // We don't need the name on the root, but we do for children. + model.name = None; + + Ok(SyncbackReturn { + fs_snapshot: FsSnapshot::new().with_added_file( + &snapshot.path, + serde_json::to_vec_pretty(&model).context("failed to serialize new JSON Model")?, + ), + children: Vec::new(), + removed_children: Vec::new(), + }) +} + +fn json_model_from_pair<'sync>( + snapshot: &SyncbackSnapshot<'sync>, + prop_buffer: &mut Vec<(Ustr, &'sync Variant)>, + new: Ref, +) -> JsonModel { + let new_inst = snapshot + .get_new_instance(new) + .expect("all new referents passed to json_model_from_pair should exist"); + + filter_properties_preallocated(snapshot.project(), new_inst, prop_buffer); + + let mut properties = IndexMap::new(); + let mut attributes = IndexMap::new(); + for (name, value) in prop_buffer.drain(..) { + match value { + Variant::Attributes(attrs) => { + for (attr_name, attr_value) in attrs.iter() { + // We (probably) don't want to preserve internal attributes, + // only user defined ones. + if attr_name.starts_with("RBX") { + continue; + } + attributes.insert( + attr_name.clone(), + UnresolvedValue::from_variant_unambiguous(attr_value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), &new_inst.class, &name), + ); + } + } + } + + let mut children = Vec::with_capacity(new_inst.children().len()); + + for new_child_ref in new_inst.children() { + children.push(json_model_from_pair(snapshot, prop_buffer, *new_child_ref)) + } + + JsonModel { + name: Some(new_inst.name.clone()), + class_name: new_inst.class, + children, + properties, + attributes, + id: None, + schema: None, + } +} + +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct JsonModel { #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] schema: Option, - #[serde(alias = "Name")] + #[serde(alias = "Name", skip_serializing_if = "Option::is_none")] name: Option, #[serde(alias = "ClassName")] @@ -87,13 +162,13 @@ struct JsonModel { #[serde( alias = "Properties", - default = "UstrMap::new", - skip_serializing_if = "HashMap::is_empty" + default, + skip_serializing_if = "IndexMap::is_empty" )] - properties: UstrMap, + properties: IndexMap, - #[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")] - attributes: HashMap, + #[serde(default = "IndexMap::new", skip_serializing_if = "IndexMap::is_empty")] + attributes: IndexMap, } impl JsonModel { diff --git a/src/snapshot_middleware/lua.rs b/src/snapshot_middleware/lua.rs index 32638d7f..eaa90952 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -1,11 +1,22 @@ use std::{path::Path, str}; +use anyhow::Context as _; use memofs::Vfs; -use rbx_dom_weak::{types::Enum, ustr, HashMapExt as _, UstrMap}; +use rbx_dom_weak::{ + types::{Enum, Variant}, + ustr, HashMapExt as _, UstrMap, +}; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; -use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata, meta_file::DirectoryMetadata}; +use super::{ + dir::{snapshot_dir_no_meta, syncback_dir_no_meta}, + meta_file::{AdjacentMetadata, DirectoryMetadata}, + PathExt as _, +}; #[derive(Debug)] pub enum ScriptType { @@ -95,10 +106,11 @@ pub fn snapshot_lua_init( context: &InstanceContext, vfs: &Vfs, init_path: &Path, + name: &str, script_type: ScriptType, ) -> anyhow::Result> { let folder_path = init_path.parent().unwrap(); - let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap(); + let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path, name)?.unwrap(); if dir_snapshot.class_name != "Folder" { anyhow::bail!( @@ -117,12 +129,89 @@ pub fn snapshot_lua_init( init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; + // The directory snapshot middleware includes all possible init paths + // so we don't need to add it here. DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?; Ok(Some(init_snapshot)) } +pub fn syncback_lua<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) { + source.as_bytes().to_vec() + } else { + anyhow::bail!("Scripts must have a `Source` property that is a String") + }; + let mut fs_snapshot = FsSnapshot::new(); + fs_snapshot.add_file(&snapshot.path, contents); + + let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(mut meta) = meta { + // Scripts have relatively few properties that we care about, so shifting + // is fine. + meta.properties.shift_remove(&ustr("Source")); + + if !meta.is_empty() { + let parent_location = snapshot.path.parent_err()?; + fs_snapshot.add_file( + parent_location.join(format!("{}.meta.json", new_inst.name)), + serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?, + ); + } + } + + Ok(SyncbackReturn { + fs_snapshot, + // Scripts don't have a child! + children: Vec::new(), + removed_children: Vec::new(), + }) +} + +pub fn syncback_lua_init<'sync>( + script_type: ScriptType, + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + let path = snapshot.path.join(match script_type { + ScriptType::Server => "init.server.luau", + ScriptType::Client => "init.client.luau", + ScriptType::Module => "init.luau", + _ => anyhow::bail!("syncback is not yet implemented for {script_type:?}"), + }); + + let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) { + source.as_bytes().to_vec() + } else { + anyhow::bail!("Scripts must have a `Source` property that is a String") + }; + + let mut dir_syncback = syncback_dir_no_meta(snapshot)?; + dir_syncback.fs_snapshot.add_file(&path, contents); + + let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, path.clone())?; + if let Some(mut meta) = meta { + // Scripts have relatively few properties that we care about, so shifting + // is fine. + meta.properties.shift_remove(&ustr("Source")); + + if !meta.is_empty() { + dir_syncback.fs_snapshot.add_file( + snapshot.path.join("init.meta.json"), + serde_json::to_vec_pretty(&meta) + .context("could not serialize new init.meta.json")?, + ); + } + } + + Ok(dir_syncback) +} + #[cfg(test)] mod test { use super::*; @@ -305,6 +394,7 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.lua"), + "root", ScriptType::Module, ) .unwrap() @@ -336,6 +426,7 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.lua"), + "root", ScriptType::Module, ) .unwrap() diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs index 59acc8d5..1757dee1 100644 --- a/src/snapshot_middleware/meta_file.rs +++ b/src/snapshot_middleware/meta_file.rs @@ -1,14 +1,18 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; use anyhow::{format_err, Context}; +use indexmap::IndexMap; use memofs::{IoResultExt as _, Vfs}; -use rbx_dom_weak::{types::Attributes, Ustr, UstrMap}; +use rbx_dom_weak::{ + types::{Attributes, Variant}, + Ustr, +}; use serde::{Deserialize, Serialize}; -use crate::{json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef}; +use crate::{ + json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, syncback::SyncbackSnapshot, + RojoRef, +}; /// Represents metadata in a sibling file with the same basename. /// @@ -26,11 +30,11 @@ pub struct AdjacentMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub ignore_unknown_instances: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub properties: UstrMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub properties: IndexMap, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub attributes: HashMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub attributes: IndexMap, #[serde(skip)] pub path: PathBuf, @@ -80,6 +84,76 @@ impl AdjacentMetadata { Ok(meta) } + /// Constructs an `AdjacentMetadata` from the provided snapshot, assuming it + /// will be at the provided path. + pub fn from_syncback_snapshot( + snapshot: &SyncbackSnapshot, + path: PathBuf, + ) -> anyhow::Result> { + let mut properties = IndexMap::new(); + let mut attributes = IndexMap::new(); + // TODO make this more granular. + // I am breaking the cycle of bad TODOs. This is in reference to the fact + // that right now, this will just not write any metadata at all for + // project nodes, which is not always desirable. We should try to be + // smarter about it. + if let Some(old_inst) = snapshot.old_inst() { + if let Some(source) = &old_inst.metadata().instigating_source { + let source = source.path(); + if source != path { + log::debug!( + "Instigating source for Instance is mismatched so its metadata is being skipped.\nPath: {}", + path.display() + ); + return Ok(None); + } + } + } + + let ignore_unknown_instances = snapshot + .old_inst() + .map(|inst| inst.metadata().ignore_unknown_instances) + .unwrap_or_default(); + + let class = &snapshot.new_inst().class; + for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() { + match value { + Variant::Attributes(attrs) => { + for (attr_name, attr_value) in attrs.iter() { + // We (probably) don't want to preserve internal + // attributes, only user defined ones. + if attr_name.starts_with("RBX") { + continue; + } + attributes.insert( + attr_name.clone(), + UnresolvedValue::from_variant_unambiguous(attr_value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), class, &name), + ); + } + } + } + + Ok(Some(Self { + ignore_unknown_instances: if ignore_unknown_instances { + Some(true) + } else { + None + }, + properties, + attributes, + path, + id: None, + schema: None, + })) + } + pub fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) { if let Some(ignore) = self.ignore_unknown_instances.take() { snapshot.metadata.ignore_unknown_instances = ignore; @@ -89,7 +163,10 @@ impl AdjacentMetadata { pub fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { let path = &self.path; - for (key, unresolved) in self.properties.drain() { + // BTreeMaps don't have an equivalent to HashMap::drain, so the next + // best option is to take ownership of the entire map. Not free, but + // very cheap. + for (key, unresolved) in std::mem::take(&mut self.properties) { let value = unresolved .resolve(&snapshot.class_name, &key) .with_context(|| format!("error applying meta file {}", path.display()))?; @@ -100,7 +177,7 @@ impl AdjacentMetadata { if !self.attributes.is_empty() { let mut attributes = Attributes::new(); - for (key, unresolved) in self.attributes.drain() { + for (key, unresolved) in std::mem::take(&mut self.attributes) { let value = unresolved.resolve_unambiguous()?; attributes.insert(key, value); } @@ -131,6 +208,18 @@ impl AdjacentMetadata { Ok(()) } + /// Returns whether the metadata is 'empty', meaning it doesn't have anything + /// worth persisting in it. Specifically: + /// + /// - The number of properties and attributes is 0 + /// - `ignore_unknown_instances` is None + #[inline] + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + && self.properties.is_empty() + && self.ignore_unknown_instances.is_none() + } + // TODO: Add method to allow selectively applying parts of metadata and // throwing errors if invalid parts are specified. } @@ -151,11 +240,11 @@ pub struct DirectoryMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub ignore_unknown_instances: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub properties: UstrMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub properties: IndexMap, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub attributes: HashMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub attributes: IndexMap, #[serde(skip_serializing_if = "Option::is_none")] pub class_name: Option, @@ -207,6 +296,80 @@ impl DirectoryMetadata { Ok(meta) } + /// Constructs a `DirectoryMetadata` from the provided snapshot, assuming it + /// will be at the provided path. + /// + /// This function does not set `ClassName` manually as most uses won't + /// want it set. + pub fn from_syncback_snapshot( + snapshot: &SyncbackSnapshot, + path: PathBuf, + ) -> anyhow::Result> { + let mut properties = IndexMap::new(); + let mut attributes = IndexMap::new(); + // TODO make this more granular. + // I am breaking the cycle of bad TODOs. This is in reference to the fact + // that right now, this will just not write any metadata at all for + // project nodes, which is not always desirable. We should try to be + // smarter about it. + if let Some(old_inst) = snapshot.old_inst() { + if let Some(source) = &old_inst.metadata().instigating_source { + let source = source.path(); + if source != path { + log::debug!( + "Instigating source for Instance is mismatched so its metadata is being skipped.\nPath: {}", + path.display() + ); + return Ok(None); + } + } + } + + let ignore_unknown_instances = snapshot + .old_inst() + .map(|inst| inst.metadata().ignore_unknown_instances) + .unwrap_or_default(); + + let class = &snapshot.new_inst().class; + for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() { + match value { + Variant::Attributes(attrs) => { + for (name, value) in attrs.iter() { + // We (probably) don't want to preserve internal + // attributes, only user defined ones. + if name.starts_with("RBX") { + continue; + } + attributes.insert( + name.to_owned(), + UnresolvedValue::from_variant_unambiguous(value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), class, &name), + ); + } + } + } + + Ok(Some(Self { + ignore_unknown_instances: if ignore_unknown_instances { + Some(true) + } else { + None + }, + properties, + attributes, + class_name: None, + path, + id: None, + schema: None, + })) + } + pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { self.apply_ignore_unknown_instances(snapshot); self.apply_class_name(snapshot)?; @@ -241,7 +404,7 @@ impl DirectoryMetadata { fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { let path = &self.path; - for (key, unresolved) in self.properties.drain() { + for (key, unresolved) in std::mem::take(&mut self.properties) { let value = unresolved .resolve(&snapshot.class_name, &key) .with_context(|| format!("error applying meta file {}", path.display()))?; @@ -252,7 +415,7 @@ impl DirectoryMetadata { if !self.attributes.is_empty() { let mut attributes = Attributes::new(); - for (key, unresolved) in self.attributes.drain() { + for (key, unresolved) in std::mem::take(&mut self.attributes) { let value = unresolved.resolve_unambiguous()?; attributes.insert(key, value); } @@ -275,6 +438,53 @@ impl DirectoryMetadata { snapshot.metadata.specified_id = self.id.take().map(RojoRef::new); Ok(()) } + + /// Returns whether the metadata is 'empty', meaning it doesn't have anything + /// worth persisting in it. Specifically: + /// + /// - The number of properties and attributes is 0 + /// - `ignore_unknown_instances` is None + /// - `class_name` is either None or not Some("Folder") + #[inline] + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + && self.properties.is_empty() + && self.ignore_unknown_instances.is_none() + && if let Some(class) = &self.class_name { + class == "Folder" + } else { + true + } + } +} + +/// Retrieves the meta file that should be applied for the provided directory, +/// if it exists. +pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result> { + let meta_path = path.join("init.meta.json"); + + if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? { + let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?; + Ok(Some(metadata)) + } else { + Ok(None) + } +} + +/// Retrieves the meta file that should be applied for the provided file, +/// if it exists. +/// +/// The `name` field should be the name the metadata should have. +pub fn file_meta(vfs: &Vfs, path: &Path, name: &str) -> anyhow::Result> { + let mut meta_path = path.with_file_name(name); + meta_path.set_extension("meta.json"); + + if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? { + let metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?; + Ok(Some(metadata)) + } else { + Ok(None) + } } #[cfg(test)] diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs index 30c5d13b..9eec5f83 100644 --- a/src/snapshot_middleware/mod.rs +++ b/src/snapshot_middleware/mod.rs @@ -28,24 +28,34 @@ use anyhow::Context; use memofs::{IoResultExt, Vfs}; use serde::{Deserialize, Serialize}; -use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule}; -use crate::{glob::Glob, project::DEFAULT_PROJECT_NAMES}; +use crate::{ + glob::Glob, + project::DEFAULT_PROJECT_NAMES, + syncback::{SyncbackReturn, SyncbackSnapshot}, +}; +use crate::{ + snapshot::{InstanceContext, InstanceSnapshot, SyncRule}, + syncback::validate_file_name, +}; use self::{ - csv::{snapshot_csv, snapshot_csv_init}, - dir::snapshot_dir, + csv::{snapshot_csv, snapshot_csv_init, syncback_csv, syncback_csv_init}, + dir::{snapshot_dir, syncback_dir}, json::snapshot_json, - json_model::snapshot_json_model, - lua::{snapshot_lua, snapshot_lua_init, ScriptType}, - project::snapshot_project, - rbxm::snapshot_rbxm, - rbxmx::snapshot_rbxmx, + json_model::{snapshot_json_model, syncback_json_model}, + lua::{snapshot_lua, snapshot_lua_init, syncback_lua, syncback_lua_init}, + project::{snapshot_project, syncback_project}, + rbxm::{snapshot_rbxm, syncback_rbxm}, + rbxmx::{snapshot_rbxmx, syncback_rbxmx}, toml::snapshot_toml, - txt::snapshot_txt, + txt::{snapshot_txt, syncback_txt}, yaml::snapshot_yaml, }; -pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default}; +pub use self::{ + lua::ScriptType, project::snapshot_project_node, util::emit_legacy_scripts_default, + util::PathExt, +}; /// Returns an `InstanceSnapshot` for the provided path. /// This will inspect the path and find the appropriate middleware for it, @@ -63,41 +73,14 @@ pub fn snapshot_from_vfs( }; if meta.is_dir() { - if let Some(init_path) = get_init_path(vfs, path)? { - // TODO: support user-defined init paths - // If and when we do, make sure to go support it in - // `Project::set_file_name`, as right now it special-cases - // `default.project.json` as an `init` path. - for rule in default_sync_rules() { - if rule.matches(&init_path) { - return match rule.middleware { - Middleware::Project => { - let name = init_path - .parent() - .and_then(Path::file_name) - .and_then(|s| s.to_str()).expect("default.project.json should be inside a folder with a unicode name"); - snapshot_project(context, vfs, &init_path, name) - } - - Middleware::ModuleScript => { - snapshot_lua_init(context, vfs, &init_path, ScriptType::Module) - } - Middleware::ServerScript => { - snapshot_lua_init(context, vfs, &init_path, ScriptType::Server) - } - Middleware::ClientScript => { - snapshot_lua_init(context, vfs, &init_path, ScriptType::Client) - } - - Middleware::Csv => snapshot_csv_init(context, vfs, &init_path), - - _ => snapshot_dir(context, vfs, path), - }; - } - } - snapshot_dir(context, vfs, path) - } else { - snapshot_dir(context, vfs, path) + let (middleware, dir_name, init_path) = get_dir_middleware(vfs, path)?; + // TODO: Support user defined init paths + // If and when we do, make sure to go support it in + // `Project::set_file_name`, as right now it special-cases + // `default.project.json` as an `init` path. + match middleware { + Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name), + _ => middleware.snapshot(context, vfs, &init_path, dir_name), } } else { let file_name = path @@ -116,55 +99,50 @@ pub fn snapshot_from_vfs( } } -/// Gets an `init` path for the given directory. -/// This uses an intrinsic priority list and for compatibility, -/// it should not be changed. -fn get_init_path>(vfs: &Vfs, dir: P) -> anyhow::Result> { - let path = dir.as_ref(); +/// Gets the appropriate middleware for a directory by checking for `init` +/// files. This uses an intrinsic priority list and for compatibility, +/// that order should be left unchanged. +/// +/// Returns the middleware, the name of the directory, and the path to +/// the init location. +fn get_dir_middleware<'path>( + vfs: &Vfs, + dir_path: &'path Path, +) -> anyhow::Result<(Middleware, &'path str, PathBuf)> { + let dir_name = dir_path + .file_name() + .expect("Could not extract directory name") + .to_str() + .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?; + + static INIT_PATHS: OnceLock> = OnceLock::new(); + let order = INIT_PATHS.get_or_init(|| { + vec![ + (Middleware::ModuleScriptDir, "init.luau"), + (Middleware::ModuleScriptDir, "init.lua"), + (Middleware::ServerScriptDir, "init.server.luau"), + (Middleware::ServerScriptDir, "init.server.lua"), + (Middleware::ClientScriptDir, "init.client.luau"), + (Middleware::ClientScriptDir, "init.client.lua"), + (Middleware::CsvDir, "init.csv"), + ] + }); for default_project_name in DEFAULT_PROJECT_NAMES { - let project_path = path.join(default_project_name); + let project_path = dir_path.join(default_project_name); if vfs.metadata(&project_path).with_not_found()?.is_some() { - return Ok(Some(project_path)); + return Ok((Middleware::Project, dir_name, project_path)); } } - let init_path = path.join("init.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); + for (middleware, name) in order { + let test_path = dir_path.join(name); + if vfs.metadata(&test_path).with_not_found()?.is_some() { + return Ok((*middleware, dir_name, test_path)); + } } - let init_path = path.join("init.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.server.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.server.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.client.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.client.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.csv"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - Ok(None) + Ok((Middleware::Dir, dir_name, dir_path.to_path_buf())) } /// Gets a snapshot for a path given an InstanceContext and Vfs, taking @@ -194,9 +172,10 @@ fn snapshot_from_path( } /// Represents a possible 'transformer' used by Rojo to turn a file system -/// item into a Roblox Instance. Missing from this list are directories and -/// metadata. This is deliberate, as metadata is not a snapshot middleware -/// and directories do not make sense to turn into files. +/// item into a Roblox Instance. Missing from this list is metadata. +/// This is deliberate, as metadata is not a snapshot middleware. +/// +/// Directories cannot be used for sync rules so they're ignored by Serde. #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum Middleware { @@ -218,6 +197,17 @@ pub enum Middleware { Text, Yaml, Ignore, + + #[serde(skip_deserializing)] + Dir, + #[serde(skip_deserializing)] + ServerScriptDir, + #[serde(skip_deserializing)] + ClientScriptDir, + #[serde(skip_deserializing)] + ModuleScriptDir, + #[serde(skip_deserializing)] + CsvDir, } impl Middleware { @@ -230,7 +220,7 @@ impl Middleware { path: &Path, name: &str, ) -> anyhow::Result> { - match self { + let mut output = match self { Self::Csv => snapshot_csv(context, vfs, path, name), Self::JsonModel => snapshot_json_model(context, vfs, path, name), Self::Json => snapshot_json(context, vfs, path, name), @@ -257,6 +247,120 @@ impl Middleware { Self::Text => snapshot_txt(context, vfs, path, name), Self::Yaml => snapshot_yaml(context, vfs, path, name), Self::Ignore => Ok(None), + + Self::Dir => snapshot_dir(context, vfs, path, name), + Self::ServerScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Server) + } + Self::ClientScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Client) + } + Self::ModuleScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Module) + } + Self::CsvDir => snapshot_csv_init(context, vfs, path, name), + }; + if let Ok(Some(ref mut snapshot)) = output { + snapshot.metadata.middleware = Some(*self); + } + output + } + + /// Runs the syncback mechanism for the provided middleware given a + /// SyncbackSnapshot. + pub fn syncback<'sync>( + &self, + snapshot: &SyncbackSnapshot<'sync>, + ) -> anyhow::Result> { + let file_name = snapshot.path.file_name().and_then(|s| s.to_str()); + if let Some(file_name) = file_name { + validate_file_name(file_name).with_context(|| { + format!("cannot create a file or directory with name {file_name}") + })?; + } + match self { + Middleware::Csv => syncback_csv(snapshot), + Middleware::JsonModel => syncback_json_model(snapshot), + Middleware::Json => anyhow::bail!("cannot syncback Json middleware"), + // Projects are only generated from files that already exist on the + // file system, so we don't need to pass a file name. + Middleware::Project => syncback_project(snapshot), + Middleware::ServerScript => syncback_lua(snapshot), + Middleware::ClientScript => syncback_lua(snapshot), + Middleware::ModuleScript => syncback_lua(snapshot), + Middleware::Rbxm => syncback_rbxm(snapshot), + Middleware::Rbxmx => syncback_rbxmx(snapshot), + Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"), + Middleware::Text => syncback_txt(snapshot), + Middleware::Yaml => anyhow::bail!("cannot syncback Yaml middleware"), + Middleware::Ignore => anyhow::bail!("cannot syncback Ignore middleware"), + Middleware::Dir => syncback_dir(snapshot), + Middleware::ServerScriptDir => syncback_lua_init(ScriptType::Server, snapshot), + Middleware::ClientScriptDir => syncback_lua_init(ScriptType::Client, snapshot), + Middleware::ModuleScriptDir => syncback_lua_init(ScriptType::Module, snapshot), + Middleware::CsvDir => syncback_csv_init(snapshot), + + Middleware::PluginScript + | Middleware::LegacyServerScript + | Middleware::LegacyClientScript + | Middleware::RunContextServerScript + | Middleware::RunContextClientScript => { + anyhow::bail!("syncback is not implemented for {self:?} yet") + } + } + } + + /// Returns whether this particular middleware would become a directory. + #[inline] + pub fn is_dir(&self) -> bool { + matches!( + self, + Middleware::Dir + | Middleware::ServerScriptDir + | Middleware::ClientScriptDir + | Middleware::ModuleScriptDir + | Middleware::CsvDir + ) + } + + /// Returns whether this particular middleware sets its own properties. + /// This applies to things like `JsonModel` and `Project`, since they + /// set properties without needing a meta.json file. + /// + /// It does not cover middleware like `ServerScript` or `Csv` because they + /// need a meta.json file to set properties that aren't their designated + /// 'special' properties. + #[inline] + pub fn handles_own_properties(&self) -> bool { + matches!( + self, + Middleware::JsonModel | Middleware::Project | Middleware::Rbxm | Middleware::Rbxmx + ) + } + + /// Attempts to return a middleware that should be used for the given path. + /// + /// Returns `Err` only if the Vfs cannot read information about the path. + pub fn middleware_for_path( + vfs: &Vfs, + sync_rules: &[SyncRule], + path: &Path, + ) -> anyhow::Result> { + let meta = match vfs.metadata(path).with_not_found()? { + Some(meta) => meta, + None => return Ok(None), + }; + + if meta.is_dir() { + let (middleware, _, _) = get_dir_middleware(vfs, path)?; + Ok(Some(middleware)) + } else { + for rule in sync_rules.iter().chain(default_sync_rules()) { + if rule.matches(path) { + return Ok(Some(rule.middleware)); + } + } + Ok(None) } } } diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs index 7d951f86..c114ac5d 100644 --- a/src/snapshot_middleware/project.rs +++ b/src/snapshot_middleware/project.rs @@ -1,19 +1,27 @@ -use std::{borrow::Cow, path::Path}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap, VecDeque}, + path::Path, +}; use anyhow::{bail, Context}; use memofs::Vfs; use rbx_dom_weak::{ - types::{Attributes, Ref}, - ustr, HashMapExt as _, Ustr, UstrMap, + types::{Attributes, Ref, Variant}, + ustr, HashMapExt as _, Instance, Ustr, UstrMap, }; use rbx_reflection::ClassTag; use crate::{ project::{PathNode, Project, ProjectNode}, + resolution::UnresolvedValue, snapshot::{ - InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule, - SyncRule, + InstanceContext, InstanceMetadata, InstanceSnapshot, InstanceWithMeta, InstigatingSource, + PathIgnoreRule, SyncRule, }, + snapshot_middleware::Middleware, + syncback::{filter_properties, FsSnapshot, SyncbackReturn, SyncbackSnapshot}, + variant_eq::variant_eq, RojoRef, }; @@ -286,12 +294,12 @@ pub fn snapshot_project_node( metadata.specified_id = Some(RojoRef::new(id.clone())) } - metadata.instigating_source = Some(InstigatingSource::ProjectNode( - project_path.to_path_buf(), - instance_name.to_string(), - Box::new(node.clone()), - parent_class.map(|name| name.to_owned()), - )); + metadata.instigating_source = Some(InstigatingSource::ProjectNode { + path: project_path.to_path_buf(), + name: instance_name.to_string(), + node: node.clone(), + parent_class: parent_class.map(|name| name.to_owned()), + }); Ok(Some(InstanceSnapshot { snapshot_id: Ref::none(), @@ -303,6 +311,318 @@ pub fn snapshot_project_node( })) } +pub fn syncback_project<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let old_inst = snapshot + .old_inst() + .expect("projects should always exist in both trees"); + // Generally, the path of a project is the first thing added to the relevant + // paths. So, we take the last one. + let project_path = old_inst + .metadata() + .relevant_paths + .last() + .expect("all projects should have a relevant path"); + let vfs = snapshot.vfs(); + + log::debug!("Reloading project {} from vfs", project_path.display(),); + let mut project = Project::load_exact(vfs, project_path, None)?; + let base_path = project.folder_location().to_path_buf(); + + // Sync rules for this project do not have their base rule set but it is + // important when performing syncback on other projects. + for rule in &mut project.sync_rules { + rule.base_path.clone_from(&base_path) + } + + let mut descendant_snapshots = Vec::new(); + let mut removed_descendants = Vec::new(); + + let mut ref_to_path_map = HashMap::new(); + let mut old_child_map = HashMap::new(); + let mut new_child_map = HashMap::new(); + + let mut node_changed_map = Vec::new(); + let mut node_queue = VecDeque::with_capacity(1); + node_queue.push_back((&mut project.tree, old_inst, snapshot.new_inst())); + + while let Some((node, old_inst, new_inst)) = node_queue.pop_front() { + log::debug!("Processing node {}", old_inst.name()); + if old_inst.class_name() != new_inst.class { + anyhow::bail!( + "Cannot change the class of {} in project file {}.\n\ + Current class is {}, it is a {} in the input file.", + old_inst.name(), + project_path.display(), + old_inst.class_name(), + new_inst.class + ); + } + + // TODO handle meta.json files in this branch. Right now, we perform + // syncback if a node has `$path` set but the Middleware aren't aware + // that the Instances they're running on originate in a project.json. + // As a result, the `meta.json` syncback code is hardcoded to not work + // if the Instance originates from a project file. However, we should + // ideally use a .meta.json over the project node if it exists already. + if node.path.is_some() { + // Since the node has a path, we have to run syncback on it. + let node_path = node.path.as_ref().map(PathNode::path).expect( + "Project nodes with a path must have a path \ + If you see this message, something went seriously wrong. Please report it.", + ); + let full_path = if node_path.is_absolute() { + node_path.to_path_buf() + } else { + base_path.join(node_path) + }; + + let middleware = match Middleware::middleware_for_path( + snapshot.vfs(), + &project.sync_rules, + &full_path, + )? { + Some(middleware) => middleware, + // The only way this can happen at this point is if the path does + // not exist on the file system or there's no middleware for it. + None => anyhow::bail!( + "path does not exist or could not be turned into a file Rojo understands: {}", + full_path.display() + ), + }; + + descendant_snapshots.push( + snapshot + .with_new_path(full_path.clone(), new_inst.referent(), Some(old_inst.id())) + .middleware(middleware), + ); + + ref_to_path_map.insert(new_inst.referent(), full_path); + + // We only want to set properties if it needs it. + if !middleware.handles_own_properties() { + project_node_property_syncback_path(snapshot, new_inst, node); + } + } else { + project_node_property_syncback_no_path(snapshot, new_inst, node); + } + + for child_ref in new_inst.children() { + let child = snapshot + .get_new_instance(*child_ref) + .expect("all children of Instances should be in new DOM"); + if new_child_map.insert(&child.name, child).is_some() { + anyhow::bail!( + "Instances that are direct children of an Instance that is made by a project file \ + must have a unique name.\nThe child '{}' of '{}' is duplicated in the place file.", child.name, old_inst.name() + ); + } + } + for child_ref in old_inst.children() { + let child = snapshot + .get_old_instance(*child_ref) + .expect("all children of Instances should be in old DOM"); + if old_child_map.insert(child.name(), child).is_some() { + anyhow::bail!( + "Instances that are direct children of an Instance that is made by a project file \ + must have a unique name.\nThe child '{}' of '{}' is duplicated on the file system.", child.name(), old_inst.name() + ); + } + } + + // This loop does basic matching of Instance children to the node's + // children. It ensures that `new_child_map` and `old_child_map` will + // only contain Instances that don't belong to the project after this. + for (child_name, child_node) in &mut node.children { + // If a node's path is optional, we want to skip it if the path + // doesn't exist since it isn't in the current old DOM. + if let Some(path) = &child_node.path { + if path.is_optional() { + let real_path = if path.path().is_absolute() { + path.path().to_path_buf() + } else { + base_path.join(path.path()) + }; + if !real_path.exists() { + log::warn!( + "Skipping node '{child_name}' of project because it is optional and not present on the disk.\n\ + If this is not deliberate, please create a file or directory at {}", real_path.display() + ); + continue; + } + } + } + let new_equivalent = new_child_map.remove(child_name); + let old_equivalent = old_child_map.remove(child_name.as_str()); + match (new_equivalent, old_equivalent) { + (Some(new), Some(old)) => node_queue.push_back((child_node, old, new)), + (_, None) => anyhow::bail!( + "The child '{child_name}' of Instance '{}' would be removed.\n\ + Syncback cannot add or remove Instances from project {}", + old_inst.name(), + project_path.display() + ), + (None, _) => anyhow::bail!( + "The child '{child_name}' of Instance '{}' is present only in a project file,\n\ + and not the provided file. Syncback cannot add or remove Instances from project:\n{}.", + old_inst.name(), project_path.display(), + ) + } + } + + // All of the children in this loop are by their nature not in the + // project, so we just need to run syncback on them. + for (name, new_child) in new_child_map.drain() { + let parent_path = match ref_to_path_map.get(&new_child.parent()) { + Some(path) => path.clone(), + None => { + log::debug!("Skipping child {name} of node because it has no parent_path"); + continue; + } + }; + + // If a child also exists in the old tree, it will be caught in the + // syncback on the project node path above (or is itself a node). + // So the only things we need to run seperately is new children. + if old_child_map.remove(name.as_str()).is_none() { + let parent_middleware = + Middleware::middleware_for_path(vfs, &project.sync_rules, &parent_path)? + .expect("project nodes should have a middleware if they have children."); + // If this node points directly to a project, it may still have + // children but they'll be handled by syncback. This isn't a + // concern with directories because they're singular things, + // files that contain their own children. + if parent_middleware != Middleware::Project { + descendant_snapshots.push(snapshot.with_base_path( + &parent_path, + new_child.referent(), + None, + )?); + } + } + } + removed_descendants.extend(old_child_map.drain().map(|(_, v)| v)); + node_changed_map.push((&node.properties, &node.attributes, old_inst)) + } + let mut fs_snapshot = FsSnapshot::new(); + + for (node_properties, node_attributes, old_inst) in node_changed_map { + if project_node_should_reserialize(node_properties, node_attributes, old_inst)? { + fs_snapshot.add_file(project_path, serde_json::to_vec_pretty(&project)?); + break; + } + } + + Ok(SyncbackReturn { + fs_snapshot, + children: descendant_snapshots, + removed_children: removed_descendants, + }) +} + +fn project_node_property_syncback( + _snapshot: &SyncbackSnapshot, + filtered_properties: UstrMap<&Variant>, + new_inst: &Instance, + node: &mut ProjectNode, +) { + let properties = &mut node.properties; + let mut attributes = BTreeMap::new(); + for (name, value) in filtered_properties { + match value { + Variant::Attributes(attrs) => { + for (attr_name, attr_value) in attrs.iter() { + // We (probably) don't want to preserve internal attributes, + // only user defined ones. + if attr_name.starts_with("RBX") { + continue; + } + attributes.insert( + attr_name.clone(), + UnresolvedValue::from_variant_unambiguous(attr_value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), &new_inst.class, &name), + ); + } + } + } + node.attributes = attributes; +} + +fn project_node_property_syncback_path( + snapshot: &SyncbackSnapshot, + new_inst: &Instance, + node: &mut ProjectNode, +) { + let filtered_properties = snapshot + .get_path_filtered_properties(new_inst.referent()) + .unwrap(); + project_node_property_syncback(snapshot, filtered_properties, new_inst, node) +} + +fn project_node_property_syncback_no_path( + snapshot: &SyncbackSnapshot, + new_inst: &Instance, + node: &mut ProjectNode, +) { + let filtered_properties = filter_properties(snapshot.project(), new_inst); + project_node_property_syncback(snapshot, filtered_properties, new_inst, node) +} + +fn project_node_should_reserialize( + node_properties: &BTreeMap, + node_attributes: &BTreeMap, + instance: InstanceWithMeta, +) -> anyhow::Result { + for (prop_name, unresolved_node_value) in node_properties { + if let Some(inst_value) = instance.properties().get(prop_name) { + let node_value = unresolved_node_value + .clone() + .resolve(&instance.class_name(), prop_name)?; + if !variant_eq(inst_value, &node_value) { + return Ok(true); + } + } else { + return Ok(true); + } + } + + match instance.properties().get(&ustr("Attributes")) { + Some(Variant::Attributes(inst_attributes)) => { + // This will also catch if one is empty but the other isn't + if node_attributes.len() != inst_attributes.len() { + Ok(true) + } else { + for (attr_name, unresolved_node_value) in node_attributes { + if let Some(inst_value) = inst_attributes.get(attr_name.as_str()) { + let node_value = unresolved_node_value.clone().resolve_unambiguous()?; + if !variant_eq(inst_value, &node_value) { + return Ok(true); + } + } else { + return Ok(true); + } + } + Ok(false) + } + } + Some(_) => Ok(true), + None => { + if !node_attributes.is_empty() { + Ok(true) + } else { + Ok(false) + } + } + } +} + fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option { // If className wasn't defined from another source, we may be able // to infer one. diff --git a/src/snapshot_middleware/rbxm.rs b/src/snapshot_middleware/rbxm.rs index f2c08dd0..f3693d52 100644 --- a/src/snapshot_middleware/rbxm.rs +++ b/src/snapshot_middleware/rbxm.rs @@ -3,7 +3,10 @@ use std::path::Path; use anyhow::Context; use memofs::Vfs; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; #[profiling::function] pub fn snapshot_rbxm( @@ -39,6 +42,24 @@ pub fn snapshot_rbxm( } } +pub fn syncback_rbxm<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let inst = snapshot.new_inst(); + + // Long-term, we probably want to have some logic for if this contains a + // script. That's a future endeavor though. + let mut serialized = Vec::new(); + rbx_binary::to_writer(&mut serialized, snapshot.new_tree(), &[inst.referent()]) + .context("failed to serialize new rbxm")?; + + Ok(SyncbackReturn { + fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), + children: Vec::new(), + removed_children: Vec::new(), + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/snapshot_middleware/rbxmx.rs b/src/snapshot_middleware/rbxmx.rs index d71e28b7..59aff679 100644 --- a/src/snapshot_middleware/rbxmx.rs +++ b/src/snapshot_middleware/rbxmx.rs @@ -2,8 +2,12 @@ use std::path::Path; use anyhow::Context; use memofs::Vfs; +use rbx_xml::EncodeOptions; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; pub fn snapshot_rbxmx( context: &InstanceContext, @@ -15,7 +19,7 @@ pub fn snapshot_rbxmx( .property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown); let temp_tree = rbx_xml::from_reader(vfs.read(path)?.as_slice(), options) - .with_context(|| format!("Malformed rbxm file: {}", path.display()))?; + .with_context(|| format!("Malformed rbxmx file: {}", path.display()))?; let root_instance = temp_tree.root(); let children = root_instance.children(); @@ -41,6 +45,32 @@ pub fn snapshot_rbxmx( } } +pub fn syncback_rbxmx<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let inst = snapshot.new_inst(); + + let options = + EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown); + + // Long-term, we probably want to have some logic for if this contains a + // script. That's a future endeavor though. + let mut serialized = Vec::new(); + rbx_xml::to_writer( + &mut serialized, + snapshot.new_tree(), + &[inst.referent()], + options, + ) + .context("failed to serialize new rbxmx")?; + + Ok(SyncbackReturn { + fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), + children: Vec::new(), + removed_children: Vec::new(), + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap index 577f0f56..f8ce15af 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap index 39abe8ed..6aabdf4b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: root class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap index 75bf31bc..7202e31b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: root class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap index ede33710..8452cc7c 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap index 6a182d33..64f74c4e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Folder properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap index 3999f376..2f29bb64 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Folder properties: {} @@ -44,6 +45,7 @@ children: context: emit_legacy_scripts: true specified_id: ~ + middleware: dir name: Child class_name: Folder properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap index 69ae3c26..d570009f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap index 580b0b38..38d66519 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap index c6c30705..bf768977 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: IntValue properties: @@ -25,6 +26,7 @@ children: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: The Child class_name: StringValue properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap index c6c30705..bf768977 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: IntValue properties: @@ -25,6 +26,7 @@ children: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: The Child class_name: StringValue properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap index 49445676..683eec85 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: LocalScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap index d0a134da..b7ca7678 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap index ad129df9..63a84714 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap index 4b870d05..c4d02c8e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: bar class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap index 0dfb278a..916d0038 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap index 5007e665..a43f7e0f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap index 8a24827b..deb32404 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: root class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap index ea560ad1..b02f9117 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: root class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap index 5f21eb7f..bf7b89b4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap index 507e25d6..e1376702 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap index 89ce9626..187ce50c 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap index b869da66..382fa03f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap index 15523090..2d6d9bdb 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: bar class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap index 182e0fd3..2e68f9d8 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap index dc22b34a..c72bdfcd 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap index d9577829..f21cca30 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap index d9577829..f21cca30 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap index 4d2a9e55..08f7159d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap index 4d2a9e55..08f7159d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap index 7c1978ca..ebf5362d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap index 7906dbbf..1b87aab8 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: direct-project class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap index 51da0621..cd32e81d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap @@ -13,6 +13,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project name: path-property-override class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap index 0c9f90ab..f9d6e4a4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: children class_name: Folder properties: {} @@ -21,14 +22,16 @@ children: ignore_unknown_instances: true instigating_source: ProjectNode: - - /foo.project.json - - Child - - $className: Model - - Folder + path: /foo.project.json + name: Child + node: + $className: Model + parent_class: Folder relevant_paths: [] context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: Child class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap index d8d59d90..da28d1ce 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap @@ -13,6 +13,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project name: path-project class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap index 30b01ebb..077ebb15 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap @@ -13,6 +13,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project name: path-child-project class_name: Folder properties: {} @@ -22,14 +23,16 @@ children: ignore_unknown_instances: true instigating_source: ProjectNode: - - /foo/other.project.json - - SomeChild - - $className: Model - - Folder + path: /foo/other.project.json + name: SomeChild + node: + $className: Model + parent_class: Folder relevant_paths: [] context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: SomeChild class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap index 75320b53..ba9f0cf7 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap @@ -15,6 +15,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: text name: path-project class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap index 99c6b3b5..27d7004a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: resolved-properties class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap index f42e2246..c6349bae 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: unresolved-properties class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap index e21e9414..22ae7577 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap index 4dd7cfc3..9905820d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap index bd877a12..769081e2 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap index c99e8910..684d8820 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap index 33d2e1a4..a59d5fdd 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap index 4d04eac5..3afe180a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/txt.rs b/src/snapshot_middleware/txt.rs index 761ba671..715354e0 100644 --- a/src/snapshot_middleware/txt.rs +++ b/src/snapshot_middleware/txt.rs @@ -1,11 +1,16 @@ use std::{path::Path, str}; +use anyhow::Context as _; use memofs::Vfs; +use rbx_dom_weak::types::Variant; use rbx_dom_weak::ustr; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; -use super::meta_file::AdjacentMetadata; +use super::{meta_file::AdjacentMetadata, PathExt as _}; pub fn snapshot_txt( context: &InstanceContext, @@ -32,6 +37,41 @@ pub fn snapshot_txt( Ok(Some(snapshot)) } +pub fn syncback_txt<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Value")) { + source.as_bytes().to_vec() + } else { + anyhow::bail!("StringValues must have a `Value` property that is a String"); + }; + let mut fs_snapshot = FsSnapshot::new(); + fs_snapshot.add_file(&snapshot.path, contents); + + let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(mut meta) = meta { + // StringValues have relatively few properties that we care about, so + // shifting is fine. + meta.properties.shift_remove(&ustr("Value")); + + if !meta.is_empty() { + let parent = snapshot.path.parent_err()?; + fs_snapshot.add_file( + parent.join(format!("{}.meta.json", new_inst.name)), + serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?, + ); + } + } + + Ok(SyncbackReturn { + fs_snapshot, + children: Vec::new(), + removed_children: Vec::new(), + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/snapshot_middleware/util.rs b/src/snapshot_middleware/util.rs index 625910b7..c16edcce 100644 --- a/src/snapshot_middleware/util.rs +++ b/src/snapshot_middleware/util.rs @@ -16,6 +16,7 @@ pub fn match_trailing<'a>(input: &'a str, suffix: &str) -> Option<&'a str> { pub trait PathExt { fn file_name_ends_with(&self, suffix: &str) -> bool; fn file_name_trim_end<'a>(&'a self, suffix: &str) -> anyhow::Result<&'a str>; + fn parent_err(&self) -> anyhow::Result<&Path>; } impl

PathExt for P @@ -40,6 +41,12 @@ where match_trailing(file_name, suffix) .with_context(|| format!("Path did not end in {}: {}", suffix, path.display())) } + + fn parent_err(&self) -> anyhow::Result<&Path> { + let path = self.as_ref(); + path.parent() + .with_context(|| format!("Path does not have a parent: {}", path.display())) + } } // TEMP function until rojo 8.0, when it can be replaced with bool::default (aka false) diff --git a/src/syncback/file_names.rs b/src/syncback/file_names.rs new file mode 100644 index 00000000..4653aabd --- /dev/null +++ b/src/syncback/file_names.rs @@ -0,0 +1,128 @@ +//! Contains logic for generating new file names for Instances based on their +//! middleware. + +use std::borrow::Cow; + +use anyhow::Context; +use rbx_dom_weak::Instance; + +use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware}; + +pub fn name_for_inst<'old>( + middleware: Middleware, + new_inst: &Instance, + old_inst: Option>, +) -> anyhow::Result> { + if let Some(old_inst) = old_inst { + if let Some(source) = old_inst.metadata().relevant_paths.first() { + source + .file_name() + .and_then(|s| s.to_str()) + .map(Cow::Borrowed) + .context("sources on the file system should be valid unicode and not be stubs") + } else { + // This is technically not /always/ true, but we want to avoid + // running syncback on anything that has no instigating source + // anyway. + anyhow::bail!( + "members of 'old' trees should have an instigating source. Somehow, {} did not.", + old_inst.name(), + ); + } + } else { + Ok(match middleware { + Middleware::Dir + | Middleware::CsvDir + | Middleware::ServerScriptDir + | Middleware::ClientScriptDir + | Middleware::ModuleScriptDir => Cow::Owned(new_inst.name.clone()), + _ => { + let extension = extension_for_middleware(middleware); + let name = &new_inst.name; + validate_file_name(name).with_context(|| { + format!("name '{name}' is not legal to write to the file system") + })?; + Cow::Owned(format!("{name}.{extension}")) + } + }) + } +} + +/// Returns the extension a provided piece of middleware is supposed to use. +pub fn extension_for_middleware(middleware: Middleware) -> &'static str { + match middleware { + Middleware::Csv => "csv", + Middleware::JsonModel => "model.json", + Middleware::Json => "json", + Middleware::ServerScript => "server.luau", + Middleware::ClientScript => "client.luau", + Middleware::ModuleScript => "luau", + Middleware::PluginScript => "plugin.luau", + Middleware::Project => "project.json", + Middleware::Rbxm => "rbxm", + Middleware::Rbxmx => "rbxmx", + Middleware::Toml => "toml", + Middleware::Text => "txt", + Middleware::Yaml => "yml", + + Middleware::LegacyServerScript + | Middleware::LegacyClientScript + | Middleware::RunContextServerScript + | Middleware::RunContextClientScript => { + todo!("syncback does not work on the middleware {middleware:?} yet") + } + // These are manually specified and not `_` to guard against future + // middleware additions missing this function. + Middleware::Ignore => unimplemented!("syncback does not work on Ignore middleware"), + Middleware::Dir + | Middleware::CsvDir + | Middleware::ServerScriptDir + | Middleware::ClientScriptDir + | Middleware::ModuleScriptDir => { + unimplemented!("directory middleware requires special treatment") + } + } +} + +/// A list of file names that are not valid on Windows. +const INVALID_WINDOWS_NAMES: [&str; 22] = [ + "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", + "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", +]; + +/// A list of all characters that are outright forbidden to be included +/// in a file's name. +const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\']; + +/// Validates a provided file name to ensure it's allowed on the file system. An +/// error is returned if the name isn't allowed, indicating why. +/// This takes into account rules for Windows, MacOS, and Linux. +/// +/// In practice however, these broadly overlap so the only unexpected behavior +/// is Windows, where there are 22 reserved names. +pub fn validate_file_name>(name: S) -> anyhow::Result<()> { + let str = name.as_ref(); + + if str.ends_with(' ') { + anyhow::bail!("file names cannot end with a space") + } + if str.ends_with('.') { + anyhow::bail!("file names cannot end with '.'") + } + + for char in str.chars() { + if FORBIDDEN_CHARS.contains(&char) { + anyhow::bail!("file names cannot contain <, >, :, \", /, |, ?, *, or \\") + } else if char.is_control() { + anyhow::bail!("file names cannot contain control characters") + } + } + + for forbidden in INVALID_WINDOWS_NAMES { + if str == forbidden { + anyhow::bail!("files cannot be named {str}") + } + } + + Ok(()) +} diff --git a/src/syncback/fs_snapshot.rs b/src/syncback/fs_snapshot.rs new file mode 100644 index 00000000..6bca52e4 --- /dev/null +++ b/src/syncback/fs_snapshot.rs @@ -0,0 +1,191 @@ +use std::{ + collections::{HashMap, HashSet}, + io, + path::{Path, PathBuf}, +}; + +use memofs::Vfs; + +/// A simple representation of a subsection of a file system. +#[derive(Default)] +pub struct FsSnapshot { + /// Paths representing new files mapped to their contents. + added_files: HashMap>, + /// Paths representing new directories. + added_dirs: HashSet, + /// Paths representing removed files. + removed_files: HashSet, + /// Paths representing removed directories. + removed_dirs: HashSet, +} + +impl FsSnapshot { + /// Creates a new `FsSnapshot`. + pub fn new() -> Self { + Self { + added_files: HashMap::new(), + added_dirs: HashSet::new(), + removed_files: HashSet::new(), + removed_dirs: HashSet::new(), + } + } + + /// Adds the given path to the `FsSnapshot` as a file with the given + /// contents, then returns it. + pub fn with_added_file>(mut self, path: P, data: Vec) -> Self { + self.added_files.insert(path.as_ref().to_path_buf(), data); + self + } + + /// Adds the given path to the `FsSnapshot` as a file with the given + /// then returns it. + pub fn with_added_dir>(mut self, path: P) -> Self { + self.added_dirs.insert(path.as_ref().to_path_buf()); + self + } + + /// Merges two `FsSnapshot`s together. + #[inline] + pub fn merge(&mut self, other: Self) { + self.added_files.extend(other.added_files); + self.added_dirs.extend(other.added_dirs); + self.removed_files.extend(other.removed_files); + self.removed_dirs.extend(other.removed_dirs); + } + + /// Merges two `FsSnapshot`s together, with a filter applied to the paths. + #[inline] + pub fn merge_with_filter(&mut self, other: Self, mut predicate: F) + where + F: FnMut(&Path) -> bool, + { + self.added_files + .extend(other.added_files.into_iter().filter(|(k, _)| predicate(k))); + self.added_dirs + .extend(other.added_dirs.into_iter().filter(|p| predicate(p))); + self.removed_files + .extend(other.removed_files.into_iter().filter(|p| predicate(p))); + self.removed_dirs + .extend(other.removed_dirs.into_iter().filter(|p| predicate(p))); + } + + /// Adds the provided path as a file with the given contents. + pub fn add_file>(&mut self, path: P, data: Vec) { + self.added_files.insert(path.as_ref().to_path_buf(), data); + } + + /// Adds the provided path as a directory. + pub fn add_dir>(&mut self, path: P) { + self.added_dirs.insert(path.as_ref().to_path_buf()); + } + + /// Removes the provided path, as a file. + pub fn remove_file>(&mut self, path: P) { + self.removed_files.insert(path.as_ref().to_path_buf()); + } + + /// Removes the provided path, as a directory. + pub fn remove_dir>(&mut self, path: P) { + self.removed_dirs.insert(path.as_ref().to_path_buf()); + } + + /// Writes the `FsSnapshot` to the provided VFS, using the provided `base` + /// as a root for the other paths in the `FsSnapshot`. + /// + /// This includes removals, but makes no effort to minimize work done. + pub fn write_to_vfs>(&self, base: P, vfs: &Vfs) -> io::Result<()> { + let mut lock = vfs.lock(); + + let base_path = base.as_ref(); + for dir_path in &self.added_dirs { + match lock.create_dir_all(base_path.join(dir_path)) { + Ok(_) => (), + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), + Err(err) => return Err(err), + }; + } + for (path, contents) in &self.added_files { + lock.write(base_path.join(path), contents)?; + } + for dir_path in &self.removed_dirs { + lock.remove_dir_all(base_path.join(dir_path))?; + } + for path in &self.removed_files { + lock.remove_file(base_path.join(path))?; + } + drop(lock); + + log::debug!( + "Wrote {} directories and {} files to the file system", + self.added_dirs.len(), + self.added_files.len() + ); + log::debug!( + "Removed {} directories and {} files from the file system", + self.removed_dirs.len(), + self.removed_files.len() + ); + Ok(()) + } + + /// Returns whether this `FsSnapshot` is empty or not. + #[inline] + pub fn is_empty(&self) -> bool { + self.added_files.is_empty() + && self.added_dirs.is_empty() + && self.removed_files.is_empty() + && self.removed_dirs.is_empty() + } + + /// Returns a list of paths that would be added by this `FsSnapshot`. + #[inline] + pub fn added_paths(&self) -> Vec<&Path> { + let mut list = Vec::with_capacity(self.added_files.len() + self.added_dirs.len()); + list.extend(self.added_files()); + list.extend(self.added_dirs()); + + list + } + + /// Returns a list of paths that would be removed by this `FsSnapshot`. + #[inline] + pub fn removed_paths(&self) -> Vec<&Path> { + let mut list = Vec::with_capacity(self.removed_files.len() + self.removed_dirs.len()); + list.extend(self.removed_files()); + list.extend(self.removed_dirs()); + + list + } + + /// Returns a list of file paths that would be added by this `FsSnapshot` + #[inline] + pub fn added_files(&self) -> Vec<&Path> { + let mut added_files: Vec<_> = self.added_files.keys().map(PathBuf::as_path).collect(); + added_files.sort_unstable(); + added_files + } + + /// Returns a list of directory paths that would be added by this `FsSnapshot` + #[inline] + pub fn added_dirs(&self) -> Vec<&Path> { + let mut added_dirs: Vec<_> = self.added_dirs.iter().map(PathBuf::as_path).collect(); + added_dirs.sort_unstable(); + added_dirs + } + + /// Returns a list of file paths that would be removed by this `FsSnapshot` + #[inline] + pub fn removed_files(&self) -> Vec<&Path> { + let mut removed_files: Vec<_> = self.removed_files.iter().map(PathBuf::as_path).collect(); + removed_files.sort_unstable(); + removed_files + } + + /// Returns a list of directory paths that would be removed by this `FsSnapshot` + #[inline] + pub fn removed_dirs(&self) -> Vec<&Path> { + let mut removed_dirs: Vec<_> = self.removed_dirs.iter().map(PathBuf::as_path).collect(); + removed_dirs.sort_unstable(); + removed_dirs + } +} diff --git a/src/syncback/hash/mod.rs b/src/syncback/hash/mod.rs new file mode 100644 index 00000000..5bc16b9f --- /dev/null +++ b/src/syncback/hash/mod.rs @@ -0,0 +1,122 @@ +//! Hashing utilities for a WeakDom. +mod variant; + +pub use variant::*; + +use blake3::{Hash, Hasher}; +use rbx_dom_weak::{ + types::{Ref, Variant}, + Instance, Ustr, WeakDom, +}; +use std::collections::HashMap; + +use crate::{variant_eq::variant_eq, Project}; + +use super::{descendants, filter_properties_preallocated}; + +/// Returns a map of every `Ref` in the `WeakDom` to a hashed version of the +/// `Instance` it points to, including the properties and descendants of the +/// `Instance`. +/// +/// The hashes **do** include the descendants of the Instances in them, +/// so they should only be used for comparing subtrees directly. +pub fn hash_tree(project: &Project, dom: &WeakDom, root_ref: Ref) -> HashMap { + let mut order = descendants(dom, root_ref); + let mut map: HashMap = HashMap::with_capacity(order.len()); + + let mut prop_list = Vec::with_capacity(2); + let mut child_hashes = Vec::new(); + + while let Some(referent) = order.pop() { + let inst = dom.get_by_ref(referent).unwrap(); + let mut hasher = hash_inst_filtered(project, inst, &mut prop_list); + add_children(inst, &map, &mut child_hashes, &mut hasher); + + map.insert(referent, hasher.finalize()); + } + + map +} + +/// Hashes a single Instance from the provided WeakDom, if it exists. +/// +/// This function filters properties using user-provided syncing rules from +/// the passed project. +#[inline] +pub fn hash_instance(project: &Project, dom: &WeakDom, referent: Ref) -> Option { + let mut prop_list = Vec::with_capacity(2); + let inst = dom.get_by_ref(referent)?; + + Some(hash_inst_filtered(project, inst, &mut prop_list).finalize()) +} + +/// Adds the hashes of children for an Instance to the provided Hasher. +fn add_children( + inst: &Instance, + map: &HashMap, + child_hashes: &mut Vec<[u8; 32]>, + hasher: &mut Hasher, +) { + for child_ref in inst.children() { + if let Some(hash) = map.get(child_ref) { + child_hashes.push(*hash.as_bytes()) + } else { + panic!("Invariant violated: child not hashed before parent") + } + } + child_hashes.sort_unstable(); + + for hash in child_hashes.drain(..) { + hasher.update(&hash); + } +} + +/// Performs hashing on an Instance using a filtered property list. +/// Does not include the hashes of any children. +fn hash_inst_filtered<'inst>( + project: &Project, + inst: &'inst Instance, + prop_list: &mut Vec<(Ustr, &'inst Variant)>, +) -> Hasher { + filter_properties_preallocated(project, inst, prop_list); + + hash_inst_prefilled(inst, prop_list) +} + +/// Performs hashing on an Instance using a pre-filled list of properties. +/// It is assumed the property list is **not** sorted, so it is sorted in-line. +fn hash_inst_prefilled<'inst>( + inst: &'inst Instance, + prop_list: &mut Vec<(Ustr, &'inst Variant)>, +) -> Hasher { + let mut hasher = Hasher::new(); + hasher.update(inst.name.as_bytes()); + hasher.update(inst.class.as_bytes()); + + prop_list.sort_unstable_by_key(|(name, _)| *name); + + let descriptor = rbx_reflection_database::get() + .unwrap() + .classes + .get(inst.class.as_str()); + + if let Some(descriptor) = descriptor { + for (name, value) in prop_list.drain(..) { + hasher.update(name.as_bytes()); + if let Some(default) = descriptor.default_properties.get(name.as_str()) { + if !variant_eq(default, value) { + hash_variant(&mut hasher, value) + } + } else { + hash_variant(&mut hasher, value) + } + } + } else { + for (name, value) in prop_list.drain(..) { + hasher.update(name.as_bytes()); + hash_variant(&mut hasher, value) + } + } + + hasher +} diff --git a/src/syncback/hash/variant.rs b/src/syncback/hash/variant.rs new file mode 100644 index 00000000..4e32bc33 --- /dev/null +++ b/src/syncback/hash/variant.rs @@ -0,0 +1,212 @@ +use blake3::Hasher; +use rbx_dom_weak::types::{ContentType, PhysicalProperties, Variant, Vector3}; + +macro_rules! round { + ($value:expr) => { + (($value * 10.0).round() / 10.0) + }; +} + +macro_rules! n_hash { + ($hash:ident, $($num:expr),*) => { + {$( + $hash.update(&($num).to_le_bytes()); + )*} + }; +} + +macro_rules! hash { + ($hash:ident, $value:expr) => {{ + $hash.update($value); + }}; +} + +/// Places `value` into the provided hasher. +pub fn hash_variant(hasher: &mut Hasher, value: &Variant) { + // We need to round floats, though I'm not sure to what degree we can + // realistically do that. + match value { + Variant::Attributes(attrs) => { + let mut sorted: Vec<(&String, &Variant)> = attrs.iter().collect(); + sorted.sort_unstable_by_key(|(name, _)| *name); + for (name, attribute) in sorted { + hasher.update(name.as_bytes()); + hash_variant(hasher, attribute); + } + } + Variant::Axes(a) => hash!(hasher, &[a.bits()]), + Variant::BinaryString(bytes) => hash!(hasher, bytes.as_ref()), + Variant::Bool(bool) => hash!(hasher, &[*bool as u8]), + Variant::BrickColor(color) => n_hash!(hasher, *color as u16), + Variant::CFrame(cf) => { + vector_hash(hasher, cf.position); + vector_hash(hasher, cf.orientation.x); + vector_hash(hasher, cf.orientation.y); + vector_hash(hasher, cf.orientation.z); + } + Variant::Color3(color) => { + n_hash!(hasher, round!(color.r), round!(color.g), round!(color.b)) + } + Variant::Color3uint8(color) => hash!(hasher, &[color.r, color.b, color.g]), + Variant::ColorSequence(seq) => { + let mut new = Vec::with_capacity(seq.keypoints.len()); + for keypoint in &seq.keypoints { + new.push(keypoint); + } + new.sort_unstable_by(|a, b| round!(a.time).partial_cmp(&round!(b.time)).unwrap()); + for keypoint in new { + n_hash!( + hasher, + round!(keypoint.time), + round!(keypoint.color.r), + round!(keypoint.color.g), + round!(keypoint.color.b) + ) + } + } + Variant::Content(content) => match content.value() { + ContentType::None => { + hash!(hasher, &[0]); + } + ContentType::Uri(uri) => { + hash!(hasher, &[1]); + hash!(hasher, uri.as_bytes()); + } + ContentType::Object(referent) => { + hash!(hasher, &[2]); + hash!(hasher, referent.to_string().as_bytes()) + } + other => { + panic!("the ContentType {other:?} cannot be hashed as a Variant") + } + }, + Variant::ContentId(content) => { + let s: &str = content.as_ref(); + hash!(hasher, s.as_bytes()) + } + Variant::Enum(e) => n_hash!(hasher, e.to_u32()), + Variant::Faces(f) => hash!(hasher, &[f.bits()]), + Variant::Float32(n) => n_hash!(hasher, round!(*n)), + Variant::Float64(n) => n_hash!(hasher, round!(n)), + Variant::Font(f) => { + n_hash!(hasher, f.weight as u16); + n_hash!(hasher, f.style as u8); + hash!(hasher, f.family.as_bytes()); + if let Some(cache) = &f.cached_face_id { + hash!(hasher, &[0x01]); + hash!(hasher, cache.as_bytes()); + } else { + hash!(hasher, &[0x00]); + } + } + Variant::Int32(n) => n_hash!(hasher, n), + Variant::Int64(n) => n_hash!(hasher, n), + Variant::MaterialColors(n) => hash!(hasher, n.encode().as_slice()), + Variant::NetAssetRef(net_asset) => hash!(hasher, net_asset.hash().as_bytes()), + Variant::NumberRange(nr) => n_hash!(hasher, round!(nr.max), round!(nr.min)), + Variant::NumberSequence(seq) => { + let mut new = Vec::with_capacity(seq.keypoints.len()); + for keypoint in &seq.keypoints { + new.push(keypoint); + } + new.sort_unstable_by(|a, b| round!(a.time).partial_cmp(&round!(b.time)).unwrap()); + for keypoint in new { + n_hash!( + hasher, + round!(keypoint.time), + round!(keypoint.value), + round!(keypoint.envelope) + ) + } + } + Variant::OptionalCFrame(maybe_cf) => { + if let Some(cf) = maybe_cf { + hash!(hasher, &[0x01]); + vector_hash(hasher, cf.position); + vector_hash(hasher, cf.orientation.x); + vector_hash(hasher, cf.orientation.y); + vector_hash(hasher, cf.orientation.z); + } else { + hash!(hasher, &[0x00]); + } + } + Variant::PhysicalProperties(properties) => match properties { + PhysicalProperties::Default => hash!(hasher, &[0x00]), + PhysicalProperties::Custom(custom) => { + hash!(hasher, &[0x00]); + n_hash!( + hasher, + round!(custom.density()), + round!(custom.friction()), + round!(custom.elasticity()), + round!(custom.friction_weight()), + round!(custom.elasticity_weight()), + round!(custom.acoustic_absorption()) + ) + } + }, + Variant::Ray(ray) => { + vector_hash(hasher, ray.origin); + vector_hash(hasher, ray.direction); + } + Variant::Rect(rect) => n_hash!( + hasher, + round!(rect.max.x), + round!(rect.max.y), + round!(rect.min.x), + round!(rect.min.y) + ), + Variant::Ref(referent) => hash!(hasher, referent.to_string().as_bytes()), + Variant::Region3(region) => { + vector_hash(hasher, region.max); + vector_hash(hasher, region.min); + } + Variant::Region3int16(region) => { + n_hash!( + hasher, + region.max.x, + region.max.y, + region.max.z, + region.min.x, + region.min.y, + region.min.z + ) + } + Variant::SecurityCapabilities(capabilities) => n_hash!(hasher, capabilities.bits()), + Variant::SharedString(sstr) => hash!(hasher, sstr.hash().as_bytes()), + Variant::String(str) => hash!(hasher, str.as_bytes()), + Variant::Tags(tags) => { + let mut dupe: Vec<&str> = tags.iter().collect(); + dupe.sort_unstable(); + for tag in dupe { + hash!(hasher, tag.as_bytes()) + } + } + Variant::UDim(udim) => n_hash!(hasher, round!(udim.scale), udim.offset), + Variant::UDim2(udim) => n_hash!( + hasher, + round!(udim.y.scale), + udim.y.offset, + round!(udim.x.scale), + udim.x.offset + ), + Variant::Vector2(v2) => n_hash!(hasher, round!(v2.x), round!(v2.y)), + Variant::Vector2int16(v2) => n_hash!(hasher, v2.x, v2.y), + Variant::Vector3(v3) => vector_hash(hasher, *v3), + Variant::Vector3int16(v3) => n_hash!(hasher, v3.x, v3.y, v3.z), + + // Hashing UniqueId properties doesn't make sense + Variant::UniqueId(_) => (), + + unknown => { + log::warn!( + "Encountered unknown Variant {:?} while hashing", + unknown.ty() + ) + } + } +} + +fn vector_hash(hasher: &mut Hasher, vector: Vector3) { + n_hash!(hasher, round!(vector.x), round!(vector.y), round!(vector.z)) +} diff --git a/src/syncback/mod.rs b/src/syncback/mod.rs new file mode 100644 index 00000000..61d26674 --- /dev/null +++ b/src/syncback/mod.rs @@ -0,0 +1,534 @@ +mod file_names; +mod fs_snapshot; +mod hash; +mod property_filter; +mod ref_properties; +mod snapshot; + +use anyhow::Context; +use indexmap::IndexMap; +use memofs::Vfs; +use rbx_dom_weak::{ + types::{Ref, Variant}, + ustr, Instance, Ustr, UstrSet, WeakDom, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + env, + path::Path, + sync::OnceLock, +}; + +use crate::{ + glob::Glob, + snapshot::{InstanceWithMeta, RojoTree}, + snapshot_middleware::Middleware, + syncback::ref_properties::{collect_referents, link_referents}, + Project, +}; + +pub use file_names::{extension_for_middleware, name_for_inst, validate_file_name}; +pub use fs_snapshot::FsSnapshot; +pub use hash::*; +pub use property_filter::{filter_properties, filter_properties_preallocated}; +pub use snapshot::{SyncbackData, SyncbackSnapshot}; + +/// The name of an enviroment variable to use to override the behavior of +/// syncback on model files. +/// By default, syncback will use `Rbxm` for model files. +/// If this is set to `1`, it will instead use `Rbxmx`. If it is set to `2`, +/// it will use `JsonModel`. +/// +/// This will **not** override existing `Rbxm` middleware. It will only impact +/// new files. +const DEBUG_MODEL_FORMAT_VAR: &str = "ROJO_SYNCBACK_DEBUG"; + +/// A glob that can be used to tell if a path contains a `.git` folder. +static GIT_IGNORE_GLOB: OnceLock = OnceLock::new(); + +pub fn syncback_loop( + vfs: &Vfs, + old_tree: &mut RojoTree, + mut new_tree: WeakDom, + project: &Project, +) -> anyhow::Result { + let ignore_patterns = project + .syncback_rules + .as_ref() + .map(|rules| rules.compile_globs()) + .transpose()?; + + // TODO: Add a better way to tell if the root of a project is a directory + let skip_pruning = if let Some(path) = &project.tree.path { + let middleware = + Middleware::middleware_for_path(vfs, &project.sync_rules, path.path()).unwrap(); + if let Some(middleware) = middleware { + middleware.is_dir() + } else { + false + } + } else { + false + }; + if !skip_pruning { + // Strip out any objects from the new tree that aren't in the old tree. This + // is necessary so that hashing the roots of each tree won't always result + // in different hashes. Shout out to Roblox for serializing a bunch of + // Services nobody cares about. + log::debug!("Pruning new tree"); + strip_unknown_root_children(&mut new_tree, old_tree); + } + + log::debug!("Collecting referents for new DOM..."); + let deferred_referents = collect_referents(&new_tree); + + // Remove any properties that are manually blocked from syncback via the + // project file. + log::debug!("Pre-filtering properties on DOMs"); + for referent in descendants(&new_tree, new_tree.root_ref()) { + let new_inst = new_tree.get_by_ref_mut(referent).unwrap(); + if let Some(filter) = get_property_filter(project, new_inst) { + for prop in filter { + new_inst.properties.remove(&prop); + } + } + } + for referent in descendants(old_tree.inner(), old_tree.get_root_id()) { + let mut old_inst_rojo = old_tree.get_instance_mut(referent).unwrap(); + let old_inst = old_inst_rojo.inner_mut(); + if let Some(filter) = get_property_filter(project, old_inst) { + for prop in filter { + old_inst.properties.remove(&prop); + } + } + } + + // Handle removing the current camera. + if let Some(syncback_rules) = &project.syncback_rules { + if !syncback_rules.sync_current_camera.unwrap_or_default() { + log::debug!("Removing CurrentCamera from new DOM"); + let mut camera_ref = None; + for child_ref in new_tree.root().children() { + let inst = new_tree.get_by_ref(*child_ref).unwrap(); + if inst.class == "Workspace" { + camera_ref = inst.properties.get(&ustr("CurrentCamera")); + break; + } + } + if let Some(Variant::Ref(camera_ref)) = camera_ref { + if new_tree.get_by_ref(*camera_ref).is_some() { + new_tree.destroy(*camera_ref); + } + } + } + } + + let ignore_referents = project + .syncback_rules + .as_ref() + .and_then(|s| s.ignore_referents) + .unwrap_or_default(); + if !ignore_referents { + log::debug!("Linking referents for new DOM"); + link_referents(deferred_referents, &mut new_tree)?; + } else { + log::debug!("Skipping referent linking as per project syncback rules"); + } + + // As with pruning the children of the new root, we need to ensure the roots + // for both DOMs have the same name otherwise their hashes will always be + // different. + new_tree.root_mut().name = old_tree.root().name().to_string(); + + log::debug!("Hashing project DOM"); + let old_hashes = hash_tree(project, old_tree.inner(), old_tree.get_root_id()); + log::debug!("Hashing file DOM"); + let new_hashes = hash_tree(project, &new_tree, new_tree.root_ref()); + + let project_path = project.folder_location(); + + let syncback_data = SyncbackData { + vfs, + old_tree, + new_tree: &new_tree, + project, + }; + + let mut snapshots = vec![SyncbackSnapshot { + data: syncback_data, + old: Some(old_tree.get_root_id()), + new: new_tree.root_ref(), + path: project.file_location.clone(), + middleware: Some(Middleware::Project), + }]; + + let mut fs_snapshot = FsSnapshot::new(); + + 'syncback: while let Some(snapshot) = snapshots.pop() { + let inst_path = snapshot.get_new_inst_path(snapshot.new); + // We can quickly check that two subtrees are identical and if they are, + // skip reconciling them. + if let Some(old_ref) = snapshot.old { + match (old_hashes.get(&old_ref), new_hashes.get(&snapshot.new)) { + (Some(old), Some(new)) => { + if old == new { + log::trace!( + "Skipping {inst_path} due to it being identically hashed as {old:?}" + ); + continue; + } + } + _ => unreachable!("All Instances in both DOMs should have hashes"), + } + } + + if !is_valid_path(&ignore_patterns, project_path, &snapshot.path) { + log::debug!("Skipping {inst_path} because its path matches ignore pattern"); + continue; + } + if let Some(syncback_rules) = &project.syncback_rules { + // Ignore trees; + for ignored in &syncback_rules.ignore_trees { + if inst_path.starts_with(ignored.as_str()) { + log::debug!("Tree {inst_path} is blocked by project"); + continue 'syncback; + } + } + } + + let middleware = get_best_middleware(&snapshot); + + log::trace!( + "Middleware for {inst_path} is {:?} (path is {})", + middleware, + snapshot.path.display() + ); + + if matches!(middleware, Middleware::Json | Middleware::Toml) { + log::warn!("Cannot syncback {middleware:?} at {inst_path}, skipping"); + continue; + } + + let syncback = match middleware.syncback(&snapshot) { + Ok(syncback) => syncback, + Err(err) if middleware == Middleware::Dir => { + let new_middleware = match env::var(DEBUG_MODEL_FORMAT_VAR) { + Ok(value) if value == "1" => Middleware::Rbxmx, + Ok(value) if value == "2" => Middleware::JsonModel, + _ => Middleware::Rbxm, + }; + let file_name = snapshot + .path + .file_name() + .and_then(|s| s.to_str()) + .context("Directory middleware should have a name in its path")?; + let mut path = snapshot.path.clone(); + path.set_file_name(format!( + "{file_name}.{}", + extension_for_middleware(new_middleware) + )); + let new_snapshot = snapshot.with_new_path(path, snapshot.new, snapshot.old); + log::warn!( + "Could not syncback {inst_path} as a Directory because: {err}.\n\ + It will instead be synced back as a {new_middleware:?}." + ); + let new_syncback_result = new_middleware + .syncback(&new_snapshot) + .with_context(|| format!("Failed to syncback {inst_path}")); + if new_syncback_result.is_ok() && snapshot.old_inst().is_some() { + // We need to remove the old FS representation if we're + // reserializing it as an rbxm. + fs_snapshot.remove_dir(&snapshot.path); + } + new_syncback_result? + } + Err(err) => anyhow::bail!("Failed to syncback {inst_path} because {err}"), + }; + + if !syncback.removed_children.is_empty() { + log::debug!( + "removed children for {inst_path}: {}", + syncback.removed_children.len() + ); + 'remove: for inst in &syncback.removed_children { + let path = inst.metadata().instigating_source.as_ref().unwrap().path(); + let inst_path = snapshot.get_old_inst_path(inst.id()); + if !is_valid_path(&ignore_patterns, project_path, path) { + log::debug!( + "Skipping removing {} because its matches an ignore pattern", + path.display() + ); + continue; + } + if let Some(syncback_rules) = &project.syncback_rules { + for ignored in &syncback_rules.ignore_trees { + if inst_path.starts_with(ignored.as_str()) { + log::debug!("Skipping removing {inst_path} because its path is blocked by project"); + continue 'remove; + } + } + } + if path.is_dir() { + fs_snapshot.remove_dir(path) + } else { + fs_snapshot.remove_file(path) + } + } + } + + // TODO provide replacement snapshots for e.g. two way sync + + fs_snapshot.merge_with_filter(syncback.fs_snapshot, |path| { + is_valid_path(&ignore_patterns, project_path, path) + }); + + snapshots.extend(syncback.children); + } + + Ok(fs_snapshot) +} + +pub struct SyncbackReturn<'sync> { + pub fs_snapshot: FsSnapshot, + pub children: Vec>, + pub removed_children: Vec>, +} + +pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware { + // At some point, we're better off using an O(1) method for checking + // equality for classes like this. + static JSON_MODEL_CLASSES: OnceLock> = OnceLock::new(); + let json_model_classes = JSON_MODEL_CLASSES.get_or_init(|| { + [ + "Sound", + "SoundGroup", + "Sky", + "Atmosphere", + "BloomEffect", + "BlurEffect", + "ColorCorrectionEffect", + "DepthOfFieldEffect", + "SunRaysEffect", + "ParticleEmitter", + "TextChannel", + "TextChatCommand", + // TODO: Implement a way to use inheritance for this + "ChatWindowConfiguration", + "ChatInputBarConfiguration", + "BubbleChatConfiguration", + "ChannelTabsConfiguration", + ] + .into() + }); + + let old_middleware = snapshot + .old_inst() + .and_then(|inst| inst.metadata().middleware); + let inst = snapshot.new_inst(); + + let mut middleware; + + if let Some(override_middleware) = snapshot.middleware { + return override_middleware; + } else if let Some(old_middleware) = old_middleware { + return old_middleware; + } else if json_model_classes.contains(inst.class.as_str()) { + middleware = Middleware::JsonModel; + } else { + middleware = match inst.class.as_str() { + "Folder" | "Configuration" | "Tool" => Middleware::Dir, + "StringValue" => Middleware::Text, + "Script" => Middleware::ServerScript, + "LocalScript" => Middleware::ClientScript, + "ModuleScript" => Middleware::ModuleScript, + "LocalizationTable" => Middleware::Csv, + // This isn't the ideal way to handle this but it works. + name if name.ends_with("Value") => Middleware::JsonModel, + _ => Middleware::Rbxm, + } + } + + if !inst.children().is_empty() { + middleware = match middleware { + Middleware::ServerScript => Middleware::ServerScriptDir, + Middleware::ClientScript => Middleware::ClientScriptDir, + Middleware::ModuleScript => Middleware::ModuleScriptDir, + Middleware::Csv => Middleware::CsvDir, + Middleware::JsonModel | Middleware::Text => Middleware::Dir, + _ => middleware, + } + } + + if middleware == Middleware::Rbxm { + middleware = match env::var(DEBUG_MODEL_FORMAT_VAR) { + Ok(value) if value == "1" => Middleware::Rbxmx, + Ok(value) if value == "2" => Middleware::JsonModel, + _ => Middleware::Rbxm, + } + } + + middleware +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct SyncbackRules { + /// A list of subtrees in a file that will be ignored by Syncback. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + ignore_trees: Vec, + /// A list of patterns to check against the path an Instance would serialize + /// to. If a path matches one of these, the Instance won't be syncbacked. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + ignore_paths: Vec, + /// A map of classes to properties to ignore for that class when doing + /// syncback. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + ignore_properties: IndexMap>, + /// Whether or not the `CurrentCamera` of `Workspace` is included in the + /// syncback or not. Defaults to `false`. + #[serde(skip_serializing_if = "Option::is_none")] + sync_current_camera: Option, + /// Whether or not to sync properties that cannot be modified via scripts. + /// Defaults to `true`. + #[serde(skip_serializing_if = "Option::is_none")] + sync_unscriptable: Option, + /// Whether to skip serializing referent properties like `Model.PrimaryPart` + /// during syncback. Defaults to `false`. + #[serde(skip_serializing_if = "Option::is_none")] + ignore_referents: Option, + /// Whether the globs specified in `ignore_paths` should be modified to also + /// match directories. Defaults to `true`. + /// + /// If this is `true`, it'll take ignore globs that end in `/**` and convert + /// them to also handle the directory they're referring to. This is + /// generally a better UX. + #[serde(skip_serializing_if = "Option::is_none")] + create_ignore_dir_paths: Option, +} + +impl SyncbackRules { + pub fn compile_globs(&self) -> anyhow::Result> { + let mut globs = Vec::with_capacity(self.ignore_paths.len()); + let dir_ignore_paths = self.create_ignore_dir_paths.unwrap_or(true); + + for pattern in &self.ignore_paths { + let glob = Glob::new(pattern) + .with_context(|| format!("the pattern '{pattern}' is not a valid glob"))?; + globs.push(glob); + + if dir_ignore_paths { + if let Some(dir_pattern) = pattern.strip_suffix("/**") { + if let Ok(glob) = Glob::new(dir_pattern) { + globs.push(glob) + } + } + } + } + + Ok(globs) + } +} + +fn is_valid_path(globs: &Option>, base_path: &Path, path: &Path) -> bool { + let git_glob = GIT_IGNORE_GLOB.get_or_init(|| Glob::new(".git/**").unwrap()); + let test_path = match path.strip_prefix(base_path) { + Ok(suffix) => suffix, + Err(_) => path, + }; + if git_glob.is_match(test_path) { + return false; + } + if let Some(ref ignore_paths) = globs { + for glob in ignore_paths { + if glob.is_match(test_path) { + return false; + } + } + } + true +} + +/// Returns a set of properties that should not be written with syncback if +/// one exists. This list is read directly from the Project and takes +/// inheritance into effect. +/// +/// It **does not** handle properties that should not serialize for other +/// reasons, such as being defaults or being marked as not serializing in the +/// ReflectionDatabase. +fn get_property_filter(project: &Project, new_inst: &Instance) -> Option { + let filter = &project.syncback_rules.as_ref()?.ignore_properties; + let mut set = UstrSet::default(); + + let database = rbx_reflection_database::get().unwrap(); + let mut current_class_name = new_inst.class.as_str(); + + loop { + if let Some(list) = filter.get(&ustr(current_class_name)) { + set.extend(list) + } + + let class = database.classes.get(current_class_name)?; + if let Some(super_class) = class.superclass.as_ref() { + current_class_name = super_class; + } else { + break; + } + } + + Some(set) +} + +/// Produces a list of descendants in the WeakDom such that all children come +/// before their parents. +fn descendants(dom: &WeakDom, root_ref: Ref) -> Vec { + let mut queue = VecDeque::new(); + let mut ordered = Vec::new(); + queue.push_front(root_ref); + + while let Some(referent) = queue.pop_front() { + let inst = dom + .get_by_ref(referent) + .expect("Invariant: WeakDom had a Ref that wasn't inside it"); + ordered.push(referent); + for child in inst.children() { + queue.push_back(*child) + } + } + + ordered +} + +/// Removes the children of `new`'s root that are not also children of `old`'s +/// root. +/// +/// This does not care about duplicates, and only filters based on names and +/// class names. +fn strip_unknown_root_children(new: &mut WeakDom, old: &RojoTree) { + let old_root = old.root(); + let old_root_children: HashMap<&str, InstanceWithMeta> = old_root + .children() + .iter() + .map(|referent| { + let inst = old + .get_instance(*referent) + .expect("all children of a DOM's root should exist"); + (inst.name(), inst) + }) + .collect(); + + let root_children = new.root().children().to_vec(); + + for child_ref in root_children { + let child = new + .get_by_ref(child_ref) + .expect("all children of the root should exist in the DOM"); + if let Some(old) = old_root_children.get(child.name.as_str()) { + if old.class_name() == child.class { + continue; + } + } + log::trace!("Pruning root child {} of class {}", child.name, child.class); + new.destroy(child_ref); + } +} diff --git a/src/syncback/property_filter.rs b/src/syncback/property_filter.rs new file mode 100644 index 00000000..47678870 --- /dev/null +++ b/src/syncback/property_filter.rs @@ -0,0 +1,111 @@ +use rbx_dom_weak::{types::Variant, Instance, Ustr, UstrMap}; +use rbx_reflection::{PropertyKind, PropertySerialization, Scriptability}; + +use crate::{variant_eq::variant_eq, Project}; + +/// Returns a map of properties from `inst` that are both allowed under the +/// user-provided settings, are not their default value, and serialize. +pub fn filter_properties<'inst>( + project: &Project, + inst: &'inst Instance, +) -> UstrMap<&'inst Variant> { + let mut map: Vec<(Ustr, &Variant)> = Vec::with_capacity(inst.properties.len()); + filter_properties_preallocated(project, inst, &mut map); + + map.into_iter().collect() +} + +/// Fills `allocation` with a list of properties from `inst` that are +/// user-provided settings, are not their default value, and serialize. +pub fn filter_properties_preallocated<'inst>( + project: &Project, + inst: &'inst Instance, + allocation: &mut Vec<(Ustr, &'inst Variant)>, +) { + let sync_unscriptable = project + .syncback_rules + .as_ref() + .and_then(|s| s.sync_unscriptable) + .unwrap_or(true); + + let class_data = rbx_reflection_database::get() + .unwrap() + .classes + .get(inst.class.as_str()); + + let predicate = |prop_name: &Ustr, prop_value: &Variant| { + // We don't want to serialize Ref or UniqueId properties in JSON files + if matches!(prop_value, Variant::Ref(_) | Variant::UniqueId(_)) { + return true; + } + if !should_property_serialize(&inst.class, prop_name) { + return true; + } + if !sync_unscriptable { + if let Some(data) = class_data { + if let Some(prop_data) = data.properties.get(prop_name.as_str()) { + if matches!(prop_data.scriptability, Scriptability::None) { + return true; + } + } + } + } + false + }; + + if let Some(class_data) = class_data { + let defaults = &class_data.default_properties; + for (name, value) in &inst.properties { + if predicate(name, value) { + continue; + } + if let Some(default) = defaults.get(name.as_str()) { + if !variant_eq(value, default) { + allocation.push((*name, value)); + } + } else { + allocation.push((*name, value)); + } + } + } else { + for (name, value) in &inst.properties { + if predicate(name, value) { + continue; + } + allocation.push((*name, value)); + } + } +} + +fn should_property_serialize(class_name: &str, prop_name: &str) -> bool { + let database = rbx_reflection_database::get().unwrap(); + let mut current_class_name = class_name; + + loop { + let class_data = match database.classes.get(current_class_name) { + Some(data) => data, + None => return true, + }; + if let Some(data) = class_data.properties.get(prop_name) { + log::trace!("found {class_name}.{prop_name} on {current_class_name}"); + return match &data.kind { + // It's not really clear if this can ever happen but I want to + // support it just in case! + PropertyKind::Alias { alias_for } => { + should_property_serialize(current_class_name, alias_for) + } + // Migrations and aliases are happily handled for us by parsers + // so we don't really need to handle them. + PropertyKind::Canonical { serialization } => { + !matches!(serialization, PropertySerialization::DoesNotSerialize) + } + kind => unimplemented!("unknown property kind {kind:?}"), + }; + } else if let Some(super_class) = class_data.superclass.as_ref() { + current_class_name = super_class; + } else { + break; + } + } + true +} diff --git a/src/syncback/ref_properties.rs b/src/syncback/ref_properties.rs new file mode 100644 index 00000000..4a8385f8 --- /dev/null +++ b/src/syncback/ref_properties.rs @@ -0,0 +1,192 @@ +//! Implements iterating through an entire WeakDom and linking all Ref +//! properties using attributes. + +use std::collections::{HashMap, HashSet, VecDeque}; + +use rbx_dom_weak::{ + types::{Attributes, Ref, UniqueId, Variant}, + ustr, Instance, Ustr, WeakDom, +}; + +use crate::{multimap::MultiMap, REF_ID_ATTRIBUTE_NAME, REF_POINTER_ATTRIBUTE_PREFIX}; + +pub struct RefLinks { + /// A map of referents to each of their Ref properties. + prop_links: MultiMap, + /// A set of referents that need their ID rewritten. This includes + /// Instances that have no existing ID. + need_rewrite: HashSet, +} + +#[derive(PartialEq, Eq)] +struct RefLink { + /// The name of a property + name: Ustr, + /// The value of the property. + value: Ref, +} + +/// Iterates through a WeakDom and collects referent properties. +/// +/// They can be linked to a dom later using `link_referents`. +pub fn collect_referents(dom: &WeakDom) -> RefLinks { + let mut ids = HashMap::new(); + let mut need_rewrite = HashSet::new(); + let mut links = MultiMap::new(); + + // Note that this is back-in, front-out. This is important because + // VecDeque::extend is the equivalent to using push_back. + let mut queue = VecDeque::new(); + queue.push_back(dom.root_ref()); + while let Some(inst_ref) = queue.pop_front() { + let pointer = dom.get_by_ref(inst_ref).unwrap(); + queue.extend(pointer.children().iter().copied()); + + for (prop_name, prop_value) in &pointer.properties { + let Variant::Ref(prop_value) = prop_value else { + continue; + }; + if prop_value.is_none() { + continue; + } + + links.insert( + inst_ref, + RefLink { + name: *prop_name, + value: *prop_value, + }, + ); + + let target = dom + .get_by_ref(*prop_value) + .expect("Refs in DOM should point to valid Instances"); + + // 1. Check if target has an ID + if let Some(id) = get_existing_id(target) { + // If it does, we need to check whether that ID is a duplicate + if let Some(id_ref) = ids.get(id) { + // If the same ID points to a new Instance, rewrite it. + if id_ref != prop_value { + if log::log_enabled!(log::Level::Trace) { + log::trace!( + "{} needs an id rewritten because it has the same id as {}", + target.name, + dom.get_by_ref(*id_ref).unwrap().name + ); + } + need_rewrite.insert(*prop_value); + } + } + ids.insert(id, *prop_value); + } else { + log::trace!("{} needs an id rewritten because it has no id but is referred to by {}.{prop_name}", target.name, pointer.name); + // If it does not, it needs one. + need_rewrite.insert(*prop_value); + } + } + } + + RefLinks { + need_rewrite, + prop_links: links, + } +} + +pub fn link_referents(links: RefLinks, dom: &mut WeakDom) -> anyhow::Result<()> { + write_id_attributes(&links, dom)?; + + let mut prop_list = Vec::new(); + + for (inst_id, properties) in links.prop_links { + for ref_link in properties { + let prop_inst = match dom.get_by_ref(ref_link.value) { + Some(inst) => inst, + None => continue, + }; + let id = get_existing_id(prop_inst) + .expect("all Instances that are pointed to should have an ID"); + prop_list.push((ref_link.name, Variant::String(id.to_owned()))); + } + let inst = match dom.get_by_ref_mut(inst_id) { + Some(inst) => inst, + None => continue, + }; + + let mut attributes: Attributes = match inst.properties.remove(&ustr("Attributes")) { + Some(Variant::Attributes(attrs)) => attrs, + None => Attributes::new(), + Some(value) => { + anyhow::bail!( + "expected Attributes to be of type 'Attributes' but it was of type '{:?}'", + value.ty() + ); + } + } + .into_iter() + .filter(|(name, _)| !name.starts_with(REF_POINTER_ATTRIBUTE_PREFIX)) + .collect(); + + for (prop_name, prop_value) in prop_list.drain(..) { + attributes.insert( + format!("{REF_POINTER_ATTRIBUTE_PREFIX}{prop_name}"), + prop_value, + ); + } + + inst.properties + .insert("Attributes".into(), attributes.into()); + } + + Ok(()) +} + +fn write_id_attributes(links: &RefLinks, dom: &mut WeakDom) -> anyhow::Result<()> { + for referent in &links.need_rewrite { + let inst = match dom.get_by_ref_mut(*referent) { + Some(inst) => inst, + None => continue, + }; + let unique_id = match inst.properties.get(&ustr("UniqueId")) { + Some(Variant::UniqueId(id)) => Some(*id), + _ => None, + } + .unwrap_or_else(|| UniqueId::now().unwrap()); + + let attributes = match inst.properties.get_mut(&ustr("Attributes")) { + Some(Variant::Attributes(attrs)) => attrs, + None => { + inst.properties + .insert("Attributes".into(), Attributes::new().into()); + match inst.properties.get_mut(&ustr("Attributes")) { + Some(Variant::Attributes(attrs)) => attrs, + _ => unreachable!(), + } + } + Some(value) => { + anyhow::bail!( + "expected Attributes to be of type 'Attributes' but it was of type '{:?}'", + value.ty() + ); + } + }; + attributes.insert( + REF_ID_ATTRIBUTE_NAME.into(), + Variant::String(unique_id.to_string()), + ); + } + Ok(()) +} + +fn get_existing_id(inst: &Instance) -> Option<&str> { + if let Variant::Attributes(attrs) = inst.properties.get(&ustr("Attributes"))? { + let id = attrs.get(REF_ID_ATTRIBUTE_NAME)?; + match id { + Variant::String(str) => Some(str), + Variant::BinaryString(bstr) => std::str::from_utf8(bstr.as_ref()).ok(), + _ => None, + } + } else { + None + } +} diff --git a/src/syncback/snapshot.rs b/src/syncback/snapshot.rs new file mode 100644 index 00000000..384033d6 --- /dev/null +++ b/src/syncback/snapshot.rs @@ -0,0 +1,259 @@ +use indexmap::IndexMap; +use memofs::Vfs; +use std::path::{Path, PathBuf}; + +use crate::{ + snapshot::{InstanceWithMeta, RojoTree}, + snapshot_middleware::Middleware, + Project, +}; +use rbx_dom_weak::{ + types::{Ref, Variant}, + Instance, Ustr, UstrMap, WeakDom, +}; + +use super::{get_best_middleware, name_for_inst, property_filter::filter_properties}; + +#[derive(Clone, Copy)] +pub struct SyncbackData<'sync> { + pub(super) vfs: &'sync Vfs, + pub(super) old_tree: &'sync RojoTree, + pub(super) new_tree: &'sync WeakDom, + pub(super) project: &'sync Project, +} + +pub struct SyncbackSnapshot<'sync> { + pub data: SyncbackData<'sync>, + pub old: Option, + pub new: Ref, + pub path: PathBuf, + pub middleware: Option, +} + +impl<'sync> SyncbackSnapshot<'sync> { + /// Constructs a SyncbackSnapshot from the provided refs + /// while inheriting this snapshot's path and data. This should be used for + /// directories. + #[inline] + pub fn with_joined_path(&self, new_ref: Ref, old_ref: Option) -> anyhow::Result { + let mut snapshot = Self { + data: self.data, + old: old_ref, + new: new_ref, + path: PathBuf::new(), + middleware: None, + }; + let middleware = get_best_middleware(&snapshot); + let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?; + snapshot.path = self.path.join(name.as_ref()); + + Ok(snapshot) + } + + /// Constructs a SyncbackSnapshot from the provided refs and a base path, + /// while inheriting this snapshot's data. + /// + /// The actual path of the snapshot is made by getting a file name for the + /// snapshot and then appending it to the provided base path. + #[inline] + pub fn with_base_path( + &self, + base_path: &Path, + new_ref: Ref, + old_ref: Option, + ) -> anyhow::Result { + let mut snapshot = Self { + data: self.data, + old: old_ref, + new: new_ref, + path: PathBuf::new(), + middleware: None, + }; + let middleware = get_best_middleware(&snapshot); + let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?; + snapshot.path = base_path.join(name.as_ref()); + + Ok(snapshot) + } + + /// Constructs a SyncbackSnapshot with the provided path and refs while + /// inheriting the data of the this snapshot. + #[inline] + pub fn with_new_path(&self, path: PathBuf, new_ref: Ref, old_ref: Option) -> Self { + Self { + data: self.data, + old: old_ref, + new: new_ref, + path, + middleware: None, + } + } + + /// Allows a middleware to be 'forced' onto a SyncbackSnapshot to override + /// the attempts to derive it. + #[inline] + pub fn middleware(mut self, middleware: Middleware) -> Self { + self.middleware = Some(middleware); + self + } + + /// Returns a map of properties for an Instance from the 'new' tree + /// with filtering done to avoid noise. This method filters out properties + /// that are not meant to be present in Instances that are represented + /// specially by a path, like `LocalScript.Source` and `StringValue.Value`. + /// + /// This method is not necessary or desired for blobs like Rbxm or non-path + /// middlewares like JsonModel. + #[inline] + #[must_use] + pub fn get_path_filtered_properties(&self, new_ref: Ref) -> Option> { + let inst = self.get_new_instance(new_ref)?; + + // The only filtering we have to do is filter out properties that are + // special-cased in some capacity. + let properties = filter_properties(self.data.project, inst) + .into_iter() + .filter(|(name, _)| !filter_out_property(inst, name)) + .collect(); + + Some(properties) + } + + /// Returns a path to the provided Instance in the new DOM. This path is + /// where you would look for the object in Roblox Studio. + #[inline] + pub fn get_new_inst_path(&self, referent: Ref) -> String { + inst_path(self.new_tree(), referent) + } + + /// Returns a path to the provided Instance in the old DOM. This path is + /// where you would look for the object in Roblox Studio. + #[inline] + pub fn get_old_inst_path(&self, referent: Ref) -> String { + inst_path(self.old_tree(), referent) + } + + /// Returns an Instance from the old tree with the provided referent, if it + /// exists. + #[inline] + pub fn get_old_instance(&self, referent: Ref) -> Option> { + self.data.old_tree.get_instance(referent) + } + + /// Returns an Instance from the new tree with the provided referent, if it + /// exists. + #[inline] + pub fn get_new_instance(&self, referent: Ref) -> Option<&'sync Instance> { + self.data.new_tree.get_by_ref(referent) + } + + /// The 'old' Instance this snapshot is for, if it exists. + #[inline] + pub fn old_inst(&self) -> Option> { + self.old + .and_then(|old| self.data.old_tree.get_instance(old)) + } + + /// The 'new' Instance this snapshot is for. + #[inline] + pub fn new_inst(&self) -> &'sync Instance { + self.data + .new_tree + .get_by_ref(self.new) + .expect("SyncbackSnapshot should not contain invalid referents") + } + + /// Returns the root Project that was used to make this snapshot. + #[inline] + pub fn project(&self) -> &'sync Project { + self.data.project + } + + /// Returns the underlying VFS being used for syncback. + #[inline] + pub fn vfs(&self) -> &'sync Vfs { + self.data.vfs + } + + /// Returns the WeakDom used for the 'new' tree. + #[inline] + pub fn new_tree(&self) -> &'sync WeakDom { + self.data.new_tree + } + + /// Returns the WeakDom used for the 'old' tree. + #[inline] + pub fn old_tree(&self) -> &'sync WeakDom { + self.data.old_tree.inner() + } + + /// Returns user-specified property ignore rules. + #[inline] + pub fn ignore_props(&self) -> Option<&IndexMap>> { + self.data + .project + .syncback_rules + .as_ref() + .map(|rules| &rules.ignore_properties) + } + + /// Returns user-specified ignore tree. + #[inline] + pub fn ignore_tree(&self) -> Option<&[String]> { + self.data + .project + .syncback_rules + .as_ref() + .map(|rules| rules.ignore_trees.as_slice()) + } +} + +pub fn filter_out_property(inst: &Instance, prop_name: &str) -> bool { + match inst.class.as_str() { + "Script" | "LocalScript" | "ModuleScript" => { + // These properties shouldn't be set by scripts that are created via + // `$path` or via being on the file system. + prop_name == "Source" || prop_name == "ScriptGuid" + } + "LocalizationTable" => prop_name == "Contents", + "StringValue" => prop_name == "Value", + _ => false, + } +} + +pub fn inst_path(dom: &WeakDom, referent: Ref) -> String { + let mut path = Vec::new(); + + let mut inst = dom.get_by_ref(referent); + while let Some(instance) = inst { + path.push(instance.name.as_str()); + inst = dom.get_by_ref(instance.parent()); + } + // This is to avoid the root's name from appearing in the path. Not + // optimal, but should be fine. + path.pop(); + + path.reverse(); + path.join("/") +} + +#[cfg(test)] +mod test { + use rbx_dom_weak::{InstanceBuilder, WeakDom}; + + use super::inst_path as inst_path_outer; + + #[test] + fn inst_path() { + let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT")); + + let child_1 = new_tree.insert(new_tree.root_ref(), InstanceBuilder::new("Child1")); + let child_2 = new_tree.insert(child_1, InstanceBuilder::new("Child2")); + let child_3 = new_tree.insert(child_2, InstanceBuilder::new("Child3")); + + assert_eq!(inst_path_outer(&new_tree, new_tree.root_ref()), ""); + assert_eq!(inst_path_outer(&new_tree, child_1), "Child1"); + assert_eq!(inst_path_outer(&new_tree, child_2), "Child1/Child2"); + assert_eq!(inst_path_outer(&new_tree, child_3), "Child1/Child2/Child3"); + } +} diff --git a/src/variant_eq.rs b/src/variant_eq.rs new file mode 100644 index 00000000..0a4c058d --- /dev/null +++ b/src/variant_eq.rs @@ -0,0 +1,191 @@ +use rbx_dom_weak::types::{PhysicalProperties, Variant, Vector3}; + +/// Accepts three argumets: a float type and two values to compare. +/// +/// Returns a bool indicating whether they're equal. This accounts for NaN such +/// that `approx_eq!(f32, f32::NAN, f32::NAN)` is `true`. +macro_rules! approx_eq { + ($Ty:ty, $a:expr, $b:expr) => { + float_cmp::approx_eq!($Ty, $a, $b) || $a.is_nan() && $b.is_nan() + }; +} + +/// Compares two variants to determine if they're equal. This correctly takes +/// float comparisons into account. +pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool { + if variant_a.ty() != variant_b.ty() { + return false; + } + + match (variant_a, variant_b) { + (Variant::Attributes(a), Variant::Attributes(b)) => { + // If they're not the same size, we can just abort + if a.len() != b.len() { + return false; + } + + // Since Attributes are stored with a BTreeMap, the keys are sorted + // and we can compare each map's keys in order. + for ((a_name, a_value), (b_name, b_value)) in a.iter().zip(b.iter()) { + if !(a_name == b_name && variant_eq(a_value, b_value)) { + return false; + } + } + + true + } + (Variant::Axes(a), Variant::Axes(b)) => a == b, + (Variant::BinaryString(a), Variant::BinaryString(b)) => a == b, + (Variant::Bool(a), Variant::Bool(b)) => a == b, + (Variant::BrickColor(a), Variant::BrickColor(b)) => a == b, + (Variant::CFrame(a), Variant::CFrame(b)) => { + vector_eq(&a.position, &b.position) + && vector_eq(&a.orientation.x, &b.orientation.x) + && vector_eq(&a.orientation.y, &b.orientation.y) + && vector_eq(&a.orientation.z, &b.orientation.z) + } + (Variant::Color3(a), Variant::Color3(b)) => { + approx_eq!(f32, a.r, b.r) && approx_eq!(f32, a.g, b.g) && approx_eq!(f32, a.b, b.b) + } + (Variant::Color3uint8(a), Variant::Color3uint8(b)) => a == b, + (Variant::ColorSequence(a), Variant::ColorSequence(b)) => { + if a.keypoints.len() != b.keypoints.len() { + return false; + } + let mut a_keypoints: Vec<_> = a.keypoints.iter().collect(); + let mut b_keypoints: Vec<_> = b.keypoints.iter().collect(); + a_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + b_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + + for (a_kp, b_kp) in a_keypoints.iter().zip(b_keypoints) { + if !(approx_eq!(f32, a_kp.time, b_kp.time) + && approx_eq!(f32, a_kp.color.r, b_kp.color.r) + && approx_eq!(f32, a_kp.color.g, b_kp.color.g) + && approx_eq!(f32, a_kp.color.b, b_kp.color.b)) + { + return false; + } + } + true + } + (Variant::Content(a), Variant::Content(b)) => a == b, + (Variant::ContentId(a), Variant::ContentId(b)) => a == b, + (Variant::Enum(a), Variant::Enum(b)) => a == b, + (Variant::Faces(a), Variant::Faces(b)) => a == b, + (Variant::Float32(a), Variant::Float32(b)) => approx_eq!(f32, *a, *b), + (Variant::Float64(a), Variant::Float64(b)) => approx_eq!(f64, *a, *b), + (Variant::Font(a), Variant::Font(b)) => { + a.weight == b.weight + && a.style == b.style + && a.family == b.family + && a.cached_face_id == b.cached_face_id + } + (Variant::Int32(a), Variant::Int32(b)) => a == b, + (Variant::Int64(a), Variant::Int64(b)) => a == b, + (Variant::MaterialColors(a), Variant::MaterialColors(b)) => a.encode() == b.encode(), + (Variant::NetAssetRef(a), Variant::NetAssetRef(b)) => a == b, + (Variant::NumberRange(a), Variant::NumberRange(b)) => { + approx_eq!(f32, a.max, b.max) && approx_eq!(f32, a.min, b.min) + } + (Variant::NumberSequence(a), Variant::NumberSequence(b)) => { + if a.keypoints.len() != b.keypoints.len() { + return false; + } + let mut a_keypoints: Vec<_> = a.keypoints.iter().collect(); + let mut b_keypoints: Vec<_> = b.keypoints.iter().collect(); + a_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + b_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + + for (a_kp, b_kp) in a_keypoints.iter().zip(b_keypoints) { + if !(approx_eq!(f32, a_kp.time, b_kp.time) + && approx_eq!(f32, a_kp.value, b_kp.value) + && approx_eq!(f32, a_kp.envelope, b_kp.envelope)) + { + return false; + } + } + true + } + (Variant::OptionalCFrame(a), Variant::OptionalCFrame(b)) => match (a, b) { + (Some(a), Some(b)) => { + vector_eq(&a.position, &b.position) + && vector_eq(&a.orientation.x, &b.orientation.x) + && vector_eq(&a.orientation.y, &b.orientation.y) + && vector_eq(&a.orientation.z, &b.orientation.z) + } + (None, None) => true, + _ => false, + }, + (Variant::PhysicalProperties(a), Variant::PhysicalProperties(b)) => match (a, b) { + (PhysicalProperties::Default, PhysicalProperties::Default) => true, + (PhysicalProperties::Custom(a2), PhysicalProperties::Custom(b2)) => { + approx_eq!(f32, a2.density(), b2.density()) + && approx_eq!(f32, a2.elasticity(), b2.elasticity()) + && approx_eq!(f32, a2.friction(), b2.friction()) + && approx_eq!(f32, a2.elasticity_weight(), b2.elasticity_weight()) + && approx_eq!(f32, a2.friction_weight(), b2.friction_weight()) + && approx_eq!(f32, a2.acoustic_absorption(), b2.acoustic_absorption()) + } + _ => false, + }, + (Variant::Ray(a), Variant::Ray(b)) => { + vector_eq(&a.direction, &b.direction) && vector_eq(&a.origin, &b.origin) + } + (Variant::Rect(a), Variant::Rect(b)) => { + approx_eq!(f32, a.max.x, b.max.x) + && approx_eq!(f32, a.max.y, b.max.y) + && approx_eq!(f32, a.min.x, b.min.x) + && approx_eq!(f32, a.min.y, b.min.y) + } + (Variant::Ref(a), Variant::Ref(b)) => a == b, + (Variant::Region3(a), Variant::Region3(b)) => { + vector_eq(&a.max, &b.max) && vector_eq(&a.min, &b.min) + } + (Variant::Region3int16(a), Variant::Region3int16(b)) => a == b, + (Variant::SecurityCapabilities(a), Variant::SecurityCapabilities(b)) => a == b, + (Variant::SharedString(a), Variant::SharedString(b)) => a == b, + (Variant::Tags(a), Variant::Tags(b)) => { + let mut a_sorted: Vec<&str> = a.iter().collect(); + let mut b_sorted: Vec<&str> = b.iter().collect(); + if a_sorted.len() == b_sorted.len() { + a_sorted.sort_unstable(); + b_sorted.sort_unstable(); + for (a_tag, b_tag) in a_sorted.into_iter().zip(b_sorted) { + if a_tag != b_tag { + return false; + } + } + true + } else { + false + } + } + (Variant::UDim(a), Variant::UDim(b)) => { + approx_eq!(f32, a.scale, b.scale) && a.offset == b.offset + } + (Variant::UDim2(a), Variant::UDim2(b)) => { + approx_eq!(f32, a.x.scale, b.x.scale) + && a.x.offset == b.x.offset + && approx_eq!(f32, a.y.scale, b.y.scale) + && a.y.offset == b.y.offset + } + (Variant::UniqueId(a), Variant::UniqueId(b)) => a == b, + (Variant::String(a), Variant::String(b)) => a == b, + (Variant::Vector2(a), Variant::Vector2(b)) => { + approx_eq!(f32, a.x, b.x) && approx_eq!(f32, a.y, b.y) + } + (Variant::Vector2int16(a), Variant::Vector2int16(b)) => a == b, + (Variant::Vector3(a), Variant::Vector3(b)) => vector_eq(a, b), + (Variant::Vector3int16(a), Variant::Vector3int16(b)) => a == b, + (a, b) => panic!( + "unsupport variant comparison: {:?} and {:?}", + a.ty(), + b.ty() + ), + } +} + +#[inline(always)] +fn vector_eq(a: &Vector3, b: &Vector3) -> bool { + approx_eq!(f32, a.x, b.x) && approx_eq!(f32, a.y, b.y) && approx_eq!(f32, a.z, b.z) +} diff --git a/src/web/ui.rs b/src/web/ui.rs index da620966..4d63eaad 100644 --- a/src/web/ui.rs +++ b/src/web/ui.rs @@ -165,6 +165,7 @@ impl UiService {

"specified_id: " { format!("{:?}", metadata.specified_id) }
"ignore_unknown_instances: " { metadata.ignore_unknown_instances.to_string() }
"instigating source: " { format!("{:?}", metadata.instigating_source) }
+
"middleware: " { format!("{:?}", metadata.middleware) }
{ relevant_paths } }; diff --git a/tests/rojo_test/io_util.rs b/tests/rojo_test/io_util.rs index ce940f4a..c18e457d 100644 --- a/tests/rojo_test/io_util.rs +++ b/tests/rojo_test/io_util.rs @@ -10,6 +10,8 @@ use walkdir::WalkDir; pub static ROJO_PATH: &str = env!("CARGO_BIN_EXE_rojo"); pub static BUILD_TESTS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/build-tests"); pub static SERVE_TESTS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/serve-tests"); +pub static SYNCBACK_TESTS_PATH: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/syncback-tests"); pub fn get_working_dir_path() -> PathBuf { let mut manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/tests/rojo_test/mod.rs b/tests/rojo_test/mod.rs index fd0cda3a..6ce465b4 100644 --- a/tests/rojo_test/mod.rs +++ b/tests/rojo_test/mod.rs @@ -1,3 +1,4 @@ pub mod internable; pub mod io_util; pub mod serve_util; +pub mod syncback_util; diff --git a/tests/rojo_test/syncback_util.rs b/tests/rojo_test/syncback_util.rs new file mode 100644 index 00000000..b4b54953 --- /dev/null +++ b/tests/rojo_test/syncback_util.rs @@ -0,0 +1,116 @@ +use std::{io::Write as _, path::Path, process::Command}; + +use insta::{assert_snapshot, assert_yaml_snapshot}; +use tempfile::tempdir; + +use crate::rojo_test::io_util::SYNCBACK_TESTS_PATH; + +use super::io_util::{copy_recursive, ROJO_PATH}; + +const INPUT_FILE_PROJECT: &str = "input-project"; +const INPUT_FILE_PLACE: &str = "input.rbxl"; +const INPUT_FILE_MODEL: &str = "input.rbxm"; + +/// Convenience method to run a `rojo syncback` test. +/// +/// Test projects should be defined in the `syncback-tests` folder; their filename +/// should be given as the first parameter. +/// +/// The passed in callback is where the actual test body should go. Setup and +/// cleanup happens automatically. +pub fn run_syncback_test(name: &str, callback: impl FnOnce(&Path)) { + let _ = env_logger::try_init(); + + // let working_dir = get_working_dir_path(); + + let source_path = Path::new(SYNCBACK_TESTS_PATH) + .join(name) + .join(INPUT_FILE_PROJECT); + // We want to support both rbxls and rbxms as input + let input_file = { + let mut path = Path::new(SYNCBACK_TESTS_PATH) + .join(name) + .join(INPUT_FILE_PLACE); + if !path.exists() { + path.set_file_name(INPUT_FILE_MODEL); + } + path + }; + + let test_dir = tempdir().expect("Couldn't create temporary directory"); + let project_path = test_dir + .path() + .canonicalize() + .expect("Couldn't canonicalize temporary directory path") + .join(name); + + let source_is_file = fs_err::metadata(&source_path).unwrap().is_file(); + + if source_is_file { + fs_err::copy(&source_path, &project_path).expect("couldn't copy project file"); + } else { + fs_err::create_dir(&project_path).expect("Couldn't create temporary project subdirectory"); + + copy_recursive(&source_path, &project_path) + .expect("Couldn't copy project to temporary directory"); + }; + + let output = Command::new(ROJO_PATH) + // I don't really understand why setting the working directory breaks this, but it does. + // It's a bit concerning but I'm more interested in writing tests than debugging it right now. + // TODO: Figure out why and fix it. + // .current_dir(working_dir) + .args([ + "--color", + "never", + "syncback", + project_path.to_str().unwrap(), + "--input", + input_file.to_str().unwrap(), + "--non-interactive", + "--list", + ]) + .output() + .expect("Couldn't spawn syncback process"); + + if !output.status.success() { + let mut lock = std::io::stderr().lock(); + writeln!( + lock, + "Rojo exited with status code {:?}", + output.status.code() + ) + .unwrap(); + writeln!(lock, "Stdout from process:").unwrap(); + lock.write_all(&output.stdout).unwrap(); + writeln!(lock, "Stderr from process:").unwrap(); + lock.write_all(&output.stderr).unwrap(); + + std::process::exit(1) + } + + let mut settings = insta::Settings::new(); + let snapshot_path = Path::new(SYNCBACK_TESTS_PATH) + .parent() + .unwrap() + .join("syncback-test-snapshots"); + settings.set_snapshot_path(snapshot_path); + settings.set_sort_maps(true); + + settings.bind(|| { + assert_snapshot!( + format!("{name}-stdout"), + String::from_utf8_lossy(&output.stdout) + ) + }); + + settings.bind(|| callback(project_path.as_path())) +} + +pub fn snapshot_rbxm(name: &str, input: Vec, file_name: &str) { + assert_yaml_snapshot!( + name, + rbx_binary::text_format::DecodedModel::from_reader(input.as_slice()), + file_name + ) +} diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs index a348e228..bc710016 100644 --- a/tests/tests/mod.rs +++ b/tests/tests/mod.rs @@ -1,2 +1,3 @@ mod build; mod serve; +mod syncback; diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs new file mode 100644 index 00000000..41794eb9 --- /dev/null +++ b/tests/tests/syncback.rs @@ -0,0 +1,81 @@ +use std::ffi::OsStr; + +use insta::assert_snapshot; + +use crate::rojo_test::syncback_util::{run_syncback_test, snapshot_rbxm}; + +macro_rules! syncback_tests { + ($($test_name:ident => $list:expr$(,)?),*) => {$( + #[test] + fn $test_name() { + run_syncback_test(stringify!($test_name), |path| { + for name in $list { + let snapshot_name = format!(concat!(stringify!($test_name), "-{}"), name); + let new = path.join::<&str>(name); + if let Some("rbxm") = new.extension().and_then(OsStr::to_str) { + let content = fs_err::read(new).unwrap(); + snapshot_rbxm(&snapshot_name, content, name); + } else { + let content = fs_err::read_to_string(new).unwrap(); + assert_snapshot!(snapshot_name, content, name); + } + } + }); + } + )*}; +} + +syncback_tests! { + // Ensures that there's only one copy written to disk if navigating a + // project file might yield two copies + child_but_not => ["OnlyOneCopy/child_of_one.luau", "ReplicatedStorage/child_replicated_storage.luau"], + // Ensures that syncback works with CSVs + csv => ["src/csv_init/init.csv", "src/csv.csv"], + // Ensures that if a RojoId is duplicated somewhere in the project, it's + // rewritten rather than synced back as a conflict + duplicate_rojo_id => ["container.model.json"], + // Ensures that the `ignorePaths` setting works for additions + ignore_paths_adding => ["src/int_value.model.json", "src/subfolder/string_value.txt"], + // Ensures that the `ignorePaths` setting works for `init` files + ignore_paths_init => ["src/non-init.luau", "src/init-file/init.luau"], + // Ensures that the `ignorePaths` setting works for removals + ignore_paths_removing => ["src/Message.rbxm"], + // Ensures that `ignoreTrees` works for additions + ignore_trees_adding => [], + // Ensures that `ignoreTrees` works for removals + ignore_trees_removing => [], + // Ensures that all of the JSON middlewares are handled as expected + json_middlewares => ["src/dir_with_meta/init.meta.json", "src/model_json.model.json", "src/project_json.project.json"], + // Ensures projects that refer to other projects work as expected + nested_projects => ["nested.project.json", "string_value.txt"], + // Ensures files that are ignored by nested projects are picked up if + // they're included in second project. Unusual but perfectly workable + // pattern that syncback has to support. + nested_projects_weird => ["src/modules/ClientModule.luau", "src/modules/ServerModule.luau"], + // Ensures that projects respect `init` files when they're directly referenced from a node + project_init => ["src/init.luau"], + // Ensures that projects can be reserialized by syncback and that + // default.project.json doesn't change unexpectedly. + project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json"], + // Confirms that Instances that cannot serialize as directories serialize as rbxms + rbxm_fallback => ["src/ChildWithDuplicates.rbxm"], + // Ensures that ref properties are linked properly on the file system + ref_properties => ["src/pointer.model.json", "src/target.model.json"], + // Ensures that ref properties are linked when no attributes are manually + // set in the DataModel + ref_properties_blank => ["src/pointer.model.json", "src/target.meta.json", "src/target.txt"], + // Ensures that if there is a conflict in RojoRefs, one of them is rewritten. + 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. + ref_properties_duplicate => [], + // 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"], + // Ensures that StringValues inside project files are written to the + // project file, but only if they don't have `$path` set + string_value_project => ["default.project.json"], + // Ensures that sync rules are respected. This is really just a test to + // ensure it uses the old path when possible, but we want the coverage. + sync_rules => ["src/module.modulescript", "src/text.text"], + // Ensures that the `syncUnscriptable` setting works + unscriptable_properties => ["default.project.json"], +} From 2b47861a4fb50dc78630e1a8977ca1988dc7515d Mon Sep 17 00:00:00 2001 From: Micah Date: Wed, 19 Nov 2025 19:18:14 -0800 Subject: [PATCH 06/11] Properly support EnumItem variants in hashing and variant_eq (#1165) --- src/syncback/hash/variant.rs | 4 ++++ src/variant_eq.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/src/syncback/hash/variant.rs b/src/syncback/hash/variant.rs index 4e32bc33..3457beac 100644 --- a/src/syncback/hash/variant.rs +++ b/src/syncback/hash/variant.rs @@ -85,6 +85,10 @@ pub fn hash_variant(hasher: &mut Hasher, value: &Variant) { hash!(hasher, s.as_bytes()) } Variant::Enum(e) => n_hash!(hasher, e.to_u32()), + Variant::EnumItem(e) => { + hash!(hasher, e.ty.as_bytes()); + n_hash!(hasher, e.value); + } Variant::Faces(f) => hash!(hasher, &[f.bits()]), Variant::Float32(n) => n_hash!(hasher, round!(*n)), Variant::Float64(n) => n_hash!(hasher, round!(n)), diff --git a/src/variant_eq.rs b/src/variant_eq.rs index 0a4c058d..ae3ca20d 100644 --- a/src/variant_eq.rs +++ b/src/variant_eq.rs @@ -71,6 +71,7 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool { (Variant::Content(a), Variant::Content(b)) => a == b, (Variant::ContentId(a), Variant::ContentId(b)) => a == b, (Variant::Enum(a), Variant::Enum(b)) => a == b, + (Variant::EnumItem(a), Variant::EnumItem(b)) => a == b, (Variant::Faces(a), Variant::Faces(b)) => a == b, (Variant::Float32(a), Variant::Float32(b)) => approx_eq!(f32, *a, *b), (Variant::Float64(a), Variant::Float64(b)) => approx_eq!(f64, *a, *b), From 015b5bda141de9e61a8c65239ac41766d15be444 Mon Sep 17 00:00:00 2001 From: Ken Loeffler Date: Fri, 21 Nov 2025 15:02:09 +0000 Subject: [PATCH 07/11] Set crate and plugin versions to 7.7.0-prealpha (#1170) --- Cargo.lock | 2 +- Cargo.toml | 2 +- plugin/Version.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52577619..659d2968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1898,7 +1898,7 @@ dependencies = [ [[package]] name = "rojo" -version = "7.6.1" +version = "7.7.0-prealpha" dependencies = [ "anyhow", "backtrace", diff --git a/Cargo.toml b/Cargo.toml index 2deb184b..94315413 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rojo" -version = "7.6.1" +version = "7.7.0-prealpha" rust-version = "1.88" authors = [ "Lucien Greathouse ", diff --git a/plugin/Version.txt b/plugin/Version.txt index bbd8e920..6bb62d27 100644 --- a/plugin/Version.txt +++ b/plugin/Version.txt @@ -1 +1 @@ -7.6.1 \ No newline at end of file +7.7.0-prealpha \ No newline at end of file From 93e9c51204767e3215e698f30c81fdae524a14c5 Mon Sep 17 00:00:00 2001 From: Ken Loeffler Date: Fri, 21 Nov 2025 15:04:34 +0000 Subject: [PATCH 08/11] Fix rojo plugin install by adding Vfs::exists (#1169) --- crates/memofs/CHANGELOG.md | 6 +++++- crates/memofs/src/in_memory_fs.rs | 5 +++++ crates/memofs/src/lib.rs | 17 +++++++++++++++++ crates/memofs/src/noop_backend.rs | 4 ++++ crates/memofs/src/std_backend.rs | 4 ++++ src/cli/plugin.rs | 22 +++++++++++++++------- src/project.rs | 2 +- 7 files changed, 51 insertions(+), 9 deletions(-) diff --git a/crates/memofs/CHANGELOG.md b/crates/memofs/CHANGELOG.md index 16d578c3..85b51523 100644 --- a/crates/memofs/CHANGELOG.md +++ b/crates/memofs/CHANGELOG.md @@ -1,11 +1,15 @@ # memofs Changelog ## Unreleased Changes +* Added `Vfs::exists`. [#1169] +* Added `create_dir` and `create_dir_all` to allow creating directories. [#937] + +[#1169]: https://github.com/rojo-rbx/rojo/pull/1169 +[#937]: https://github.com/rojo-rbx/rojo/pull/937 ## 0.3.0 (2024-03-15) * Changed `StdBackend` file watching component to use minimal recursive watches. [#830] * Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854] -* Added `create_dir` and `create_dir_all` to allow creating directories. [#830]: https://github.com/rojo-rbx/rojo/pull/830 [#854]: https://github.com/rojo-rbx/rojo/pull/854 diff --git a/crates/memofs/src/in_memory_fs.rs b/crates/memofs/src/in_memory_fs.rs index acca08c4..3c825575 100644 --- a/crates/memofs/src/in_memory_fs.rs +++ b/crates/memofs/src/in_memory_fs.rs @@ -157,6 +157,11 @@ impl VfsBackend for InMemoryFs { ) } + fn exists(&mut self, path: &Path) -> io::Result { + let inner = self.inner.lock().unwrap(); + Ok(inner.entries.contains_key(path)) + } + fn read_dir(&mut self, path: &Path) -> io::Result { let inner = self.inner.lock().unwrap(); diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index 028a413b..bc7e329e 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -70,6 +70,7 @@ impl IoResultExt for io::Result { pub trait VfsBackend: sealed::Sealed + Send + 'static { fn read(&mut self, path: &Path) -> io::Result>; fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>; + fn exists(&mut self, path: &Path) -> io::Result; fn read_dir(&mut self, path: &Path) -> io::Result; fn create_dir(&mut self, path: &Path) -> io::Result<()>; fn create_dir_all(&mut self, path: &Path) -> io::Result<()>; @@ -175,6 +176,11 @@ impl VfsInner { Ok(Arc::new(contents_str.into())) } + fn exists>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + self.backend.exists(path) + } + fn write, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> { let path = path.as_ref(); let contents = contents.as_ref(); @@ -338,6 +344,17 @@ impl Vfs { self.inner.lock().unwrap().read_dir(path) } + /// Return whether the given path exists. + /// + /// Roughly equivalent to [`std::fs::exists`][std::fs::exists]. + /// + /// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html + #[inline] + pub fn exists>(&self, path: P) -> io::Result { + let path = path.as_ref(); + self.inner.lock().unwrap().exists(path) + } + /// Creates a directory at the provided location. /// /// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir]. diff --git a/crates/memofs/src/noop_backend.rs b/crates/memofs/src/noop_backend.rs index 18f6f809..dd10d452 100644 --- a/crates/memofs/src/noop_backend.rs +++ b/crates/memofs/src/noop_backend.rs @@ -22,6 +22,10 @@ impl VfsBackend for NoopBackend { Err(io::Error::other("NoopBackend doesn't do anything")) } + fn exists(&mut self, _path: &Path) -> io::Result { + Err(io::Error::other("NoopBackend doesn't do anything")) + } + fn read_dir(&mut self, _path: &Path) -> io::Result { Err(io::Error::other("NoopBackend doesn't do anything")) } diff --git a/crates/memofs/src/std_backend.rs b/crates/memofs/src/std_backend.rs index d39fb179..f354d806 100644 --- a/crates/memofs/src/std_backend.rs +++ b/crates/memofs/src/std_backend.rs @@ -63,6 +63,10 @@ impl VfsBackend for StdBackend { fs_err::write(path, data) } + fn exists(&mut self, path: &Path) -> io::Result { + std::fs::exists(path) + } + fn read_dir(&mut self, path: &Path) -> io::Result { let entries: Result, _> = fs_err::read_dir(path)?.collect(); let mut entries = entries?; diff --git a/src/cli/plugin.rs b/src/cli/plugin.rs index aa8773b3..03177654 100644 --- a/src/cli/plugin.rs +++ b/src/cli/plugin.rs @@ -46,10 +46,18 @@ impl PluginSubcommand { } } -fn install_plugin() -> anyhow::Result<()> { +fn initialize_plugin() -> anyhow::Result { let plugin_snapshot: VfsSnapshot = bincode::deserialize(PLUGIN_BINCODE) .expect("Rojo's plugin was not properly packed into Rojo's binary"); + let mut in_memory_fs = InMemoryFs::new(); + in_memory_fs.load_snapshot("/plugin", plugin_snapshot)?; + + let vfs = Vfs::new(in_memory_fs); + Ok(ServeSession::new(vfs, "/plugin")?) +} + +fn install_plugin() -> anyhow::Result<()> { let studio = RobloxStudio::locate()?; let plugins_folder_path = studio.plugins_path(); @@ -59,17 +67,12 @@ fn install_plugin() -> anyhow::Result<()> { 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 session = initialize_plugin()?; let tree = session.tree(); let root_id = tree.get_root_id(); @@ -92,3 +95,8 @@ fn uninstall_plugin() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn plugin_initialize() { + assert!(initialize_plugin().is_ok()) +} diff --git a/src/project.rs b/src/project.rs index 20c2dc9c..5cc8278e 100644 --- a/src/project.rs +++ b/src/project.rs @@ -295,7 +295,7 @@ impl Project { // Check for default projects. for default_project_name in DEFAULT_PROJECT_NAMES { let project_path = path.join(default_project_name); - if project_path.exists() { + if let Ok(true) = vfs.exists(&project_path) { return Self::load_exact(vfs, &project_path, None); } } From a99e877b7cf7d7f4a2f4aef9dfc555347f0515dd Mon Sep 17 00:00:00 2001 From: Micah Date: Wed, 26 Nov 2025 13:59:12 -0800 Subject: [PATCH 09/11] Actually skip `.gitignore` if `--skip-git` is passed to `init` (#1172) --- CHANGELOG.md | 2 ++ src/cli/init.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b267ff8c..6013f778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* 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]) This command takes a Roblox file and pulls Instances out of it and places them in the correct position in the provided project. Syncback is primarily controlled by the project file. Any Instances who are either referenced in the project file or a descendant @@ -73,6 +74,7 @@ Making a new release? Simply add the new header with the version and date undern [#937]: https://github.com/rojo-rbx/rojo/pull/937 [#994]: https://github.com/rojo-rbx/rojo/pull/994 [#1159]: https://github.com/rojo-rbx/rojo/pull/1159 +[#1172]: https://github.com/rojo-rbx/rojo/pull/1172 ## [7.6.1] (November 6th, 2025) diff --git a/src/cli/init.rs b/src/cli/init.rs index 03a4bc36..eb323310 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -80,8 +80,12 @@ impl InitCommand { } else { let content = vfs.read_to_string_lf_normalized(&path)?; if let Some(file_stem) = path.file_name().and_then(OsStr::to_str) { - if file_stem == GIT_IGNORE_PLACEHOLDER && !self.skip_git { - path.set_file_name(".gitignore"); + if file_stem == GIT_IGNORE_PLACEHOLDER { + if self.skip_git { + continue; + } else { + path.set_file_name(".gitignore"); + } } } write_if_not_exists( From a61a1bef5550da8a72262d81f3737b8907af8c9e Mon Sep 17 00:00:00 2001 From: Micah Date: Wed, 26 Nov 2025 16:11:39 -0800 Subject: [PATCH 10/11] Roundtrip schemas in syncback (#1173) --- ...yncback_util__schema_roundtrip-stdout.snap | 10 ++++++ ...schema_roundtrip-default.project.json.snap | 20 ++++++++++++ ...ema_roundtrip-src__adjacent.meta.json.snap | 10 ++++++ ...a_roundtrip-src__init__init.meta.json.snap | 10 ++++++ ...chema_roundtrip-src__model.model.json.snap | 8 +++++ .../input-project/default.project.json | 13 ++++++++ .../input-project/src/adjacent.luau | 1 + .../input-project/src/adjacent.meta.json | 3 ++ .../input-project/src/init/init.meta.json | 3 ++ .../input-project/src/model.model.json | 7 +++++ .../schema_roundtrip/input.rbxm | Bin 0 -> 1469 bytes src/snapshot/metadata.rs | 9 ++++++ ..._snapshot__tests__apply__add_property.snap | 2 +- ...s__apply__remove_property_after_patch.snap | 2 +- ...tests__apply__remove_property_initial.snap | 2 +- ...tests__apply__set_name_and_class_name.snap | 2 +- ...__snapshot__tests__compute__add_child.snap | 2 +- src/snapshot_middleware/json_model.rs | 11 ++++++- src/snapshot_middleware/meta_file.rs | 29 ++++++++++++++++-- ...t_middleware__csv__test__csv_from_vfs.snap | 1 + ...pshot_middleware__csv__test__csv_init.snap | 1 + ...leware__csv__test__csv_init_with_meta.snap | 1 + ..._middleware__csv__test__csv_with_meta.snap | 1 + ...t_middleware__dir__test__empty_folder.snap | 1 + ...ddleware__dir__test__folder_in_folder.snap | 2 ++ ...leware__json__test__instance_from_vfs.snap | 1 + ...middleware__json__test__with_metadata.snap | 1 + ...are__json_model__test__model_from_vfs.snap | 3 +- ...on_model__test__model_from_vfs_legacy.snap | 3 +- ...are__lua__test__class_client_from_vfs.snap | 1 + ...are__lua__test__class_module_from_vfs.snap | 1 + ...re__lua__test__class_module_with_meta.snap | 1 + ...are__lua__test__class_script_disabled.snap | 1 + ...re__lua__test__class_script_with_meta.snap | 1 + ...are__lua__test__class_server_from_vfs.snap | 1 + ...ware__lua__test__init_module_from_vfs.snap | 1 + ..._test__init_module_from_vfs_with_meta.snap | 1 + ...re__lua__test__plugin_module_from_vfs.snap | 1 + ...lua__test__runcontext_client_from_vfs.snap | 1 + ...lua__test__runcontext_module_from_vfs.snap | 1 + ...ua__test__runcontext_module_with_meta.snap | 1 + ...lua__test__runcontext_script_disabled.snap | 1 + ...ua__test__runcontext_script_with_meta.snap | 1 + ...lua__test__runcontext_server_from_vfs.snap | 1 + ...__meta_file__test__adjacent_read_json.snap | 1 + ..._meta_file__test__adjacent_read_jsonc.snap | 1 + ..._meta_file__test__directory_read_json.snap | 1 + ...meta_file__test__directory_read_jsonc.snap | 1 + ...eware__project__test__no_name_project.snap | 1 + ...oject__test__project_from_direct_file.snap | 2 +- ...test__project_path_property_overrides.snap | 2 +- ..._project__test__project_with_children.snap | 3 +- ...t__test__project_with_path_to_project.snap | 2 +- ...ct_with_path_to_project_with_children.snap | 3 +- ...oject__test__project_with_path_to_txt.snap | 1 + ...est__project_with_resolved_properties.snap | 2 +- ...t__project_with_unresolved_properties.snap | 2 +- ...leware__toml__test__instance_from_vfs.snap | 1 + ...middleware__toml__test__with_metadata.snap | 1 + ...dleware__txt__test__instance_from_vfs.snap | 1 + ..._middleware__txt__test__with_metadata.snap | 1 + ...leware__yaml__test__instance_from_vfs.snap | 1 + ...middleware__yaml__test__with_metadata.snap | 1 + tests/tests/syncback.rs | 4 +++ 64 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-default.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__adjacent.meta.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init__init.meta.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__model.model.json.snap create mode 100644 rojo-test/syncback-tests/schema_roundtrip/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.luau create mode 100644 rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.meta.json create mode 100644 rojo-test/syncback-tests/schema_roundtrip/input-project/src/init/init.meta.json create mode 100644 rojo-test/syncback-tests/schema_roundtrip/input-project/src/model.model.json create mode 100644 rojo-test/syncback-tests/schema_roundtrip/input.rbxm diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap new file mode 100644 index 00000000..dfbdabd6 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__schema_roundtrip-stdout.snap @@ -0,0 +1,10 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json +Writing src/adjacent.luau +Writing src/adjacent.meta.json +Writing src/init/init.meta.json +Writing src/model.model.json +Writing src/init diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-default.project.json.snap new file mode 100644 index 00000000..3fb7e5c2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-default.project.json.snap @@ -0,0 +1,20 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "$schema": "rojo/project-schema", + "name": "schema_roundtrip", + "tree": { + "$className": "Folder", + "src": { + "$path": "src" + }, + "trigger_reserialization": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__adjacent.meta.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__adjacent.meta.json.snap new file mode 100644 index 00000000..9633f1e6 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__adjacent.meta.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/adjacent.meta.json +--- +{ + "$schema": "rojo/adjacent-meta", + "attributes": { + "trigger_reserialization": true + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init__init.meta.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init__init.meta.json.snap new file mode 100644 index 00000000..dd89fc90 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__init__init.meta.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/init/init.meta.json +--- +{ + "$schema": "rojo/init-meta", + "attributes": { + "trigger_reserialization": true + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__model.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__model.model.json.snap new file mode 100644 index 00000000..65f0d725 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__schema_roundtrip-src__model.model.json.snap @@ -0,0 +1,8 @@ +--- +source: tests/tests/syncback.rs +expression: src/model.model.json +--- +{ + "$schema": "rojo/model.json", + "className": "BoolValue" +} diff --git a/rojo-test/syncback-tests/schema_roundtrip/input-project/default.project.json b/rojo-test/syncback-tests/schema_roundtrip/input-project/default.project.json new file mode 100644 index 00000000..11afd6a1 --- /dev/null +++ b/rojo-test/syncback-tests/schema_roundtrip/input-project/default.project.json @@ -0,0 +1,13 @@ +{ + "$schema": "rojo/project-schema", + "name": "schema_roundtrip", + "tree": { + "$className": "Folder", + "src": { + "$path": "src" + }, + "trigger_reserialization": { + "$className": "BoolValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.luau b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.luau new file mode 100644 index 00000000..e6a3e324 --- /dev/null +++ b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.luau @@ -0,0 +1 @@ +-- This file should be ignored. diff --git a/rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.meta.json b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.meta.json new file mode 100644 index 00000000..dfb24cd3 --- /dev/null +++ b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/adjacent.meta.json @@ -0,0 +1,3 @@ +{ + "$schema": "rojo/adjacent-meta" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/schema_roundtrip/input-project/src/init/init.meta.json b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/init/init.meta.json new file mode 100644 index 00000000..4c03d42f --- /dev/null +++ b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/init/init.meta.json @@ -0,0 +1,3 @@ +{ + "$schema": "rojo/init-meta" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/schema_roundtrip/input-project/src/model.model.json b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/model.model.json new file mode 100644 index 00000000..41bf34ce --- /dev/null +++ b/rojo-test/syncback-tests/schema_roundtrip/input-project/src/model.model.json @@ -0,0 +1,7 @@ +{ + "$schema": "rojo/model.json", + "className": "BoolValue", + "properties": { + "Value": true + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/schema_roundtrip/input.rbxm b/rojo-test/syncback-tests/schema_roundtrip/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..e8bbc388a9b0fc6e3dca786f4bbdfca3a915aa67 GIT binary patch literal 1469 zcmZ`(?M@Rx6rJ4`DGex!XoRSXqVdlHr5I>}V!t#PsKhq@#cg+JN0*&uKN6zx0sQA9 z_y|6Nk2DGKFM7}HE&_#<+}-&&bLZYOcV;t?T~8h@eE&ODoGUVxV~kxR%dp$*R2Ql9 zC>>BU2DNUHHIIg#=!vKrNAj%{eiY_uGzwzA-D-C%YCj+Y)D3(m$ZAr09~>{{%*2pJ z!32Mv+7Hu_KPL7-6tAV{@_>Re*dBGwJgA`ECj-=VIGQBem2T|ub}taai0B&mz4!0- zo>CJT0BRQB8M0~=1)>{AJZ$qoIG*^z(?yJ774m%oGC^rUuu0t+I$hz3NbqoBmgP(x zRq=>OYnPy95fWmeR`n8-xZj4dmP`cc^OAU&%-{--8z{50B zClb<2RTU6Os6yUl(EJy3)%BFKjwm&kyV z0(Kl#JPFZA^u9+F81NhXOzuhcLl5Eu&s+4brX$CTsOl^9G!O$kU69f`C*|dr>3%M zw4%7jX?V-jv~8<15TSJ-JZ^;p8GEkP<(BCCGT`n~(PF;>#}6&KfP*N6ON&R#3m$YG?vpP+)U8 Id9+{t17)fTqyPW_ literal 0 HcmV?d00001 diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs index 71a731ef..79f7a2d0 100644 --- a/src/snapshot/metadata.rs +++ b/src/snapshot/metadata.rs @@ -66,6 +66,10 @@ pub struct InstanceMetadata { /// The Middleware that was used to create this Instance. Should generally /// not be `None` except if the snapshotting process is not completed. pub middleware: Option, + + /// A schema provided via a JSON file, if one exists. Will be `None` for + /// all non-JSON middleware. + pub schema: Option, } impl InstanceMetadata { @@ -77,6 +81,7 @@ impl InstanceMetadata { context: InstanceContext::default(), specified_id: None, middleware: None, + schema: None, } } @@ -121,6 +126,10 @@ impl InstanceMetadata { ..self } } + + pub fn schema(self, schema: Option) -> Self { + Self { schema, ..self } + } } impl Default for InstanceMetadata { diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap index c4390b6d..930119b4 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap @@ -15,5 +15,5 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ children: [] - diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap index fa4d3ebc..fdf5ecd6 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap @@ -13,5 +13,5 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ children: [] - diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap index f07da090..b2111922 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap @@ -15,5 +15,5 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ children: [] - diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap index cc582633..9209a554 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap @@ -13,5 +13,5 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ children: [] - diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap index 4c404604..64541974 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap @@ -14,9 +14,9 @@ added_instances: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: New class_name: Folder properties: {} children: [] updated_instances: [] - diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs index a6f0148f..d19792cb 100644 --- a/src/snapshot_middleware/json_model.rs +++ b/src/snapshot_middleware/json_model.rs @@ -50,6 +50,7 @@ pub fn snapshot_json_model( instance.name = Some(name.to_owned()); let id = instance.id.take().map(RojoRef::new); + let schema = instance.schema.take(); let mut snapshot = instance .into_snapshot() @@ -60,7 +61,8 @@ pub fn snapshot_json_model( .instigating_source(path) .relevant_paths(vec![path.to_path_buf()]) .context(context) - .specified_id(id); + .specified_id(id) + .schema(schema); Ok(Some(snapshot)) } @@ -74,6 +76,13 @@ pub fn syncback_json_model<'sync>( // We don't need the name on the root, but we do for children. model.name = None; + if let Some(old_inst) = snapshot.old_inst() { + // TODO: Is it worth this being an Arc or Rc? I doubt that enough + // schemas will ever exist in one project for it to matter, but it + // could have a performance cost. + model.schema = old_inst.metadata().schema.clone(); + } + Ok(SyncbackReturn { fs_snapshot: FsSnapshot::new().with_added_file( &snapshot.path, diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs index 1757dee1..84ca9a80 100644 --- a/src/snapshot_middleware/meta_file.rs +++ b/src/snapshot_middleware/meta_file.rs @@ -115,6 +115,10 @@ impl AdjacentMetadata { .map(|inst| inst.metadata().ignore_unknown_instances) .unwrap_or_default(); + let schema = snapshot + .old_inst() + .and_then(|inst| inst.metadata().schema.clone()); + let class = &snapshot.new_inst().class; for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() { match value { @@ -150,7 +154,7 @@ impl AdjacentMetadata { attributes, path, id: None, - schema: None, + schema, })) } @@ -201,10 +205,19 @@ impl AdjacentMetadata { Ok(()) } + fn apply_schema(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { + if self.schema.is_some() && snapshot.metadata.schema.is_some() { + anyhow::bail!("cannot specify a schema using {} (instance has a schema from somewhere else. how did we get here?)", self.path.display()); + } + snapshot.metadata.schema = self.schema.take(); + Ok(()) + } + pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { self.apply_ignore_unknown_instances(snapshot); self.apply_properties(snapshot)?; self.apply_id(snapshot)?; + self.apply_schema(snapshot)?; Ok(()) } @@ -330,6 +343,10 @@ impl DirectoryMetadata { .map(|inst| inst.metadata().ignore_unknown_instances) .unwrap_or_default(); + let schema = snapshot + .old_inst() + .and_then(|inst| inst.metadata().schema.clone()); + let class = &snapshot.new_inst().class; for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() { match value { @@ -366,7 +383,7 @@ impl DirectoryMetadata { class_name: None, path, id: None, - schema: None, + schema, })) } @@ -375,6 +392,7 @@ impl DirectoryMetadata { self.apply_class_name(snapshot)?; self.apply_properties(snapshot)?; self.apply_id(snapshot)?; + self.apply_schema(snapshot)?; Ok(()) } @@ -439,6 +457,13 @@ impl DirectoryMetadata { Ok(()) } + fn apply_schema(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { + if self.schema.is_some() && snapshot.metadata.schema.is_some() { + anyhow::bail!("cannot specify a schema using {} (instance has a schema from somewhere else. how did we get here?)", self.path.display()); + } + snapshot.metadata.schema = self.schema.take(); + Ok(()) + } /// Returns whether the metadata is 'empty', meaning it doesn't have anything /// worth persisting in it. Specifically: /// diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap index f8ce15af..6356256d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap index 6aabdf4b..a275a6cf 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap @@ -22,6 +22,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: root class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap index 7202e31b..603bd2df 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap @@ -22,6 +22,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: root class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap index 8452cc7c..f90eccc8 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap index 64f74c4e..813a6767 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap @@ -22,6 +22,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Folder properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap index 2f29bb64..2f44988d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap @@ -22,6 +22,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Folder properties: {} @@ -46,6 +47,7 @@ children: emit_legacy_scripts: true specified_id: ~ middleware: dir + schema: ~ name: Child class_name: Folder properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap index d570009f..0803eed1 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap index 38d66519..7e6c260d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap index bf768977..f579d015 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap @@ -13,6 +13,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: IntValue properties: @@ -27,8 +28,8 @@ children: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: The Child class_name: StringValue properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap index bf768977..f579d015 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap @@ -13,6 +13,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: IntValue properties: @@ -27,8 +28,8 @@ children: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: The Child class_name: StringValue properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap index 683eec85..37fdec4e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: LocalScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap index b7ca7678..e898dfbd 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap index 63a84714..a9facd2d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap index c4d02c8e..ae7e22db 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: bar class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap index 916d0038..ce577d1e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap index a43f7e0f..9ce4624c 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap index deb32404..80178738 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap @@ -22,6 +22,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: root class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap index b02f9117..d5f8aa9f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap @@ -22,6 +22,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: root class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap index bf7b89b4..91535701 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap index e1376702..abe11f93 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap index 187ce50c..0d1ecd32 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap index 382fa03f..03ac12e5 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap index 2d6d9bdb..706b6ff5 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: bar class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap index 2e68f9d8..64a697d4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap index c72bdfcd..1b972cf2 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: false specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap index f21cca30..44c2123b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap @@ -12,6 +12,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap index f21cca30..44c2123b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap @@ -12,6 +12,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap index 08f7159d..6d819bfb 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap @@ -12,6 +12,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap index 08f7159d..6d819bfb 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap @@ -12,6 +12,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap index ebf5362d..dc817e76 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap @@ -13,6 +13,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap index 1b87aab8..1d739949 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap @@ -13,8 +13,8 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: direct-project class_name: Model properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap index cd32e81d..5b787c2a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap @@ -14,10 +14,10 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: project + schema: ~ name: path-property-override class_name: StringValue properties: Value: String: Changed children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap index f9d6e4a4..779afb4e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap @@ -13,6 +13,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: children class_name: Folder properties: {} @@ -32,8 +33,8 @@ children: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: Child class_name: Model properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap index da28d1ce..99432ea0 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap @@ -14,8 +14,8 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: project + schema: ~ name: path-project class_name: Model properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap index 077ebb15..88f8e479 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap @@ -14,6 +14,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: project + schema: ~ name: path-child-project class_name: Folder properties: {} @@ -33,8 +34,8 @@ children: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: SomeChild class_name: Model properties: {} children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap index ba9f0cf7..dea385ac 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap @@ -16,6 +16,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: text + schema: ~ name: path-project class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap index 27d7004a..ae95e39b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap @@ -13,10 +13,10 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: resolved-properties class_name: StringValue properties: Value: String: "Hello, world!" children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap index c6349bae..589ef207 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap @@ -13,10 +13,10 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: unresolved-properties class_name: StringValue properties: Value: String: Hi! children: [] - diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap index 22ae7577..ce112886 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap index 9905820d..8c8aef67 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap index 769081e2..bde4798e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap index 684d8820..a5047bc3 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: foo class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap index a59d5fdd..ef749325 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: ~ middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap index 3afe180a..8a9e1778 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap @@ -15,6 +15,7 @@ metadata: emit_legacy_scripts: true specified_id: manually specified middleware: ~ + schema: ~ name: foo class_name: ModuleScript properties: diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs index 41794eb9..851b4e4d 100644 --- a/tests/tests/syncback.rs +++ b/tests/tests/syncback.rs @@ -12,6 +12,9 @@ macro_rules! syncback_tests { for name in $list { let snapshot_name = format!(concat!(stringify!($test_name), "-{}"), name); let new = path.join::<&str>(name); + if !new.exists() { + panic!("the path stub '{}' does not exist after syncback runs. consider double checking for typos.", name); + } if let Some("rbxm") = new.extension().and_then(OsStr::to_str) { let content = fs_err::read(new).unwrap(); snapshot_rbxm(&snapshot_name, content, name); @@ -78,4 +81,5 @@ syncback_tests! { sync_rules => ["src/module.modulescript", "src/text.text"], // Ensures that the `syncUnscriptable` setting works unscriptable_properties => ["default.project.json"], + schema_roundtrip => ["default.project.json", "src/model.model.json", "src/init/init.meta.json", "src/adjacent.meta.json"] } From 87f58e0a55d148d649ca75350cdfe7907e4adeef Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 26 Nov 2025 19:57:01 -0800 Subject: [PATCH 11/11] Use WebSocket instead of Long Polling (#1142) --- CHANGELOG.md | 2 + Cargo.lock | 1566 +++++++++++------ Cargo.toml | 4 +- plugin/src/ApiContext.lua | 83 +- plugin/src/App/init.lua | 2 + plugin/src/Config.lua | 2 +- plugin/src/ServeSession.lua | 49 +- plugin/src/Types.lua | 15 +- ...to_end__tests__serve__add_folder_info.snap | 2 +- ...d__tests__serve__add_folder_subscribe.snap | 32 +- ...nd__tests__serve__add_optional_folder.snap | 2 +- ..._to_end__tests__serve__edit_init_info.snap | 2 +- ...nd__tests__serve__edit_init_subscribe.snap | 30 +- .../end_to_end__tests__serve__empty_info.snap | 2 +- ...__tests__serve__empty_json_model_info.snap | 2 +- ...ts__serve__empty_json_model_subscribe.snap | 36 +- ...end__tests__serve__forced_parent_info.snap | 2 +- ...__tests__serve__meshpart_with_id_info.snap | 2 +- ...sts__serve__model_pivot_migration_all.snap | 48 +- ...sts__serve__move_folder_of_stuff_info.snap | 2 +- ...serve__move_folder_of_stuff_subscribe.snap | 274 +-- ...__serve__no_name_default_project_info.snap | 2 +- ...d__tests__serve__no_name_project_info.snap | 2 +- ...serve__no_name_top_level_project_info.snap | 2 +- ...d__tests__serve__pivot_migration_info.snap | 2 +- ...nd__tests__serve__ref_properties_info.snap | 2 +- ...rve__ref_properties_patch_update_info.snap | 2 +- ...ref_properties_patch_update_subscribe.snap | 28 +- ...ts__serve__ref_properties_remove_info.snap | 2 +- ...erve__ref_properties_remove_subscribe.snap | 17 +- ...ests__serve__ref_properties_subscribe.snap | 82 +- ...o_end__tests__serve__remove_file_info.snap | 2 +- ...__tests__serve__remove_file_subscribe.snap | 16 +- ...nd_to_end__tests__serve__scripts_info.snap | 2 +- ..._end__tests__serve__scripts_subscribe.snap | 30 +- ...d__tests__serve__sync_rule_alone_info.snap | 2 +- ..._tests__serve__sync_rule_complex_info.snap | 2 +- ...s__serve__sync_rule_no_extension_info.snap | 2 +- ...serve__sync_rule_no_name_project_info.snap | 2 +- src/web/api.rs | 188 +- src/web/interface.rs | 38 +- tests/rojo_test/internable.rs | 15 +- tests/rojo_test/serve_util.rs | 54 +- tests/tests/serve.rs | 68 +- 44 files changed, 1750 insertions(+), 971 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6013f778..83f385fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,10 +69,12 @@ Making a new release? Simply add the new header with the version and date undern - `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]) +* 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]) [#937]: https://github.com/rojo-rbx/rojo/pull/937 [#994]: https://github.com/rojo-rbx/rojo/pull/994 +[#1142]: https://github.com/rojo-rbx/rojo/pull/1142 [#1159]: https://github.com/rojo-rbx/rojo/pull/1159 [#1172]: https://github.com/rojo-rbx/rojo/pull/1172 diff --git a/Cargo.lock b/Cargo.lock index 659d2968..000115ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,27 +4,27 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if 1.0.0", - "getrandom", + "cfg-if 1.0.4", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -32,18 +32,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arraydeque" @@ -53,15 +53,15 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "atty" @@ -76,23 +76,23 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", - "cc", - "cfg-if 1.0.0", + "cfg-if 1.0.4", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-link 0.2.1", ] [[package]] @@ -124,20 +124,20 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake3" -version = "1.5.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if 1.0.0", + "cfg-if 1.0.4", "constant_time_eq", ] @@ -152,20 +152,20 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata 0.4.6", + "regex-automata", "serde", ] [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -175,9 +175,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cast" @@ -187,12 +187,14 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.89" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ + "find-msvc-tools", "jobserver", "libc", + "shlex", ] [[package]] @@ -203,9 +205,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" @@ -232,7 +234,7 @@ dependencies = [ "once_cell", "strsim", "termcolor", - "textwrap 0.16.1", + "textwrap 0.16.2", ] [[package]] @@ -241,10 +243,10 @@ version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2 1.0.103", - "quote 1.0.35", + "quote 1.0.42", "syn 1.0.109", ] @@ -259,21 +261,21 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation" @@ -287,15 +289,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -338,18 +340,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -366,15 +368,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -382,21 +384,21 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -466,10 +468,21 @@ dependencies = [ ] [[package]] -name = "either" -version = "1.10.0" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embed-resource" @@ -486,17 +499,17 @@ dependencies = [ [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", ] [[package]] @@ -514,38 +527,44 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "float-cmp" version = "0.9.0" @@ -569,9 +588,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -622,9 +641,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -637,9 +656,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -647,15 +666,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -664,38 +683,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -711,11 +730,12 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" dependencies = [ "cc", + "cfg-if 1.0.4", "libc", "log", "rustversion", @@ -734,39 +754,51 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", "libc", "wasi", ] [[package]] -name = "gimli" -version = "0.28.1" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata", + "regex-syntax", ] [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -774,7 +806,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -795,20 +827,26 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -817,6 +855,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -828,9 +872,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "htmlescape" @@ -862,9 +906,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -874,15 +918,15 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -895,7 +939,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -917,13 +961,118 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.5.0" +name = "hyper-tungstenite" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "7cc7dcb1ab67cd336f468a12491765672e61a3b6b148634dbfe2fe8acd3fe7d9" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "hyper", + "pin-project-lite", + "tokio", + "tokio-tungstenite", + "tungstenite", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -938,13 +1087,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -969,18 +1119,16 @@ dependencies = [ [[package]] name = "insta" -version = "1.36.1" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e" +checksum = "e8732d3774162a0851e3f2b150eb98f31a9885dd75985099421d393385a01dfd" dependencies = [ "console", - "lazy_static", - "linked-hash-map", + "once_cell", "pest", "pest_derive", "serde", "similar", - "yaml-rust", ] [[package]] @@ -994,9 +1142,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "itertools" @@ -1009,16 +1157,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] @@ -1030,18 +1179,19 @@ checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "jsonc-parser" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec4ac49f13c7b00f435f8a5bb55d725705e2cf620df35a5859321595102eb7e" +checksum = "01958dcb05b69d9612853b47df8f7881810e4f61b5cedd8894be04291f28ccb9" dependencies = [ "serde_json", ] @@ -1070,19 +1220,19 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "libc", - "redox_syscall 0.4.1", + "redox_syscall", ] [[package]] @@ -1093,33 +1243,38 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.21" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e045d70ddfbc984eacfa964ded019534e8f6cbf36f6410aee0ed5cefa5a9175" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", "generator", "scoped-tls", "tracing", @@ -1143,18 +1298,18 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memofs" @@ -1174,11 +1329,11 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -1202,13 +1357,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1248,9 +1403,9 @@ dependencies = [ [[package]] name = "notify" -version = "4.0.17" +version = "4.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +checksum = "b72dd35279a5dc895a30965e247b0961ba36c233dc48454a2de8ccd459f1afd3" dependencies = [ "bitflags 1.3.2", "filetime", @@ -1266,53 +1421,52 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi 0.3.9", + "windows-sys 0.61.2", ] [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.5.2", "libc", ] [[package]] name = "object" -version = "0.32.2" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "opener" @@ -1336,17 +1490,11 @@ version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1354,45 +1502,44 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", "libc", - "redox_syscall 0.5.10", + "redox_syscall", "smallvec", - "windows-targets 0.52.4", + "windows-link 0.2.1", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.7.8" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", - "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.8" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" dependencies = [ "pest", "pest_generator", @@ -1400,33 +1547,32 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.8" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" dependencies = [ "pest", "pest_meta", "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] name = "pest_meta" -version = "2.7.8" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" dependencies = [ - "once_cell", "pest", "sha2", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1442,9 +1588,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -1455,30 +1601,42 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -1492,7 +1650,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2 1.0.103", - "quote 1.0.35", + "quote 1.0.42", "syn 1.0.109", "version_check", ] @@ -1504,7 +1662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", + "quote 1.0.42", "version_check", ] @@ -1540,9 +1698,9 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", "tracy-client", @@ -1550,12 +1708,12 @@ dependencies = [ [[package]] name = "profiling-procmacros" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ - "quote 1.0.35", - "syn 2.0.108", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] @@ -1569,13 +1727,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2 1.0.103", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1603,14 +1767,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] name = "rayon" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1618,9 +1782,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1712,82 +1876,58 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", ] [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", "bytes", @@ -1826,15 +1966,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.0", - "getrandom", + "cfg-if 1.0.4", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -1865,9 +2004,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", @@ -1876,9 +2015,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", @@ -1917,7 +2056,8 @@ dependencies = [ "globset", "humantime", "hyper", - "indexmap 2.10.0", + "hyper-tungstenite", + "indexmap 2.12.1", "insta", "jod-thread", "jsonc-parser", @@ -1943,6 +2083,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "strum", "tempfile", "termcolor", "thiserror", @@ -1964,37 +2105,37 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.31" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.21.11" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", @@ -2023,15 +2164,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2066,9 +2207,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" @@ -2106,8 +2247,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] @@ -2148,12 +2289,23 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.10.8" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -2168,25 +2320,28 @@ dependencies = [ ] [[package]] -name = "similar" -version = "2.4.0" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snax" @@ -2199,19 +2354,29 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] -name = "spin" -version = "0.9.8" +name = "socket2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -2219,6 +2384,27 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + [[package]] name = "syn" version = "1.0.109" @@ -2226,18 +2412,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", + "quote 1.0.42", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", + "quote 1.0.42", "unicode-ident", ] @@ -2247,6 +2433,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -2270,14 +2467,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if 1.0.0", "fastrand", + "getrandom 0.3.4", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2300,38 +2498,47 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "cfg-if 1.0.0", - "once_cell", + "cfg-if 1.0.4", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] @@ -2345,34 +2552,29 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tokio" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "tinyvec_macros", + "bytes", + "libc", + "mio 1.1.0", + "pin-project-lite", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "tokio-macros" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ - "backtrace", - "bytes", - "libc", - "mio 0.8.11", - "num_cpus", - "pin-project-lite", - "socket2", - "windows-sys 0.48.0", + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] @@ -2386,17 +2588,28 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.7.10" +name = "tokio-tungstenite" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -2410,37 +2623,25 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", -] - [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -2459,14 +2660,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -2477,9 +2678,9 @@ dependencies = [ [[package]] name = "tracy-client" -version = "0.17.0" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb931a64ff88984f86d3e9bcd1ae8843aa7fe44dd0f8097527bc172351741d" +checksum = "91d722a05fe49b31fef971c4732a7d4aa6a18283d9ba46abddab35f484872947" dependencies = [ "loom", "once_cell", @@ -2488,11 +2689,12 @@ dependencies = [ [[package]] name = "tracy-client-sys" -version = "0.22.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d104d610dfa9dd154535102cc9c6164ae1fa37842bc2d9e83f9ac82b0ae0882" +checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f" dependencies = [ "cc", + "windows-targets 0.52.6", ] [[package]] @@ -2501,6 +2703,25 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "twox-hash" version = "2.1.2" @@ -2509,42 +2730,27 @@ checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" @@ -2560,13 +2766,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2583,26 +2790,40 @@ dependencies = [ ] [[package]] -name = "uuid" -version = "1.7.0" +name = "utf-8" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom", + "getrandom 0.3.4", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vswhom" @@ -2616,9 +2837,9 @@ dependencies = [ [[package]] name = "vswhom-sys" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" dependencies = [ "cc", "libc", @@ -2645,81 +2866,82 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasm-bindgen" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ - "bumpalo", - "log", + "cfg-if 1.0.4", "once_cell", - "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ - "quote 1.0.35", + "quote 1.0.42", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", - "wasm-bindgen-backend", + "quote 1.0.42", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -2761,11 +2983,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi 0.3.9", + "windows-sys 0.61.2", ] [[package]] @@ -2776,11 +2998,110 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.48.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-targets 0.48.5", + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -2798,7 +3119,34 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -2818,17 +3166,44 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -2839,9 +3214,15 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2851,9 +3232,15 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2863,9 +3250,27 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2875,9 +3280,15 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2887,9 +3298,15 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2899,9 +3316,15 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2911,9 +3334,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winreg" @@ -2939,10 +3368,22 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -2955,9 +3396,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "yaml-rust" @@ -2981,28 +3422,105 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", + "synstructure", +] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" dependencies = [ "proc-macro2 1.0.103", - "quote 1.0.35", - "syn 2.0.108", + "quote 1.0.42", + "syn 2.0.111", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2 1.0.103", + "quote 1.0.42", + "syn 2.0.111", ] [[package]] @@ -3025,9 +3543,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 94315413..9fcea18d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ futures = "0.3.30" globset = "0.4.14" humantime = "2.1.0" hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] } +hyper-tungstenite = "0.11.0" jod-thread = "0.1.2" log = "0.4.21" num_cpus = "1.16.0" @@ -87,10 +88,11 @@ roblox_install = "1.0.0" serde = { version = "1.0.197", features = ["derive", "rc"] } serde_json = "1.0.145" jsonc-parser = { version = "0.27.0", features = ["serde"] } +strum = { version = "0.27", features = ["derive"] } toml = "0.5.11" termcolor = "1.4.1" thiserror = "1.0.57" -tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] } +tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] } uuid = { version = "1.7.0", features = ["v4", "serde"] } clap = { version = "3.2.25", features = ["derive"] } profiling = "1.0.15" diff --git a/plugin/src/ApiContext.lua b/plugin/src/ApiContext.lua index 02d4964d..cbbffcc1 100644 --- a/plugin/src/ApiContext.lua +++ b/plugin/src/ApiContext.lua @@ -1,4 +1,5 @@ local Packages = script.Parent.Parent.Packages +local HttpService = game:GetService("HttpService") local Http = require(Packages.Http) local Log = require(Packages.Log) local Promise = require(Packages.Promise) @@ -9,7 +10,7 @@ local Version = require(script.Parent.Version) local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse) local validateApiRead = Types.ifEnabled(Types.ApiReadResponse) -local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse) +local validateApiSocketPacket = Types.ifEnabled(Types.ApiSocketPacket) local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse) local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse) @@ -99,6 +100,7 @@ function ApiContext.new(baseUrl) __baseUrl = baseUrl, __sessionId = nil, __messageCursor = -1, + __wsClient = nil, __connected = true, __activeRequests = {}, } @@ -126,6 +128,12 @@ function ApiContext:disconnect() request:cancel() end self.__activeRequests = {} + + if self.__wsClient then + Log.trace("Closing WebSocket client") + self.__wsClient:Close() + end + self.__wsClient = nil end function ApiContext:setMessageCursor(index) @@ -207,38 +215,65 @@ function ApiContext:write(patch) end) end -function ApiContext:retrieveMessages() - local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor) +function ApiContext:connectWebSocket(packetHandlers) + local url = ("%s/api/socket/%s"):format(self.__baseUrl, self.__messageCursor) + -- Convert HTTP/HTTPS URL to WS/WSS + url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://") - local function sendRequest() - local request = Http.get(url):catch(function(err) - if err.type == Http.Error.Kind.Timeout and self.__connected then - return sendRequest() + return Promise.new(function(resolve, reject) + local success, wsClient = + pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, { + Url = url, + }) + if not success then + reject("Failed to create WebSocket client: " .. tostring(wsClient)) + return + end + self.__wsClient = wsClient + + local closed, errored, received + + received = self.__wsClient.MessageReceived:Connect(function(msg) + local data = Http.jsonDecode(msg) + if data.sessionId ~= self.__sessionId then + Log.warn("Received message with wrong session ID; ignoring") + return end - return Promise.reject(err) + assert(validateApiSocketPacket(data)) + + Log.trace("Received websocket packet: {:#?}", data) + + local handler = packetHandlers[data.packetType] + if handler then + local ok, err = pcall(handler, data.body) + if not ok then + Log.error("Error in WebSocket packet handler for type '%s': %s", data.packetType, err) + end + else + Log.warn("No handler for WebSocket packet type '%s'", data.packetType) + end end) - Log.trace("Tracking request {}", request) - self.__activeRequests[request] = true + closed = self.__wsClient.Closed:Connect(function() + closed:Disconnect() + errored:Disconnect() + received:Disconnect() - return request:finally(function(...) - Log.trace("Cleaning up request {}", request) - self.__activeRequests[request] = nil - return ... + if self.__connected then + reject("WebSocket connection closed unexpectedly") + else + resolve() + end end) - end - return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) - if body.sessionId ~= self.__sessionId then - return Promise.reject("Server changed ID") - end + errored = self.__wsClient.Error:Connect(function(code, msg) + closed:Disconnect() + errored:Disconnect() + received:Disconnect() - assert(validateApiSubscribe(body)) - - self:setMessageCursor(body.messageCursor) - - return body.messages + reject("WebSocket error: " .. code .. " - " .. msg) + end) end) end diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 07203eef..c83d9d1c 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -174,6 +174,8 @@ function App:init() end function App:willUnmount() + self:endSession() + self.waypointConnection:Disconnect() self.confirmationBindable:Destroy() diff --git a/plugin/src/Config.lua b/plugin/src/Config.lua index 5567efca..8897996a 100644 --- a/plugin/src/Config.lua +++ b/plugin/src/Config.lua @@ -21,7 +21,7 @@ return strict("Config", { codename = "Epiphany", version = realVersion, expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]), - protocolVersion = 4, + protocolVersion = 5, defaultHost = "localhost", defaultPort = "34872", }) diff --git a/plugin/src/ServeSession.lua b/plugin/src/ServeSession.lua index 47ba2bb7..adee9e4d 100644 --- a/plugin/src/ServeSession.lua +++ b/plugin/src/ServeSession.lua @@ -201,7 +201,20 @@ function ServeSession:start() self:__setStatus(Status.Connected, serverInfo.projectName) self:__applyGameAndPlaceId(serverInfo) - return self:__mainSyncLoop() + return self.__apiContext:connectWebSocket({ + ["messages"] = function(messagesPacket) + if self.__status == Status.Disconnected then + return + end + + Log.debug("Received {} messages from Rojo server", #messagesPacket.messages) + + for _, message in messagesPacket.messages do + self:__applyPatch(message) + end + self.__apiContext:setMessageCursor(messagesPacket.messageCursor) + end, + }) end) end) :catch(function(err) @@ -536,40 +549,6 @@ function ServeSession:__initialSync(serverInfo) end) end -function ServeSession:__mainSyncLoop() - return Promise.new(function(resolve, reject) - while self.__status == Status.Connected do - local success, result = self.__apiContext - :retrieveMessages() - :andThen(function(messages) - if self.__status == Status.Disconnected then - -- In the time it took to retrieve messages, we disconnected - -- so we just resolve immediately without patching anything - return - end - - Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages) - - for _, message in messages do - self:__applyPatch(message) - end - end) - :await() - - if self.__status == Status.Disconnected then - -- If we are no longer connected after applying, we stop silently - -- without checking for errors as they are no longer relevant - break - elseif success == false then - reject(result) - end - end - - -- We are no longer connected, so we resolve the promise - resolve() - end) -end - function ServeSession:__stopInternal(err) self:__setStatus(Status.Disconnected, err) self.__apiContext:disconnect() diff --git a/plugin/src/Types.lua b/plugin/src/Types.lua index d11c9822..fd04bed2 100644 --- a/plugin/src/Types.lua +++ b/plugin/src/Types.lua @@ -49,12 +49,21 @@ local ApiReadResponse = t.interface({ instances = t.map(RbxId, ApiInstance), }) -local ApiSubscribeResponse = t.interface({ - sessionId = t.string, +local SocketPacketType = t.union(t.literal("messages")) + +local MessagesPacket = t.interface({ messageCursor = t.number, messages = t.array(ApiSubscribeMessage), }) +local SocketPacketBody = t.union(MessagesPacket) + +local ApiSocketPacket = t.interface({ + sessionId = t.string, + packetType = SocketPacketType, + body = SocketPacketBody, +}) + local ApiSerializeResponse = t.interface({ sessionId = t.string, modelContents = t.buffer, @@ -85,7 +94,7 @@ return strict("Types", { ApiInfoResponse = ApiInfoResponse, ApiReadResponse = ApiReadResponse, - ApiSubscribeResponse = ApiSubscribeResponse, + ApiSocketPacket = ApiSocketPacket, ApiError = ApiError, ApiInstance = ApiInstance, diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_info.snap index ea946219..4de31360 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: add_folder -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_subscribe.snap index 09d4512c..4a724f1e 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_folder_subscribe.snap @@ -1,19 +1,21 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: - id-3: - Children: [] - ClassName: Folder - Id: id-3 - Metadata: - ignoreUnknownInstances: false - Name: my-new-folder - Parent: id-2 - Properties: {} - removed: [] - updated: [] +body: + messageCursor: 1 + messages: + - added: + id-3: + Children: [] + ClassName: Folder + Id: id-3 + Metadata: + ignoreUnknownInstances: false + Name: my-new-folder + Parent: id-2 + Properties: {} + removed: [] + updated: [] +packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_optional_folder.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_optional_folder.snap index 42a31374..4107de74 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_optional_folder.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__add_optional_folder.snap @@ -7,7 +7,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: optional -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_info.snap index eb3c203e..97f5fc9c 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: edit_init -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_subscribe.snap index cef27f2c..32a266ea 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__edit_init_subscribe.snap @@ -1,19 +1,19 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" - +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: {} - removed: [] - updated: - - changedClassName: ~ - changedMetadata: ~ - changedName: ~ - changedProperties: - Source: - String: "-- Edited contents" - id: id-2 +body: + messageCursor: 1 + messages: + - added: {} + removed: [] + updated: + - changedClassName: ~ + changedMetadata: ~ + changedName: ~ + changedProperties: + Source: + String: "-- Edited contents" + id: id-2 +packetType: messages sessionId: id-1 - diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_info.snap index 7dc5960d..bb9159c5 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: empty -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_info.snap index 0adbe189..0e5958ac 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: empty_folder -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_subscribe.snap index 95306f15..74ce73a2 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__empty_json_model_subscribe.snap @@ -1,21 +1,23 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: - id-3: - Children: [] - ClassName: Model - Id: id-3 - Metadata: - ignoreUnknownInstances: false - Name: test - Parent: id-2 - Properties: - NeedsPivotMigration: - Bool: false - removed: [] - updated: [] +body: + messageCursor: 1 + messages: + - added: + id-3: + Children: [] + ClassName: Model + Id: id-3 + Metadata: + ignoreUnknownInstances: false + Name: test + Parent: id-2 + Properties: + NeedsPivotMigration: + Bool: false + removed: [] + updated: [] +packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__forced_parent_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__forced_parent_info.snap index 67b95d51..91712fcc 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__forced_parent_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__forced_parent_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: forced_parent -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__meshpart_with_id_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__meshpart_with_id_info.snap index 0220db0a..0de57dcb 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__meshpart_with_id_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__meshpart_with_id_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: meshpart -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__model_pivot_migration_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__model_pivot_migration_all.snap index be5a46a8..9b7a6045 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__model_pivot_migration_all.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__model_pivot_migration_all.snap @@ -1,27 +1,29 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: - id-6: - Children: [] - ClassName: Actor - Id: id-6 - Metadata: - ignoreUnknownInstances: true - Name: Actor - Parent: id-3 - Properties: - NeedsPivotMigration: - Bool: false - removed: [] - updated: - - changedClassName: ~ - changedMetadata: - ignoreUnknownInstances: true - changedName: ~ - changedProperties: {} - id: id-3 +body: + messageCursor: 1 + messages: + - added: + id-6: + Children: [] + ClassName: Actor + Id: id-6 + Metadata: + ignoreUnknownInstances: true + Name: Actor + Parent: id-3 + Properties: + NeedsPivotMigration: + Bool: false + removed: [] + updated: + - changedClassName: ~ + changedMetadata: + ignoreUnknownInstances: true + changedName: ~ + changedProperties: {} + id: id-3 +packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_info.snap index 7f482638..692bae6f 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: move_folder_of_stuff -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_subscribe.snap index 40afc888..547ddd58 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__move_folder_of_stuff_subscribe.snap @@ -1,141 +1,141 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" - +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: - id-10: - Children: [] - ClassName: StringValue - Id: id-10 - Metadata: - ignoreUnknownInstances: false - Name: "6" - Parent: id-3 - Properties: - Value: - String: "File #6" - id-11: - Children: [] - ClassName: StringValue - Id: id-11 - Metadata: - ignoreUnknownInstances: false - Name: "7" - Parent: id-3 - Properties: - Value: - String: "File #7" - id-12: - Children: [] - ClassName: StringValue - Id: id-12 - Metadata: - ignoreUnknownInstances: false - Name: "8" - Parent: id-3 - Properties: - Value: - String: "File #8" - id-13: - Children: [] - ClassName: StringValue - Id: id-13 - Metadata: - ignoreUnknownInstances: false - Name: "9" - Parent: id-3 - Properties: - Value: - String: "File #9" - id-3: - Children: - - id-4 - - id-5 - - id-6 - - id-7 - - id-8 - - id-9 - - id-10 - - id-11 - - id-12 - - id-13 - ClassName: Folder - Id: id-3 - Metadata: - ignoreUnknownInstances: false - Name: new-stuff - Parent: id-2 - Properties: {} - id-4: - Children: [] - ClassName: StringValue - Id: id-4 - Metadata: - ignoreUnknownInstances: false - Name: "0" - Parent: id-3 - Properties: - Value: - String: "File #0" - id-5: - Children: [] - ClassName: StringValue - Id: id-5 - Metadata: - ignoreUnknownInstances: false - Name: "1" - Parent: id-3 - Properties: - Value: - String: "File #1" - id-6: - Children: [] - ClassName: StringValue - Id: id-6 - Metadata: - ignoreUnknownInstances: false - Name: "2" - Parent: id-3 - Properties: - Value: - String: "File #2" - id-7: - Children: [] - ClassName: StringValue - Id: id-7 - Metadata: - ignoreUnknownInstances: false - Name: "3" - Parent: id-3 - Properties: - Value: - String: "File #3" - id-8: - Children: [] - ClassName: StringValue - Id: id-8 - Metadata: - ignoreUnknownInstances: false - Name: "4" - Parent: id-3 - Properties: - Value: - String: "File #4" - id-9: - Children: [] - ClassName: StringValue - Id: id-9 - Metadata: - ignoreUnknownInstances: false - Name: "5" - Parent: id-3 - Properties: - Value: - String: "File #5" - removed: [] - updated: [] +body: + messageCursor: 1 + messages: + - added: + id-10: + Children: [] + ClassName: StringValue + Id: id-10 + Metadata: + ignoreUnknownInstances: false + Name: "6" + Parent: id-3 + Properties: + Value: + String: "File #6" + id-11: + Children: [] + ClassName: StringValue + Id: id-11 + Metadata: + ignoreUnknownInstances: false + Name: "7" + Parent: id-3 + Properties: + Value: + String: "File #7" + id-12: + Children: [] + ClassName: StringValue + Id: id-12 + Metadata: + ignoreUnknownInstances: false + Name: "8" + Parent: id-3 + Properties: + Value: + String: "File #8" + id-13: + Children: [] + ClassName: StringValue + Id: id-13 + Metadata: + ignoreUnknownInstances: false + Name: "9" + Parent: id-3 + Properties: + Value: + String: "File #9" + id-3: + Children: + - id-4 + - id-5 + - id-6 + - id-7 + - id-8 + - id-9 + - id-10 + - id-11 + - id-12 + - id-13 + ClassName: Folder + Id: id-3 + Metadata: + ignoreUnknownInstances: false + Name: new-stuff + Parent: id-2 + Properties: {} + id-4: + Children: [] + ClassName: StringValue + Id: id-4 + Metadata: + ignoreUnknownInstances: false + Name: "0" + Parent: id-3 + Properties: + Value: + String: "File #0" + id-5: + Children: [] + ClassName: StringValue + Id: id-5 + Metadata: + ignoreUnknownInstances: false + Name: "1" + Parent: id-3 + Properties: + Value: + String: "File #1" + id-6: + Children: [] + ClassName: StringValue + Id: id-6 + Metadata: + ignoreUnknownInstances: false + Name: "2" + Parent: id-3 + Properties: + Value: + String: "File #2" + id-7: + Children: [] + ClassName: StringValue + Id: id-7 + Metadata: + ignoreUnknownInstances: false + Name: "3" + Parent: id-3 + Properties: + Value: + String: "File #3" + id-8: + Children: [] + ClassName: StringValue + Id: id-8 + Metadata: + ignoreUnknownInstances: false + Name: "4" + Parent: id-3 + Properties: + Value: + String: "File #4" + id-9: + Children: [] + ClassName: StringValue + Id: id-9 + Metadata: + ignoreUnknownInstances: false + Name: "5" + Parent: id-3 + Properties: + Value: + String: "File #5" + removed: [] + updated: [] +packetType: messages sessionId: id-1 - diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap index eb95a44c..f8f2e14e 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: top-level -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap index 7cd631ee..f7018efe 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: no_name_project -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap index 3400cc06..040c02f2 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: no_name_top_level_project -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__pivot_migration_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__pivot_migration_info.snap index 325e4488..a7e293b7 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__pivot_migration_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__pivot_migration_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: pivot_migration -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_info.snap index a37231dd..8777473c 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: ref_properties -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_info.snap index a37231dd..8777473c 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: ref_properties -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_subscribe.snap index 5ee76986..1aac7624 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_patch_update_subscribe.snap @@ -1,17 +1,19 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: {} - removed: [] - updated: - - changedClassName: ~ - changedMetadata: ~ - changedName: ~ - changedProperties: - Scale: - Float32: 1 - id: id-8 +body: + messageCursor: 1 + messages: + - added: {} + removed: [] + updated: + - changedClassName: ~ + changedMetadata: ~ + changedName: ~ + changedProperties: + Scale: + Float32: 1 + id: id-8 +packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_info.snap index 4fea62ef..bb598446 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: ref_properties_remove -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_subscribe.snap index 92432570..b03e68dc 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_remove_subscribe.snap @@ -1,12 +1,13 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: {} - removed: - - id-4 - updated: [] +body: + messageCursor: 1 + messages: + - added: {} + removed: + - id-4 + updated: [] +packetType: messages sessionId: id-1 - diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_subscribe.snap index 37563913..b601ae47 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__ref_properties_subscribe.snap @@ -1,47 +1,49 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: - id-11: - Children: [] - ClassName: Model - Id: id-11 - Metadata: - ignoreUnknownInstances: false - Name: ProjectPointer - Parent: id-7 - Properties: - Attributes: +body: + messageCursor: 1 + messages: + - added: + id-11: + Children: [] + ClassName: Model + Id: id-11 + Metadata: + ignoreUnknownInstances: false + Name: ProjectPointer + Parent: id-7 + Properties: Attributes: - Rojo_Target_PrimaryPart: - String: project target - NeedsPivotMigration: - Bool: false - PrimaryPart: - Ref: id-9 - removed: [] - updated: - - changedClassName: ~ - changedMetadata: - ignoreUnknownInstances: false - changedName: ~ - changedProperties: - Attributes: + Attributes: + Rojo_Target_PrimaryPart: + String: project target + NeedsPivotMigration: + Bool: false + PrimaryPart: + Ref: id-9 + removed: [] + updated: + - changedClassName: ~ + changedMetadata: + ignoreUnknownInstances: false + changedName: ~ + changedProperties: Attributes: - Rojo_Id: - String: model target 2 - id: id-7 - - changedClassName: ~ - changedMetadata: ~ - changedName: ~ - changedProperties: - Attributes: + Attributes: + Rojo_Id: + String: model target 2 + id: id-7 + - changedClassName: ~ + changedMetadata: ~ + changedName: ~ + changedProperties: Attributes: - Rojo_Target_PrimaryPart: - String: model target 2 - PrimaryPart: ~ - id: id-8 + Attributes: + Rojo_Target_PrimaryPart: + String: model target 2 + PrimaryPart: ~ + id: id-8 +packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_info.snap index 27622724..a1a8a801 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: remove_file -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_subscribe.snap index e70d17f9..e9e39e04 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__remove_file_subscribe.snap @@ -1,11 +1,13 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: {} - removed: - - id-3 - updated: [] +body: + messageCursor: 1 + messages: + - added: {} + removed: + - id-3 + updated: [] +packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_info.snap index fb85d7cd..cead5d7f 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: scripts -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_subscribe.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_subscribe.snap index 19bea59e..d4050101 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_subscribe.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_subscribe.snap @@ -1,19 +1,19 @@ --- source: tests/tests/serve.rs -expression: "subscribe_response.intern_and_redact(&mut redactions, ())" - +expression: "socket_packet.intern_and_redact(&mut redactions, ())" --- -messageCursor: 1 -messages: - - added: {} - removed: [] - updated: - - changedClassName: ~ - changedMetadata: ~ - changedName: ~ - changedProperties: - Source: - String: Updated foo! - id: id-4 +body: + messageCursor: 1 + messages: + - added: {} + removed: [] + updated: + - changedClassName: ~ + changedMetadata: ~ + changedName: ~ + changedProperties: + Source: + String: Updated foo! + id: id-4 +packetType: messages sessionId: id-1 - diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap index 8154c8ad..920f7b22 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: sync_rule_alone -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap index 6755cc99..2961cc7b 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: sync_rule_complex -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap index cf07c891..54915213 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: sync_rule_no_extension -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_name_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_name_project_info.snap index 8eeb02f9..b8fcc474 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_name_project_info.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_name_project_info.snap @@ -6,7 +6,7 @@ expectedPlaceIds: ~ gameId: ~ placeId: ~ projectName: sync_rule_no_name_project -protocolVersion: 4 +protocolVersion: 5 rootInstanceId: id-2 serverVersion: "[server-version]" sessionId: id-1 diff --git a/src/web/api.rs b/src/web/api.rs index 88f74aa4..8e7ce72f 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -9,7 +9,9 @@ use std::{ sync::Arc, }; +use futures::{sink::SinkExt, stream::StreamExt}; use hyper::{body, Body, Method, Request, Response, StatusCode}; +use hyper_tungstenite::{is_upgrade_request, tungstenite::Message, upgrade, HyperWebsocket}; use opener::OpenError; use rbx_dom_weak::{ types::{Ref, Variant}, @@ -22,16 +24,16 @@ use crate::{ snapshot::{InstanceWithMeta, PatchSet, PatchUpdate}, web::{ interface::{ - ErrorResponse, Instance, OpenResponse, ReadResponse, ServerInfoResponse, - SubscribeMessage, SubscribeResponse, WriteRequest, WriteResponse, PROTOCOL_VERSION, - SERVER_VERSION, + ErrorResponse, Instance, MessagesPacket, OpenResponse, ReadResponse, + ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage, + WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION, }, util::{json, json_ok}, }, web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse}, }; -pub async fn call(serve_session: Arc, request: Request) -> Response { +pub async fn call(serve_session: Arc, mut request: Request) -> Response { let service = ApiService::new(serve_session); match (request.method(), request.uri().path()) { @@ -39,8 +41,17 @@ pub async fn call(serve_session: Arc, request: Request) -> R (&Method::GET, path) if path.starts_with("/api/read/") => { service.handle_api_read(request).await } - (&Method::GET, path) if path.starts_with("/api/subscribe/") => { - service.handle_api_subscribe(request).await + (&Method::GET, path) if path.starts_with("/api/socket/") => { + if is_upgrade_request(&request) { + service.handle_api_socket(&mut request).await + } else { + json( + ErrorResponse::bad_request( + "/api/socket must be called as a websocket upgrade request", + ), + StatusCode::BAD_REQUEST, + ) + } } (&Method::GET, path) if path.starts_with("/api/serialize/") => { service.handle_api_serialize(request).await @@ -88,10 +99,9 @@ impl ApiService { }) } - /// Retrieve any messages past the given cursor index, and if - /// there weren't any, subscribe to receive any new messages. - async fn handle_api_subscribe(&self, request: Request) -> Response { - let argument = &request.uri().path()["/api/subscribe/".len()..]; + /// Handle WebSocket upgrade for real-time message streaming + async fn handle_api_socket(&self, request: &mut Request) -> Response { + let argument = &request.uri().path()["/api/socket/".len()..]; let input_cursor: u32 = match argument.parse() { Ok(v) => v, Err(err) => { @@ -102,36 +112,29 @@ impl ApiService { } }; - let session_id = self.serve_session.session_id(); - - let result = self - .serve_session - .message_queue() - .subscribe(input_cursor) - .await; - - let tree_handle = self.serve_session.tree_handle(); - - match result { - Ok((message_cursor, messages)) => { - let tree = tree_handle.lock().unwrap(); - - let api_messages = messages - .into_iter() - .map(|patch| SubscribeMessage::from_patch_update(&tree, patch)) - .collect(); - - json_ok(SubscribeResponse { - session_id, - message_cursor, - messages: api_messages, - }) + // Upgrade the connection to WebSocket + let (response, websocket) = match upgrade(request, None) { + Ok(result) => result, + Err(err) => { + return json( + ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)), + StatusCode::INTERNAL_SERVER_ERROR, + ); } - Err(_) => json( - ErrorResponse::internal_error("Message queue disconnected sender"), - StatusCode::INTERNAL_SERVER_ERROR, - ), - } + }; + + let serve_session = Arc::clone(&self.serve_session); + + // Spawn a task to handle the WebSocket connection + tokio::spawn(async move { + if let Err(e) = + handle_websocket_subscription(serve_session, websocket, input_cursor).await + { + log::error!("Error in websocket subscription: {}", e); + } + }); + + response } async fn handle_api_write(&self, request: Request) -> Response { @@ -444,6 +447,113 @@ fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option { .map(|path| path.to_owned()) } +/// Handle WebSocket connection for streaming subscription messages +async fn handle_websocket_subscription( + serve_session: Arc, + websocket: HyperWebsocket, + input_cursor: u32, +) -> anyhow::Result<()> { + let mut websocket = websocket.await?; + + let session_id = serve_session.session_id(); + let tree_handle = serve_session.tree_handle(); + let message_queue = serve_session.message_queue(); + + log::debug!( + "WebSocket subscription established for session {}", + session_id + ); + + // Now continuously listen for new messages using select to handle both incoming messages + // and WebSocket control messages concurrently + let mut cursor = input_cursor; + loop { + let receiver = message_queue.subscribe(cursor); + + tokio::select! { + // Handle new messages from the message queue + result = receiver => { + match result { + Ok((new_cursor, messages)) => { + if !messages.is_empty() { + let json_message = { + let tree = tree_handle.lock().unwrap(); + let api_messages = messages + .into_iter() + .map(|patch| SubscribeMessage::from_patch_update(&tree, patch)) + .collect(); + + let response = SocketPacket { + session_id, + packet_type: SocketPacketType::Messages, + body: SocketPacketBody::Messages(MessagesPacket { + message_cursor: new_cursor, + messages: api_messages, + }), + }; + + serde_json::to_string(&response)? + }; + + log::debug!("Sending batch of messages over WebSocket subscription"); + + if websocket.send(Message::Text(json_message)).await.is_err() { + // Client disconnected + log::debug!("WebSocket subscription closed by client"); + break; + } + cursor = new_cursor; + } + } + Err(_) => { + // Message queue disconnected + log::debug!("Message queue disconnected; closing WebSocket subscription"); + let _ = websocket.send(Message::Close(None)).await; + break; + } + } + } + + // Handle incoming WebSocket messages (ping/pong/close) + msg = websocket.next() => { + match msg { + Some(Ok(Message::Close(_))) => { + log::debug!("WebSocket subscription closed by client"); + break; + } + Some(Ok(Message::Ping(data))) => { + // tungstenite handles pong automatically + log::debug!("Received ping: {:?}", data); + } + Some(Ok(Message::Pong(data))) => { + log::debug!("Received pong: {:?}", data); + } + Some(Ok(Message::Text(_))) | Some(Ok(Message::Binary(_))) => { + // Ignore text/binary messages from client for subscription endpoint + // TODO: Use this for bidirectional sync or requesting fallbacks? + log::debug!("Ignoring message from client since we don't use it for anything yet: {:?}", msg); + } + Some(Ok(Message::Frame(_))) => { + // This should never happen according to tungstenite docs + unreachable!(); + } + Some(Err(e)) => { + log::error!("WebSocket error: {}", e); + break; + } + None => { + // WebSocket stream ended + log::debug!("WebSocket stream ended"); + break; + } + } + } + } + } + + Ok(()) +} + /// Certain Instances MUST be a child of specific classes. This function /// tracks that information for the Serialize endpoint. /// diff --git a/src/web/interface.rs b/src/web/interface.rs index fffa6654..4871ec97 100644 --- a/src/web/interface.rs +++ b/src/web/interface.rs @@ -12,6 +12,7 @@ use rbx_dom_weak::{ Ustr, UstrMap, }; use serde::{Deserialize, Serialize}; +use strum::Display; use crate::{ session_id::SessionId, @@ -24,7 +25,7 @@ use crate::{ pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Current protocol version, which is required to match. -pub const PROTOCOL_VERSION: u64 = 4; +pub const PROTOCOL_VERSION: u64 = 5; /// Message returned by Rojo API when a change has occurred. #[derive(Debug, Serialize, Deserialize)] @@ -192,15 +193,44 @@ pub struct WriteResponse { pub session_id: SessionId, } -/// Response body from /api/subscribe/{cursor} +/// Packet type enum for different websocket message types +#[derive(Debug, Serialize, Deserialize, Display, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum SocketPacketType { + Messages, + // TODO: Can we cleanly use the socket for all communication? + // Serialize, + // RefPatch, +} + +/// Body content for messages packet type #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SubscribeResponse<'a> { - pub session_id: SessionId, +pub struct MessagesPacket<'a> { pub message_cursor: u32, pub messages: Vec>, } +/// Body content for different packet types +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SocketPacketBody<'a> { + Messages(MessagesPacket<'a>), + // TODO: Can we cleanly use the socket for all communication? + // Serialize(SerializePacket), + // RefPatch(RefPatchPacket<'a>), +} + +/// Message content from /api/socket +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SocketPacket<'a> { + pub session_id: SessionId, + pub packet_type: SocketPacketType, + pub body: SocketPacketBody<'a>, +} + /// Response body from /api/open/{id} #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/tests/rojo_test/internable.rs b/tests/rojo_test/internable.rs index 5cd09984..5dc1be61 100644 --- a/tests/rojo_test/internable.rs +++ b/tests/rojo_test/internable.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use rbx_dom_weak::types::Ref; use serde::Serialize; -use librojo::web_api::{Instance, InstanceUpdate, ReadResponse, SubscribeResponse}; +use librojo::web_api::{ + Instance, InstanceUpdate, MessagesPacket, ReadResponse, SocketPacket, SocketPacketBody, +}; use rojo_insta_ext::RedactionMap; /// A convenience method to store all of the redactable data from a piece of @@ -54,7 +56,16 @@ impl<'a> Internable<&'a HashMap>> for Instance<'a> { } } -impl Internable<()> for SubscribeResponse<'_> { +impl Internable<()> for SocketPacket<'_> { + fn intern(&self, redactions: &mut RedactionMap, extra: ()) { + redactions.intern(&self.session_id); + match &self.body { + SocketPacketBody::Messages(packet) => packet.intern(redactions, extra), + } + } +} + +impl Internable<()> for MessagesPacket<'_> { fn intern(&self, redactions: &mut RedactionMap, _extra: ()) { for message in &self.messages { intern_instance_updates(redactions, &message.updated); diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index 5c042db5..9036316f 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -8,11 +8,14 @@ use std::{ time::Duration, }; +use hyper_tungstenite::tungstenite::{connect, Message}; use rbx_dom_weak::types::Ref; use tempfile::{tempdir, TempDir}; -use librojo::web_api::{ReadResponse, SerializeResponse, ServerInfoResponse, SubscribeResponse}; +use librojo::web_api::{ + ReadResponse, SerializeResponse, ServerInfoResponse, SocketPacket, SocketPacketType, +}; use rojo_insta_ext::RedactionMap; use crate::rojo_test::io_util::{ @@ -173,13 +176,54 @@ impl TestServeSession { Ok(serde_json::from_value(value).expect("Server returned malformed response")) } - pub fn get_api_subscribe( + pub fn get_api_socket_packet( &self, + packet_type: SocketPacketType, cursor: u32, - ) -> Result, reqwest::Error> { - let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor); + ) -> Result, Box> { + let url = format!("ws://localhost:{}/api/socket/{}", self.port, cursor); - reqwest::blocking::get(url)?.json() + let (mut socket, _response) = connect(url)?; + + // Wait for messages with a timeout + let timeout = Duration::from_secs(10); + let start = std::time::Instant::now(); + + loop { + if start.elapsed() > timeout { + return Err("Timeout waiting for packet from WebSocket".into()); + } + + match socket.read() { + Ok(Message::Text(text)) => { + let packet: SocketPacket = serde_json::from_str(&text)?; + if packet.packet_type != packet_type { + continue; + } + + // Close the WebSocket connection now that we got what we were waiting for + let _ = socket.close(None); + return Ok(packet); + } + Ok(Message::Close(_)) => { + return Err("WebSocket closed before receiving messages".into()); + } + Ok(_) => { + // Ignore other message types (ping, pong, binary) + continue; + } + Err(hyper_tungstenite::tungstenite::Error::Io(e)) + if e.kind() == std::io::ErrorKind::WouldBlock => + { + // No data available yet, sleep a bit and try again + thread::sleep(Duration::from_millis(100)); + continue; + } + Err(e) => { + return Err(e.into()); + } + } + } } pub fn get_api_serialize(&self, ids: &[Ref]) -> Result { diff --git a/tests/tests/serve.rs b/tests/tests/serve.rs index e3dae2d7..ee0528e1 100644 --- a/tests/tests/serve.rs +++ b/tests/tests/serve.rs @@ -8,6 +8,8 @@ use crate::rojo_test::{ serve_util::{run_serve_test, serialize_to_xml_model}, }; +use librojo::web_api::SocketPacketType; + #[test] fn empty() { run_serve_test("empty", |session, mut redactions| { @@ -42,10 +44,12 @@ fn scripts() { fs::write(session.path().join("src/foo.lua"), "Updated foo!").unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "scripts_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -74,10 +78,12 @@ fn add_folder() { fs::create_dir(session.path().join("src/my-new-folder")).unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "add_folder_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -104,10 +110,12 @@ fn remove_file() { fs::remove_file(session.path().join("src/hello.txt")).unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "remove_file_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -134,10 +142,12 @@ fn edit_init() { fs::write(session.path().join("src/init.lua"), b"-- Edited contents").unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "edit_init_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -180,10 +190,12 @@ fn move_folder_of_stuff() { // will fail otherwise. fs::rename(stuff_path, session.path().join("src/new-stuff")).unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "move_folder_of_stuff_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -214,10 +226,12 @@ fn empty_json_model() { ) .unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "empty_json_model_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -245,10 +259,12 @@ fn add_optional_folder() { fs::create_dir(session.path().join("create-later")).unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "add_optional_folder_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -437,10 +453,12 @@ fn ref_properties() { ) .unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "ref_properties_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -467,10 +485,12 @@ fn ref_properties_remove() { fs::remove_file(session.path().join("src/target.model.json")).unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "ref_properties_remove_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -529,10 +549,12 @@ fn ref_properties_patch_update() { ) .unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "ref_properties_patch_update_subscribe", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap(); @@ -581,10 +603,12 @@ fn model_pivot_migration() { ) .unwrap(); - let subscribe_response = session.get_api_subscribe(0).unwrap(); + let socket_packet = session + .get_api_socket_packet(SocketPacketType::Messages, 0) + .unwrap(); assert_yaml_snapshot!( "model_pivot_migration_all", - subscribe_response.intern_and_redact(&mut redactions, ()) + socket_packet.intern_and_redact(&mut redactions, ()) ); let read_response = session.get_api_read(root_id).unwrap();