forked from rojo-rbx/rojo
Compare commits
6 Commits
feature/in
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbb1edd79 | ||
|
|
a2adf2b517 | ||
|
|
4deda0e155 | ||
| 4df2d3c5f8 | |||
|
|
4965165ad5 | ||
|
|
68eab3479a |
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -45,6 +45,13 @@ jobs:
|
|||||||
name: Rojo.rbxm
|
name: Rojo.rbxm
|
||||||
path: Rojo.rbxm
|
path: Rojo.rbxm
|
||||||
|
|
||||||
|
- name: Upload Plugin to Roblox
|
||||||
|
env:
|
||||||
|
RBX_API_KEY: ${{ secrets.PLUGIN_UPLOAD_TOKEN }}
|
||||||
|
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_CI_PLACE_ID }}
|
||||||
|
RBX_PLACE_ID: ${{ vars.PLUGIN_CI_UNIVERSE_ID }}
|
||||||
|
run: lune run upload-plugin Rojo.rbxm
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: ["create-release"]
|
needs: ["create-release"]
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -16,3 +16,9 @@
|
|||||||
[submodule "plugin/Packages/Highlighter"]
|
[submodule "plugin/Packages/Highlighter"]
|
||||||
path = plugin/Packages/Highlighter
|
path = plugin/Packages/Highlighter
|
||||||
url = https://github.com/boatbomber/highlighter.git
|
url = https://github.com/boatbomber/highlighter.git
|
||||||
|
[submodule "plugin/Packages/msgpack-luau"]
|
||||||
|
path = plugin/Packages/msgpack-luau
|
||||||
|
url = https://github.com/cipharius/msgpack-luau/
|
||||||
|
[submodule ".lune/opencloud-execute"]
|
||||||
|
path = .lune/opencloud-execute
|
||||||
|
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git
|
||||||
|
|||||||
8
.lune/.config.luau
Normal file
8
.lune/.config.luau
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
return {
|
||||||
|
luau = {
|
||||||
|
languagemode = "strict",
|
||||||
|
aliases = {
|
||||||
|
lune = "~/.lune/.typedefs/0.10.4/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
.lune/opencloud-execute
Submodule
1
.lune/opencloud-execute
Submodule
Submodule .lune/opencloud-execute added at 8ae86dd3ad
51
.lune/scripts/plugin-upload.luau
Normal file
51
.lune/scripts/plugin-upload.luau
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
local args: any = ...
|
||||||
|
assert(args, "no arguments passed to script")
|
||||||
|
|
||||||
|
local input: buffer = args.BinaryInput
|
||||||
|
|
||||||
|
local AssetService = game:GetService("AssetService")
|
||||||
|
local SerializationService = game:GetService("SerializationService")
|
||||||
|
local EncodingService = game:GetService("EncodingService")
|
||||||
|
|
||||||
|
local input_hash: buffer = EncodingService:ComputeBufferHash(input, Enum.HashAlgorithm.Sha256)
|
||||||
|
local hex_hash: { string } = table.create(buffer.len(input_hash))
|
||||||
|
for i = 0, buffer.len(input_hash) - 1 do
|
||||||
|
table.insert(hex_hash, string.format("%02x", buffer.readu8(input_hash, i)))
|
||||||
|
end
|
||||||
|
|
||||||
|
print(`Deserializing plugin file (size: {buffer.len(input)} bytes, hash: {table.concat(hex_hash, "")})`)
|
||||||
|
local plugin = SerializationService:DeserializeInstancesAsync(input)[1]
|
||||||
|
|
||||||
|
local UploadDetails = require(plugin.UploadDetails) :: any
|
||||||
|
local PLUGIN_ID = UploadDetails.assetId
|
||||||
|
local PLUGIN_NAME = UploadDetails.name
|
||||||
|
local PLUGIN_DESCRIPTION = UploadDetails.description
|
||||||
|
local PLUGIN_CREATOR_ID = UploadDetails.creatorId
|
||||||
|
local PLUGIN_CREATOR_TYPE = UploadDetails.creatorType
|
||||||
|
|
||||||
|
assert(typeof(PLUGIN_ID) == "number", "UploadDetails did not contain a number field 'assetId'")
|
||||||
|
assert(typeof(PLUGIN_NAME) == "string", "UploadDetails did not contain a string field 'name'")
|
||||||
|
assert(typeof(PLUGIN_DESCRIPTION) == "string", "UploadDetails did not contain a string field 'description'")
|
||||||
|
assert(typeof(PLUGIN_CREATOR_ID) == "number", "UploadDetails did not contain a number field 'creatorId'")
|
||||||
|
assert(typeof(PLUGIN_CREATOR_TYPE) == "string", "UploadDetails did not contain a string field 'creatorType'")
|
||||||
|
assert(
|
||||||
|
Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE) ~= nil,
|
||||||
|
"UploadDetails field 'creatorType' was not a valid member of Enum.AssetCreatorType"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(`Uploading to {PLUGIN_ID}`)
|
||||||
|
print(`Plugin Name: {PLUGIN_NAME}`)
|
||||||
|
print(`Plugin Description: {PLUGIN_DESCRIPTION}`)
|
||||||
|
|
||||||
|
local result, version_or_err = AssetService:CreateAssetVersionAsync(plugin, Enum.AssetType.Plugin, PLUGIN_ID, {
|
||||||
|
["Name"] = PLUGIN_NAME,
|
||||||
|
["Description"] = PLUGIN_DESCRIPTION,
|
||||||
|
["CreatorId"] = PLUGIN_CREATOR_ID,
|
||||||
|
["CreatorType"] = Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE),
|
||||||
|
})
|
||||||
|
|
||||||
|
if result ~= Enum.CreateAssetResult.Success then
|
||||||
|
error(`Plugin failed to upload because: {result.Name} - {version_or_err}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
print(`Plugin uploaded successfully. New version is {version_or_err}.`)
|
||||||
78
.lune/upload-plugin.luau
Normal file
78
.lune/upload-plugin.luau
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
local fs = require("@lune/fs")
|
||||||
|
local process = require("@lune/process")
|
||||||
|
local stdio = require("@lune/stdio")
|
||||||
|
|
||||||
|
local luau_execute = require("./opencloud-execute")
|
||||||
|
|
||||||
|
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
|
||||||
|
local PLACE_ID = process.env["RBX_PLACE_ID"]
|
||||||
|
|
||||||
|
local version_string = fs.readFile("plugin/Version.txt")
|
||||||
|
local versions = { string.match(version_string, "^v?(%d+)%.(%d+)%.(%d+)(.*)$") }
|
||||||
|
if versions[4] ~= "" then
|
||||||
|
print("This release is a pre-release. Skipping uploading plugin.")
|
||||||
|
process.exit(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local plugin_path = process.args[1]
|
||||||
|
assert(
|
||||||
|
typeof(plugin_path) == "string",
|
||||||
|
"no plugin path provided, expected usage is `lune run upload-plugin [PATH TO RBXM]`."
|
||||||
|
)
|
||||||
|
|
||||||
|
-- For local testing
|
||||||
|
if process.env["CI"] ~= "true" then
|
||||||
|
local rojo = process.exec("rojo", { "build", "plugin.project.json", "--output", plugin_path })
|
||||||
|
if not rojo.ok then
|
||||||
|
stdio.ewrite("plugin upload failed because: could not build plugin.rbxm\n\n")
|
||||||
|
stdio.ewrite(rojo.stderr)
|
||||||
|
stdio.ewrite("\n")
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
assert(fs.isFile(plugin_path), `Plugin file did not exist at {plugin_path}`)
|
||||||
|
end
|
||||||
|
local plugin_content = fs.readFile(plugin_path)
|
||||||
|
|
||||||
|
local engine_script = fs.readFile(".lune/scripts/plugin-upload.luau")
|
||||||
|
|
||||||
|
print("Creating task to upload plugin")
|
||||||
|
local task = luau_execute.create_task_latest(UNIVERSE_ID, PLACE_ID, engine_script, 300, false, plugin_content)
|
||||||
|
|
||||||
|
print("Waiting for task to finish")
|
||||||
|
local success = luau_execute.await_finish(task)
|
||||||
|
if not success then
|
||||||
|
local error = luau_execute.get_error(task)
|
||||||
|
assert(error, "could not fetch error from task")
|
||||||
|
stdio.ewrite("plugin upload failed because: task did not finish successfully\n\n")
|
||||||
|
stdio.ewrite(error.code)
|
||||||
|
stdio.ewrite("\n")
|
||||||
|
stdio.ewrite(error.message)
|
||||||
|
stdio.ewrite("\n")
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
print("Output from task:\n")
|
||||||
|
for _, log in luau_execute.get_structured_logs(task) do
|
||||||
|
if log.messageType == "ERROR" then
|
||||||
|
stdio.write(stdio.color("red"))
|
||||||
|
stdio.write(log.message)
|
||||||
|
stdio.write("\n")
|
||||||
|
stdio.write(stdio.color("reset"))
|
||||||
|
elseif log.messageType == "INFO" then
|
||||||
|
stdio.write(stdio.color("cyan"))
|
||||||
|
stdio.write(log.message)
|
||||||
|
stdio.write("\n")
|
||||||
|
stdio.write(stdio.color("reset"))
|
||||||
|
elseif log.messageType == "WARNING" then
|
||||||
|
stdio.write(stdio.color("yellow"))
|
||||||
|
stdio.write(log.message)
|
||||||
|
stdio.write("\n")
|
||||||
|
stdio.write(stdio.color("reset"))
|
||||||
|
else
|
||||||
|
stdio.write(stdio.color("reset"))
|
||||||
|
stdio.write(log.message)
|
||||||
|
stdio.write("\n")
|
||||||
|
stdio.write(stdio.color("reset"))
|
||||||
|
end
|
||||||
|
end
|
||||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -30,15 +30,24 @@ Making a new release? Simply add the new header with the version and date undern
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
|
||||||
* Implemented support for the "name" property in meta/model JSON files. ([#1187])
|
|
||||||
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
|
||||||
* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201])
|
|
||||||
|
|
||||||
|
* `inf` and `nan` values in properties are now synced ([#1176])
|
||||||
|
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
||||||
|
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
||||||
|
* Added actors and bindable/remote event/function variants to be synced back as JSON files. ([#1199])
|
||||||
|
* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201])
|
||||||
|
* Fixed a bug where the notification timeout thread would fail to cancel on unmount ([#1211])
|
||||||
|
* Added a "Forget" option to the sync reminder notification to avoid being reminded for that place in the future ([#1215])
|
||||||
|
* Improves relative path calculation for sourcemap generation to avoid issues with Windows UNC paths. ([#1217])
|
||||||
|
|
||||||
|
[#1176]: https://github.com/rojo-rbx/rojo/pull/1176
|
||||||
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
|
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
|
||||||
[#1187]: https://github.com/rojo-rbx/rojo/pull/1187
|
|
||||||
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
|
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
|
||||||
|
[#1199]: https://github.com/rojo-rbx/rojo/pull/1199
|
||||||
[#1201]: https://github.com/rojo-rbx/rojo/pull/1201
|
[#1201]: https://github.com/rojo-rbx/rojo/pull/1201
|
||||||
|
[#1211]: https://github.com/rojo-rbx/rojo/pull/1211
|
||||||
|
[#1215]: https://github.com/rojo-rbx/rojo/pull/1215
|
||||||
|
[#1217]: https://github.com/rojo-rbx/rojo/pull/1217
|
||||||
|
|
||||||
## [7.7.0-rc.1] (November 27th, 2025)
|
## [7.7.0-rc.1] (November 27th, 2025)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Code contributions are welcome for features and bugs that have been reported in
|
|||||||
You'll want these tools to work on Rojo:
|
You'll want these tools to work on Rojo:
|
||||||
|
|
||||||
* Latest stable Rust compiler
|
* Latest stable Rust compiler
|
||||||
|
* Rustfmt and Clippy are used for code formatting and linting.
|
||||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||||
* [Rokit](https://github.com/rojo-rbx/rokit)
|
* [Rokit](https://github.com/rojo-rbx/rokit)
|
||||||
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
||||||
|
|||||||
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -1520,6 +1520,12 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -2068,6 +2074,7 @@ dependencies = [
|
|||||||
"num_cpus",
|
"num_cpus",
|
||||||
"opener",
|
"opener",
|
||||||
"paste",
|
"paste",
|
||||||
|
"pathdiff",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"profiling",
|
"profiling",
|
||||||
"rayon",
|
"rayon",
|
||||||
@@ -2078,10 +2085,12 @@ dependencies = [
|
|||||||
"rbx_xml",
|
"rbx_xml",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ritz",
|
"ritz",
|
||||||
|
"rmp-serde",
|
||||||
"roblox_install",
|
"roblox_install",
|
||||||
"rojo-insta-ext",
|
"rojo-insta-ext",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"strum",
|
"strum",
|
||||||
@@ -2222,6 +2231,16 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_bytes"
|
||||||
|
version = "0.11.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_cbor"
|
name = "serde_cbor"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
|||||||
@@ -100,10 +100,13 @@ clap = { version = "3.2.25", features = ["derive"] }
|
|||||||
profiling = "1.0.15"
|
profiling = "1.0.15"
|
||||||
yaml-rust2 = "0.10.3"
|
yaml-rust2 = "0.10.3"
|
||||||
data-encoding = "2.8.0"
|
data-encoding = "2.8.0"
|
||||||
|
pathdiff = "0.2.3"
|
||||||
|
|
||||||
blake3 = "1.5.0"
|
blake3 = "1.5.0"
|
||||||
float-cmp = "0.9.0"
|
float-cmp = "0.9.0"
|
||||||
indexmap = { version = "2.10.0", features = ["serde"] }
|
indexmap = { version = "2.10.0", features = ["serde"] }
|
||||||
|
rmp-serde = "1.3.0"
|
||||||
|
serde_bytes = "0.11.19"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
@@ -122,7 +125,7 @@ semver = "1.0.22"
|
|||||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||||
|
|
||||||
criterion = "0.3.6"
|
criterion = "0.3.6"
|
||||||
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
|
insta = { version = "1.36.1", features = ["redactions", "yaml", "json"] }
|
||||||
paste = "1.0.14"
|
paste = "1.0.14"
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
serde_yaml = "0.8.26"
|
serde_yaml = "0.8.26"
|
||||||
|
|||||||
6
build.rs
6
build.rs
@@ -30,6 +30,11 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore images in msgpack-luau because they aren't UTF-8 encoded.
|
||||||
|
if file_name.ends_with(".png") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
|
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
|
||||||
children.push((file_name, child_snapshot));
|
children.push((file_name, child_snapshot));
|
||||||
}
|
}
|
||||||
@@ -70,6 +75,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||||
|
"UploadDetails.json" => snapshot_from_fs_path(&plugin_dir.join("UploadDetails.json"))?,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
},
|
},
|
||||||
"Version": {
|
"Version": {
|
||||||
"$path": "plugin/Version.txt"
|
"$path": "plugin/Version.txt"
|
||||||
|
},
|
||||||
|
"UploadDetails": {
|
||||||
|
"$path": "plugin/UploadDetails.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
plugin/Packages/msgpack-luau
Submodule
1
plugin/Packages/msgpack-luau
Submodule
Submodule plugin/Packages/msgpack-luau added at 40f67fc0f6
7
plugin/UploadDetails.json
Normal file
7
plugin/UploadDetails.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"assetId": 13916111004,
|
||||||
|
"name": "Rojo",
|
||||||
|
"description": "The plugin portion of Rojo, a tool to enable professional tooling for Roblox developers.",
|
||||||
|
"creatorId": 32644114,
|
||||||
|
"creatorType": "Group"
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
|
local msgpack = require(script.Parent.Parent.msgpack)
|
||||||
|
|
||||||
local stringTemplate = [[
|
local stringTemplate = [[
|
||||||
Http.Response {
|
Http.Response {
|
||||||
code: %d
|
code: %d
|
||||||
@@ -31,4 +33,8 @@ function Response:json()
|
|||||||
return HttpService:JSONDecode(self.body)
|
return HttpService:JSONDecode(self.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Response:msgpack()
|
||||||
|
return msgpack.decode(self.body)
|
||||||
|
end
|
||||||
|
|
||||||
return Response
|
return Response
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local Promise = require(script.Parent.Promise)
|
|
||||||
local Log = require(script.Parent.Log)
|
local Log = require(script.Parent.Log)
|
||||||
|
local msgpack = require(script.Parent.msgpack)
|
||||||
|
local Promise = require(script.Parent.Promise)
|
||||||
|
|
||||||
local HttpError = require(script.Error)
|
local HttpError = require(script.Error)
|
||||||
local HttpResponse = require(script.Response)
|
local HttpResponse = require(script.Response)
|
||||||
@@ -68,4 +69,12 @@ function Http.jsonDecode(source)
|
|||||||
return HttpService:JSONDecode(source)
|
return HttpService:JSONDecode(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Http.msgpackEncode(object)
|
||||||
|
return msgpack.encode(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Http.msgpackDecode(source)
|
||||||
|
return msgpack.decode(source)
|
||||||
|
end
|
||||||
|
|
||||||
return Http
|
return Http
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function ApiContext:connect()
|
|||||||
|
|
||||||
return Http.get(url)
|
return Http.get(url)
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(Http.Response.json)
|
:andThen(Http.Response.msgpack)
|
||||||
:andThen(rejectWrongProtocolVersion)
|
:andThen(rejectWrongProtocolVersion)
|
||||||
:andThen(function(body)
|
:andThen(function(body)
|
||||||
assert(validateApiInfo(body))
|
assert(validateApiInfo(body))
|
||||||
@@ -163,7 +163,7 @@ end
|
|||||||
function ApiContext:read(ids)
|
function ApiContext:read(ids)
|
||||||
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
|
|
||||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body)
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
@@ -191,9 +191,9 @@ function ApiContext:write(patch)
|
|||||||
table.insert(updated, fixedUpdate)
|
table.insert(updated, fixedUpdate)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Only add the 'added' field if the table is non-empty, or else Roblox's
|
-- Only add the 'added' field if the table is non-empty, or else the msgpack
|
||||||
-- JSON implementation will turn the table into an array instead of an
|
-- encode implementation will turn the table into an array instead of a map,
|
||||||
-- object, causing API validation to fail.
|
-- causing API validation to fail.
|
||||||
local added
|
local added
|
||||||
if next(patch.added) ~= nil then
|
if next(patch.added) ~= nil then
|
||||||
added = patch.added
|
added = patch.added
|
||||||
@@ -206,13 +206,16 @@ function ApiContext:write(patch)
|
|||||||
added = added,
|
added = added,
|
||||||
}
|
}
|
||||||
|
|
||||||
body = Http.jsonEncode(body)
|
body = Http.msgpackEncode(body)
|
||||||
|
|
||||||
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody)
|
return Http.post(url, body)
|
||||||
Log.info("Write response: {:?}", responseBody)
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.msgpack)
|
||||||
|
:andThen(function(responseBody)
|
||||||
|
Log.info("Write response: {:?}", responseBody)
|
||||||
|
|
||||||
return responseBody
|
return responseBody
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:connectWebSocket(packetHandlers)
|
function ApiContext:connectWebSocket(packetHandlers)
|
||||||
@@ -234,7 +237,7 @@ function ApiContext:connectWebSocket(packetHandlers)
|
|||||||
local closed, errored, received
|
local closed, errored, received
|
||||||
|
|
||||||
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
||||||
local data = Http.jsonDecode(msg)
|
local data = Http.msgpackDecode(msg)
|
||||||
if data.sessionId ~= self.__sessionId then
|
if data.sessionId ~= self.__sessionId then
|
||||||
Log.warn("Received message with wrong session ID; ignoring")
|
Log.warn("Received message with wrong session ID; ignoring")
|
||||||
return
|
return
|
||||||
@@ -280,7 +283,7 @@ end
|
|||||||
function ApiContext:open(id)
|
function ApiContext:open(id)
|
||||||
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
||||||
|
|
||||||
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body)
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
@@ -291,11 +294,11 @@ end
|
|||||||
|
|
||||||
function ApiContext:serialize(ids: { string })
|
function ApiContext:serialize(ids: { string })
|
||||||
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
||||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
local request_body = Http.msgpackEncode({ sessionId = self.__sessionId, ids = ids })
|
||||||
|
|
||||||
return Http.post(url, request_body)
|
return Http.post(url, request_body)
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(Http.Response.json)
|
:andThen(Http.Response.msgpack)
|
||||||
:andThen(function(response_body)
|
:andThen(function(response_body)
|
||||||
if response_body.sessionId ~= self.__sessionId then
|
if response_body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
@@ -309,11 +312,11 @@ end
|
|||||||
|
|
||||||
function ApiContext:refPatch(ids: { string })
|
function ApiContext:refPatch(ids: { string })
|
||||||
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
||||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
local request_body = Http.msgpackEncode({ sessionId = self.__sessionId, ids = ids })
|
||||||
|
|
||||||
return Http.post(url, request_body)
|
return Http.post(url, request_body)
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(Http.Response.json)
|
:andThen(Http.Response.msgpack)
|
||||||
:andThen(function(response_body)
|
:andThen(function(response_body)
|
||||||
if response_body.sessionId ~= self.__sessionId then
|
if response_body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
|
|||||||
@@ -19,9 +19,15 @@ local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotifi
|
|||||||
function FullscreenNotification:init()
|
function FullscreenNotification:init()
|
||||||
self.transparency, self.setTransparency = Roact.createBinding(0)
|
self.transparency, self.setTransparency = Roact.createBinding(0)
|
||||||
self.lifetime = self.props.timeout
|
self.lifetime = self.props.timeout
|
||||||
|
self.dismissed = false
|
||||||
end
|
end
|
||||||
|
|
||||||
function FullscreenNotification:dismiss()
|
function FullscreenNotification:dismiss()
|
||||||
|
if self.dismissed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.dismissed = true
|
||||||
|
|
||||||
if self.props.onClose then
|
if self.props.onClose then
|
||||||
self.props.onClose()
|
self.props.onClose()
|
||||||
end
|
end
|
||||||
@@ -59,7 +65,7 @@ function FullscreenNotification:didMount()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function FullscreenNotification:willUnmount()
|
function FullscreenNotification:willUnmount()
|
||||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
if self.timeout and coroutine.status(self.timeout) == "suspended" then
|
||||||
task.cancel(self.timeout)
|
task.cancel(self.timeout)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function Notification:init()
|
|||||||
self.binding = bindingUtil.fromMotor(self.motor)
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
|
|
||||||
self.lifetime = self.props.timeout
|
self.lifetime = self.props.timeout
|
||||||
|
self.dismissed = false
|
||||||
|
|
||||||
self.motor:onStep(function(value)
|
self.motor:onStep(function(value)
|
||||||
if value <= 0 and self.props.onClose then
|
if value <= 0 and self.props.onClose then
|
||||||
@@ -34,6 +35,11 @@ function Notification:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Notification:dismiss()
|
function Notification:dismiss()
|
||||||
|
if self.dismissed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.dismissed = true
|
||||||
|
|
||||||
self.motor:setGoal(Flipper.Spring.new(0, {
|
self.motor:setGoal(Flipper.Spring.new(0, {
|
||||||
frequency = 5,
|
frequency = 5,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
@@ -75,7 +81,7 @@ function Notification:didMount()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Notification:willUnmount()
|
function Notification:willUnmount()
|
||||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
if self.timeout and coroutine.status(self.timeout) == "suspended" then
|
||||||
task.cancel(self.timeout)
|
task.cancel(self.timeout)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -301,6 +301,19 @@ function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
|||||||
Settings:set("priorEndpoints", priorSyncInfos)
|
Settings:set("priorEndpoints", priorSyncInfos)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function App:forgetPriorSyncInfo()
|
||||||
|
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||||
|
if not priorSyncInfos then
|
||||||
|
priorSyncInfos = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = tostring(game.PlaceId)
|
||||||
|
priorSyncInfos[id] = nil
|
||||||
|
Log.trace("Erased last used endpoint for {}", game.PlaceId)
|
||||||
|
|
||||||
|
Settings:set("priorEndpoints", priorSyncInfos)
|
||||||
|
end
|
||||||
|
|
||||||
function App:getHostAndPort()
|
function App:getHostAndPort()
|
||||||
local host = self.host:getValue()
|
local host = self.host:getValue()
|
||||||
local port = self.port:getValue()
|
local port = self.port:getValue()
|
||||||
@@ -435,7 +448,8 @@ function App:checkSyncReminder()
|
|||||||
self:findActiveServer()
|
self:findActiveServer()
|
||||||
:andThen(function(serverInfo, host, port)
|
:andThen(function(serverInfo, host, port)
|
||||||
self:sendSyncReminder(
|
self:sendSyncReminder(
|
||||||
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
|
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`,
|
||||||
|
{ "Connect", "Dismiss" }
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
:catch(function()
|
:catch(function()
|
||||||
@@ -446,7 +460,8 @@ function App:checkSyncReminder()
|
|||||||
|
|
||||||
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
||||||
self:sendSyncReminder(
|
self:sendSyncReminder(
|
||||||
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
|
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`,
|
||||||
|
{ "Connect", "Forget", "Dismiss" }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -486,12 +501,16 @@ function App:stopSyncReminderPolling()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:sendSyncReminder(message: string)
|
function App:sendSyncReminder(message: string, shownActions: { string })
|
||||||
local syncReminderMode = Settings:get("syncReminderMode")
|
local syncReminderMode = Settings:get("syncReminderMode")
|
||||||
if syncReminderMode == "None" then
|
if syncReminderMode == "None" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local connectIndex = table.find(shownActions, "Connect")
|
||||||
|
local forgetIndex = table.find(shownActions, "Forget")
|
||||||
|
local dismissIndex = table.find(shownActions, "Dismiss")
|
||||||
|
|
||||||
self.dismissSyncReminder = self:addNotification({
|
self.dismissSyncReminder = self:addNotification({
|
||||||
text = message,
|
text = message,
|
||||||
timeout = 120,
|
timeout = 120,
|
||||||
@@ -500,24 +519,39 @@ function App:sendSyncReminder(message: string)
|
|||||||
self.dismissSyncReminder = nil
|
self.dismissSyncReminder = nil
|
||||||
end,
|
end,
|
||||||
actions = {
|
actions = {
|
||||||
Connect = {
|
Connect = if connectIndex
|
||||||
text = "Connect",
|
then {
|
||||||
style = "Solid",
|
text = "Connect",
|
||||||
layoutOrder = 1,
|
style = "Solid",
|
||||||
onClick = function()
|
layoutOrder = connectIndex,
|
||||||
self:startSession()
|
onClick = function()
|
||||||
end,
|
self:startSession()
|
||||||
},
|
end,
|
||||||
Dismiss = {
|
}
|
||||||
text = "Dismiss",
|
else nil,
|
||||||
style = "Bordered",
|
Forget = if forgetIndex
|
||||||
layoutOrder = 2,
|
then {
|
||||||
onClick = function()
|
text = "Forget",
|
||||||
-- If the user dismisses the reminder,
|
style = "Bordered",
|
||||||
-- then we don't need to remind them again
|
layoutOrder = forgetIndex,
|
||||||
self:stopSyncReminderPolling()
|
onClick = function()
|
||||||
end,
|
-- The user doesn't want to be reminded again about this sync
|
||||||
},
|
self:forgetPriorSyncInfo()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
else nil,
|
||||||
|
Dismiss = if dismissIndex
|
||||||
|
then {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = dismissIndex,
|
||||||
|
onClick = function()
|
||||||
|
-- If the user dismisses the reminder,
|
||||||
|
-- then we don't need to remind them again
|
||||||
|
self:stopSyncReminderPolling()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
else nil,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ local function trueEquals(a, b): boolean
|
|||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
-- For NaN, check if both values are not equal to themselves
|
||||||
|
elseif a ~= a and b ~= b then
|
||||||
|
return true
|
||||||
|
|
||||||
-- For numbers, compare with epsilon of 0.0001 to avoid floating point inequality
|
-- For numbers, compare with epsilon of 0.0001 to avoid floating point inequality
|
||||||
elseif typeA == "number" and typeB == "number" then
|
elseif typeA == "number" and typeB == "number" then
|
||||||
return fuzzyEq(a, b, 0.0001)
|
return fuzzyEq(a, b, 0.0001)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/build.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="Folder" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">json_model_legacy_name</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="Folder" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Expected Name</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/build.rs
|
|
||||||
assertion_line: 109
|
|
||||||
expression: contents
|
|
||||||
---
|
|
||||||
<roblox version="4">
|
|
||||||
<Item class="DataModel" referent="0">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">model_json_name_input</string>
|
|
||||||
</Properties>
|
|
||||||
<Item class="Workspace" referent="1">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">Workspace</string>
|
|
||||||
<bool name="NeedsPivotMigration">false</bool>
|
|
||||||
</Properties>
|
|
||||||
<Item class="StringValue" referent="2">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">/Bar</string>
|
|
||||||
</Properties>
|
|
||||||
</Item>
|
|
||||||
</Item>
|
|
||||||
</Item>
|
|
||||||
</roblox>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/build.rs
|
|
||||||
assertion_line: 108
|
|
||||||
expression: contents
|
|
||||||
---
|
|
||||||
<roblox version="4">
|
|
||||||
<Item class="Folder" referent="0">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">slugified_name_roundtrip</string>
|
|
||||||
</Properties>
|
|
||||||
<Item class="Script" referent="1">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">/Script</string>
|
|
||||||
<token name="RunContext">0</token>
|
|
||||||
<string name="Source"><![CDATA[print("Hello world!")
|
|
||||||
]]></string>
|
|
||||||
</Properties>
|
|
||||||
</Item>
|
|
||||||
</Item>
|
|
||||||
</roblox>
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "json_model_legacy_name",
|
||||||
|
"tree": {
|
||||||
|
"$path": "folder"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Name": "Overridden Name",
|
||||||
|
"ClassName": "Folder"
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "model_json_name_input",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Bar",
|
|
||||||
"className": "StringValue"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
print("Hello world!")
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "slugified_name_roundtrip",
|
|
||||||
"tree": {
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
print("Hello world!")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/rojo_test/syncback_util.rs
|
|
||||||
assertion_line: 101
|
|
||||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
---
|
---
|
||||||
source: tests/rojo_test/syncback_util.rs
|
source: tests/rojo_test/syncback_util.rs
|
||||||
assertion_line: 101
|
|
||||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||||
---
|
---
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild/.gitkeep
|
Writing src/ChildWithDuplicates.rbxm
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild1/.gitkeep
|
|
||||||
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild
|
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild1
|
|
||||||
Writing src/ChildWithoutDuplicates
|
Writing src/ChildWithoutDuplicates
|
||||||
Writing src/ChildWithoutDuplicates/Child
|
Writing src/ChildWithoutDuplicates/Child
|
||||||
|
Removing src/ChildWithDuplicates
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/rojo_test/syncback_util.rs
|
|
||||||
assertion_line: 101
|
|
||||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
|
||||||
---
|
|
||||||
Writing default.project.json
|
|
||||||
Writing src/Camera.rbxm
|
|
||||||
Writing src/Terrain.rbxm
|
|
||||||
Writing src/_Folder/init.meta.json
|
|
||||||
Writing src/_Script.meta.json
|
|
||||||
Writing src/_Script.server.luau
|
|
||||||
Writing src
|
|
||||||
Writing src/_Folder
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/foo.model.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Bar",
|
|
||||||
"className": "StringValue"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/ChildWithDuplicates/DuplicateChild1/.gitkeep
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/ChildWithDuplicates/DuplicateChild/.gitkeep
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Folder.model.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"className": "Folder"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Folder/init.meta.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Folder"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script.meta.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script.server.luau
|
|
||||||
---
|
|
||||||
print("Hello world!")
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script/init.meta.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script/init.server.luau
|
|
||||||
---
|
|
||||||
print("Hello world!")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "model_json_name",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Bar",
|
|
||||||
"className": "StringValue"
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "slugified_name",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -3,3 +3,4 @@ rojo = "rojo-rbx/rojo@7.5.1"
|
|||||||
selene = "Kampfkarren/selene@0.29.0"
|
selene = "Kampfkarren/selene@0.29.0"
|
||||||
stylua = "JohnnyMorganz/stylua@2.1.0"
|
stylua = "JohnnyMorganz/stylua@2.1.0"
|
||||||
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
||||||
|
lune = "lune-org/lune@0.10.4"
|
||||||
|
|||||||
@@ -98,5 +98,5 @@ fn uninstall_plugin() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn plugin_initialize() {
|
fn plugin_initialize() {
|
||||||
assert!(initialize_plugin().is_ok())
|
let _ = initialize_plugin().unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
source: src/cli/sourcemap.rs
|
||||||
|
expression: sourcemap_contents
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"className": "DataModel",
|
||||||
|
"filePaths": "[...1 path omitted...]",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "ReplicatedStorage",
|
||||||
|
"className": "ReplicatedStorage",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Project",
|
||||||
|
"className": "ModuleScript",
|
||||||
|
"filePaths": "[...1 path omitted...]",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Module",
|
||||||
|
"className": "Folder",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "module",
|
||||||
|
"className": "ModuleScript",
|
||||||
|
"filePaths": "[...1 path omitted...]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
source: src/cli/sourcemap.rs
|
||||||
|
expression: sourcemap_contents
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"className": "DataModel",
|
||||||
|
"filePaths": [
|
||||||
|
"default.project.json"
|
||||||
|
],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "ReplicatedStorage",
|
||||||
|
"className": "ReplicatedStorage",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Project",
|
||||||
|
"className": "ModuleScript",
|
||||||
|
"filePaths": [
|
||||||
|
"src/init.luau"
|
||||||
|
],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Module",
|
||||||
|
"className": "Folder",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "module",
|
||||||
|
"className": "ModuleScript",
|
||||||
|
"filePaths": [
|
||||||
|
"../module/module.luau"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use fs_err::File;
|
|||||||
use memofs::Vfs;
|
use memofs::Vfs;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use rbx_dom_weak::{types::Ref, Ustr};
|
use rbx_dom_weak::{types::Ref, Ustr};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -24,19 +24,20 @@ const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project
|
|||||||
const ABSOLUTE_PATH_FAILED_ERR: &str = "Failed to turn relative path into absolute path!";
|
const ABSOLUTE_PATH_FAILED_ERR: &str = "Failed to turn relative path into absolute path!";
|
||||||
|
|
||||||
/// Representation of a node in the generated sourcemap tree.
|
/// Representation of a node in the generated sourcemap tree.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SourcemapNode<'a> {
|
struct SourcemapNode<'a> {
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
class_name: Ustr,
|
class_name: Ustr,
|
||||||
|
|
||||||
#[serde(
|
#[serde(
|
||||||
|
default,
|
||||||
skip_serializing_if = "Vec::is_empty",
|
skip_serializing_if = "Vec::is_empty",
|
||||||
serialize_with = "crate::path_serializer::serialize_vec_absolute"
|
serialize_with = "crate::path_serializer::serialize_vec_absolute"
|
||||||
)]
|
)]
|
||||||
file_paths: Vec<Cow<'a, Path>>,
|
file_paths: Vec<Cow<'a, Path>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
children: Vec<SourcemapNode<'a>>,
|
children: Vec<SourcemapNode<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +71,13 @@ pub struct SourcemapCommand {
|
|||||||
|
|
||||||
impl SourcemapCommand {
|
impl SourcemapCommand {
|
||||||
pub fn run(self) -> anyhow::Result<()> {
|
pub fn run(self) -> anyhow::Result<()> {
|
||||||
let project_path = resolve_path(&self.project);
|
let project_path = fs_err::canonicalize(resolve_path(&self.project))?;
|
||||||
|
|
||||||
log::trace!("Constructing in-memory filesystem");
|
log::trace!("Constructing filesystem with StdBackend");
|
||||||
let vfs = Vfs::new_default();
|
let vfs = Vfs::new_default();
|
||||||
vfs.set_watch_enabled(self.watch);
|
vfs.set_watch_enabled(self.watch);
|
||||||
|
|
||||||
|
log::trace!("Setting up session for sourcemap generation");
|
||||||
let session = ServeSession::new(vfs, project_path)?;
|
let session = ServeSession::new(vfs, project_path)?;
|
||||||
let mut cursor = session.message_queue().cursor();
|
let mut cursor = session.message_queue().cursor();
|
||||||
|
|
||||||
@@ -87,14 +89,17 @@ impl SourcemapCommand {
|
|||||||
|
|
||||||
// Pre-build a rayon threadpool with a low number of threads to avoid
|
// Pre-build a rayon threadpool with a low number of threads to avoid
|
||||||
// dynamic creation overhead on systems with a high number of cpus.
|
// dynamic creation overhead on systems with a high number of cpus.
|
||||||
|
log::trace!("Setting rayon global threadpool");
|
||||||
rayon::ThreadPoolBuilder::new()
|
rayon::ThreadPoolBuilder::new()
|
||||||
.num_threads(num_cpus::get().min(6))
|
.num_threads(num_cpus::get().min(6))
|
||||||
.build_global()
|
.build_global()
|
||||||
.unwrap();
|
.ok();
|
||||||
|
|
||||||
|
log::trace!("Writing initial sourcemap");
|
||||||
write_sourcemap(&session, self.output.as_deref(), filter, self.absolute)?;
|
write_sourcemap(&session, self.output.as_deref(), filter, self.absolute)?;
|
||||||
|
|
||||||
if self.watch {
|
if self.watch {
|
||||||
|
log::trace!("Setting up runtime for watch mode");
|
||||||
let rt = Runtime::new().unwrap();
|
let rt = Runtime::new().unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -208,7 +213,7 @@ fn recurse_create_node<'a>(
|
|||||||
} else {
|
} else {
|
||||||
for val in file_paths {
|
for val in file_paths {
|
||||||
output_file_paths.push(Cow::from(
|
output_file_paths.push(Cow::from(
|
||||||
val.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR),
|
pathdiff::diff_paths(val, project_dir).expect(PATH_STRIP_FAILED_ERR),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -250,3 +255,80 @@ fn write_sourcemap(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::cli::sourcemap::SourcemapNode;
|
||||||
|
use crate::cli::SourcemapCommand;
|
||||||
|
use insta::internals::Content;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_relative_paths() {
|
||||||
|
let sourcemap_dir = tempfile::tempdir().unwrap();
|
||||||
|
let sourcemap_output = sourcemap_dir.path().join("sourcemap.json");
|
||||||
|
let project_path = fs_err::canonicalize(
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("test-projects")
|
||||||
|
.join("relative_paths")
|
||||||
|
.join("project"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let sourcemap_command = SourcemapCommand {
|
||||||
|
project: project_path,
|
||||||
|
output: Some(sourcemap_output.clone()),
|
||||||
|
include_non_scripts: false,
|
||||||
|
watch: false,
|
||||||
|
absolute: false,
|
||||||
|
};
|
||||||
|
assert!(sourcemap_command.run().is_ok());
|
||||||
|
|
||||||
|
let raw_sourcemap_contents = fs_err::read_to_string(sourcemap_output.as_path()).unwrap();
|
||||||
|
let sourcemap_contents =
|
||||||
|
serde_json::from_str::<SourcemapNode>(&raw_sourcemap_contents).unwrap();
|
||||||
|
insta::assert_json_snapshot!(sourcemap_contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_absolute_paths() {
|
||||||
|
let sourcemap_dir = tempfile::tempdir().unwrap();
|
||||||
|
let sourcemap_output = sourcemap_dir.path().join("sourcemap.json");
|
||||||
|
let project_path = fs_err::canonicalize(
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("test-projects")
|
||||||
|
.join("relative_paths")
|
||||||
|
.join("project"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let sourcemap_command = SourcemapCommand {
|
||||||
|
project: project_path,
|
||||||
|
output: Some(sourcemap_output.clone()),
|
||||||
|
include_non_scripts: false,
|
||||||
|
watch: false,
|
||||||
|
absolute: true,
|
||||||
|
};
|
||||||
|
assert!(sourcemap_command.run().is_ok());
|
||||||
|
|
||||||
|
let raw_sourcemap_contents = fs_err::read_to_string(sourcemap_output.as_path()).unwrap();
|
||||||
|
let sourcemap_contents =
|
||||||
|
serde_json::from_str::<SourcemapNode>(&raw_sourcemap_contents).unwrap();
|
||||||
|
insta::assert_json_snapshot!(sourcemap_contents, {
|
||||||
|
".**.filePaths" => insta::dynamic_redaction(|mut value, _path| {
|
||||||
|
let mut paths_count = 0;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Content::Seq(ref mut vec) => {
|
||||||
|
for path in vec.iter().map(|i| i.as_str().unwrap()) {
|
||||||
|
assert_eq!(fs_err::canonicalize(path).is_ok(), true, "path was not valid");
|
||||||
|
assert_eq!(Path::new(path).is_absolute(), true, "path was not absolute");
|
||||||
|
|
||||||
|
paths_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected filePaths to be a sequence"),
|
||||||
|
}
|
||||||
|
format!("[...{} path{} omitted...]", paths_count, if paths_count != 1 { "s" } else { "" } )
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,12 +70,6 @@ pub struct InstanceMetadata {
|
|||||||
/// A schema provided via a JSON file, if one exists. Will be `None` for
|
/// A schema provided via a JSON file, if one exists. Will be `None` for
|
||||||
/// all non-JSON middleware.
|
/// all non-JSON middleware.
|
||||||
pub schema: Option<String>,
|
pub schema: Option<String>,
|
||||||
|
|
||||||
/// A custom name specified via meta.json or model.json files. If present,
|
|
||||||
/// this name will be used for the instance while the filesystem name will
|
|
||||||
/// be slugified to remove illegal characters.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub specified_name: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceMetadata {
|
impl InstanceMetadata {
|
||||||
@@ -88,7 +82,6 @@ impl InstanceMetadata {
|
|||||||
specified_id: None,
|
specified_id: None,
|
||||||
middleware: None,
|
middleware: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
specified_name: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +130,6 @@ impl InstanceMetadata {
|
|||||||
pub fn schema(self, schema: Option<String>) -> Self {
|
pub fn schema(self, schema: Option<String>) -> Self {
|
||||||
Self { schema, ..self }
|
Self { schema, ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn specified_name(self, specified_name: Option<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
specified_name,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InstanceMetadata {
|
impl Default for InstanceMetadata {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use rbx_dom_weak::{
|
|||||||
ustr, HashMapExt as _, UstrMap, UstrSet,
|
ustr, HashMapExt as _, UstrMap, UstrSet,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{RojoRef, REF_POINTER_ATTRIBUTE_PREFIX};
|
use crate::{variant_eq::variant_eq, RojoRef, REF_POINTER_ATTRIBUTE_PREFIX};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
patch::{PatchAdd, PatchSet, PatchUpdate},
|
patch::{PatchAdd, PatchSet, PatchUpdate},
|
||||||
@@ -127,7 +127,7 @@ fn compute_property_patches(
|
|||||||
|
|
||||||
match instance.properties().get(&name) {
|
match instance.properties().get(&name) {
|
||||||
Some(instance_value) => {
|
Some(instance_value) => {
|
||||||
if &snapshot_value != instance_value {
|
if !variant_eq(&snapshot_value, instance_value) {
|
||||||
changed_properties.insert(name, Some(snapshot_value));
|
changed_properties.insert(name, Some(snapshot_value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,14 +109,8 @@ pub fn syncback_csv<'sync>(
|
|||||||
|
|
||||||
if !meta.is_empty() {
|
if !meta.is_empty() {
|
||||||
let parent = snapshot.path.parent_err()?;
|
let parent = snapshot.path.parent_err()?;
|
||||||
let file_name = snapshot
|
|
||||||
.path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let meta_stem = file_name.strip_suffix(".csv").unwrap_or(file_name);
|
|
||||||
fs_snapshot.add_file(
|
fs_snapshot.add_file(
|
||||||
parent.join(format!("{meta_stem}.meta.json")),
|
parent.join(format!("{}.meta.json", new_inst.name)),
|
||||||
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,10 @@ use memofs::{DirEntry, Vfs};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource},
|
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource},
|
||||||
syncback::{
|
syncback::{hash_instance, FsSnapshot, SyncbackReturn, SyncbackSnapshot},
|
||||||
extension_for_middleware, hash_instance, FsSnapshot, SyncbackReturn,
|
|
||||||
SyncbackSnapshot,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{meta_file::DirectoryMetadata, snapshot_from_vfs, Middleware};
|
use super::{meta_file::DirectoryMetadata, snapshot_from_vfs};
|
||||||
|
|
||||||
const EMPTY_DIR_KEEP_NAME: &str = ".gitkeep";
|
const EMPTY_DIR_KEEP_NAME: &str = ".gitkeep";
|
||||||
|
|
||||||
@@ -94,22 +91,6 @@ pub fn snapshot_dir_no_meta(
|
|||||||
Ok(Some(snapshot))
|
Ok(Some(snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a filesystem name into (stem, extension) based on middleware type.
|
|
||||||
/// For directory middleware, the extension is empty. For file middleware,
|
|
||||||
/// the extension comes from `extension_for_middleware`.
|
|
||||||
fn split_name_and_ext(name: &str, middleware: Middleware) -> (&str, &str) {
|
|
||||||
if middleware.is_dir() {
|
|
||||||
(name, "")
|
|
||||||
} else {
|
|
||||||
let ext = extension_for_middleware(middleware);
|
|
||||||
if let Some(stem) = name.strip_suffix(&format!(".{ext}")) {
|
|
||||||
(stem, ext)
|
|
||||||
} else {
|
|
||||||
(name, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn syncback_dir<'sync>(
|
pub fn syncback_dir<'sync>(
|
||||||
snapshot: &SyncbackSnapshot<'sync>,
|
snapshot: &SyncbackSnapshot<'sync>,
|
||||||
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
||||||
@@ -153,128 +134,65 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
let mut removed_children = Vec::new();
|
let mut removed_children = Vec::new();
|
||||||
|
|
||||||
// Build the old child map early so it can be used for deduplication below.
|
// We have to enforce unique child names for the file system.
|
||||||
let mut old_child_map = HashMap::new();
|
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::<Vec<&str>>().join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
anyhow::bail!("Instance has more than 25 children with duplicate names");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(old_inst) = snapshot.old_inst() {
|
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() {
|
for child in old_inst.children() {
|
||||||
let inst = snapshot.get_old_instance(*child).unwrap();
|
let inst = snapshot.get_old_instance(*child).unwrap();
|
||||||
old_child_map.insert(inst.name(), inst);
|
old_child_map.insert(inst.name(), inst);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- Two-pass collision resolution ---
|
for new_child_ref in new_inst.children() {
|
||||||
//
|
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
||||||
// Pass 1: Collect each child's base filesystem name and old ref, applying
|
if let Some(old_child) = old_child_map.remove(new_child.name.as_str()) {
|
||||||
// skip conditions. Track which names are used (lowercased) so we can
|
if old_child.metadata().relevant_paths.is_empty() {
|
||||||
// detect collisions.
|
log::debug!(
|
||||||
struct ChildEntry {
|
"Skipping instance {} because it doesn't exist on the disk",
|
||||||
new_ref: rbx_dom_weak::types::Ref,
|
old_child.name()
|
||||||
old_ref: Option<rbx_dom_weak::types::Ref>,
|
);
|
||||||
base_name: String,
|
continue;
|
||||||
middleware: Middleware,
|
} else if matches!(
|
||||||
skip: bool,
|
old_child.metadata().instigating_source,
|
||||||
}
|
Some(InstigatingSource::ProjectNode { .. })
|
||||||
|
) {
|
||||||
let mut entries = Vec::with_capacity(new_inst.children().len());
|
log::debug!(
|
||||||
let mut used_names: HashSet<String> = HashSet::with_capacity(new_inst.children().len());
|
"Skipping instance {} because it originates in a project file",
|
||||||
let mut collision_indices: Vec<usize> = Vec::new();
|
old_child.name()
|
||||||
|
);
|
||||||
for new_child_ref in new_inst.children() {
|
continue;
|
||||||
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
}
|
||||||
|
// This child exists in both doms. Pass it on.
|
||||||
// Determine old_ref and apply skip conditions.
|
children.push(snapshot.with_joined_path(*new_child_ref, Some(old_child.id()))?);
|
||||||
let old_child = if snapshot.old_inst().is_some() {
|
|
||||||
old_child_map.remove(new_child.name.as_str())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut skip = false;
|
|
||||||
if let Some(ref old) = old_child {
|
|
||||||
if old.metadata().relevant_paths.is_empty() {
|
|
||||||
log::debug!(
|
|
||||||
"Skipping instance {} because it doesn't exist on the disk",
|
|
||||||
old.name()
|
|
||||||
);
|
|
||||||
skip = true;
|
|
||||||
} else if matches!(
|
|
||||||
old.metadata().instigating_source,
|
|
||||||
Some(InstigatingSource::ProjectNode { .. })
|
|
||||||
) {
|
|
||||||
log::debug!(
|
|
||||||
"Skipping instance {} because it originates in a project file",
|
|
||||||
old.name()
|
|
||||||
);
|
|
||||||
skip = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let old_ref = old_child.as_ref().map(|o| o.id());
|
|
||||||
|
|
||||||
if skip {
|
|
||||||
entries.push(ChildEntry {
|
|
||||||
new_ref: *new_child_ref,
|
|
||||||
old_ref,
|
|
||||||
base_name: String::new(),
|
|
||||||
middleware: Middleware::Dir,
|
|
||||||
skip: true,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (middleware, base_name) =
|
|
||||||
snapshot.child_middleware_and_name(*new_child_ref, old_ref)?;
|
|
||||||
|
|
||||||
let idx = entries.len();
|
|
||||||
let lower = base_name.to_lowercase();
|
|
||||||
if !used_names.insert(lower) {
|
|
||||||
// Name already claimed — needs resolution.
|
|
||||||
collision_indices.push(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(ChildEntry {
|
|
||||||
new_ref: *new_child_ref,
|
|
||||||
old_ref,
|
|
||||||
base_name,
|
|
||||||
middleware,
|
|
||||||
skip: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: Resolve collisions by appending incrementing suffixes.
|
|
||||||
for idx in collision_indices {
|
|
||||||
let entry = &entries[idx];
|
|
||||||
let (stem, ext) = split_name_and_ext(&entry.base_name, entry.middleware);
|
|
||||||
let mut counter = 1u32;
|
|
||||||
loop {
|
|
||||||
let candidate = if ext.is_empty() {
|
|
||||||
format!("{stem}{counter}")
|
|
||||||
} else {
|
} else {
|
||||||
format!("{stem}{counter}.{ext}")
|
// The child only exists in the the new dom
|
||||||
};
|
children.push(snapshot.with_joined_path(*new_child_ref, None)?);
|
||||||
let lower = candidate.to_lowercase();
|
|
||||||
if used_names.insert(lower) {
|
|
||||||
// Safe to mutate — we only visit each collision index once.
|
|
||||||
let entry = &mut entries[idx];
|
|
||||||
entry.base_name = candidate;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
counter += 1;
|
|
||||||
}
|
}
|
||||||
}
|
// Any children that are in the old dom but not the new one are removed.
|
||||||
|
|
||||||
// Create snapshots from resolved entries.
|
|
||||||
for entry in &entries {
|
|
||||||
if entry.skip {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let resolved_path = snapshot.path.join(&entry.base_name);
|
|
||||||
children.push(snapshot.with_new_path(resolved_path, entry.new_ref, entry.old_ref));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any children that are in the old dom but not the new one are removed.
|
|
||||||
if snapshot.old_inst().is_some() {
|
|
||||||
removed_children.extend(old_child_map.into_values());
|
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();
|
let mut fs_snapshot = FsSnapshot::new();
|
||||||
|
|
||||||
@@ -307,12 +225,6 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
snapshot::{InstanceMetadata, InstanceSnapshot},
|
|
||||||
Project, RojoTree, SyncbackData, SyncbackSnapshot,
|
|
||||||
};
|
|
||||||
use memofs::{InMemoryFs, VfsSnapshot};
|
use memofs::{InMemoryFs, VfsSnapshot};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -349,302 +261,4 @@ mod test {
|
|||||||
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_project() -> Project {
|
|
||||||
serde_json::from_str(r#"{"tree": {"$className": "DataModel"}}"#).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_vfs() -> Vfs {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/root", VfsSnapshot::empty_dir()).unwrap();
|
|
||||||
Vfs::new(imfs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two children whose Roblox names are identical when lowercased ("Alpha"
|
|
||||||
/// and "alpha") but live at different filesystem paths because of the
|
|
||||||
/// `name` property ("Beta/" and "Alpha/" respectively). The dedup check
|
|
||||||
/// must use the actual filesystem paths, not the raw Roblox names, to
|
|
||||||
/// avoid a false-positive duplicate error.
|
|
||||||
#[test]
|
|
||||||
fn syncback_no_false_duplicate_with_name_prop() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
// Old child A: Roblox name "Alpha", on disk at "/root/Beta"
|
|
||||||
// (name property maps "Alpha" → "Beta" on the filesystem)
|
|
||||||
let old_child_a = InstanceSnapshot::new()
|
|
||||||
.name("Alpha")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root/Beta"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root/Beta")]),
|
|
||||||
);
|
|
||||||
// Old child B: Roblox name "alpha", on disk at "/root/Alpha"
|
|
||||||
let old_child_b = InstanceSnapshot::new()
|
|
||||||
.name("alpha")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root/Alpha"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root/Alpha")]),
|
|
||||||
);
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.children(vec![old_child_a, old_child_b])
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
// New state: same two children in Roblox.
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Alpha"));
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("alpha"));
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should not error when two children have the same lowercased Roblox \
|
|
||||||
name but map to distinct filesystem paths: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two completely new children with the same name get resolved via
|
|
||||||
/// incrementing suffixes instead of erroring.
|
|
||||||
#[test]
|
|
||||||
fn syncback_resolves_sibling_duplicate_names() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Foo"));
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Foo"));
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should resolve duplicate names with suffixes, not error: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
let children = result.unwrap().children;
|
|
||||||
let mut names: Vec<String> = children
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.path.file_name().unwrap().to_string_lossy().into_owned())
|
|
||||||
.collect();
|
|
||||||
names.sort();
|
|
||||||
assert_eq!(names, vec!["Foo", "Foo1"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A new child named "Init" (as a ModuleScript) would naively become
|
|
||||||
/// "Init.luau", which case-insensitively matches the parent's reserved
|
|
||||||
/// "init.luau". Syncback must resolve this automatically by prefixing the
|
|
||||||
/// filesystem name with '_' (→ "_Init.luau") rather than erroring.
|
|
||||||
#[test]
|
|
||||||
fn syncback_resolves_init_name_conflict() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(
|
|
||||||
new_parent,
|
|
||||||
InstanceBuilder::new("ModuleScript").with_name("Init"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should resolve init-name conflict by prefixing '_', not error: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
// The child should have been placed at "_Init.luau", not "Init.luau".
|
|
||||||
let child_file_name = result
|
|
||||||
.unwrap()
|
|
||||||
.children
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.and_then(|c| c.path.file_name().map(|n| n.to_string_lossy().into_owned()))
|
|
||||||
.unwrap_or_default();
|
|
||||||
assert!(
|
|
||||||
child_file_name.starts_with('_'),
|
|
||||||
"child filesystem name should start with '_' to avoid init collision, \
|
|
||||||
got: {child_file_name}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A child whose filesystem name is stored with a slugified prefix (e.g.
|
|
||||||
/// "_Init") must NOT be blocked — only the bare "init" stem is reserved.
|
|
||||||
#[test]
|
|
||||||
fn syncback_allows_slugified_init_name() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
// Existing child: on disk as "_Init" (slugified from a name with an
|
|
||||||
// illegal character), its stem is "_init" which is not reserved.
|
|
||||||
let old_child = InstanceSnapshot::new()
|
|
||||||
.name("Init")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root/_Init"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root/_Init")]),
|
|
||||||
);
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.children(vec![old_child])
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Init"));
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should allow a child whose filesystem name is slugified away from \
|
|
||||||
the reserved 'init' stem: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two new children both named "Init" (ModuleScripts) should get
|
|
||||||
/// "_Init.luau" and "_Init1.luau" respectively.
|
|
||||||
#[test]
|
|
||||||
fn syncback_resolves_multiple_init_conflicts() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(
|
|
||||||
new_parent,
|
|
||||||
InstanceBuilder::new("ModuleScript").with_name("Init"),
|
|
||||||
);
|
|
||||||
new_tree.insert(
|
|
||||||
new_parent,
|
|
||||||
InstanceBuilder::new("ModuleScript").with_name("Init"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should resolve multiple init conflicts with suffixes: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
let children = result.unwrap().children;
|
|
||||||
let mut names: Vec<String> = children
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.path.file_name().unwrap().to_string_lossy().into_owned())
|
|
||||||
.collect();
|
|
||||||
names.sort();
|
|
||||||
assert_eq!(names, vec!["_Init.luau", "_Init1.luau"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,20 @@ pub fn snapshot_json_model(
|
|||||||
format!("File is not a valid JSON model: {}", path.display())
|
format!("File is not a valid JSON model: {}", path.display())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// If the JSON has a name property, preserve it in metadata for syncback
|
if let Some(top_level_name) = &instance.name {
|
||||||
let specified_name = instance.name.clone();
|
let new_name = format!("{}.model.json", top_level_name);
|
||||||
|
|
||||||
// Use the name from JSON if present, otherwise fall back to filename-derived name
|
log::warn!(
|
||||||
if instance.name.is_none() {
|
"Model at path {} had a top-level Name field. \
|
||||||
instance.name = Some(name.to_owned());
|
This field has been ignored since Rojo 6.0.\n\
|
||||||
|
Consider removing this field and renaming the file to {}.",
|
||||||
|
new_name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance.name = Some(name.to_owned());
|
||||||
|
|
||||||
let id = instance.id.take().map(RojoRef::new);
|
let id = instance.id.take().map(RojoRef::new);
|
||||||
let schema = instance.schema.take();
|
let schema = instance.schema.take();
|
||||||
|
|
||||||
@@ -56,8 +62,7 @@ pub fn snapshot_json_model(
|
|||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||||
.context(context)
|
.context(context)
|
||||||
.specified_id(id)
|
.specified_id(id)
|
||||||
.schema(schema)
|
.schema(schema);
|
||||||
.specified_name(specified_name);
|
|
||||||
|
|
||||||
Ok(Some(snapshot))
|
Ok(Some(snapshot))
|
||||||
}
|
}
|
||||||
@@ -76,7 +81,6 @@ pub fn syncback_json_model<'sync>(
|
|||||||
// schemas will ever exist in one project for it to matter, but it
|
// schemas will ever exist in one project for it to matter, but it
|
||||||
// could have a performance cost.
|
// could have a performance cost.
|
||||||
model.schema = old_inst.metadata().schema.clone();
|
model.schema = old_inst.metadata().schema.clone();
|
||||||
model.name = old_inst.metadata().specified_name.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SyncbackReturn {
|
Ok(SyncbackReturn {
|
||||||
|
|||||||
@@ -158,23 +158,8 @@ pub fn syncback_lua<'sync>(
|
|||||||
|
|
||||||
if !meta.is_empty() {
|
if !meta.is_empty() {
|
||||||
let parent_location = snapshot.path.parent_err()?;
|
let parent_location = snapshot.path.parent_err()?;
|
||||||
let file_name = snapshot
|
|
||||||
.path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let meta_stem = file_name
|
|
||||||
.strip_suffix(".server.luau")
|
|
||||||
.or_else(|| file_name.strip_suffix(".server.lua"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".client.luau"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".client.lua"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".plugin.luau"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".plugin.lua"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".luau"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".lua"))
|
|
||||||
.unwrap_or(file_name);
|
|
||||||
fs_snapshot.add_file(
|
fs_snapshot.add_file(
|
||||||
parent_location.join(format!("{meta_stem}.meta.json")),
|
parent_location.join(format!("{}.meta.json", new_inst.name)),
|
||||||
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ use rbx_dom_weak::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json,
|
json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, syncback::SyncbackSnapshot,
|
||||||
resolution::UnresolvedValue,
|
|
||||||
snapshot::InstanceSnapshot,
|
|
||||||
syncback::{validate_file_name, SyncbackSnapshot},
|
|
||||||
RojoRef,
|
RojoRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,9 +36,6 @@ pub struct AdjacentMetadata {
|
|||||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||||
pub attributes: IndexMap<String, UnresolvedValue>,
|
pub attributes: IndexMap<String, UnresolvedValue>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -150,26 +144,6 @@ impl AdjacentMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = snapshot
|
|
||||||
.old_inst()
|
|
||||||
.and_then(|inst| inst.metadata().specified_name.clone())
|
|
||||||
.or_else(|| {
|
|
||||||
// Write name when name_for_inst would produce a different
|
|
||||||
// filesystem stem (slugification or init-prefix).
|
|
||||||
if snapshot.old_inst().is_none() {
|
|
||||||
let instance_name = &snapshot.new_inst().name;
|
|
||||||
if validate_file_name(instance_name).is_err()
|
|
||||||
|| instance_name.to_lowercase() == "init"
|
|
||||||
{
|
|
||||||
Some(instance_name.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
ignore_unknown_instances: if ignore_unknown_instances {
|
ignore_unknown_instances: if ignore_unknown_instances {
|
||||||
Some(true)
|
Some(true)
|
||||||
@@ -181,7 +155,6 @@ impl AdjacentMetadata {
|
|||||||
path,
|
path,
|
||||||
id: None,
|
id: None,
|
||||||
schema,
|
schema,
|
||||||
name,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,26 +213,11 @@ impl AdjacentMetadata {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_name(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
|
||||||
if self.name.is_some() && snapshot.metadata.specified_name.is_some() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"cannot specify a name using {} (instance has a name from somewhere else)",
|
|
||||||
self.path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(name) = &self.name {
|
|
||||||
snapshot.name = name.clone().into();
|
|
||||||
}
|
|
||||||
snapshot.metadata.specified_name = self.name.take();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
||||||
self.apply_ignore_unknown_instances(snapshot);
|
self.apply_ignore_unknown_instances(snapshot);
|
||||||
self.apply_properties(snapshot)?;
|
self.apply_properties(snapshot)?;
|
||||||
self.apply_id(snapshot)?;
|
self.apply_id(snapshot)?;
|
||||||
self.apply_schema(snapshot)?;
|
self.apply_schema(snapshot)?;
|
||||||
self.apply_name(snapshot)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,13 +226,11 @@ impl AdjacentMetadata {
|
|||||||
///
|
///
|
||||||
/// - The number of properties and attributes is 0
|
/// - The number of properties and attributes is 0
|
||||||
/// - `ignore_unknown_instances` is None
|
/// - `ignore_unknown_instances` is None
|
||||||
/// - `name` is None
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.attributes.is_empty()
|
self.attributes.is_empty()
|
||||||
&& self.properties.is_empty()
|
&& self.properties.is_empty()
|
||||||
&& self.ignore_unknown_instances.is_none()
|
&& self.ignore_unknown_instances.is_none()
|
||||||
&& self.name.is_none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add method to allow selectively applying parts of metadata and
|
// TODO: Add method to allow selectively applying parts of metadata and
|
||||||
@@ -306,9 +262,6 @@ pub struct DirectoryMetadata {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub class_name: Option<Ustr>,
|
pub class_name: Option<Ustr>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -419,26 +372,6 @@ impl DirectoryMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = snapshot
|
|
||||||
.old_inst()
|
|
||||||
.and_then(|inst| inst.metadata().specified_name.clone())
|
|
||||||
.or_else(|| {
|
|
||||||
// Write name when name_for_inst would produce a different
|
|
||||||
// directory name (slugification or init-prefix).
|
|
||||||
if snapshot.old_inst().is_none() {
|
|
||||||
let instance_name = &snapshot.new_inst().name;
|
|
||||||
if validate_file_name(instance_name).is_err()
|
|
||||||
|| instance_name.to_lowercase() == "init"
|
|
||||||
{
|
|
||||||
Some(instance_name.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
ignore_unknown_instances: if ignore_unknown_instances {
|
ignore_unknown_instances: if ignore_unknown_instances {
|
||||||
Some(true)
|
Some(true)
|
||||||
@@ -451,7 +384,6 @@ impl DirectoryMetadata {
|
|||||||
path,
|
path,
|
||||||
id: None,
|
id: None,
|
||||||
schema,
|
schema,
|
||||||
name,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +393,6 @@ impl DirectoryMetadata {
|
|||||||
self.apply_properties(snapshot)?;
|
self.apply_properties(snapshot)?;
|
||||||
self.apply_id(snapshot)?;
|
self.apply_id(snapshot)?;
|
||||||
self.apply_schema(snapshot)?;
|
self.apply_schema(snapshot)?;
|
||||||
self.apply_name(snapshot)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -533,33 +464,17 @@ impl DirectoryMetadata {
|
|||||||
snapshot.metadata.schema = self.schema.take();
|
snapshot.metadata.schema = self.schema.take();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_name(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
|
||||||
if self.name.is_some() && snapshot.metadata.specified_name.is_some() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"cannot specify a name using {} (instance has a name from somewhere else)",
|
|
||||||
self.path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(name) = &self.name {
|
|
||||||
snapshot.name = name.clone().into();
|
|
||||||
}
|
|
||||||
snapshot.metadata.specified_name = self.name.take();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Returns whether the metadata is 'empty', meaning it doesn't have anything
|
/// Returns whether the metadata is 'empty', meaning it doesn't have anything
|
||||||
/// worth persisting in it. Specifically:
|
/// worth persisting in it. Specifically:
|
||||||
///
|
///
|
||||||
/// - The number of properties and attributes is 0
|
/// - The number of properties and attributes is 0
|
||||||
/// - `ignore_unknown_instances` is None
|
/// - `ignore_unknown_instances` is None
|
||||||
/// - `class_name` is either None or not Some("Folder")
|
/// - `class_name` is either None or not Some("Folder")
|
||||||
/// - `name` is None
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.attributes.is_empty()
|
self.attributes.is_empty()
|
||||||
&& self.properties.is_empty()
|
&& self.properties.is_empty()
|
||||||
&& self.ignore_unknown_instances.is_none()
|
&& self.ignore_unknown_instances.is_none()
|
||||||
&& self.name.is_none()
|
|
||||||
&& if let Some(class) = &self.class_name {
|
&& if let Some(class) = &self.class_name {
|
||||||
class == "Folder"
|
class == "Folder"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -58,14 +58,8 @@ pub fn syncback_txt<'sync>(
|
|||||||
|
|
||||||
if !meta.is_empty() {
|
if !meta.is_empty() {
|
||||||
let parent = snapshot.path.parent_err()?;
|
let parent = snapshot.path.parent_err()?;
|
||||||
let file_name = snapshot
|
|
||||||
.path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let meta_stem = file_name.strip_suffix(".txt").unwrap_or(file_name);
|
|
||||||
fs_snapshot.add_file(
|
fs_snapshot.add_file(
|
||||||
parent.join(format!("{meta_stem}.meta.json")),
|
parent.join(format!("{}.meta.json", new_inst.name)),
|
||||||
serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?,
|
serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use rbx_dom_weak::Instance;
|
|||||||
|
|
||||||
use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware};
|
use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware};
|
||||||
|
|
||||||
pub fn name_for_inst<'a>(
|
pub fn name_for_inst<'old>(
|
||||||
middleware: Middleware,
|
middleware: Middleware,
|
||||||
new_inst: &'a Instance,
|
new_inst: &Instance,
|
||||||
old_inst: Option<InstanceWithMeta<'a>>,
|
old_inst: Option<InstanceWithMeta<'old>>,
|
||||||
) -> anyhow::Result<Cow<'a, str>> {
|
) -> anyhow::Result<Cow<'old, str>> {
|
||||||
if let Some(old_inst) = old_inst {
|
if let Some(old_inst) = old_inst {
|
||||||
if let Some(source) = old_inst.metadata().relevant_paths.first() {
|
if let Some(source) = old_inst.metadata().relevant_paths.first() {
|
||||||
source
|
source
|
||||||
@@ -35,34 +35,14 @@ pub fn name_for_inst<'a>(
|
|||||||
| Middleware::CsvDir
|
| Middleware::CsvDir
|
||||||
| Middleware::ServerScriptDir
|
| Middleware::ServerScriptDir
|
||||||
| Middleware::ClientScriptDir
|
| Middleware::ClientScriptDir
|
||||||
| Middleware::ModuleScriptDir => {
|
| Middleware::ModuleScriptDir => Cow::Owned(new_inst.name.clone()),
|
||||||
let name = if validate_file_name(&new_inst.name).is_err() {
|
|
||||||
Cow::Owned(slugify_name(&new_inst.name))
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(new_inst.name.as_str())
|
|
||||||
};
|
|
||||||
// Prefix "init" to avoid colliding with reserved init files.
|
|
||||||
if name.to_lowercase() == "init" {
|
|
||||||
Cow::Owned(format!("_{name}"))
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
let extension = extension_for_middleware(middleware);
|
let extension = extension_for_middleware(middleware);
|
||||||
let slugified;
|
let name = &new_inst.name;
|
||||||
let stem: &str = if validate_file_name(&new_inst.name).is_err() {
|
validate_file_name(name).with_context(|| {
|
||||||
slugified = slugify_name(&new_inst.name);
|
format!("name '{name}' is not legal to write to the file system")
|
||||||
&slugified
|
})?;
|
||||||
} else {
|
Cow::Owned(format!("{name}.{extension}"))
|
||||||
&new_inst.name
|
|
||||||
};
|
|
||||||
// Prefix "init" stems to avoid colliding with reserved init files.
|
|
||||||
if stem.to_lowercase() == "init" {
|
|
||||||
Cow::Owned(format!("_{stem}.{extension}"))
|
|
||||||
} else {
|
|
||||||
Cow::Owned(format!("{stem}.{extension}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -114,39 +94,6 @@ const INVALID_WINDOWS_NAMES: [&str; 22] = [
|
|||||||
/// in a file's name.
|
/// in a file's name.
|
||||||
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
||||||
|
|
||||||
/// Slugifies a name by replacing forbidden characters with underscores
|
|
||||||
/// and ensuring the result is a valid file name
|
|
||||||
pub fn slugify_name(name: &str) -> String {
|
|
||||||
let mut result = String::with_capacity(name.len());
|
|
||||||
|
|
||||||
for ch in name.chars() {
|
|
||||||
if FORBIDDEN_CHARS.contains(&ch) {
|
|
||||||
result.push('_');
|
|
||||||
} else {
|
|
||||||
result.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Windows reserved names by appending an underscore
|
|
||||||
let result_lower = result.to_lowercase();
|
|
||||||
for forbidden in INVALID_WINDOWS_NAMES {
|
|
||||||
if result_lower == forbidden.to_lowercase() {
|
|
||||||
result.push('_');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while result.ends_with(' ') || result.ends_with('.') {
|
|
||||||
result.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.is_empty() || result.chars().all(|c| c == '_') {
|
|
||||||
result = "instance".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates a provided file name to ensure it's allowed on the file system. An
|
/// 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.
|
/// error is returned if the name isn't allowed, indicating why.
|
||||||
/// This takes into account rules for Windows, MacOS, and Linux.
|
/// This takes into account rules for Windows, MacOS, and Linux.
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware {
|
|||||||
static JSON_MODEL_CLASSES: OnceLock<HashSet<&str>> = OnceLock::new();
|
static JSON_MODEL_CLASSES: OnceLock<HashSet<&str>> = OnceLock::new();
|
||||||
let json_model_classes = JSON_MODEL_CLASSES.get_or_init(|| {
|
let json_model_classes = JSON_MODEL_CLASSES.get_or_init(|| {
|
||||||
[
|
[
|
||||||
|
"Actor",
|
||||||
"Sound",
|
"Sound",
|
||||||
"SoundGroup",
|
"SoundGroup",
|
||||||
"Sky",
|
"Sky",
|
||||||
@@ -318,6 +319,11 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware {
|
|||||||
"ChatInputBarConfiguration",
|
"ChatInputBarConfiguration",
|
||||||
"BubbleChatConfiguration",
|
"BubbleChatConfiguration",
|
||||||
"ChannelTabsConfiguration",
|
"ChannelTabsConfiguration",
|
||||||
|
"RemoteEvent",
|
||||||
|
"UnreliableRemoteEvent",
|
||||||
|
"RemoteFunction",
|
||||||
|
"BindableEvent",
|
||||||
|
"BindableFunction",
|
||||||
]
|
]
|
||||||
.into()
|
.into()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,25 +31,6 @@ pub struct SyncbackSnapshot<'sync> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'sync> SyncbackSnapshot<'sync> {
|
impl<'sync> SyncbackSnapshot<'sync> {
|
||||||
/// Computes the middleware and filesystem name for a child without
|
|
||||||
/// creating a full snapshot. Uses the same logic as `with_joined_path`.
|
|
||||||
pub fn child_middleware_and_name(
|
|
||||||
&self,
|
|
||||||
new_ref: Ref,
|
|
||||||
old_ref: Option<Ref>,
|
|
||||||
) -> anyhow::Result<(Middleware, String)> {
|
|
||||||
let temp = Self {
|
|
||||||
data: self.data,
|
|
||||||
old: old_ref,
|
|
||||||
new: new_ref,
|
|
||||||
path: PathBuf::new(),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
let middleware = get_best_middleware(&temp, self.data.force_json);
|
|
||||||
let name = name_for_inst(middleware, temp.new_inst(), temp.old_inst())?;
|
|
||||||
Ok((middleware, name.into_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a SyncbackSnapshot from the provided refs
|
/// Constructs a SyncbackSnapshot from the provided refs
|
||||||
/// while inheriting this snapshot's path and data. This should be used for
|
/// while inheriting this snapshot's path and data. This should be used for
|
||||||
/// directories.
|
/// directories.
|
||||||
@@ -256,25 +237,6 @@ pub fn inst_path(dom: &WeakDom, referent: Ref) -> String {
|
|||||||
path.join("/")
|
path.join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'sync> SyncbackData<'sync> {
|
|
||||||
/// Constructs a `SyncbackData` for use in unit tests.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn for_test(
|
|
||||||
vfs: &'sync Vfs,
|
|
||||||
old_tree: &'sync RojoTree,
|
|
||||||
new_tree: &'sync WeakDom,
|
|
||||||
project: &'sync Project,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
vfs,
|
|
||||||
old_tree,
|
|
||||||
new_tree,
|
|
||||||
project,
|
|
||||||
force_json: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use rbx_dom_weak::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json,
|
|
||||||
serve_session::ServeSession,
|
serve_session::ServeSession,
|
||||||
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
||||||
web::{
|
web::{
|
||||||
@@ -22,11 +21,10 @@ use crate::{
|
|||||||
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
||||||
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
||||||
},
|
},
|
||||||
util::{json, json_ok},
|
util::{deserialize_msgpack, msgpack, msgpack_ok, serialize_msgpack},
|
||||||
},
|
},
|
||||||
web_api::{
|
web_api::{
|
||||||
BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest,
|
InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest, SerializeResponse,
|
||||||
SerializeResponse,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
|
|||||||
if is_upgrade_request(&request) {
|
if is_upgrade_request(&request) {
|
||||||
service.handle_api_socket(&mut request).await
|
service.handle_api_socket(&mut request).await
|
||||||
} else {
|
} else {
|
||||||
json(
|
msgpack(
|
||||||
ErrorResponse::bad_request(
|
ErrorResponse::bad_request(
|
||||||
"/api/socket must be called as a websocket upgrade request",
|
"/api/socket must be called as a websocket upgrade request",
|
||||||
),
|
),
|
||||||
@@ -58,7 +56,7 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
|
|||||||
}
|
}
|
||||||
(&Method::POST, "/api/write") => service.handle_api_write(request).await,
|
(&Method::POST, "/api/write") => service.handle_api_write(request).await,
|
||||||
|
|
||||||
(_method, path) => json(
|
(_method, path) => msgpack(
|
||||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
),
|
),
|
||||||
@@ -79,7 +77,7 @@ impl ApiService {
|
|||||||
let tree = self.serve_session.tree();
|
let tree = self.serve_session.tree();
|
||||||
let root_instance_id = tree.get_root_id();
|
let root_instance_id = tree.get_root_id();
|
||||||
|
|
||||||
json_ok(&ServerInfoResponse {
|
msgpack_ok(&ServerInfoResponse {
|
||||||
server_version: SERVER_VERSION.to_owned(),
|
server_version: SERVER_VERSION.to_owned(),
|
||||||
protocol_version: PROTOCOL_VERSION,
|
protocol_version: PROTOCOL_VERSION,
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
@@ -98,7 +96,7 @@ impl ApiService {
|
|||||||
let input_cursor: u32 = match argument.parse() {
|
let input_cursor: u32 = match argument.parse() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
|
ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -109,7 +107,7 @@ impl ApiService {
|
|||||||
let (response, websocket) = match upgrade(request, None) {
|
let (response, websocket) = match upgrade(request, None) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
|
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
@@ -136,10 +134,10 @@ impl ApiService {
|
|||||||
|
|
||||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||||
|
|
||||||
let request: WriteRequest = match json::from_slice(&body) {
|
let request: WriteRequest = match deserialize_msgpack(&body) {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -147,7 +145,7 @@ impl ApiService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if request.session_id != session_id {
|
if request.session_id != session_id {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Wrong session ID"),
|
ErrorResponse::bad_request("Wrong session ID"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -173,7 +171,7 @@ impl ApiService {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
json_ok(WriteResponse { session_id })
|
msgpack_ok(WriteResponse { session_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
||||||
@@ -183,7 +181,7 @@ impl ApiService {
|
|||||||
let requested_ids = match requested_ids {
|
let requested_ids = match requested_ids {
|
||||||
Ok(ids) => ids,
|
Ok(ids) => ids,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Malformed ID list"),
|
ErrorResponse::bad_request("Malformed ID list"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -207,7 +205,7 @@ impl ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json_ok(ReadResponse {
|
msgpack_ok(ReadResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
message_cursor,
|
message_cursor,
|
||||||
instances,
|
instances,
|
||||||
@@ -225,10 +223,10 @@ impl ApiService {
|
|||||||
let session_id = self.serve_session.session_id();
|
let session_id = self.serve_session.session_id();
|
||||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||||
|
|
||||||
let request: SerializeRequest = match json::from_slice(&body) {
|
let request: SerializeRequest = match deserialize_msgpack(&body) {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -236,7 +234,7 @@ impl ApiService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if request.session_id != session_id {
|
if request.session_id != session_id {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Wrong session ID"),
|
ErrorResponse::bad_request("Wrong session ID"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -269,7 +267,7 @@ impl ApiService {
|
|||||||
|
|
||||||
response_dom.transfer_within(child_ref, object_value);
|
response_dom.transfer_within(child_ref, object_value);
|
||||||
} else {
|
} else {
|
||||||
json(
|
msgpack(
|
||||||
ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
|
ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -280,9 +278,9 @@ impl ApiService {
|
|||||||
let mut source = Vec::new();
|
let mut source = Vec::new();
|
||||||
rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap();
|
rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap();
|
||||||
|
|
||||||
json_ok(SerializeResponse {
|
msgpack_ok(SerializeResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
model_contents: BufferEncode::new(source),
|
model_contents: source,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,10 +292,10 @@ impl ApiService {
|
|||||||
let session_id = self.serve_session.session_id();
|
let session_id = self.serve_session.session_id();
|
||||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||||
|
|
||||||
let request: RefPatchRequest = match json::from_slice(&body) {
|
let request: RefPatchRequest = match deserialize_msgpack(&body) {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -305,7 +303,7 @@ impl ApiService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if request.session_id != session_id {
|
if request.session_id != session_id {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Wrong session ID"),
|
ErrorResponse::bad_request("Wrong session ID"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -338,7 +336,7 @@ impl ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json_ok(RefPatchResponse {
|
msgpack_ok(RefPatchResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
patch: SubscribeMessage {
|
patch: SubscribeMessage {
|
||||||
added: HashMap::new(),
|
added: HashMap::new(),
|
||||||
@@ -354,7 +352,7 @@ impl ApiService {
|
|||||||
let requested_id = match Ref::from_str(argument) {
|
let requested_id = match Ref::from_str(argument) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Invalid instance ID"),
|
ErrorResponse::bad_request("Invalid instance ID"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
@@ -366,7 +364,7 @@ impl ApiService {
|
|||||||
let instance = match tree.get_instance(requested_id) {
|
let instance = match tree.get_instance(requested_id) {
|
||||||
Some(instance) => instance,
|
Some(instance) => instance,
|
||||||
None => {
|
None => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Instance not found"),
|
ErrorResponse::bad_request("Instance not found"),
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
);
|
);
|
||||||
@@ -376,7 +374,7 @@ impl ApiService {
|
|||||||
let script_path = match pick_script_path(instance) {
|
let script_path = match pick_script_path(instance) {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => {
|
None => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(
|
ErrorResponse::bad_request(
|
||||||
"No appropriate file could be found to open this script",
|
"No appropriate file could be found to open this script",
|
||||||
),
|
),
|
||||||
@@ -389,7 +387,7 @@ impl ApiService {
|
|||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(error) => match error {
|
Err(error) => match error {
|
||||||
OpenError::Io(io_error) => {
|
OpenError::Io(io_error) => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::internal_error(format!(
|
ErrorResponse::internal_error(format!(
|
||||||
"Attempting to open {} failed because of the following io error: {}",
|
"Attempting to open {} failed because of the following io error: {}",
|
||||||
script_path.display(),
|
script_path.display(),
|
||||||
@@ -403,7 +401,7 @@ impl ApiService {
|
|||||||
status,
|
status,
|
||||||
stderr,
|
stderr,
|
||||||
} => {
|
} => {
|
||||||
return json(
|
return msgpack(
|
||||||
ErrorResponse::internal_error(format!(
|
ErrorResponse::internal_error(format!(
|
||||||
r#"The command '{}' to open '{}' failed with the error code '{}'.
|
r#"The command '{}' to open '{}' failed with the error code '{}'.
|
||||||
Error logs:
|
Error logs:
|
||||||
@@ -419,7 +417,7 @@ impl ApiService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
json_ok(OpenResponse {
|
msgpack_ok(OpenResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -483,7 +481,7 @@ async fn handle_websocket_subscription(
|
|||||||
match result {
|
match result {
|
||||||
Ok((new_cursor, messages)) => {
|
Ok((new_cursor, messages)) => {
|
||||||
if !messages.is_empty() {
|
if !messages.is_empty() {
|
||||||
let json_message = {
|
let msgpack_message = {
|
||||||
let tree = tree_handle.lock().unwrap();
|
let tree = tree_handle.lock().unwrap();
|
||||||
let api_messages = messages
|
let api_messages = messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -499,12 +497,12 @@ async fn handle_websocket_subscription(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
serde_json::to_string(&response)?
|
serialize_msgpack(response)?
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("Sending batch of messages over WebSocket subscription");
|
log::debug!("Sending batch of messages over WebSocket subscription");
|
||||||
|
|
||||||
if websocket.send(Message::Text(json_message)).await.is_err() {
|
if websocket.send(Message::Binary(msgpack_message)).await.is_err() {
|
||||||
// Client disconnected
|
// Client disconnected
|
||||||
log::debug!("WebSocket subscription closed by client");
|
log::debug!("WebSocket subscription closed by client");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -249,31 +249,8 @@ pub struct SerializeRequest {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SerializeResponse {
|
pub struct SerializeResponse {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
pub model_contents: BufferEncode,
|
#[serde(with = "serde_bytes")]
|
||||||
}
|
pub model_contents: Vec<u8>,
|
||||||
|
|
||||||
/// Using this struct we can force Roblox to JSONDecode this as a buffer.
|
|
||||||
/// This is what Roblox's serde APIs use, so it saves a step in the plugin.
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct BufferEncode {
|
|
||||||
m: (),
|
|
||||||
t: Cow<'static, str>,
|
|
||||||
base64: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BufferEncode {
|
|
||||||
pub fn new(content: Vec<u8>) -> Self {
|
|
||||||
let base64 = data_encoding::BASE64.encode(&content);
|
|
||||||
Self {
|
|
||||||
m: (),
|
|
||||||
t: Cow::Borrowed("buffer"),
|
|
||||||
base64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn model(&self) -> &str {
|
|
||||||
&self.base64
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -1,8 +1,48 @@
|
|||||||
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
|
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub fn json_ok<T: Serialize>(value: T) -> Response<Body> {
|
pub fn msgpack_ok<T: Serialize>(value: T) -> Response<Body> {
|
||||||
json(value, StatusCode::OK)
|
msgpack(value, StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msgpack<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||||
|
let mut serialized = Vec::new();
|
||||||
|
let mut serializer = rmp_serde::Serializer::new(&mut serialized)
|
||||||
|
.with_human_readable()
|
||||||
|
.with_struct_map();
|
||||||
|
|
||||||
|
if let Err(err) = value.serialize(&mut serializer) {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.header(CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(err.to_string()))
|
||||||
|
.unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(code)
|
||||||
|
.header(CONTENT_TYPE, "application/msgpack")
|
||||||
|
.body(Body::from(serialized))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize_msgpack<T: Serialize>(value: T) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let mut serialized = Vec::new();
|
||||||
|
let mut serializer = rmp_serde::Serializer::new(&mut serialized)
|
||||||
|
.with_human_readable()
|
||||||
|
.with_struct_map();
|
||||||
|
|
||||||
|
value.serialize(&mut serializer)?;
|
||||||
|
|
||||||
|
Ok(serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_msgpack<'a, T: Deserialize<'a>>(
|
||||||
|
input: &'a [u8],
|
||||||
|
) -> Result<T, rmp_serde::decode::Error> {
|
||||||
|
let mut deserializer = rmp_serde::Deserializer::new(input).with_human_readable();
|
||||||
|
|
||||||
|
T::deserialize(&mut deserializer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
pub fn json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||||
|
|||||||
14
test-projects/relative_paths/default.project.json
Normal file
14
test-projects/relative_paths/default.project.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"Project": {
|
||||||
|
"$path": "project/src",
|
||||||
|
"Module": {
|
||||||
|
"$path": "module"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test-projects/relative_paths/module/module.luau
Normal file
1
test-projects/relative_paths/module/module.luau
Normal file
@@ -0,0 +1 @@
|
|||||||
|
return nil
|
||||||
14
test-projects/relative_paths/project/default.project.json
Normal file
14
test-projects/relative_paths/project/default.project.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"Project": {
|
||||||
|
"$path": "src/",
|
||||||
|
"Module": {
|
||||||
|
"$path": "../module"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test-projects/relative_paths/project/src/init.luau
Normal file
1
test-projects/relative_paths/project/src/init.luau
Normal file
@@ -0,0 +1 @@
|
|||||||
|
return nil
|
||||||
@@ -10,6 +10,7 @@ use std::{
|
|||||||
use hyper_tungstenite::tungstenite::{connect, Message};
|
use hyper_tungstenite::tungstenite::{connect, Message};
|
||||||
use rbx_dom_weak::types::Ref;
|
use rbx_dom_weak::types::Ref;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tempfile::{tempdir, TempDir};
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
use librojo::{
|
use librojo::{
|
||||||
@@ -161,22 +162,16 @@ impl TestServeSession {
|
|||||||
|
|
||||||
pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> {
|
pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> {
|
||||||
let url = format!("http://localhost:{}/api/rojo", self.port);
|
let url = format!("http://localhost:{}/api/rojo", self.port);
|
||||||
let body = reqwest::blocking::get(url)?.text()?;
|
let body = reqwest::blocking::get(url)?.bytes()?;
|
||||||
|
|
||||||
let value = jsonc_parser::parse_to_serde_value(&body, &Default::default())
|
Ok(deserialize_msgpack(&body).expect("Server returned malformed response"))
|
||||||
.expect("Failed to parse JSON")
|
|
||||||
.expect("No JSON value");
|
|
||||||
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse<'_>, reqwest::Error> {
|
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse<'_>, reqwest::Error> {
|
||||||
let url = format!("http://localhost:{}/api/read/{}", self.port, id);
|
let url = format!("http://localhost:{}/api/read/{}", self.port, id);
|
||||||
let body = reqwest::blocking::get(url)?.text()?;
|
let body = reqwest::blocking::get(url)?.bytes()?;
|
||||||
|
|
||||||
let value = jsonc_parser::parse_to_serde_value(&body, &Default::default())
|
Ok(deserialize_msgpack(&body).expect("Server returned malformed response"))
|
||||||
.expect("Failed to parse JSON")
|
|
||||||
.expect("No JSON value");
|
|
||||||
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_api_socket_packet(
|
pub fn get_api_socket_packet(
|
||||||
@@ -198,8 +193,8 @@ impl TestServeSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match socket.read() {
|
match socket.read() {
|
||||||
Ok(Message::Text(text)) => {
|
Ok(Message::Binary(binary)) => {
|
||||||
let packet: SocketPacket = serde_json::from_str(&text)?;
|
let packet: SocketPacket = deserialize_msgpack(&binary)?;
|
||||||
if packet.packet_type != packet_type {
|
if packet.packet_type != packet_type {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -212,7 +207,7 @@ impl TestServeSession {
|
|||||||
return Err("WebSocket closed before receiving messages".into());
|
return Err("WebSocket closed before receiving messages".into());
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Ignore other message types (ping, pong, binary)
|
// Ignore other message types (ping, pong, text)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(hyper_tungstenite::tungstenite::Error::Io(e))
|
Err(hyper_tungstenite::tungstenite::Error::Io(e))
|
||||||
@@ -236,15 +231,37 @@ impl TestServeSession {
|
|||||||
) -> Result<SerializeResponse, reqwest::Error> {
|
) -> Result<SerializeResponse, reqwest::Error> {
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let url = format!("http://localhost:{}/api/serialize", self.port);
|
let url = format!("http://localhost:{}/api/serialize", self.port);
|
||||||
let body = serde_json::to_string(&SerializeRequest {
|
let body = serialize_msgpack(&SerializeRequest {
|
||||||
session_id,
|
session_id,
|
||||||
ids: ids.to_vec(),
|
ids: ids.to_vec(),
|
||||||
});
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
client.post(url).body((body).unwrap()).send()?.json()
|
let body = client.post(url).body(body).send()?.bytes()?;
|
||||||
|
|
||||||
|
Ok(deserialize_msgpack(&body).expect("Server returned malformed response"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_msgpack<T: Serialize>(value: T) -> Result<Vec<u8>, rmp_serde::encode::Error> {
|
||||||
|
let mut serialized = Vec::new();
|
||||||
|
let mut serializer = rmp_serde::Serializer::new(&mut serialized)
|
||||||
|
.with_human_readable()
|
||||||
|
.with_struct_map();
|
||||||
|
|
||||||
|
value.serialize(&mut serializer)?;
|
||||||
|
|
||||||
|
Ok(serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_msgpack<'a, T: Deserialize<'a>>(
|
||||||
|
input: &'a [u8],
|
||||||
|
) -> Result<T, rmp_serde::decode::Error> {
|
||||||
|
let mut deserializer = rmp_serde::Deserializer::new(input).with_human_readable();
|
||||||
|
|
||||||
|
T::deserialize(&mut deserializer)
|
||||||
|
}
|
||||||
|
|
||||||
/// Probably-okay way to generate random enough port numbers for running the
|
/// Probably-okay way to generate random enough port numbers for running the
|
||||||
/// Rojo live server.
|
/// Rojo live server.
|
||||||
///
|
///
|
||||||
@@ -262,11 +279,7 @@ fn get_port_number() -> usize {
|
|||||||
/// Since the provided structure intentionally includes unredacted referents,
|
/// Since the provided structure intentionally includes unredacted referents,
|
||||||
/// some post-processing is done to ensure they don't show up in the model.
|
/// some post-processing is done to ensure they don't show up in the model.
|
||||||
pub fn serialize_to_xml_model(response: &SerializeResponse, redactions: &RedactionMap) -> String {
|
pub fn serialize_to_xml_model(response: &SerializeResponse, redactions: &RedactionMap) -> String {
|
||||||
let model_content = data_encoding::BASE64
|
let mut dom = rbx_binary::from_reader(response.model_contents.as_slice()).unwrap();
|
||||||
.decode(response.model_contents.model().as_bytes())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut dom = rbx_binary::from_reader(model_content.as_slice()).unwrap();
|
|
||||||
// This makes me realize that maybe we need a `descendants_mut` iter.
|
// This makes me realize that maybe we need a `descendants_mut` iter.
|
||||||
let ref_list: Vec<Ref> = dom.descendants().map(|inst| inst.referent()).collect();
|
let ref_list: Vec<Ref> = dom.descendants().map(|inst| inst.referent()).collect();
|
||||||
for referent in ref_list {
|
for referent in ref_list {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ gen_build_tests! {
|
|||||||
issue_546,
|
issue_546,
|
||||||
json_as_lua,
|
json_as_lua,
|
||||||
json_model_in_folder,
|
json_model_in_folder,
|
||||||
|
json_model_legacy_name,
|
||||||
module_in_folder,
|
module_in_folder,
|
||||||
module_init,
|
module_init,
|
||||||
nested_runcontext,
|
nested_runcontext,
|
||||||
@@ -54,8 +55,6 @@ gen_build_tests! {
|
|||||||
script_meta_disabled,
|
script_meta_disabled,
|
||||||
server_in_folder,
|
server_in_folder,
|
||||||
server_init,
|
server_init,
|
||||||
slugified_name_roundtrip,
|
|
||||||
model_json_name_input,
|
|
||||||
txt,
|
txt,
|
||||||
txt_in_folder,
|
txt_in_folder,
|
||||||
unresolved_values,
|
unresolved_values,
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ syncback_tests! {
|
|||||||
// Ensures that projects can be reserialized by syncback and that
|
// Ensures that projects can be reserialized by syncback and that
|
||||||
// default.project.json doesn't change unexpectedly.
|
// default.project.json doesn't change unexpectedly.
|
||||||
project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json"],
|
project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json"],
|
||||||
// Confirms that duplicate children are resolved with incrementing suffixes
|
// Confirms that Instances that cannot serialize as directories serialize as rbxms
|
||||||
rbxm_fallback => ["src/ChildWithDuplicates/DuplicateChild/.gitkeep", "src/ChildWithDuplicates/DuplicateChild1/.gitkeep"],
|
rbxm_fallback => ["src/ChildWithDuplicates.rbxm"],
|
||||||
// Ensures that ref properties are linked properly on the file system
|
// Ensures that ref properties are linked properly on the file system
|
||||||
ref_properties => ["src/pointer.model.json", "src/target.model.json"],
|
ref_properties => ["src/pointer.model.json", "src/target.model.json"],
|
||||||
// Ensures that ref properties are linked when no attributes are manually
|
// Ensures that ref properties are linked when no attributes are manually
|
||||||
@@ -86,9 +86,4 @@ syncback_tests! {
|
|||||||
sync_rules => ["src/module.modulescript", "src/text.text"],
|
sync_rules => ["src/module.modulescript", "src/text.text"],
|
||||||
// Ensures that the `syncUnscriptable` setting works
|
// Ensures that the `syncUnscriptable` setting works
|
||||||
unscriptable_properties => ["default.project.json"],
|
unscriptable_properties => ["default.project.json"],
|
||||||
// Ensures that instances with names containing illegal characters get slugified filenames
|
|
||||||
// and preserve their original names in meta.json without forcing directories for leaf scripts
|
|
||||||
slugified_name => ["src/_Script.meta.json", "src/_Script.server.luau", "src/_Folder/init.meta.json"],
|
|
||||||
// Ensures that .model.json files preserve the name property
|
|
||||||
model_json_name => ["src/foo.model.json"],
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user