diff --git a/CHANGELOG.md b/CHANGELOG.md index d1000b0a..f5478f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,53 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased * Added headless API for Studio companion plugins. ([#639]) +* 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 + 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]) +* 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]) [#639]: https://github.com/rojo-rbx/rojo/pull/639 +[#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 ## [7.6.1] (November 6th, 2025) diff --git a/Cargo.lock b/Cargo.lock index 46b973f7..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,36 +527,51 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", ] [[package]] @@ -560,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", ] @@ -613,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", @@ -628,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", @@ -638,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", @@ -655,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", @@ -702,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", @@ -725,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", @@ -765,7 +806,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.5", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -786,26 +827,26 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - -[[package]] -name = "hashbrown" -version = "0.15.4" -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]] @@ -814,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" @@ -825,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" @@ -859,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" @@ -871,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", @@ -892,7 +939,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -914,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]] @@ -935,12 +1087,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -965,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]] @@ -990,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" @@ -1005,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", ] @@ -1026,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", ] @@ -1066,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]] @@ -1089,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", @@ -1139,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" @@ -1170,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]] @@ -1198,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]] @@ -1244,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", @@ -1262,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" @@ -1332,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", @@ -1350,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", @@ -1396,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" @@ -1438,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", @@ -1451,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", @@ -1488,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", ] @@ -1500,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", ] @@ -1536,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", @@ -1546,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]] @@ -1565,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" @@ -1599,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", @@ -1614,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", @@ -1635,6 +1803,7 @@ dependencies = [ "rbx_dom_weak", "rbx_reflection", "rbx_reflection_database", + "serde", "thiserror", "zstd", ] @@ -1707,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", @@ -1821,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", ] @@ -1860,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", @@ -1871,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", @@ -1893,11 +2037,12 @@ dependencies = [ [[package]] name = "rojo" -version = "7.6.1" +version = "7.7.0-prealpha" dependencies = [ "anyhow", "backtrace", "bincode", + "blake3", "clap 3.2.25", "criterion", "crossbeam-channel", @@ -1905,11 +2050,14 @@ dependencies = [ "data-encoding", "embed-resource", "env_logger", + "float-cmp", "fs-err", "futures", "globset", "humantime", "hyper", + "hyper-tungstenite", + "indexmap 2.12.1", "insta", "jod-thread", "jsonc-parser", @@ -1935,6 +2083,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "strum", "tempfile", "termcolor", "thiserror", @@ -1956,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", @@ -2015,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" @@ -2058,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" @@ -2098,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]] @@ -2140,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", ] @@ -2160,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" @@ -2191,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" @@ -2211,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" @@ -2218,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", ] @@ -2239,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" @@ -2262,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]] @@ -2292,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]] @@ -2337,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]] @@ -2378,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]] @@ -2402,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", @@ -2451,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", @@ -2469,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", @@ -2480,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]] @@ -2493,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" @@ -2501,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" @@ -2552,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]] @@ -2575,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" @@ -2608,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", @@ -2637,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", @@ -2753,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]] @@ -2768,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]] @@ -2790,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]] @@ -2810,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]] @@ -2831,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" @@ -2843,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" @@ -2855,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" @@ -2867,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" @@ -2879,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" @@ -2891,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" @@ -2903,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" @@ -2931,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" @@ -2947,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" @@ -2962,9 +3411,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", @@ -2973,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]] @@ -3017,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 4a3b80bf..9fcea18d 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 ", @@ -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" @@ -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,16 +88,21 @@ 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" 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..85b51523 100644 --- a/crates/memofs/CHANGELOG.md +++ b/crates/memofs/CHANGELOG.md @@ -1,6 +1,11 @@ # 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] diff --git a/crates/memofs/src/in_memory_fs.rs b/crates/memofs/src/in_memory_fs.rs index 7bc654af..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(); @@ -176,6 +181,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..bc7e329e 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -70,7 +70,10 @@ 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<()>; 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<()>; @@ -173,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(); @@ -190,6 +198,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 +344,42 @@ 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]. + /// 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 +482,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..dd10d452 100644 --- a/crates/memofs/src/noop_backend.rs +++ b/crates/memofs/src/noop_backend.rs @@ -22,10 +22,28 @@ 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")) } + 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..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?; @@ -78,6 +82,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/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/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 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/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/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/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/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() 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, }), }), }), 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/App/init.lua b/plugin/src/App/init.lua index 7f03cf06..b5f54a64 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -230,6 +230,8 @@ function App:init() end function App:willUnmount() + self:endSession() + self.waypointConnection:Disconnect() self.confirmationBindable:Destroy() diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 88ff9302..a5d513fb 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -78,7 +78,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/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/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/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/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 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/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__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__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__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-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 00000000..6a10886d Binary files /dev/null and b/rojo-test/syncback-tests/child_but_not/input.rbxl differ 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 00000000..d42d3ef5 Binary files /dev/null and b/rojo-test/syncback-tests/csv/input.rbxm differ diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/input-project/container.model.json b/rojo-test/syncback-tests/duplicate_rojo_id/input-project/container.model.json new file mode 100644 index 00000000..9c551865 --- /dev/null +++ b/rojo-test/syncback-tests/duplicate_rojo_id/input-project/container.model.json @@ -0,0 +1,13 @@ +{ + "className": "Folder", + "children": [ + { + "name": "value_1", + "className": "ObjectValue" + }, + { + "name": "value_2", + "className": "ObjectValue" + } + ] +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/input-project/default.project.json b/rojo-test/syncback-tests/duplicate_rojo_id/input-project/default.project.json new file mode 100644 index 00000000..3343677a --- /dev/null +++ b/rojo-test/syncback-tests/duplicate_rojo_id/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "duplicate_rojo_id", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "container": { + "$path": "container.model.json" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl b/rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl new file mode 100644 index 00000000..8e3e8369 Binary files /dev/null and b/rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl differ 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 00000000..93331ba0 Binary files /dev/null and b/rojo-test/syncback-tests/ignore_paths_adding/input.rbxm differ 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 00000000..1cc07ef2 Binary files /dev/null and b/rojo-test/syncback-tests/ignore_paths_init/input.rbxm differ diff --git a/rojo-test/syncback-tests/ignore_paths_removing/input-project/default.project.json b/rojo-test/syncback-tests/ignore_paths_removing/input-project/default.project.json new file mode 100644 index 00000000..ee27ba53 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_removing/input-project/default.project.json @@ -0,0 +1,14 @@ +{ + "name": "ignore_paths_removing", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + }, + "syncbackRules": { + "ignorePaths": [ + "*.luau" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_paths_removing/input-project/src/not_in_place.luau b/rojo-test/syncback-tests/ignore_paths_removing/input-project/src/not_in_place.luau new file mode 100644 index 00000000..1ba9bedc --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_removing/input-project/src/not_in_place.luau @@ -0,0 +1 @@ +-- This script is not in the input place file. diff --git a/rojo-test/syncback-tests/ignore_paths_removing/input.rbxl b/rojo-test/syncback-tests/ignore_paths_removing/input.rbxl new file mode 100644 index 00000000..b19b0fba Binary files /dev/null and b/rojo-test/syncback-tests/ignore_paths_removing/input.rbxl differ diff --git a/rojo-test/syncback-tests/ignore_trees_adding/input-project/default.project.json b/rojo-test/syncback-tests/ignore_trees_adding/input-project/default.project.json new file mode 100644 index 00000000..30c2ad9e --- /dev/null +++ b/rojo-test/syncback-tests/ignore_trees_adding/input-project/default.project.json @@ -0,0 +1,14 @@ +{ + "name": "ignore_trees_adding", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + }, + "syncbackRules": { + "ignoreTrees": [ + "ReplicatedStorage/IgnoreMe" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_trees_adding/input-project/src/.gitkeep b/rojo-test/syncback-tests/ignore_trees_adding/input-project/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ignore_trees_adding/input.rbxl b/rojo-test/syncback-tests/ignore_trees_adding/input.rbxl new file mode 100644 index 00000000..afab040a Binary files /dev/null and b/rojo-test/syncback-tests/ignore_trees_adding/input.rbxl differ 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 00000000..422bbf35 Binary files /dev/null and b/rojo-test/syncback-tests/ignore_trees_removing/input.rbxl differ 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 00000000..ef4d7d78 Binary files /dev/null and b/rojo-test/syncback-tests/json_middlewares/input.rbxm differ 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 00000000..4b471172 Binary files /dev/null and b/rojo-test/syncback-tests/nested_projects/input.rbxl differ diff --git a/rojo-test/syncback-tests/nested_projects_weird/input-project/client-only.project.json b/rojo-test/syncback-tests/nested_projects_weird/input-project/client-only.project.json new file mode 100644 index 00000000..28468e74 --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects_weird/input-project/client-only.project.json @@ -0,0 +1,9 @@ +{ + "name": "client_only", + "tree": { + "$path": "src/modules" + }, + "globIgnorePaths": [ + "**Server**" + ] +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects_weird/input-project/default.project.json b/rojo-test/syncback-tests/nested_projects_weird/input-project/default.project.json new file mode 100644 index 00000000..9fb46e6f --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects_weird/input-project/default.project.json @@ -0,0 +1,16 @@ +{ + "name": "nested_projects_but_weird", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "modules": { + "$path": "client-only.project.json" + } + }, + "ServerStorage": { + "modules": { + "$path": "server-only.project.json" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects_weird/input-project/server-only.project.json b/rojo-test/syncback-tests/nested_projects_weird/input-project/server-only.project.json new file mode 100644 index 00000000..44532ba5 --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects_weird/input-project/server-only.project.json @@ -0,0 +1,9 @@ +{ + "name": "server_only", + "tree": { + "$path": "src/modules" + }, + "globIgnorePaths": [ + "**Client**" + ] +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects_weird/input-project/src/modules/.gitkeep b/rojo-test/syncback-tests/nested_projects_weird/input-project/src/modules/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/nested_projects_weird/input.rbxl b/rojo-test/syncback-tests/nested_projects_weird/input.rbxl new file mode 100644 index 00000000..c4fad004 Binary files /dev/null and b/rojo-test/syncback-tests/nested_projects_weird/input.rbxl differ diff --git a/rojo-test/syncback-tests/project_init/input-project/default.project.json b/rojo-test/syncback-tests/project_init/input-project/default.project.json new file mode 100644 index 00000000..34530a84 --- /dev/null +++ b/rojo-test/syncback-tests/project_init/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "project_init", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "script": { + "$path": "src" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_init/input-project/src/init.luau b/rojo-test/syncback-tests/project_init/input-project/src/init.luau new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/project_init/input.rbxl b/rojo-test/syncback-tests/project_init/input.rbxl new file mode 100644 index 00000000..f1e146c9 Binary files /dev/null and b/rojo-test/syncback-tests/project_init/input.rbxl differ diff --git a/rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau b/rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau new file mode 100644 index 00000000..b6c5da36 --- /dev/null +++ b/rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau @@ -0,0 +1 @@ +-- satellite beef psychology response supply \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_reserialize/expected/default.project.json b/rojo-test/syncback-tests/project_reserialize/expected/default.project.json new file mode 100644 index 00000000..60605da2 --- /dev/null +++ b/rojo-test/syncback-tests/project_reserialize/expected/default.project.json @@ -0,0 +1,29 @@ +{ + "name": "project_reserialize", + "tree": { + "$className": "DataModel", + "Workspace": { + "attribute_mismatch": { + "$attributes": { + "foo": "bar" + }, + "$path": "attribute_mismatch.luau" + }, + "property_mismatch": { + "$path": "property_mismatch.project.json" + }, + "$properties": { + "EditorLiveScripting": { + "Enum": 0 + }, + "SignalBehavior": "Deferred", + "StreamOutBehavior": "Opportunistic", + "StreamingEnabled": true, + "StreamingIntegrityMode": "PauseOutsideLoadedArea" + }, + "$attributes": { + "Rojo_Target_CurrentCamera": "6d6ae1d713c82fae0620aa1300000375" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json b/rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json new file mode 100644 index 00000000..1ca8b303 --- /dev/null +++ b/rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json @@ -0,0 +1,11 @@ +{ + "name": "property_mismatch", + "tree": { + "$className": "BrickColorValue", + "$properties": { + "Value": { + "BrickColor": 345 + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_reserialize/input-project/attribute_mismatch.luau b/rojo-test/syncback-tests/project_reserialize/input-project/attribute_mismatch.luau new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/project_reserialize/input-project/default.project.json b/rojo-test/syncback-tests/project_reserialize/input-project/default.project.json new file mode 100644 index 00000000..1991bb91 --- /dev/null +++ b/rojo-test/syncback-tests/project_reserialize/input-project/default.project.json @@ -0,0 +1,15 @@ +{ + "name": "project_reserialize", + "tree": { + "$className": "Folder", + "attribute_mismatch": { + "$attributes": { + "foo": "bar" + }, + "$path": "attribute_mismatch.luau" + }, + "property_mismatch": { + "$path": "property_mismatch.project.json" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_reserialize/input-project/property_mismatch.project.json b/rojo-test/syncback-tests/project_reserialize/input-project/property_mismatch.project.json new file mode 100644 index 00000000..cb34ee96 --- /dev/null +++ b/rojo-test/syncback-tests/project_reserialize/input-project/property_mismatch.project.json @@ -0,0 +1,6 @@ +{ + "name": "property_mismatch", + "tree": { + "$className": "BrickColorValue" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_reserialize/input.rbxm b/rojo-test/syncback-tests/project_reserialize/input.rbxm new file mode 100644 index 00000000..ae955999 Binary files /dev/null and b/rojo-test/syncback-tests/project_reserialize/input.rbxm differ 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 00000000..37659d94 Binary files /dev/null and b/rojo-test/syncback-tests/rbxm_fallback/input.rbxm differ diff --git a/rojo-test/syncback-tests/ref_properties/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties/input-project/default.project.json new file mode 100644 index 00000000..c2dd62c3 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "ref_properties", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties/input-project/src/pointer.model.json b/rojo-test/syncback-tests/ref_properties/input-project/src/pointer.model.json new file mode 100644 index 00000000..54160c22 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties/input-project/src/pointer.model.json @@ -0,0 +1,3 @@ +{ + "className": "ObjectValue" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties/input-project/src/target.model.json b/rojo-test/syncback-tests/ref_properties/input-project/src/target.model.json new file mode 100644 index 00000000..f450e412 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties/input-project/src/target.model.json @@ -0,0 +1,3 @@ +{ + "className": "Folder" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties/input.rbxl b/rojo-test/syncback-tests/ref_properties/input.rbxl new file mode 100644 index 00000000..af3e1ac0 Binary files /dev/null and b/rojo-test/syncback-tests/ref_properties/input.rbxl differ diff --git a/rojo-test/syncback-tests/ref_properties_blank/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties_blank/input-project/default.project.json new file mode 100644 index 00000000..dd721ce7 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_blank/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "ref_properties_blank", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_blank/input-project/src/.gitkeep b/rojo-test/syncback-tests/ref_properties_blank/input-project/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ref_properties_blank/input.rbxl b/rojo-test/syncback-tests/ref_properties_blank/input.rbxl new file mode 100644 index 00000000..3675729e Binary files /dev/null and b/rojo-test/syncback-tests/ref_properties_blank/input.rbxl differ 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 00000000..0aee7958 Binary files /dev/null and b/rojo-test/syncback-tests/ref_properties_conflict/input.rbxl differ 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 00000000..f26cc347 Binary files /dev/null and b/rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm differ 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 00000000..c29baaaf Binary files /dev/null and b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm differ diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxmx.rbxmx b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxmx.rbxmx new file mode 100644 index 00000000..19d4451d --- /dev/null +++ b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxmx.rbxmx @@ -0,0 +1,15 @@ + + 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 00000000..6bee5530 Binary files /dev/null and b/rojo-test/syncback-tests/respect_old_middleware/input.rbxm differ 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 00000000..e8bbc388 Binary files /dev/null and b/rojo-test/syncback-tests/schema_roundtrip/input.rbxm differ 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 00000000..7cafbe93 Binary files /dev/null and b/rojo-test/syncback-tests/string_value_project/input.rbxm differ 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 00000000..c6f3beef Binary files /dev/null and b/rojo-test/syncback-tests/sync_rules/input.rbxm differ diff --git a/rojo-test/syncback-tests/unscriptable_properties/input-project/default.project.json b/rojo-test/syncback-tests/unscriptable_properties/input-project/default.project.json new file mode 100644 index 00000000..08a4e829 --- /dev/null +++ b/rojo-test/syncback-tests/unscriptable_properties/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "unscriptable_properties", + "tree": { + "$className": "BinaryStringValue" + }, + "syncbackRules": { + "syncUnscriptable": true + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/unscriptable_properties/input.rbxm b/rojo-test/syncback-tests/unscriptable_properties/input.rbxm new file mode 100644 index 00000000..b4fb42c6 Binary files /dev/null and b/rojo-test/syncback-tests/unscriptable_properties/input.rbxm differ 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/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( 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/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/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 83c1b1bb..5cc8278e 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,13 +7,16 @@ 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, +}; -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)] @@ -113,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. @@ -131,7 +138,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 +156,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 +189,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 +269,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 +288,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 let Ok(true) = vfs.exists(&project_path) { + 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) { @@ -304,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. @@ -339,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 @@ -530,7 +573,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/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 4f585018..f50d3926 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()); @@ -219,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..79f7a2d0 100644 --- a/src/snapshot/metadata.rs +++ b/src/snapshot/metadata.rs @@ -62,6 +62,14 @@ 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, + + /// A schema provided via a JSON file, if one exists. Will be `None` for + /// all non-JSON middleware. + pub schema: Option, } impl InstanceMetadata { @@ -72,6 +80,8 @@ impl InstanceMetadata { relevant_paths: Vec::new(), context: InstanceContext::default(), specified_id: None, + middleware: None, + schema: None, } } @@ -109,6 +119,17 @@ impl InstanceMetadata { ..self } } + + pub fn middleware(self, middleware: Middleware) -> Self { + Self { + middleware: Some(middleware), + ..self + } + } + + pub fn schema(self, schema: Option) -> Self { + Self { schema, ..self } + } } impl Default for InstanceMetadata { @@ -215,22 +236,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..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 @@ -14,5 +14,6 @@ metadata: context: 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 31bdfecc..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 @@ -12,5 +12,6 @@ metadata: context: 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 cafc3f5e..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 @@ -14,5 +14,6 @@ metadata: context: 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 359ee86e..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 @@ -12,5 +12,6 @@ metadata: context: 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 9147ca9b..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 @@ -13,9 +13,10 @@ added_instances: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: New class_name: Folder properties: {} children: [] updated_instances: [] - 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 96c45e91..75a043d1 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -1,15 +1,23 @@ -use std::{collections::BTreeMap, path::Path}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + path::Path, +}; use anyhow::Context; -use memofs::{IoResultExt, Vfs}; -use rbx_dom_weak::ustr; -use serde::Serialize; +use memofs::Vfs; +use rbx_dom_weak::{types::Variant, ustr}; +use serde::{Deserialize, Serialize}; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; use super::{ - dir::{dir_meta, snapshot_dir_no_meta}, - meta_file::AdjacentMetadata, + dir::{snapshot_dir_no_meta, syncback_dir_no_meta}, + meta_file::{AdjacentMetadata, DirectoryMetadata}, + PathExt as _, }; pub fn snapshot_csv( @@ -18,7 +26,6 @@ pub fn snapshot_csv( 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 +42,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)) } @@ -55,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!( @@ -74,35 +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. - 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)) } +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. @@ -136,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)); } } } @@ -159,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::*; @@ -223,4 +357,74 @@ 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"), + "root", + ) + .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"), + "root", + ) + .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..b825542d 100644 --- a/src/snapshot_middleware/dir.rs +++ b/src/snapshot_middleware/dir.rs @@ -1,41 +1,36 @@ -use std::path::Path; +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; -use memofs::{DirEntry, IoResultExt, Vfs}; +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), }; - 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` @@ -44,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 @@ -66,18 +62,8 @@ 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 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? @@ -91,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( @@ -104,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::*; @@ -118,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); } @@ -136,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.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/json_model.rs b/src/snapshot_middleware/json_model.rs index 2b472095..d19792cb 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, }; @@ -48,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() @@ -58,18 +61,99 @@ 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)) } -#[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; + + 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, + 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 +171,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 c4174cf0..eaa90952 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -1,13 +1,21 @@ use std::{path::Path, str}; -use memofs::{IoResultExt, Vfs}; -use rbx_dom_weak::{types::Enum, ustr, HashMapExt as _, UstrMap}; +use anyhow::Context as _; +use memofs::Vfs; +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::{dir_meta, snapshot_dir_no_meta}, - meta_file::AdjacentMetadata, + dir::{snapshot_dir_no_meta, syncback_dir_no_meta}, + meta_file::{AdjacentMetadata, DirectoryMetadata}, + PathExt as _, }; #[derive(Debug)] @@ -73,8 +81,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 +88,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)) } @@ -103,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!( @@ -125,14 +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. - 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)) } +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::*; @@ -300,7 +379,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,10 +390,42 @@ 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"), + Path::new("/root/init.lua"), + "root", + 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"), "root", ScriptType::Module, ) diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs index f07e015c..84ca9a80 100644 --- a/src/snapshot_middleware/meta_file.rs +++ b/src/snapshot_middleware/meta_file.rs @@ -1,10 +1,18 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::path::{Path, PathBuf}; use anyhow::{format_err, Context}; -use rbx_dom_weak::{types::Attributes, Ustr, UstrMap}; +use indexmap::IndexMap; +use memofs::{IoResultExt as _, Vfs}; +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. /// @@ -22,18 +30,49 @@ 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, } 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: {}", @@ -45,6 +84,80 @@ 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 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 { + 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, + })) + } + 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; @@ -54,7 +167,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()))?; @@ -65,7 +181,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); } @@ -89,13 +205,34 @@ 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(()) } + /// 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. } @@ -116,11 +253,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, @@ -130,7 +267,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: {}", @@ -142,11 +309,90 @@ 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 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 { + 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, + })) + } + pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { self.apply_ignore_unknown_instances(snapshot); self.apply_class_name(snapshot)?; self.apply_properties(snapshot)?; self.apply_id(snapshot)?; + self.apply_schema(snapshot)?; Ok(()) } @@ -176,7 +422,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()))?; @@ -187,7 +433,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); } @@ -210,4 +456,137 @@ impl DirectoryMetadata { snapshot.metadata.specified_id = self.id.take().map(RojoRef::new); 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: + /// + /// - 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)] +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..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::glob::Glob; -use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule}; +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,53 +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()))?; - let project_path = path.join("default.project.json"); - if vfs.metadata(&project_path).with_not_found()?.is_some() { - return Ok(Some(project_path)); + 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 = dir_path.join(default_project_name); + if vfs.metadata(&project_path).with_not_found()?.is_some() { + 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 @@ -192,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 { @@ -216,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 { @@ -228,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), @@ -255,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) } } } @@ -312,8 +418,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/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 9b92ec7d..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.csv - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo 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.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap new file mode 100644 index 00000000..a275a6cf --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap @@ -0,0 +1,31 @@ +--- +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: ~ + middleware: ~ + schema: ~ +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..603bd2df --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap @@ -0,0 +1,31 @@ +--- +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 + middleware: ~ + schema: ~ +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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.csv - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo 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__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap index ab27bc67..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 @@ -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,11 +16,14 @@ 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: ~ + middleware: ~ + schema: ~ 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..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 @@ -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,9 +16,13 @@ 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: ~ + middleware: ~ + schema: ~ name: foo class_name: Folder properties: {} @@ -31,7 +34,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,11 +41,14 @@ 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: ~ + middleware: dir + schema: ~ name: Child 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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.json - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: ModuleScript 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..7e6c260d --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap @@ -0,0 +1,24 @@ +--- +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 + middleware: ~ + schema: ~ +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__json_model__test__model_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap index c6c30705..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 @@ -12,6 +12,8 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: IntValue properties: @@ -25,8 +27,9 @@ children: context: 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 c6c30705..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 @@ -12,6 +12,8 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: IntValue properties: @@ -25,8 +27,9 @@ children: context: 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 92321572..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.client.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: LocalScript 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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: ModuleScript 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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: ModuleScript 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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /bar.server.lua - /bar.meta.json + - /bar.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: bar class_name: Script properties: @@ -23,4 +26,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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: Script properties: @@ -21,4 +24,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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: Script properties: @@ -21,4 +24,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..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 @@ -2,23 +2,30 @@ 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: ~ + middleware: ~ + schema: ~ 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..d5f8aa9f --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap @@ -0,0 +1,31 @@ +--- +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 + middleware: ~ + schema: ~ +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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.plugin.lua - /foo.meta.json + - /foo.meta.jsonc context: 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 e9704b1c..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.client.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: Script properties: @@ -21,4 +24,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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: ModuleScript 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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: ModuleScript 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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /bar.server.lua - /bar.meta.json + - /bar.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ + schema: ~ name: bar class_name: Script properties: @@ -23,4 +26,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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: Script properties: @@ -21,4 +24,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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.server.lua - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: Script properties: @@ -21,4 +24,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..44c2123b --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap @@ -0,0 +1,19 @@ +--- +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 + middleware: ~ + schema: ~ +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..44c2123b --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap @@ -0,0 +1,19 @@ +--- +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 + middleware: ~ + schema: ~ +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..6d819bfb --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap @@ -0,0 +1,19 @@ +--- +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 + middleware: ~ + schema: ~ +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..6d819bfb --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap @@ -0,0 +1,19 @@ +--- +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 + middleware: ~ + schema: ~ +name: DEFAULT +class_name: DEFAULT +properties: {} +children: [] 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..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 @@ -12,6 +12,8 @@ metadata: context: 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 7906dbbf..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 @@ -12,8 +12,9 @@ metadata: context: 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 51da0621..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 @@ -13,10 +13,11 @@ metadata: context: 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 0c9f90ab..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 @@ -12,6 +12,8 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: children class_name: Folder properties: {} @@ -21,16 +23,18 @@ 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: ~ + 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 d8d59d90..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 @@ -13,8 +13,9 @@ metadata: context: 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 30b01ebb..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 @@ -13,6 +13,8 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project + schema: ~ name: path-child-project class_name: Folder properties: {} @@ -22,16 +24,18 @@ 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: ~ + 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 e723e6d2..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 @@ -10,14 +10,16 @@ metadata: relevant_paths: - /foo/other.txt - /foo/other.meta.json + - /foo/other.meta.jsonc - /foo/default.project.json context: emit_legacy_scripts: true specified_id: ~ + middleware: text + schema: ~ name: path-project class_name: StringValue properties: Value: String: "Hello, world!" children: [] - 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..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 @@ -12,10 +12,11 @@ metadata: context: 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 f42e2246..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 @@ -12,10 +12,11 @@ metadata: context: 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 71a14153..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.toml - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: ModuleScript 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..8c8aef67 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap @@ -0,0 +1,24 @@ +--- +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 + middleware: ~ + schema: ~ +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..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 @@ -10,13 +10,15 @@ metadata: relevant_paths: - /foo.txt - /foo.meta.json + - /foo.meta.jsonc context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ + schema: ~ name: foo class_name: StringValue 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..a5047bc3 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap @@ -0,0 +1,24 @@ +--- +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 + middleware: ~ + schema: ~ +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..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 @@ -10,9 +10,12 @@ metadata: relevant_paths: - /foo.yaml - /foo.meta.json + - /foo.meta.jsonc context: 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 new file mode 100644 index 00000000..8a9e1778 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap @@ -0,0 +1,24 @@ +--- +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 + middleware: ~ + schema: ~ +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..715354e0 100644 --- a/src/snapshot_middleware/txt.rs +++ b/src/snapshot_middleware/txt.rs @@ -1,11 +1,16 @@ use std::{path::Path, str}; -use memofs::{IoResultExt, Vfs}; +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, @@ -16,8 +21,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,18 +28,50 @@ 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)) } +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::*; @@ -62,4 +97,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/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/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); + } } 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..3457beac --- /dev/null +++ b/src/syncback/hash/variant.rs @@ -0,0 +1,216 @@ +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::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)), + 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..ae3ca20d --- /dev/null +++ b/src/variant_eq.rs @@ -0,0 +1,192 @@ +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::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), + (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/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/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/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/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/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/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/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(); diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs new file mode 100644 index 00000000..851b4e4d --- /dev/null +++ b/tests/tests/syncback.rs @@ -0,0 +1,85 @@ +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 !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); + } 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"], + schema_roundtrip => ["default.project.json", "src/model.model.json", "src/init/init.meta.json", "src/adjacent.meta.json"] +}