Compare commits

..

4 Commits

Author SHA1 Message Date
Micah
47db569c52 Correct my own mistake and move plugin test to its own step 2025-09-23 21:19:26 -07:00
Micah
1d3f8c8e9d Run tests in CI (this is the part where I have to try at least twice) 2025-09-23 21:13:24 -07:00
Micah
a894313a4b Make script exit with code 1 when tests fail 2025-09-23 21:09:13 -07:00
Micah
7f73ae80dc Add script for running plugin tests locally
They current fail
2025-09-23 21:02:23 -07:00
55 changed files with 1601 additions and 9250 deletions

View File

@@ -60,7 +60,7 @@ jobs:
submodules: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.88.0
uses: dtolnay/rust-toolchain@1.79.0
- name: Restore Rust Cache
uses: actions/cache/restore@v4
@@ -83,6 +83,27 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
test-plugin:
name: Test Plugin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup Rokit
uses: CompeyDev/setup-rokit@v0.1.2
with:
version: 'v1.1.0'
- name: Test
run: lune run test-plugin
env:
RBX_API_KEY: ${{ secrets.PLUGIN_TEST_API_KEY }}
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_TEST_UNIVERSE_ID }}
RBX_PLACE_ID: ${{ vars.PLUGIN_TEST_PLACE_ID }}
lint:
name: Rustfmt, Clippy, Stylua, & Selene
runs-on: ubuntu-latest

3
.gitmodules vendored
View File

@@ -16,3 +16,6 @@
[submodule "plugin/Packages/Highlighter"]
path = plugin/Packages/Highlighter
url = https://github.com/boatbomber/highlighter.git
[submodule ".lune/opencloud-execute"]
path = .lune/opencloud-execute
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git

5
.lune/.luaurc Normal file
View File

@@ -0,0 +1,5 @@
{
"aliases": {
"lune": "~/.lune/.typedefs/0.10.2/"
}
}

112
.lune/test-plugin.luau Normal file
View File

@@ -0,0 +1,112 @@
local serde = require("@lune/serde")
local net = require("@lune/net")
local stdio = require("@lune/stdio")
local process = require("@lune/process")
local fs = require("@lune/fs")
local luau_execute = require("./opencloud-execute")
local TEST_SCRIPT = fs.readFile("plugin/run-tests.server.lua")
local PATH_VERSION_MATCH = "assets/%d+/versions/(.+)"
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
local PLACE_ID = process.env["RBX_PLACE_ID"]
local API_KEY = process.env["RBX_API_KEY"]
if not UNIVERSE_ID then
error("no universe ID specified. try providing one with the env var `RBX_UNIVERSE_ID`")
end
if not PLACE_ID then
error("no place ID specified. try providing one with the env var `RBX_PLACE_ID`")
end
if not API_KEY then
error("no API key specified. try providing one with the env var `RBX_API_KEY`")
end
--stylua: ignore
local upload_result = process.exec("cargo", {
"run", "--",
"upload", "plugin/test-place.project.json",
"--api_key", API_KEY,
"--universe_id", UNIVERSE_ID,
"--asset_id", PLACE_ID
}, {
stdio = "none"
})
if not upload_result.ok then
print("Failed to upload plugin test place")
print("Not dumping stdout or stderr to avoid leaking secrets")
process.exit(1)
end
-- This is /probably/ not necessary because Rojo generally does not have enough
-- activity that there will be multiple CI runs happening at once, but
-- it's better safe than sorry.
local version_response = net.request({
method = "GET",
url = `https://apis.roblox.com/assets/v1/assets/{PLACE_ID}/versions`,
query = {
maxPageSize = 1,
},
headers = {
["User-Agent"] = `Rojo/PluginTesting 1.0.0; {_VERSION}`,
["x-api-key"] = API_KEY,
},
})
if not version_response.ok then
error(
`Failed to fetch version of Roblox place to run tests on because: {version_response.statusCode} - {version_response.statusMessage}\n{version_response.body}`
)
end
local place_version_raw = serde.decode("json", version_response.body).assetVersions[1].path
assert(typeof(place_version_raw) == "string", "the result from asset version endpoint was not as expected")
local place_version = string.match(place_version_raw, PATH_VERSION_MATCH)
local task = luau_execute.create_task_versioned(UNIVERSE_ID, PLACE_ID, place_version, TEST_SCRIPT)
print(`Running test script on {UNIVERSE_ID}/{PLACE_ID}@{place_version}`)
print(`Task ID: {luau_execute.task_id(task)}`)
luau_execute.await_finish(task)
print("Output from task:\n")
local logs = luau_execute.get_structured_logs(task)
for _, log in logs do
if log.messageType == "OUTPUT" or log.messageType == "MESSAGE_TYPE_UNSPECIFIED" then
stdio.write(stdio.color("reset"))
elseif log.messageType == "INFO" then
stdio.write(stdio.color("cyan"))
elseif log.messageType == "WARNING" then
stdio.write(stdio.color("yellow"))
elseif log.messageType == "ERROR" then
stdio.write(stdio.color("red"))
end
stdio.write(log.message)
stdio.write(`{stdio.color("reset")}\n`)
end
local results = luau_execute.get_output(task)[1]
if not results then
error("plugin tests did not return any results")
end
local status = luau_execute.check_status(task)
if status == "COMPLETE" then
if results.failureCount == 0 then
process.exit(0)
else
process.exit(1)
end
else
print()
print("Task did not finish successfully")
local err = luau_execute.get_error(task)
if err then
print(`Error from task: {err.code}`)
print(err.message)
end
process.exit(1)
end

File diff suppressed because it is too large Load Diff

