forked from rojo-rbx/rojo
Compare commits
4 Commits
v7.6.1
...
plugin-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47db569c52 | ||
|
|
1d3f8c8e9d | ||
|
|
a894313a4b | ||
|
|
7f73ae80dc |
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -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
3
.gitmodules
vendored
@@ -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
5
.lune/.luaurc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"aliases": {
|
||||
"lune": "~/.lune/.typedefs/0.10.2/"
|
||||
}
|
||||
}
|
||||
1
.lune/opencloud-execute
Submodule
1
.lune/opencloud-execute
Submodule
Submodule .lune/opencloud-execute added at 8ae86dd3ad
112
.lune/test-plugin.luau
Normal file
112
.lune/test-plugin.luau
Normal 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
|
||||
967
CHANGELOG.md
967
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
177
Cargo.lock
generated
177
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
/*.rbxl.lock
|
||||
@@ -2,4 +2,4 @@ return {
|
||||
hello = function()
|
||||
print("Hello world, from {project_name}!")
|
||||
end,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,4 @@
|
||||
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
/*.rbxl.lock
|
||||
@@ -1,5 +1,3 @@
|
||||
# Plugin model files
|
||||
/{project_name}.rbxmx
|
||||
/{project_name}.rbxm
|
||||
|
||||
sourcemap.json
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from client!")
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from server!")
|
||||
@@ -1,3 +0,0 @@
|
||||
return function()
|
||||
print("Hello, world!")
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from plugin!")
|
||||
12
build.rs
12
build.rs
@@ -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");
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
7.6.1
|
||||
7.5.1
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
242
src/cli/init.rs
242
src/cli/init.rs
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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={}",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
313
src/json.rs
313
src/json.rs
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ pub enum InstigatingSource {
|
||||
ProjectNode(
|
||||
#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf,
|
||||
String,
|
||||
Box<ProjectNode>,
|
||||
ProjectNode,
|
||||
Option<String>,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user