177
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "addr2line"
@@ -243,7 +243,7 @@ checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 1.0.109",
]
@@ -430,16 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
"cfg-if 0.1.10",
"dirs-sys 0.3.7",
]
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys 0.4.1",
"dirs-sys",
]
[[package]]
@@ -453,18 +444,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "either"
version = "1.10.0"
@@ -665,9 +644,9 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]
@@ -1033,15 +1012,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonc-parser"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ec4ac49f13c7b00f435f8a5bb55d725705e2cf620df35a5859321595102eb7e"
dependencies = [
"serde_json",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@@ -1123,12 +1093,23 @@ dependencies = [
]
[[package]]
name = "lz4_flex"
version = "0.11.5"
name = "lz4"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1"
dependencies = [
"twox-hash",
"libc",
"lz4-sys",
]
[[package]]
name = "lz4-sys"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900"
dependencies = [
"cc",
"libc",
]
[[package]]
@@ -1320,12 +1301,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_str_bytes"
version = "6.6.1"
@@ -1402,9 +1377,9 @@ checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80"
dependencies = [
"pest",
"pest_meta",
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]
@@ -1487,7 +1462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 1.0.109",
"version_check",
@@ -1499,7 +1474,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"version_check",
]
@@ -1527,9 +1502,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@@ -1551,7 +1526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]
@@ -1569,7 +1544,7 @@ version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
]
[[package]]
@@ -1624,13 +1599,13 @@ dependencies = [
[[package]]
name = "rbx_binary"
version = "2.0.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d419f67c8012bf83569086e1208c541478b3b8e4f523deaa0b80d723fb5ef22"
checksum = "9573fee5e073d7b303f475c285197fdc8179468de66ca60ee115a58fbac99296"
dependencies = [
"ahash",
"log",
"lz4_flex",
"lz4",
"profiling",
"rbx_dom_weak",
"rbx_reflection",
@@ -1641,9 +1616,9 @@ dependencies = [
[[package]]
name = "rbx_dom_weak"
version = "4.0.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74878a4a801afc8014b14ede4b38015a13de5d29ab0095d5ed284a744253f6"
checksum = "04425cf6e9376e5486f4fb35906c120d1b1b45618a490318cf563fab1fa230a9"
dependencies = [
"ahash",
"rbx_types",
@@ -1653,9 +1628,9 @@ dependencies = [
[[package]]
name = "rbx_reflection"
version = "6.0.0"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "565dd3430991f35443fa6d23cc239fade2110c5089deb6bae5de77c400df4fd2"
checksum = "1b6d0d62baa613556b058a5f94a53b01cf0ccde0ea327ce03056e335b982e77e"
dependencies = [
"rbx_types",
"serde",
@@ -1664,12 +1639,11 @@ dependencies = [
[[package]]
name = "rbx_reflection_database"
version = "2.0.1+roblox-697"
version = "1.0.3+roblox-670"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d69035a14b103c5a9b8bc6a61d30f4ee6f2608afdee137dae09b26037dba5dc8"
checksum = "e22c05ef92528c0fb0cc580592a65ca178d3ea9beb07a1d9ca0a2503c4f3721c"
dependencies = [
"dirs 5.0.1",
"log",
"lazy_static",
"rbx_reflection",
"rmp-serde",
"serde",
@@ -1677,9 +1651,9 @@ dependencies = [
[[package]]
name = "rbx_types"
version = "3.0.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03220ffce2bd06ad04f77a003cb807f2e5b2a18e97623066a5ac735a978398af"
checksum = "78e4fdde46493def107e5f923d82e813dec9b3eef52c2f75fbad3a716023eda2"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
@@ -1692,9 +1666,9 @@ dependencies = [
[[package]]
name = "rbx_xml"
version = "2.0.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be6c302cefe9c92ed09bcbb075cd24379271de135b0af331409a64c2ea3646ee"
checksum = "bb623833c31cc43bbdaeb32f5e91db8ecd63fc46e438d0d268baf9e61539cf1c"
dependencies = [
"ahash",
"base64 0.13.1",
@@ -1886,14 +1860,14 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bb8c693a387f1ae8d2026d82d8b0c175cc4777b97c1f7b12fdb3be595bb13"
dependencies = [
"dirs 2.0.2",
"dirs",
"thiserror",
"winreg 0.6.2",
]
[[package]]
name = "rojo"
version = "7.6.1"
version = "7.5.1"
dependencies = [
"anyhow",
"backtrace",
@@ -1912,7 +1886,6 @@ dependencies = [
"hyper",
"insta",
"jod-thread",
"jsonc-parser",
"log",
"maplit",
"memofs",
@@ -2064,11 +2037,10 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.228"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_core",
"serde_derive",
]
@@ -2082,37 +2054,26 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
@@ -2217,18 +2178,18 @@ version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.108"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"unicode-ident",
]
@@ -2311,9 +2272,9 @@ version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]
@@ -2423,9 +2384,9 @@ version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]
@@ -2493,12 +2454,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twox-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]]
name = "typenum"
version = "1.17.0"
@@ -2660,9 +2615,9 @@ dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
"wasm-bindgen-shared",
]
@@ -2694,9 +2649,9 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -2992,9 +2947,9 @@ version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2 1.0.103",
"proc-macro2 1.0.78",
"quote 1.0.35",
"syn 2.0.108",
"syn 2.0.52",
]
[[package]]

View File

@@ -1,7 +1,7 @@
[package]
name = "rojo"
version = "7.6.1"
rust-version = "1.88"
version = "7.5.1"
rust-version = "1.79.0"
authors = [
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
@@ -55,11 +55,11 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "2.0.0"
rbx_dom_weak = "4.0.0"
rbx_reflection = "6.0.0"
rbx_reflection_database = "2.0.1"
rbx_xml = "2.0.0"
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.3"
rbx_xml = "1.0.0"
anyhow = "1.0.80"
backtrace = "0.3.69"
@@ -85,8 +85,7 @@ reqwest = { version = "0.11.24", default-features = false, features = [
ritz = "0.1.0"
roblox_install = "1.0.0"
serde = { version = "1.0.197", features = ["derive", "rc"] }
serde_json = "1.0.145"
jsonc-parser = { version = "0.27.0", features = ["serde"] }
serde_json = "1.0.114"
toml = "0.5.11"
termcolor = "1.4.1"
thiserror = "1.0.57"

View File

@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome!
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
Rojo supports Rust 1.70.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

View File

@@ -1,5 +1,3 @@
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock
sourcemap.json
/*.rbxl.lock

View File

@@ -2,4 +2,4 @@ return {
hello = function()
print("Hello world, from {project_name}!")
end,
}
}

View File

@@ -3,6 +3,4 @@
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock
sourcemap.json
/*.rbxl.lock

View File

@@ -1,5 +1,3 @@
# Plugin model files
/{project_name}.rbxmx
/{project_name}.rbxm
sourcemap.json

View File

@@ -1 +0,0 @@
print("Hello world, from client!")

View File

@@ -1 +0,0 @@
print("Hello world, from server!")

View File

@@ -1,3 +0,0 @@
return function()
print("Hello, world!")
end

View File

@@ -1 +0,0 @@
print("Hello world, from plugin!")

View File

@@ -47,7 +47,6 @@ fn main() -> Result<(), anyhow::Error> {
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let plugin_dir = root_dir.join("plugin");
let templates_dir = root_dir.join("assets").join("project-templates");
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
let plugin_version =
@@ -58,9 +57,7 @@ fn main() -> Result<(), anyhow::Error> {
"plugin version does not match Cargo version"
);
let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
let plugin_snapshot = VfsSnapshot::dir(hashmap! {
let snapshot = VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
"plugin" => VfsSnapshot::dir(hashmap! {
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
@@ -73,11 +70,10 @@ fn main() -> Result<(), anyhow::Error> {
}),
});
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?;
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
let out_path = Path::new(&out_dir).join("plugin.bincode");
let out_file = File::create(out_path)?;
bincode::serialize_into(plugin_file, &plugin_snapshot)?;
bincode::serialize_into(template_file, &template_snapshot)?;
bincode::serialize_into(out_file, &snapshot)?;
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");

View File

@@ -228,17 +228,23 @@ impl VfsBackend for InMemoryFs {
}
fn must_be_file<T>(path: &Path) -> io::Result<T> {
Err(io::Error::other(format!(
"path {} was a directory, but must be a file",
path.display()
)))
Err(io::Error::new(
io::ErrorKind::Other,
format!(
"path {} was a directory, but must be a file",
path.display()
),
))
}
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
Err(io::Error::other(format!(
"path {} was a file, but must be a directory",
path.display()
)))
Err(io::Error::new(
io::ErrorKind::Other,
format!(
"path {} was a file, but must be a directory",
path.display()
),
))
}
fn not_found<T>(path: &Path) -> io::Result<T> {

View File

@@ -15,27 +15,45 @@ impl NoopBackend {
impl VfsBackend for NoopBackend {
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -43,11 +61,17 @@ impl VfsBackend for NoopBackend {
}
fn watch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
}

View File

@@ -109,13 +109,15 @@ impl VfsBackend for StdBackend {
self.watches.insert(path.to_path_buf());
self.watcher
.watch(path, RecursiveMode::Recursive)
.map_err(io::Error::other)
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
}
}
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
self.watches.remove(path);
self.watcher.unwatch(path).map_err(io::Error::other)
self.watcher
.unwatch(path)
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
}
}

View File

@@ -1 +1 @@
7.6.1
7.5.1

View File

@@ -378,26 +378,13 @@ types = {
if pod == "Default" then
return nil
else
-- Passing `nil` instead of not passing anything gives
-- different results, so we have to branch here.
if pod.acousticAbsorption then
return (PhysicalProperties.new :: any)(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight,
pod.acousticAbsorption
)
else
return PhysicalProperties.new(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight
)
end
return PhysicalProperties.new(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight
)
end
end,
@@ -411,7 +398,6 @@ types = {
elasticity = roblox.Elasticity,
frictionWeight = roblox.FrictionWeight,
elasticityWeight = roblox.ElasticityWeight,
acousticAbsorption = roblox.AcousticAbsorption,
}
end
end,

View File

@@ -441,8 +441,7 @@
"friction": 1.0,
"elasticity": 0.0,
"frictionWeight": 50.0,
"elasticityWeight": 25.0,
"acousticAbsorption": 0.15625
"elasticityWeight": 25.0
}
},
"ty": "PhysicalProperties"

File diff suppressed because it is too large Load Diff

View File

@@ -8,4 +8,12 @@ local Settings = require(Rojo.Plugin.Settings)
Settings:set("logLevel", "Trace")
Settings:set("typecheckingEnabled", true)
require(Rojo.Plugin.runTests)(TestEZ)
local results = require(Rojo.Plugin.runTests)(TestEZ)
-- Roblox's Luau execution gets mad about cyclical tables.
-- Rather than making TestEZ not do that, we just send back the important info.
return {
failureCount = results.failureCount,
successCount = results.successCount,
skippedCount = results.skippedCount,
}

View File

@@ -4,6 +4,8 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local PatchTree = require(Plugin.PatchTree)
local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton)
@@ -22,6 +24,7 @@ function ConfirmingPage:init()
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({
patchTree = nil,
showingStringDiff = false,
oldString = "",
newString = "",
@@ -29,6 +32,28 @@ function ConfirmingPage:init()
oldTable = {},
newTable = {},
})
if self.props.confirmData and self.props.confirmData.patch and self.props.confirmData.instanceMap then
self:buildPatchTree()
end
end
function ConfirmingPage:didUpdate(prevProps)
if prevProps.confirmData ~= self.props.confirmData then
self:buildPatchTree()
end
end
function ConfirmingPage:buildPatchTree()
Timer.start("ConfirmingPage:buildPatchTree")
self:setState({
patchTree = PatchTree.build(
self.props.confirmData.patch,
self.props.confirmData.instanceMap,
{ "Property", "Current", "Incoming" }
),
})
Timer.stop()
end
function ConfirmingPage:render()
@@ -54,7 +79,7 @@ function ConfirmingPage:render()
transparency = self.props.transparency,
layoutOrder = 3,
patchTree = self.props.patchTree,
patchTree = self.state.patchTree,
showStringDiff = function(oldString: string, newString: string)
self:setState({

View File

@@ -4,8 +4,6 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme)
local Spinner = require(Plugin.App.Components.Spinner)
local e = Roact.createElement
@@ -13,35 +11,11 @@ local e = Roact.createElement
local ConnectingPage = Roact.Component:extend("ConnectingPage")
function ConnectingPage:render()
return Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Spinner = e(Spinner, {
position = UDim2.new(0.5, 0, 0.5, 0),
anchorPoint = Vector2.new(0.5, 0.5),
transparency = self.props.transparency,
}),
Text = if type(self.props.text) == "string" and #self.props.text > 0
then e("TextLabel", {
Text = self.props.text,
Position = UDim2.new(0.5, 0, 0.5, 30),
Size = UDim2.new(1, -40, 0.5, -40),
AnchorPoint = Vector2.new(0.5, 0),
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Top,
RichText = true,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Medium,
TextColor3 = theme.SubTextColor,
TextTruncate = Enum.TextTruncate.AtEnd,
TextTransparency = self.props.transparency,
BackgroundTransparency = 1,
})
else nil,
})
end)
return e(Spinner, {
position = UDim2.new(0.5, 0, 0.5, 0),
anchorPoint = Vector2.new(0.5, 0.5),
transparency = self.props.transparency,
})
end
return ConnectingPage

View File

@@ -595,12 +595,6 @@ function App:startSession()
twoWaySync = Settings:get("twoWaySync"),
})
serveSession:setUpdateLoadingTextCallback(function(text: string)
self:setState({
connectingText = text,
})
end)
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
-- Build new tree for patch
self:setState({
@@ -608,32 +602,46 @@ function App:startSession()
})
end)
self.cleanupPostcommit = serveSession:hookPostcommit(function(patch, instanceMap, unappliedPatch)
local now = DateTime.now().UnixTimestamp
-- Update tree with unapplied metadata
self:setState(function(prevState)
local oldPatchData = prevState.patchData
local newPatchData = {
patch = patch,
unapplied = unappliedPatch,
timestamp = now,
}
if PatchSet.isEmpty(patch) then
-- Keep existing patch info, but use new timestamp
newPatchData.patch = oldPatchData.patch
newPatchData.unapplied = oldPatchData.unapplied
elseif now - oldPatchData.timestamp < 2 then
-- Patches that apply in the same second are combined for human clarity
newPatchData.patch = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.patch, patch)
newPatchData.unapplied = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.unapplied, unappliedPatch)
end
return {
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
patchData = newPatchData,
}
end)
end)
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
local now = DateTime.now().UnixTimestamp
local old = self.state.patchData
if PatchSet.isEmpty(patch) then
-- Ignore empty patch, but update timestamp
self:setState({
patchData = {
patch = old.patch,
unapplied = old.unapplied,
timestamp = now,
},
})
return
end
if now - old.timestamp < 2 then
-- Patches that apply in the same second are
-- considered to be part of the same change for human clarity
patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
end
self:setState({
patchData = {
patch = patch,
unapplied = unapplied,
timestamp = now,
},
})
end)
serveSession:onStatusChanged(function(status, details)
if status == ServeSession.Status.Connecting then
if self.dismissSyncReminder then
@@ -765,13 +773,11 @@ function App:startSession()
end
end
self:setState({
connectingText = "Computing diff view...",
})
self:setState({
appStatus = AppStatus.Confirming,
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }),
confirmData = {
instanceMap = instanceMap,
patch = patch,
serverInfo = serverInfo,
},
toolbarIcon = Assets.Images.PluginButton,
@@ -882,7 +888,6 @@ function App:render()
ConfirmingPage = createPageElement(AppStatus.Confirming, {
confirmData = self.state.confirmData,
patchTree = self.state.patchTree,
createPopup = not self.state.guiEnabled,
onAbort = function()
@@ -896,9 +901,7 @@ function App:render()
end,
}),
Connecting = createPageElement(AppStatus.Connecting, {
text = self.state.connectingText,
}),
Connecting = createPageElement(AppStatus.Connecting),
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,

View File

@@ -16,14 +16,6 @@ local Types = require(Plugin.Types)
local decodeValue = require(Plugin.Reconciler.decodeValue)
local getProperty = require(Plugin.Reconciler.getProperty)
local function yieldIfNeeded(clock)
if os.clock() - clock > 1 / 20 then
task.wait()
return os.clock()
end
return clock
end
local function alphabeticalNext(t, state)
-- Equivalent of the next function, but returns the keys in the alphabetic
-- order of node names. We use a temporary ordered key table that is stored in the
@@ -140,6 +132,7 @@ end
-- props must contain id, and cannot contain children or parentId
-- other than those three, it can hold anything
function Tree:addNode(parent, props)
Timer.start("Tree:addNode")
assert(props.id, "props must contain id")
parent = parent or "ROOT"
@@ -150,6 +143,7 @@ function Tree:addNode(parent, props)
for k, v in props do
node[k] = v
end
Timer.stop()
return node
end
@@ -160,25 +154,25 @@ function Tree:addNode(parent, props)
local parentNode = self:getNode(parent)
if not parentNode then
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
Timer.stop()
return
end
parentNode.children[node.id] = node
self.idToNode[node.id] = node
Timer.stop()
return node
end
-- Given a list of ancestor ids in descending order, builds the nodes for them
-- using the patch and instanceMap info
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
local clock = os.clock()
Timer.start("Tree:buildAncestryNodes")
-- Build nodes for ancestry by going up the tree
previousId = previousId or "ROOT"
for _, ancestorId in ancestryIds do
clock = yieldIfNeeded(clock)
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
if not value then
Log.warn("Failed to find ancestor object for " .. ancestorId)
@@ -192,6 +186,8 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
})
previousId = ancestorId
end
Timer.stop()
end
local PatchTree = {}
@@ -200,16 +196,12 @@ local PatchTree = {}
-- uses changeListHeaders in node.changeList
function PatchTree.build(patch, instanceMap, changeListHeaders)
Timer.start("PatchTree.build")
local clock = os.clock()
local tree = Tree.new()
local knownAncestors = {}
Timer.start("patch.updated")
for _, change in patch.updated do
clock = yieldIfNeeded(clock)
local instance = instanceMap.fromIds[change.id]
if not instance then
continue
@@ -289,8 +281,6 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
Timer.start("patch.removed")
for _, idOrInstance in patch.removed do
clock = yieldIfNeeded(clock)
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
if not instance then
-- If we're viewing a past patch, the instance is already removed
@@ -335,8 +325,6 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
Timer.start("patch.added")
for id, change in patch.added do
clock = yieldIfNeeded(clock)
-- Gather ancestors from existing DOM or future additions
local ancestryIds = {}
local parentId = change.Parent

View File

@@ -48,12 +48,6 @@ local function debugPatch(object)
end)
end
local function attemptReparent(instance, parent)
return pcall(function()
instance.Parent = parent
end)
end
local ServeSession = {}
ServeSession.__index = ServeSession
@@ -107,7 +101,6 @@ function ServeSession.new(options)
__connections = connections,
__precommitCallbacks = {},
__postcommitCallbacks = {},
__updateLoadingText = function() end,
}
setmetatable(self, ServeSession)
@@ -138,14 +131,6 @@ function ServeSession:setConfirmCallback(callback)
self.__userConfirmCallback = callback
end
function ServeSession:setUpdateLoadingTextCallback(callback)
self.__updateLoadingText = callback
end
function ServeSession:setLoadingText(text: string)
self.__updateLoadingText(text)
end
--[=[
Hooks a function to run before patch application.
The provided function is called with the incoming patch and an InstanceMap
@@ -190,14 +175,11 @@ end
function ServeSession:start()
self:__setStatus(Status.Connecting)
self:setLoadingText("Connecting to server...")
self.__apiContext
:connect()
:andThen(function(serverInfo)
self:setLoadingText("Loading initial data from server...")
return self:__initialSync(serverInfo):andThen(function()
self:setLoadingText("Starting sync loop...")
self:__setStatus(Status.Connected, serverInfo.projectName)
self:__applyGameAndPlaceId(serverInfo)
@@ -309,52 +291,18 @@ function ServeSession:__replaceInstances(idList)
for id, replacement in replacements do
local oldInstance = self.__instanceMap.fromIds[id]
if not oldInstance then
-- TODO: Why would this happen?
Log.warn("Instance {} not found in InstanceMap during sync replacement", id)
continue
end
self.__instanceMap:insert(id, replacement)
Log.trace("Swapping Instance {} out via api/models/ endpoint", id)
local oldParent = oldInstance.Parent
for _, child in oldInstance:GetChildren() do
-- Some children cannot be reparented, such as a TouchTransmitter
local reparentSuccess, reparentError = attemptReparent(child, replacement)
if not reparentSuccess then
Log.warn(
"Could not reparent child {} of instance {} during sync replacement: {}",
child.Name,
oldInstance.Name,
reparentError
)
end
child.Parent = replacement
end
replacement.Parent = oldParent
-- ChangeHistoryService doesn't like it if an Instance has been
-- Destroyed. So, we have to accept the potential memory hit and
-- just set the parent to `nil`.
local deleteSuccess, deleteError = attemptReparent(oldInstance, nil)
local replaceSuccess, replaceError = attemptReparent(replacement, oldParent)
if not (deleteSuccess and replaceSuccess) then
Log.warn(
"Could not swap instances {} and {} during sync replacement: {}",
oldInstance.Name,
replacement.Name,
(deleteError or "") .. "\n" .. (replaceError or "")
)
-- We need to revert the failed swap to avoid losing the old instance and children.
for _, child in replacement:GetChildren() do
attemptReparent(child, oldInstance)
end
attemptReparent(oldInstance, oldParent)
-- Our replacement should never have existed in the first place, so we can just destroy it.
replacement:Destroy()
continue
end
oldInstance.Parent = nil
if selectionMap[oldInstance] then
-- This is a bit funky, but it saves the order of Selection
@@ -401,11 +349,18 @@ function ServeSession:__applyPatch(patch)
error(unappliedPatch)
end
if Settings:get("enableSyncFallback") and not PatchSet.isEmpty(unappliedPatch) then
-- Some changes did not apply, let's try replacing them instead
local addedIdList = PatchSet.addedIdList(unappliedPatch)
local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
if PatchSet.isEmpty(unappliedPatch) then
if historyRecording then
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
return
end
local addedIdList = PatchSet.addedIdList(unappliedPatch)
local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
local actualUnappliedPatches = PatchSet.newEmpty()
if Settings:get("enableSyncFallback") then
Log.debug("ServeSession:__replaceInstances(unappliedPatch.added)")
Timer.start("ServeSession:__replaceInstances(unappliedPatch.added)")
local addSuccess, unappliedAddedRefs = self:__replaceInstances(addedIdList)
@@ -416,18 +371,20 @@ function ServeSession:__applyPatch(patch)
local updateSuccess, unappliedUpdateRefs = self:__replaceInstances(updatedIdList)
Timer.stop()
-- Update the unapplied patch to reflect which Instances were replaced successfully
if addSuccess then
table.clear(unappliedPatch.added)
PatchSet.assign(unappliedPatch, unappliedAddedRefs)
PatchSet.assign(actualUnappliedPatches, unappliedAddedRefs)
end
if updateSuccess then
table.clear(unappliedPatch.updated)
PatchSet.assign(unappliedPatch, unappliedUpdateRefs)
PatchSet.assign(actualUnappliedPatches, unappliedUpdateRefs)
end
else
Log.debug("Skipping ServeSession:__replaceInstances because of setting")
end
PatchSet.assign(actualUnappliedPatches, unappliedPatch)
if not PatchSet.isEmpty(unappliedPatch) then
if not PatchSet.isEmpty(actualUnappliedPatches) then
Log.debug(
"Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
@@ -439,7 +396,7 @@ function ServeSession:__applyPatch(patch)
-- guaranteed to be called after the commit
for _, callback in self.__postcommitCallbacks do
task.spawn(function()
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
local success, err = pcall(callback, patch, self.__instanceMap, actualUnappliedPatches)
if not success then
Log.warn("Postcommit hook errored: {}", err)
end
@@ -461,13 +418,11 @@ function ServeSession:__initialSync(serverInfo)
-- For any instances that line up with the Rojo server's view, start
-- tracking them in the reconciler.
Log.trace("Matching existing Roblox instances to Rojo IDs")
self:setLoadingText("Hydrating instance map...")
self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game)
-- Calculate the initial patch to apply to the DataModel to catch us
-- up to what Rojo thinks the place should look like.
Log.trace("Computing changes that plugin needs to make to catch up to server...")
self:setLoadingText("Finding differences between server and Studio...")
local success, catchUpPatch =
self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, game)

View File

@@ -2,5 +2,5 @@ return function(TestEZ)
local Rojo = script.Parent.Parent
local Packages = Rojo.Packages
TestEZ.TestBootstrap:run({ Rojo.Plugin, Packages.Http, Packages.Log, Packages.RbxDom })
return TestEZ.TestBootstrap:run({ Rojo.Plugin, Packages.Http, Packages.Log, Packages.RbxDom })
end

View File

@@ -3,3 +3,4 @@ rojo = "rojo-rbx/rojo@7.5.1"
selene = "Kampfkarren/selene@0.29.0"
stylua = "JohnnyMorganz/stylua@2.1.0"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
lune = "lune-org/lune@0.10.2"

View File

@@ -1,49 +1,45 @@
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::{
collections::VecDeque,
path::{Path, PathBuf},
};
use std::{
ffi::OsStr,
io::{self, Write},
};
use anyhow::{bail, format_err};
use clap::Parser;
use fs_err as fs;
use fs_err::OpenOptions;
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
use super::resolve_path;
const GIT_IGNORE_PLACEHOLDER: &str = "gitignore.txt";
static MODEL_PROJECT: &str =
include_str!("../../assets/default-model-project/default.project.json");
static MODEL_README: &str = include_str!("../../assets/default-model-project/README.md");
static MODEL_INIT: &str = include_str!("../../assets/default-model-project/src-init.luau");
static MODEL_GIT_IGNORE: &str = include_str!("../../assets/default-model-project/gitignore.txt");
static TEMPLATE_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/templates.bincode"));
static PLACE_PROJECT: &str =
include_str!("../../assets/default-place-project/default.project.json");
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
static PLUGIN_PROJECT: &str =
include_str!("../../assets/default-plugin-project/default.project.json");
static PLUGIN_README: &str = include_str!("../../assets/default-plugin-project/README.md");
static PLUGIN_GIT_IGNORE: &str = include_str!("../../assets/default-plugin-project/gitignore.txt");
/// Initializes a new Rojo project.
///
/// By default, this will attempt to initialize a 'git' repository in the
/// project directory if `git` is installed. To avoid this, pass `--skip-git`.
#[derive(Debug, Parser)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[clap(default_value = "")]
pub path: PathBuf,
/// The kind of project to create, 'place', 'plugin', or 'model'.
/// The kind of project to create, 'place', 'plugin', or 'model'. Defaults to place.
#[clap(long, default_value = "place")]
pub kind: InitKind,
/// Skips the initialization of a git repository.
#[clap(long)]
pub skip_git: bool,
}
impl InitCommand {
pub fn run(self) -> anyhow::Result<()> {
let template = self.kind.template();
let base_path = resolve_path(&self.path);
fs::create_dir_all(&base_path)?;
@@ -57,51 +53,10 @@ impl InitCommand {
name: project_name.to_owned(),
};
println!(
"Creating new {:?} project '{}'",
self.kind, project_params.name
);
let vfs = Vfs::new(template);
vfs.set_watch_enabled(false);
let mut queue = VecDeque::with_capacity(8);
for entry in vfs.read_dir("")? {
queue.push_back(entry?.path().to_path_buf())
}
while let Some(mut path) = queue.pop_front() {
let metadata = vfs.metadata(&path)?;
if metadata.is_dir() {
fs_err::create_dir(base_path.join(&path))?;
for entry in vfs.read_dir(&path)? {
queue.push_back(entry?.path().to_path_buf());
}
} else {
let content = vfs.read_to_string_lf_normalized(&path)?;
if let Some(file_stem) = path.file_name().and_then(OsStr::to_str) {
if file_stem == GIT_IGNORE_PLACEHOLDER && !self.skip_git {
path.set_file_name(".gitignore");
}
}
write_if_not_exists(
&base_path.join(&path),
&project_params.render_template(&content),
)?;
}
}
if !self.skip_git && should_git_init(&base_path) {
log::debug!("Initializing Git repository...");
let status = Command::new("git")
.arg("init")
.current_dir(&base_path)
.status()?;
if !status.success() {
bail!("git init failed: status code {:?}", status.code());
}
match self.kind {
InitKind::Place => init_place(&base_path, project_params)?,
InitKind::Model => init_model(&base_path, project_params)?,
InitKind::Plugin => init_plugin(&base_path, project_params)?,
}
println!("Created project successfully.");
@@ -123,32 +78,6 @@ pub enum InitKind {
Plugin,
}
impl InitKind {
fn template(&self) -> InMemoryFs {
let template_path = match self {
Self::Place => "place",
Self::Model => "model",
Self::Plugin => "plugin",
};
let snapshot: VfsSnapshot = bincode::deserialize(TEMPLATE_BINCODE)
.expect("Rojo's templates were not properly packed into Rojo's binary");
if let VfsSnapshot::Dir { mut children } = snapshot {
if let Some(template) = children.remove(template_path) {
let mut fs = InMemoryFs::new();
fs.load_snapshot("", template)
.expect("loading a template in memory should never fail");
fs
} else {
panic!("template for project type {:?} is missing", self)
}
} else {
panic!("Rojo's templates were packed as a file instead of a directory")
}
}
}
impl FromStr for InitKind {
type Err = anyhow::Error;
@@ -165,6 +94,92 @@ impl FromStr for InitKind {
}
}
fn init_place(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
println!("Creating new place project '{}'", project_params.name);
let project_file = project_params.render_template(PLACE_PROJECT);
try_create_project(base_path, &project_file)?;
let readme = project_params.render_template(PLACE_README);
write_if_not_exists(&base_path.join("README.md"), &readme)?;
let src = base_path.join("src");
fs::create_dir_all(&src)?;
let src_shared = src.join("shared");
fs::create_dir_all(src.join(&src_shared))?;
let src_server = src.join("server");
fs::create_dir_all(src.join(&src_server))?;
let src_client = src.join("client");
fs::create_dir_all(src.join(&src_client))?;
write_if_not_exists(
&src_shared.join("Hello.luau"),
"return function()\n\tprint(\"Hello, world!\")\nend",
)?;
write_if_not_exists(
&src_server.join("init.server.luau"),
"print(\"Hello world, from server!\")",
)?;
write_if_not_exists(
&src_client.join("init.client.luau"),
"print(\"Hello world, from client!\")",
)?;
let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
Ok(())
}
fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
println!("Creating new model project '{}'", project_params.name);
let project_file = project_params.render_template(MODEL_PROJECT);
try_create_project(base_path, &project_file)?;
let readme = project_params.render_template(MODEL_README);
write_if_not_exists(&base_path.join("README.md"), &readme)?;
let src = base_path.join("src");
fs::create_dir_all(&src)?;
let init = project_params.render_template(MODEL_INIT);
write_if_not_exists(&src.join("init.luau"), &init)?;
let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
Ok(())
}
fn init_plugin(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
println!("Creating new plugin project '{}'", project_params.name);
let project_file = project_params.render_template(PLUGIN_PROJECT);
try_create_project(base_path, &project_file)?;
let readme = project_params.render_template(PLUGIN_README);
write_if_not_exists(&base_path.join("README.md"), &readme)?;
let src = base_path.join("src");
fs::create_dir_all(&src)?;
write_if_not_exists(
&src.join("init.server.luau"),
"print(\"Hello world, from plugin!\")\n",
)?;
let git_ignore = project_params.render_template(PLUGIN_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?;
Ok(())
}
/// Contains parameters used in templates to create a project.
struct ProjectParams {
name: String,
@@ -179,6 +194,23 @@ impl ProjectParams {
}
}
/// Attempt to initialize a Git repository if necessary, and create .gitignore.
fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
if should_git_init(path) {
log::debug!("Initializing Git repository...");
let status = Command::new("git").arg("init").current_dir(path).status()?;
if !status.success() {
bail!("git init failed: status code {:?}", status.code());
}
}
write_if_not_exists(&path.join(".gitignore"), git_ignore)?;
Ok(())
}
/// Tells whether we should initialize a Git repository inside the given path.
///
/// Will return false if the user doesn't have Git installed or if the path is
@@ -219,3 +251,29 @@ fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error>
Ok(())
}
/// Try to create a project file and fail if it already exists.
fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Error> {
let project_path = base_path.join("default.project.json");
let file_res = OpenOptions::new()
.write(true)
.create_new(true)
.open(&project_path);
let mut file = match file_res {
Ok(file) => file,
Err(err) => {
return match err.kind() {
io::ErrorKind::AlreadyExists => {
bail!("Project file already exists: {}", project_path.display())
}
_ => Err(err.into()),
}
}
};
file.write_all(contents.as_bytes())?;
Ok(())
}

View File

@@ -1,6 +1,7 @@
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{bail, Context};
use anyhow::{bail, format_err, Context};
use clap::Parser;
use memofs::Vfs;
use reqwest::{
@@ -90,6 +91,32 @@ impl UploadCommand {
}
}
/// The kind of asset to upload to the website. Affects what endpoints Rojo uses
/// and changes how the asset is built.
#[derive(Debug, Clone, Copy)]
enum UploadKind {
/// Upload to a place.
Place,
/// Upload to a model-like asset, like a Model, Plugin, or Package.
Model,
}
impl FromStr for UploadKind {
type Err = anyhow::Error;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(UploadKind::Place),
"model" => Ok(UploadKind::Model),
attempted => Err(format_err!(
"Invalid upload kind '{}'. Valid kinds are: place, model",
attempted
)),
}
}
}
fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
let url = format!(
"https://data.roblox.com/Data/Upload.ashx?assetid={}",

View File

@@ -43,8 +43,8 @@ impl Serialize for Glob {
impl<'de> Deserialize<'de> for Glob {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let glob = String::deserialize(deserializer)?;
let glob = <&str as Deserialize>::deserialize(deserializer)?;
Glob::new(&glob).map_err(D::Error::custom)
Glob::new(glob).map_err(D::Error::custom)
}
}

View File

@@ -1,313 +0,0 @@
//! Utilities for parsing JSON with comments (JSONC) and deserializing to Rust types.
//!
//! This module provides convenient wrappers around `jsonc_parser` and `serde_json`
//! to reduce boilerplate and improve ergonomics when working with JSONC files.
use anyhow::Context as _;
use serde::de::DeserializeOwned;
/// Parse JSONC text into a `serde_json::Value`.
///
/// This handles the common pattern of calling `jsonc_parser::parse_to_serde_value`
/// and unwrapping the `Option` with a clear error message.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
pub fn parse_value(text: &str) -> anyhow::Result<serde_json::Value> {
jsonc_parser::parse_to_serde_value(text, &Default::default())
.context("Failed to parse JSONC")?
.ok_or_else(|| anyhow::anyhow!("File contains no JSON value"))
}
/// Parse JSONC text into a `serde_json::Value` with a custom context message.
///
/// This is useful when you want to provide a specific error message that includes
/// additional information like the file path.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
pub fn parse_value_with_context(
text: &str,
context: impl Fn() -> String,
) -> anyhow::Result<serde_json::Value> {
jsonc_parser::parse_to_serde_value(text, &Default::default())
.with_context(|| format!("{}: JSONC parse error", context()))?
.ok_or_else(|| anyhow::anyhow!("{}: File contains no JSON value", context()))
}
/// Parse JSONC text and deserialize it into a specific type.
///
/// This combines parsing JSONC and deserializing into a single operation,
/// eliminating the need to manually chain `parse_to_serde_value` and `from_value`.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_str<T: DeserializeOwned>(text: &str) -> anyhow::Result<T> {
let value = parse_value(text)?;
serde_json::from_value(value).context("Failed to deserialize JSON")
}
/// Parse JSONC text and deserialize it into a specific type with a custom context message.
///
/// This is useful when you want to provide a specific error message that includes
/// additional information like the file path.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_str_with_context<T: DeserializeOwned>(
text: &str,
context: impl Fn() -> String,
) -> anyhow::Result<T> {
let value = parse_value_with_context(text, &context)?;
serde_json::from_value(value).with_context(|| format!("{}: Invalid JSON structure", context()))
}
/// Parse JSONC bytes into a `serde_json::Value` with a custom context message.
///
/// This handles UTF-8 conversion and JSONC parsing in one step.
///
/// # Errors
///
/// Returns an error if:
/// - The bytes are not valid UTF-8
/// - The text is not valid JSONC
/// - The text contains no JSON value
pub fn parse_value_from_slice_with_context(
slice: &[u8],
context: impl Fn() -> String,
) -> anyhow::Result<serde_json::Value> {
let text = std::str::from_utf8(slice)
.with_context(|| format!("{}: File is not valid UTF-8", context()))?;
parse_value_with_context(text, context)
}
/// Parse JSONC bytes and deserialize it into a specific type.
///
/// This handles UTF-8 conversion, JSONC parsing, and deserialization in one step.
///
/// # Errors
///
/// Returns an error if:
/// - The bytes are not valid UTF-8
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_slice<T: DeserializeOwned>(slice: &[u8]) -> anyhow::Result<T> {
let text = std::str::from_utf8(slice).context("File is not valid UTF-8")?;
from_str(text)
}
/// Parse JSONC bytes and deserialize it into a specific type with a custom context message.
///
/// This handles UTF-8 conversion, JSONC parsing, and deserialization in one step.
///
/// # Errors
///
/// Returns an error if:
/// - The bytes are not valid UTF-8
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_slice_with_context<T: DeserializeOwned>(
slice: &[u8],
context: impl Fn() -> String,
) -> anyhow::Result<T> {
let text = std::str::from_utf8(slice)
.with_context(|| format!("{}: File is not valid UTF-8", context()))?;
from_str_with_context(text, context)
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[test]
fn test_parse_value() {
let value = parse_value(r#"{"foo": "bar"}"#).unwrap();
assert_eq!(value["foo"], "bar");
}
#[test]
fn test_parse_value_with_comments() {
let value = parse_value(
r#"{
// This is a comment
"foo": "bar" // Inline comment
}"#,
)
.unwrap();
assert_eq!(value["foo"], "bar");
}
#[test]
fn test_parse_value_with_trailing_comma() {
let value = parse_value(
r#"{
"foo": "bar",
"baz": 123,
}"#,
)
.unwrap();
assert_eq!(value["foo"], "bar");
assert_eq!(value["baz"], 123);
}
#[test]
fn test_parse_value_empty() {
let err = parse_value("").unwrap_err();
assert!(err.to_string().contains("no JSON value"));
}
#[test]
fn test_parse_value_invalid() {
let err = parse_value("{invalid}").unwrap_err();
assert!(err.to_string().contains("parse"));
}
#[test]
fn test_parse_value_with_context() {
let err = parse_value_with_context("{invalid}", || "test.json".to_string()).unwrap_err();
assert!(err.to_string().contains("test.json"));
assert!(err.to_string().contains("parse"));
}
#[derive(Debug, Deserialize, PartialEq)]
struct TestStruct {
foo: String,
bar: i32,
}
#[test]
fn test_from_str() {
let result: TestStruct = from_str(r#"{"foo": "hello", "bar": 42}"#).unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_str_with_comments() {
let result: TestStruct = from_str(
r#"{
// Comment
"foo": "hello",
"bar": 42, // Trailing comma is fine
}"#,
)
.unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_str_invalid_type() {
let err = from_str::<TestStruct>(r#"{"foo": "hello"}"#).unwrap_err();
assert!(err.to_string().contains("deserialize"));
}
#[test]
fn test_from_str_with_context() {
let err = from_str_with_context::<TestStruct>(r#"{"foo": "hello"}"#, || {
"config.json".to_string()
})
.unwrap_err();
assert!(err.to_string().contains("config.json"));
assert!(err.to_string().contains("Invalid JSON structure"));
}
#[test]
fn test_parse_value_from_slice_with_context() {
let err = parse_value_from_slice_with_context(b"{invalid}", || "test.json".to_string())
.unwrap_err();
assert!(err.to_string().contains("test.json"));
assert!(err.to_string().contains("parse"));
}
#[test]
fn test_parse_value_from_slice_with_context_invalid_utf8() {
let err = parse_value_from_slice_with_context(&[0xFF, 0xFF], || "test.json".to_string())
.unwrap_err();
assert!(err.to_string().contains("test.json"));
assert!(err.to_string().contains("UTF-8"));
}
#[test]
fn test_from_slice() {
let result: TestStruct = from_slice(br#"{"foo": "hello", "bar": 42}"#).unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_slice_with_comments() {
let result: TestStruct = from_slice(
br#"{
// Comment
"foo": "hello",
"bar": 42, // Trailing comma is fine
}"#,
)
.unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_slice_invalid_utf8() {
let err = from_slice::<TestStruct>(&[0xFF, 0xFF]).unwrap_err();
assert!(err.to_string().contains("UTF-8"));
}
#[test]
fn test_from_slice_with_context() {
let err = from_slice_with_context::<TestStruct>(br#"{"foo": "hello"}"#, || {
"config.json".to_string()
})
.unwrap_err();
assert!(err.to_string().contains("config.json"));
assert!(err.to_string().contains("Invalid JSON structure"));
}
#[test]
fn test_from_slice_with_context_invalid_utf8() {
let err =
from_slice_with_context::<TestStruct>(&[0xFF, 0xFF], || "config.json".to_string())
.unwrap_err();
assert!(err.to_string().contains("config.json"));
assert!(err.to_string().contains("UTF-8"));
}
}

View File

@@ -10,7 +10,6 @@ mod tree_view;
mod auth_cookie;
mod change_processor;
mod glob;
mod json;
mod lua_ast;
mod message_queue;
mod multimap;

View File

@@ -11,7 +11,7 @@ use rbx_dom_weak::{Ustr, UstrMap};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule};
use crate::{glob::Glob, resolution::UnresolvedValue, snapshot::SyncRule};
static PROJECT_FILENAME: &str = "default.project.json";
@@ -214,11 +214,8 @@ impl Project {
project_file_location: PathBuf,
fallback_name: Option<&str>,
) -> Result<Self, Error> {
let mut project: Self = json::from_slice(contents).map_err(|e| Error::Json {
source: serde_json::Error::io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_string(),
)),
let mut project: Self = serde_json::from_slice(contents).map_err(|source| Error::Json {
source,
path: project_file_location.clone(),
})?;
project.file_location = project_file_location;
@@ -402,13 +399,13 @@ mod test {
#[test]
fn path_node_required() {
let path_node: PathNode = json::from_str(r#""src""#).unwrap();
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = json::from_str(r#"{ "optional": "src" }"#).unwrap();
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
@@ -417,7 +414,7 @@ mod test {
#[test]
fn project_node_required() {
let project_node: ProjectNode = json::from_str(
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
@@ -432,7 +429,7 @@ mod test {
#[test]
fn project_node_optional() {
let project_node: ProjectNode = json::from_str(
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
@@ -449,7 +446,7 @@ mod test {
#[test]
fn project_node_none() {
let project_node: ProjectNode = json::from_str(
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
@@ -461,7 +458,7 @@ mod test {
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = json::from_str(
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
@@ -474,7 +471,7 @@ mod test {
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = json::from_str(
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
@@ -487,7 +484,7 @@ mod test {
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = json::from_str(
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
@@ -497,57 +494,4 @@ mod test {
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
#[test]
fn project_with_jsonc_features() {
// Test that JSONC features (comments and trailing commas) are properly handled
let project_json = r#"{
// This is a single-line comment
"name": "TestProject",
/* This is a
multi-line comment */
"tree": {
"$path": "src", // Comment after value
},
"servePort": 34567,
"emitLegacyScripts": false,
// Test glob parsing with comments
"globIgnorePaths": [
"**/*.spec.lua", // Ignore test files
"**/*.test.lua",
],
"syncRules": [
{
"pattern": "*.data.json",
"use": "json", // Trailing comma in object
},
{
"pattern": "*.module.lua",
"use": "moduleScript",
}, // Trailing comma in array
], // Another trailing comma
}"#;
let project = Project::load_from_slice(
project_json.as_bytes(),
PathBuf::from("/test/default.project.json"),
None,
)
.expect("Failed to parse project with JSONC features");
// Verify the parsed values
assert_eq!(project.name, Some("TestProject".to_string()));
assert_eq!(project.serve_port, Some(34567));
assert_eq!(project.emit_legacy_scripts, Some(false));
// Verify glob_ignore_paths were parsed correctly
assert_eq!(project.glob_ignore_paths.len(), 2);
assert!(project.glob_ignore_paths[0].is_match("test/foo.spec.lua"));
assert!(project.glob_ignore_paths[1].is_match("test/bar.test.lua"));
// Verify sync_rules were parsed correctly
assert_eq!(project.sync_rules.len(), 2);
assert!(project.sync_rules[0].include.is_match("data.data.json"));
assert!(project.sync_rules[1].include.is_match("init.module.lua"));
}
}

View File

@@ -62,7 +62,7 @@ impl AmbiguousValue {
match &property.data_type {
DataType::Enum(enum_name) => {
let database = rbx_reflection_database::get().unwrap();
let database = rbx_reflection_database::get();
let enum_descriptor = database.enums.get(enum_name).ok_or_else(|| {
format_err!("Unknown enum {}. This is a Rojo bug!", enum_name)
@@ -203,7 +203,7 @@ fn find_descriptor(
class_name: &str,
prop_name: &str,
) -> Option<&'static PropertyDescriptor<'static>> {
let database = rbx_reflection_database::get().unwrap();
let database = rbx_reflection_database::get();
let mut current_class_name = class_name;
loop {
@@ -248,15 +248,14 @@ fn nonexhaustive_list(values: &[&str]) -> String {
#[cfg(test)]
mod test {
use super::*;
use crate::json;
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
let unresolved: UnresolvedValue = json::from_str(json_value).unwrap();
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve(class, prop).unwrap()
}
fn resolve_unambiguous(json_value: &str) -> Variant {
let unresolved: UnresolvedValue = json::from_str(json_value).unwrap();
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve_unambiguous().unwrap()
}

View File

@@ -221,7 +221,7 @@ pub enum InstigatingSource {
ProjectNode(
#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf,
String,
Box<ProjectNode>,
ProjectNode,
Option<String>,
),
}

View File

@@ -73,7 +73,7 @@ impl RojoTree {
self.inner.root_ref()
}
pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta<'_>> {
pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_by_ref(id) {
let metadata = self.metadata_map.get(&id).unwrap();
@@ -83,7 +83,7 @@ impl RojoTree {
}
}
pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut<'_>> {
pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_by_ref_mut(id) {
let metadata = self.metadata_map.get_mut(&id).unwrap();

View File

@@ -1,10 +1,10 @@
use std::path::Path;
use anyhow::Context;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::ustr;
use crate::{
json,
lua_ast::{Expression, Statement},
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
};
@@ -19,9 +19,8 @@ pub fn snapshot_json(
) -> anyhow::Result<Option<InstanceSnapshot>> {
let contents = vfs.read(path)?;
let value = json::parse_value_from_slice_with_context(&contents, || {
format!("File contains malformed JSON: {}", path.display())
})?;
let value: serde_json::Value = serde_json::from_slice(&contents)
.with_context(|| format!("File contains malformed JSON: {}", path.display()))?;
let as_lua = json_to_lua(value).to_string();

View File

@@ -9,7 +9,6 @@ use rbx_dom_weak::{
use serde::Deserialize;
use crate::{
json,
resolution::UnresolvedValue,
snapshot::{InstanceContext, InstanceSnapshot},
RojoRef,
@@ -29,9 +28,8 @@ pub fn snapshot_json_model(
return Ok(None);
}
let mut instance: JsonModel = json::from_str_with_context(contents_str, || {
format!("File is not a valid JSON model: {}", path.display())
})?;
let mut instance: JsonModel = serde_json::from_str(contents_str)
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
if let Some(top_level_name) = &instance.name {
let new_name = format!("{}.model.json", top_level_name);

View File

@@ -31,7 +31,6 @@ pub fn snapshot_lua(
script_type: ScriptType,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let run_context_enums = &rbx_reflection_database::get()
.unwrap()
.enums
.get("RunContext")
.expect("Unable to get RunContext enums!")

View File

@@ -4,7 +4,7 @@ use anyhow::{format_err, Context};
use rbx_dom_weak::{types::Attributes, Ustr, UstrMap};
use serde::{Deserialize, Serialize};
use crate::{json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef};
use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef};
/// Represents metadata in a sibling file with the same basename.
///
@@ -34,7 +34,7 @@ pub struct AdjacentMetadata {
impl AdjacentMetadata {
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
let mut meta: Self = json::from_slice_with_context(slice, || {
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
format!(
"File contained malformed .meta.json data: {}",
path.display()
@@ -131,7 +131,7 @@ pub struct DirectoryMetadata {
impl DirectoryMetadata {
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
let mut meta: Self = json::from_slice_with_context(slice, || {
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
format!(
"File contained malformed init.meta.json data: {}",
path.display()

View File

@@ -289,7 +289,7 @@ pub fn snapshot_project_node(
metadata.instigating_source = Some(InstigatingSource::ProjectNode(
project_path.to_path_buf(),
instance_name.to_string(),
Box::new(node.clone()),
node.clone(),
parent_class.map(|name| name.to_owned()),
));
@@ -313,7 +313,7 @@ fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Ustr> {
// Members of DataModel with names that match known services are
// probably supposed to be those services.
let descriptor = rbx_reflection_database::get().unwrap().classes.get(name)?;
let descriptor = rbx_reflection_database::get().classes.get(name)?;
if descriptor.tags.contains(&ClassTag::Service) {
return Some(ustr(name));

View File

@@ -17,7 +17,6 @@ use rbx_dom_weak::{
};
use crate::{
json,
serve_session::ServeSession,
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{
@@ -140,7 +139,7 @@ impl ApiService {
let body = body::to_bytes(request.into_body()).await.unwrap();
let request: WriteRequest = match json::from_slice(&body) {
let request: WriteRequest = match serde_json::from_slice(&body) {
Ok(request) => request,
Err(err) => {
return json(

View File

@@ -157,20 +157,14 @@ impl TestServeSession {
let url = format!("http://localhost:{}/api/rojo", self.port);
let body = reqwest::blocking::get(url)?.text()?;
let value = jsonc_parser::parse_to_serde_value(&body, &Default::default())
.expect("Failed to parse JSON")
.expect("No JSON value");
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
Ok(serde_json::from_str(&body).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 body = reqwest::blocking::get(url)?.text()?;
let value = jsonc_parser::parse_to_serde_value(&body, &Default::default())
.expect("Failed to parse JSON")
.expect("No JSON value");
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
}
pub fn get_api_subscribe(