Compare commits

...

21 Commits

Author SHA1 Message Date
Lucien Greathouse
e0198e626b Build Linux release on Ubuntu 20.04, use fixed artifact names 2022-08-20 21:34:41 -04:00
boatbomber
142705f386 Fix security permission error (#619) 2022-08-10 15:57:24 -04:00
boatbomber
4cb49c7825 Add sync locking for Team Create (#590)
* Add sync locking

* Steal lock from users who left without releasing

* Do not remove lock as unknown instance

* Don't delete non Archivable instance
2022-08-08 04:08:55 -04:00
Barocena
05adb82dda Renamed Common to Shared (#611) 2022-08-08 03:59:52 -04:00
boatbomber
faf7671799 Make error messages copyable (#614)
* Make error copyable

* Allow partial copying or double click full copy
2022-08-08 03:58:32 -04:00
Lucien Greathouse
d64db329dd Fix release workflow to use Wally 2022-08-08 00:28:52 -04:00
Lucien Greathouse
e34d2339ad Vendor OpenSSL via native-tls-vendored reqwest feature 2022-08-08 00:15:05 -04:00
Max
d196c5091c Simplify usage of attributes. (#574)
* Support implicit values for primitive attributes

This commit adds support for strings, numbers, and booleans to be implicitly typed in attribute maps, reducing the redundancy of needing to specify their types.

I also quietly adjusted one of the tests to use a more stable class/property pair. Since SourceAssetId is locked to Roblox, it could potentially disappear at any time.

* Apply formatting.

* Address feedback

* Backwards compatible format usage.

* Axe UnresolvedValueMap in favor of $attributes

Attributes can be defined directly on instances, with support for unambiguous types.

* Adjust test.

* to_string() -> into()

* Made attribute test more concise.

* small cleanup

* Update src/resolution.rs

* Update src/resolution.rs

* Update src/resolution.rs

* Update src/resolution.rs

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 20:07:06 -04:00
Lucien Greathouse
3e83f92532 Update MSRV to 1.58.1 for format string capturing 2022-08-03 19:39:36 -04:00
James Onnen
41d7aaf323 Add uipadding to notifications (#589) 2022-08-03 19:01:07 -04:00
boatbomber
e110f3726a Real-time status about sync details (#569)
* Rough prototype of patch info display

* Remove extra newline

* Switch to binding

* Update slower for older timestamps

* Batch patches within a second of each other

* Fix indentation

* Less wasteful refresh hz

* More apt variable name

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 18:58:28 -04:00
Boegie19
eb5c897ac0 fix relevant_paths not being set for init.csv (#599)
* fix relevant_paths not being set for init.csv

* fix failing tests

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 18:38:08 -04:00
boatbomber
e864cf0c7d Switch git submodules to Wally packages (#584)
* Switch git submodules to Wally packages

* Update build snapshot

* Add wally to foreman and use latest versions

* Install packages in CI runners

* Fix indents

* Install packages in the correct directory

* Install packages in correct dir of release action too

* Remove submodules from ci checkout

* Remove submodules from release checkout

* Update selene with latest fix

* Fix whitespace

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-08-03 18:36:58 -04:00
Lucien Greathouse
565c12405e Skip empty AppliedPatchSets for sending changes. 2022-08-03 17:19:23 -04:00
JohnnyMorganz
2a6a8b42a6 Add --watch to sourcemap generation (#602)
* Implement watch argument

* Add forget call

* Clippy fixes

* Update changelog
2022-08-01 04:07:07 -04:00
Boegie19
5cb4cc0d1d feature init csv (#594)
* init csv feature + test

* fmt fixes
2022-07-29 21:45:19 -04:00
boatbomber
62eb4f026f Fix errors after session already ended (#587) 2022-07-23 12:24:16 -04:00
wackbyte
411d1a89c1 Really default to the current directory in 'rojo fmt-project' (#581)
Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-07-18 19:47:30 -04:00
boatbomber
6ae0bf366a Use singleton settings outside the Roact tree (#576)
* Use singleton settings outside the Roact tree

* Cleanup listener on unmount

* Refactor setting page components

* Fix willUnmount being added to the wrong table

* Remove bindings in favor of state
2022-07-18 19:36:38 -04:00
wackbyte
178cdc9dfa Update benches so they compile (#582) 2022-07-17 18:50:12 -04:00
wackbyte
5bf1f86886 Fix link to v7.2.1 in the changelog (#578) 2022-07-11 00:44:47 -04:00
92 changed files with 1221 additions and 644 deletions

View File

@@ -16,12 +16,10 @@ jobs:
strategy: strategy:
matrix: matrix:
rust_version: [stable, 1.57.0] rust_version: [stable, 1.58.1]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -30,6 +28,17 @@ jobs:
override: true override: true
profile: minimal profile: minimal
- name: Setup Foreman
uses: Roblox/setup-foreman@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install packages
run: |
cd plugin
wally install
cd ..
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -42,8 +51,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -52,8 +59,19 @@ jobs:
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Setup Foreman
uses: Roblox/setup-foreman@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install packages
run: |
cd plugin
wally install
cd ..
- name: Rustfmt - name: Rustfmt
run: cargo fmt -- --check run: cargo fmt -- --check
- name: Clippy - name: Clippy
run: cargo clippy run: cargo clippy

View File

@@ -28,14 +28,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
submodules: true
- name: Setup Foreman - name: Setup Foreman
uses: Roblox/setup-foreman@v1 uses: Roblox/setup-foreman@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Install packages
run: |
cd plugin
wally install
cd ..
- name: Build Plugin - name: Build Plugin
run: rojo build plugin --output Rojo.rbxm run: rojo build plugin --output Rojo.rbxm
@@ -61,25 +65,21 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# https://doc.rust-lang.org/rustc/platform-support.html # https://doc.rust-lang.org/rustc/platform-support.html
#
# FIXME: After the Rojo VS Code extension updates, add architecture
# names to each of these releases. We'll rename win64 to windows and add
# -x86_64 to each release.
include: include:
- host: linux - host: linux
os: ubuntu-18.04 os: ubuntu-20.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux label: linux-x86_64
- host: windows - host: windows
os: windows-latest os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
label: win64 label: windows-x86_64
- host: macos - host: macos
os: macos-latest os: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
label: macos label: macos-x86_64
- host: macos - host: macos
os: macos-latest os: macos-latest
@@ -92,8 +92,6 @@ jobs:
BIN: rojo BIN: rojo
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
submodules: true
- name: Get Version from Tag - name: Get Version from Tag
shell: bash shell: bash
@@ -110,6 +108,17 @@ jobs:
override: true override: true
profile: minimal profile: minimal
- name: Setup Foreman
uses: Roblox/setup-foreman@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install packages
run: |
cd plugin
wally install
cd ..
- name: Build Release - name: Build Release
run: cargo build --release --locked --verbose run: cargo build --release --locked --verbose
env: env:
@@ -150,4 +159,4 @@ jobs:
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
path: release.zip path: release.zip

3
.gitignore vendored
View File

@@ -13,6 +13,9 @@
# Test places for the Roblox Studio Plugin # Test places for the Roblox Studio Plugin
/plugin/*.rbxlx /plugin/*.rbxlx
# Packages for the Roblox Studio Plugin
/plugin/*Packages
# Roblox Studio holds 'lock' files on places # Roblox Studio holds 'lock' files on places
*.rbxl.lock *.rbxl.lock
*.rbxlx.lock *.rbxlx.lock

15
.gitmodules vendored
View File

@@ -1,15 +0,0 @@
[submodule "plugin/modules/roact"]
path = plugin/modules/roact
url = https://github.com/Roblox/roact.git
[submodule "plugin/modules/testez"]
path = plugin/modules/testez
url = https://github.com/Roblox/testez.git
[submodule "plugin/modules/promise"]
path = plugin/modules/promise
url = https://github.com/LPGhatguy/roblox-lua-promise.git
[submodule "plugin/modules/t"]
path = plugin/modules/t
url = https://github.com/osyrisrblx/t.git
[submodule "plugin/modules/flipper"]
path = plugin/modules/flipper
url = https://github.com/Reselim/Flipper

View File

@@ -1,6 +1,9 @@
# Rojo Changelog # Rojo Changelog
## Unreleased Changes ## Unreleased Changes
* Added `--watch` flag to `rojo sourcemap` ([#602])
[#602]: https://github.com/rojo-rbx/rojo/pull/602
## [7.2.1] - July 8, 2022 ## [7.2.1] - July 8, 2022
* Fixed notification sound by changing it to a generic sound. ([#566]) * Fixed notification sound by changing it to a generic sound. ([#566])
@@ -8,7 +11,7 @@
[#566]: https://github.com/rojo-rbx/rojo/pull/566 [#566]: https://github.com/rojo-rbx/rojo/pull/566
[#568]: https://github.com/rojo-rbx/rojo/pull/568 [#568]: https://github.com/rojo-rbx/rojo/pull/568
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0 [7.2.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.1
## [7.2.0] - June 29, 2022 ## [7.2.0] - June 29, 2022
* Added support for `.luau` files. ([#552]) * Added support for `.luau` files. ([#552])

10
Cargo.lock generated
View File

@@ -1338,6 +1338,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.22.0+1.1.1q"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.74" version = "0.9.74"
@@ -1347,6 +1356,7 @@ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.2.1" version = "7.2.1"
rust-version = "1.57.0" rust-version = "1.58.1"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers" description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0" license = "MPL-2.0"
@@ -73,7 +73,7 @@ log = "0.4.14"
maplit = "1.0.2" maplit = "1.0.2"
notify = "4.0.17" notify = "4.0.17"
opener = "0.5.0" opener = "0.5.0"
reqwest = { version = "0.11.10", features = ["blocking", "json"] } reqwest = { version = "0.11.10", features = ["blocking", "json", "native-tls-vendored"] }
ritz = "0.1.0" ritz = "0.1.0"
roblox_install = "1.0.0" roblox_install = "1.0.0"
serde = { version = "1.0.130", features = ["derive", "rc"] } serde = { version = "1.0.130", features = ["derive", "rc"] }

View File

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

View File

@@ -4,7 +4,7 @@
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"Common": { "Shared": {
"$path": "src/shared" "$path": "src/shared"
} }
}, },

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use librojo::cli::{build, BuildCommand}; use librojo::cli::BuildCommand;
pub fn benchmark_small_place(c: &mut Criterion) { pub fn benchmark_small_place(c: &mut Criterion) {
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place") bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
group.bench_function("build", |b| { group.bench_function("build", |b| {
b.iter_batched( b.iter_batched(
|| place_setup(path), || place_setup(path),
|(_dir, options)| build(options).unwrap(), |(_dir, options)| options.run().unwrap(),
BatchSize::SmallInput, BatchSize::SmallInput,
) )
}); });

View File

@@ -43,8 +43,6 @@ fn main() -> Result<(), anyhow::Error> {
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let plugin_root = PathBuf::from(root_dir).join("plugin"); let plugin_root = PathBuf::from(root_dir).join("plugin");
let plugin_modules = plugin_root.join("modules");
let snapshot = VfsSnapshot::dir(hashmap! { let snapshot = VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?, "default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?, "fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
@@ -52,20 +50,7 @@ fn main() -> Result<(), anyhow::Error> {
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?, "log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?, "rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?, "src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
"modules" => VfsSnapshot::dir(hashmap! { "Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
"roact" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
}),
"promise" => VfsSnapshot::dir(hashmap! {
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
}),
"t" => VfsSnapshot::dir(hashmap! {
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
}),
"flipper" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
}),
}),
}); });
let out_path = Path::new(&out_dir).join("plugin.bincode"); let out_path = Path::new(&out_dir).join("plugin.bincode");

View File

@@ -1,4 +1,5 @@
[tools] [tools]
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" } rojo = { source = "rojo-rbx/rojo", version = "7.2.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" } run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
selene = { source = "Kampfkarren/selene", version = "0.18.2" } selene = { source = "Kampfkarren/selene", version = "0.20.0" }
wally = { source = "UpliftGames/wally", version = "0.3.1"}

View File

@@ -1,33 +1,25 @@
{ {
"name": "Rojo", "name": "Rojo",
"tree": { "tree": {
"$className": "Folder", "$className": "Folder",
"Plugin": { "Plugin": {
"$path": "src" "$path": "src"
}, },
"Log": { "Packages": {
"$path": "log" "$path": "Packages",
},
"Http": { "Log": {
"$path": "http" "$path": "log"
}, },
"Fmt": { "Http": {
"$path": "fmt" "$path": "http"
}, },
"RbxDom": { "Fmt": {
"$path": "rbx_dom_lua" "$path": "fmt"
}, },
"Roact": { "RbxDom": {
"$path": "modules/roact/src" "$path": "rbx_dom_lua"
}, }
"Promise": { }
"$path": "modules/promise/lib" }
}, }
"t": {
"$path": "modules/t/lib"
},
"Flipper": {
"$path": "modules/flipper/src"
}
}
}

Submodule plugin/modules/t deleted from f643b50682

View File

@@ -1,6 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.TestEZ) local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
local Rojo = ReplicatedStorage.Rojo local Rojo = ReplicatedStorage.Rojo
@@ -16,4 +16,4 @@ require(Rojo.Plugin.runTests)(TestEZ)
if setDevSettings then if setDevSettings then
DevSettings:resetValues() DevSettings:resetValues()
end end

View File

@@ -1,6 +1,7 @@
local Http = require(script.Parent.Parent.Http) local Packages = script.Parent.Parent.Packages
local Log = require(script.Parent.Parent.Log) local Http = require(Packages.Http)
local Promise = require(script.Parent.Parent.Promise) local Log = require(Packages.Log)
local Promise = require(Packages.Promise)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Types = require(script.Parent.Types) local Types = require(script.Parent.Types)
@@ -85,7 +86,7 @@ local ApiContext = {}
ApiContext.__index = ApiContext ApiContext.__index = ApiContext
function ApiContext.new(baseUrl) function ApiContext.new(baseUrl)
assert(type(baseUrl) == "string") assert(type(baseUrl) == "string", "baseUrl must be a string")
local self = { local self = {
__baseUrl = baseUrl, __baseUrl = baseUrl,
@@ -248,4 +249,4 @@ function ApiContext:open(id)
end) end)
end end
return ApiContext return ApiContext

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -38,4 +39,4 @@ local function BorderedContainer(props)
end) end)
end end
return BorderedContainer return BorderedContainer

View File

@@ -1,8 +1,9 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
@@ -93,4 +94,4 @@ function Checkbox:render()
end) end)
end end
return Checkbox return Checkbox

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -52,4 +53,4 @@ local function Header(props)
end) end)
end end
return Header return Header

View File

@@ -1,8 +1,9 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
@@ -76,4 +77,4 @@ function IconButton:render()
}) })
end end
return IconButton return IconButton

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
@@ -39,4 +40,4 @@ local function ScrollingFrame(props)
end) end)
end end
return ScrollingFrame return ScrollingFrame

View File

@@ -1,6 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local e = Roact.createElement local e = Roact.createElement
@@ -26,4 +27,4 @@ local function SlicedImage(props)
}, props[Roact.Children]) }, props[Roact.Children])
end end
return SlicedImage return SlicedImage

View File

@@ -2,8 +2,9 @@ local RunService = game:GetService("RunService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -63,4 +64,4 @@ function Spinner:willUnmount()
self.stepper:Disconnect() self.stepper:Disconnect()
end end
return Spinner return Spinner

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local StudioPluginContext = Roact.createContext(nil) local StudioPluginContext = Roact.createContext(nil)
return StudioPluginContext return StudioPluginContext

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -81,4 +82,4 @@ local function StudioPluginGuiWrapper(props)
}) })
end end
return StudioPluginGuiWrapper return StudioPluginGuiWrapper

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -42,4 +43,4 @@ local function StudioToolbarWrapper(props)
}) })
end end
return StudioToolbarWrapper return StudioToolbarWrapper

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local StudioToolbarContext = Roact.createContext(nil) local StudioToolbarContext = Roact.createContext(nil)
return StudioToolbarContext return StudioToolbarContext

View File

@@ -2,9 +2,10 @@ local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -134,4 +135,4 @@ function TextButton:render()
end) end)
end end
return TextButton return TextButton

View File

@@ -1,8 +1,9 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
@@ -142,4 +143,4 @@ function TouchRipple:render()
}) })
end end
return TouchRipple return TouchRipple

View File

@@ -3,9 +3,10 @@ local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local bindingUtil = require(script.Parent.bindingUtil) local bindingUtil = require(script.Parent.bindingUtil)

View File

@@ -1,8 +1,9 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Rojo.Flipper) local Flipper = require(Packages.Flipper)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -67,4 +68,4 @@ function Page:didUpdate(lastProps)
end end
end end
return Page return Page

View File

@@ -1,123 +0,0 @@
--[[
Persistent plugin settings that can be accessed via Roact context.
]]
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
showNotifications = true,
playSounds = true,
}
local Settings = {}
Settings.__index = Settings
function Settings.fromPlugin(plugin)
local values = {}
for name, defaultValue in pairs(defaultSettings) do
local savedValue = plugin:GetSetting("Rojo_" .. name)
if savedValue == nil then
plugin:SetSetting("Rojo_" .. name, defaultValue)
values[name] = defaultValue
else
values[name] = savedValue
end
end
return setmetatable({
__values = values,
__plugin = plugin,
__updateListeners = {},
}, Settings)
end
function Settings:get(name)
if defaultSettings[name] == nil then
error("Invalid setings name " .. tostring(name), 2)
end
return self.__values[name]
end
function Settings:set(name, value)
self.__plugin:SetSetting("Rojo_" .. name, value)
self.__values[name] = value
for callback in pairs(self.__updateListeners) do
callback(name, value)
end
end
function Settings:onUpdate(newCallback)
local newListeners = {}
for callback in pairs(self.__updateListeners) do
newListeners[callback] = true
end
newListeners[newCallback] = true
self.__updateListeners = newListeners
return function()
local newListeners = {}
for callback in pairs(self.__updateListeners) do
if callback ~= newCallback then
newListeners[callback] = true
end
end
self.__updateListeners = newListeners
end
end
local Context = Roact.createContext(nil)
local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:init()
self.settings = Settings.fromPlugin(self.props.plugin)
end
function StudioProvider:render()
return Roact.createElement(Context.Provider, {
value = self.settings,
}, self.props[Roact.Children])
end
local InternalConsumer = Roact.Component:extend("InternalConsumer")
function InternalConsumer:render()
return self.props.render(self.props.settings)
end
function InternalConsumer:didMount()
self.disconnect = self.props.settings:onUpdate(function()
-- Trigger a dummy state update to update the settings consumer.
self:setState({})
end)
end
function InternalConsumer:willUnmount()
self.disconnect()
end
local function with(callback)
return Roact.createElement(Context.Consumer, {
render = function(settings)
return Roact.createElement(InternalConsumer, {
settings = settings,
render = callback,
})
end,
})
end
return {
StudioProvider = StudioProvider,
with = with,
}

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -12,6 +13,26 @@ local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local e = Roact.createElement local e = Roact.createElement
local AGE_UNITS = { {31556909, "year"}, {2629743, "month"}, {604800, "week"}, {86400, "day"}, {3600, "hour"}, {60, "minute"}, }
function timeSinceText(elapsed: number): string
if elapsed < 3 then
return "just now"
end
local ageText = string.format("%d seconds ago", elapsed)
for _,UnitData in ipairs(AGE_UNITS) do
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then
local c = math.floor(elapsed/UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c>1 and "s" or "")
break
end
end
return ageText
end
local function ConnectionDetails(props) local function ConnectionDetails(props)
return Theme.with(function(theme) return Theme.with(function(theme)
return e(BorderedContainer, { return e(BorderedContainer, {
@@ -82,33 +103,59 @@ end
local ConnectedPage = Roact.Component:extend("ConnectedPage") local ConnectedPage = Roact.Component:extend("ConnectedPage")
function ConnectedPage:render() function ConnectedPage:render()
return Roact.createFragment({ return Theme.with(function(theme)
Header = e(Header, { return Roact.createFragment({
transparency = self.props.transparency, Header = e(Header, {
layoutOrder = 1, transparency = self.props.transparency,
}), layoutOrder = 1,
}),
ConnectionDetails = e(ConnectionDetails, { ConnectionDetails = e(ConnectionDetails, {
projectName = self.state.projectName, projectName = self.state.projectName,
address = self.state.address, address = self.state.address,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 2, layoutOrder = 2,
onDisconnect = self.props.onDisconnect, onDisconnect = self.props.onDisconnect,
}), }),
Layout = e("UIListLayout", { Info = e("TextLabel", {
VerticalAlignment = Enum.VerticalAlignment.Center, Text = self.props.patchInfo:map(function(info)
FillDirection = Enum.FillDirection.Vertical, return string.format(
SortOrder = Enum.SortOrder.LayoutOrder, "<i>Synced %d change%s %s</i>",
Padding = UDim.new(0, 10), info.changes,
}), info.changes == 1 and "" or "s",
timeSinceText(os.time() - info.timestamp)
)
end),
Font = Enum.Font.Gotham,
TextSize = 14,
TextWrapped = true,
RichText = true,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency,
Padding = e("UIPadding", { Size = UDim2.new(1, 0, 0, 28),
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20), LayoutOrder = 3,
}), BackgroundTransparency = 1,
}) }),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
})
end)
end end
function ConnectedPage.getDerivedStateFromProps(props) function ConnectedPage.getDerivedStateFromProps(props)
@@ -122,4 +169,4 @@ function ConnectedPage.getDerivedStateFromProps(props)
} }
end end
return ConnectedPage return ConnectedPage

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Spinner = require(Plugin.App.Components.Spinner) local Spinner = require(Plugin.App.Components.Spinner)
@@ -17,4 +18,4 @@ function ConnectingPage:render()
}) })
end end
return ConnectingPage return ConnectingPage

View File

@@ -2,8 +2,9 @@ local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
@@ -56,8 +57,16 @@ function Error:render()
end, end,
}, { }, {
ErrorMessage = Theme.with(function(theme) ErrorMessage = Theme.with(function(theme)
return e("TextLabel", { return e("TextBox", {
[Roact.Event.InputBegan] = function(rbx, input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
rbx.SelectionStart = 0
rbx.CursorPosition = #rbx.Text+1
end,
Text = self.props.errorMessage, Text = self.props.errorMessage,
TextEditable = false,
Font = Enum.Font.Code, Font = Enum.Font.Code,
TextSize = 16, TextSize = 16,
TextColor3 = theme.ErrorColor, TextColor3 = theme.ErrorColor,
@@ -65,10 +74,9 @@ function Error:render()
TextYAlignment = Enum.TextYAlignment.Top, TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
TextWrapped = true, TextWrapped = true,
ClearTextOnFocus = false,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
}) })
end), end),
@@ -150,4 +158,4 @@ function ErrorPage.getDerivedStateFromProps(props)
} }
end end
return ErrorPage return ErrorPage

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Config = require(Plugin.Config) local Config = require(Plugin.Config)
@@ -148,4 +149,4 @@ function NotConnectedPage:render()
}) })
end end
return NotConnectedPage return NotConnectedPage

View File

@@ -1,246 +0,0 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local PluginSettings = require(Plugin.App.PluginSettings)
local Checkbox = require(Plugin.App.Components.Checkbox)
local IconButton = require(Plugin.App.Components.IconButton)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local e = Roact.createElement
local DIVIDER_FADE_SIZE = 0.1
local function getTextBounds(text, textSize, font, lineHeight, bounds)
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
local lineCount = textBounds.Y / textSize
local lineHeightAbsolute = textSize * lineHeight
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
end
local function Navbar(props)
return Theme.with(function(theme)
theme = theme.Settings.Navbar
return e("Frame", {
Size = UDim2.new(1, 0, 0, 46),
LayoutOrder = props.layoutOrder,
BackgroundTransparency = 1,
}, {
Back = e(IconButton, {
icon = Assets.Images.Icons.Back,
iconSize = 24,
color = theme.BackButtonColor,
transparency = props.transparency,
position = UDim2.new(0, 0, 0.5, 0),
anchorPoint = Vector2.new(0, 0.5),
onClick = props.onBack,
}),
Text = e("TextLabel", {
Text = "Settings",
Font = Enum.Font.Gotham,
TextSize = 18,
TextColor3 = theme.TextColor,
TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
})
})
end)
end
local Setting = Roact.Component:extend("Setting")
function Setting:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
end
function Setting:render()
return Theme.with(function(theme)
theme = theme.Settings
return PluginSettings.with(function(settings)
return e("Frame", {
Size = self.contentSize:map(function(value)
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
end),
LayoutOrder = self.props.layoutOrder,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(object)
self.setContainerSize(object.AbsoluteSize)
end,
}, {
Checkbox = e(Checkbox, {
active = settings:get(self.props.id),
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
anchorPoint = Vector2.new(1, 0.5),
onClick = function()
local currentValue = settings:get(self.props.id)
settings:set(self.props.id, not currentValue)
end,
}),
Text = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Name = e("TextLabel", {
Text = self.props.name,
Font = Enum.Font.GothamBold,
TextSize = 17,
TextColor3 = theme.Setting.NameColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 17),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Description = e("TextLabel", {
Text = self.props.description,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
TextColor3 = theme.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
TextWrapped = true,
Size = self.containerSize:map(function(value)
local textBounds = getTextBounds(
self.props.description, 14, Enum.Font.Gotham, 1.2,
Vector2.new(value.X - 50, math.huge)
)
return UDim2.new(1, -50, 0, textBounds.Y)
end),
LayoutOrder = 2,
BackgroundTransparency = 1,
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 20),
}),
}),
Divider = e("Frame", {
BackgroundColor3 = theme.DividerColor,
BackgroundTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 1),
BorderSizePixel = 0,
}, {
Gradient = e("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0),
NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0),
NumberSequenceKeypoint.new(1, 1),
}),
}),
}),
})
end)
end)
end
local SettingsPage = Roact.Component:extend("SettingsPage")
function SettingsPage:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
end
function SettingsPage:render()
return Theme.with(function(theme)
theme = theme.Settings
return e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize,
transparency = self.props.transparency,
}, {
Navbar = e(Navbar, {
onBack = self.props.onBack,
transparency = self.props.transparency,
layoutOrder = 0,
}),
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor",
transparency = self.props.transparency,
layoutOrder = 1,
}),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
layoutOrder = 2,
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = 3,
}),
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency,
layoutOrder = 4,
}),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
})
end)
end
return SettingsPage

View File

@@ -0,0 +1,150 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme)
local Checkbox = require(Plugin.App.Components.Checkbox)
local e = Roact.createElement
local DIVIDER_FADE_SIZE = 0.1
local function getTextBounds(text, textSize, font, lineHeight, bounds)
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
local lineCount = textBounds.Y / textSize
local lineHeightAbsolute = textSize * lineHeight
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
end
local Setting = Roact.Component:extend("Setting")
function Setting:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({
setting = Settings:get(self.props.id),
})
self.changedCleanup = Settings:onChanged(self.props.id, function(value)
self:setState({
setting = value,
})
end)
end
function Setting:willUnmount()
self.changedCleanup()
end
function Setting:render()
return Theme.with(function(theme)
theme = theme.Settings
return e("Frame", {
Size = self.contentSize:map(function(value)
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
end),
LayoutOrder = self.props.layoutOrder,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(object)
self.setContainerSize(object.AbsoluteSize)
end,
}, {
Checkbox = e(Checkbox, {
active = self.state.setting,
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
anchorPoint = Vector2.new(1, 0.5),
onClick = function()
local currentValue = Settings:get(self.props.id)
Settings:set(self.props.id, not currentValue)
end,
}),
Text = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Name = e("TextLabel", {
Text = self.props.name,
Font = Enum.Font.GothamBold,
TextSize = 17,
TextColor3 = theme.Setting.NameColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 17),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Description = e("TextLabel", {
Text = self.props.description,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
TextColor3 = theme.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
TextWrapped = true,
Size = self.containerSize:map(function(value)
local textBounds = getTextBounds(
self.props.description, 14, Enum.Font.Gotham, 1.2,
Vector2.new(value.X - 50, math.huge)
)
return UDim2.new(1, -50, 0, textBounds.Y)
end),
LayoutOrder = 2,
BackgroundTransparency = 1,
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 20),
}),
}),
Divider = e("Frame", {
BackgroundColor3 = theme.DividerColor,
BackgroundTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 1),
BorderSizePixel = 0,
}, {
Gradient = e("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0),
NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0),
NumberSequenceKeypoint.new(1, 1),
}),
}),
}),
})
end)
end
return Setting

View File

@@ -0,0 +1,122 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local IconButton = require(Plugin.App.Components.IconButton)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local Setting = require(script.Setting)
local e = Roact.createElement
local function Navbar(props)
return Theme.with(function(theme)
theme = theme.Settings.Navbar
return e("Frame", {
Size = UDim2.new(1, 0, 0, 46),
LayoutOrder = props.layoutOrder,
BackgroundTransparency = 1,
}, {
Back = e(IconButton, {
icon = Assets.Images.Icons.Back,
iconSize = 24,
color = theme.BackButtonColor,
transparency = props.transparency,
position = UDim2.new(0, 0, 0.5, 0),
anchorPoint = Vector2.new(0, 0.5),
onClick = props.onBack,
}),
Text = e("TextLabel", {
Text = "Settings",
Font = Enum.Font.Gotham,
TextSize = 18,
TextColor3 = theme.TextColor,
TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
})
})
end)
end
local SettingsPage = Roact.Component:extend("SettingsPage")
function SettingsPage:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
end
function SettingsPage:render()
return Theme.with(function(theme)
theme = theme.Settings
return e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize,
transparency = self.props.transparency,
}, {
Navbar = e(Navbar, {
onBack = self.props.onBack,
transparency = self.props.transparency,
layoutOrder = 0,
}),
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor",
transparency = self.props.transparency,
layoutOrder = 1,
}),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
layoutOrder = 2,
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = 3,
}),
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency,
layoutOrder = 4,
}),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
})
end)
end
return SettingsPage

View File

@@ -16,9 +16,10 @@ local function getStudio()
end end
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Log = require(Rojo.Log) local Log = require(Packages.Log)
local strict = require(script.Parent.Parent.strict) local strict = require(script.Parent.Parent.strict)

View File

@@ -1,7 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Log = require(Rojo.Log) local Log = require(Packages.Log)
local LERP_DATA_TYPES = { local LERP_DATA_TYPES = {
Color3 = true, Color3 = true,
@@ -55,4 +56,4 @@ return {
mapLerp = mapLerp, mapLerp = mapLerp,
deriveProperty = deriveProperty, deriveProperty = deriveProperty,
blendAlpha = blendAlpha, blendAlpha = blendAlpha,
} }

View File

@@ -1,12 +1,17 @@
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Rojo.Roact) local Roact = require(Packages.Roact)
local Log = require(Rojo.Log) local Log = require(Packages.Log)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Version = require(Plugin.Version) local Version = require(Plugin.Version)
local Config = require(Plugin.Config) local Config = require(Plugin.Config)
local Settings = require(Plugin.Settings)
local strict = require(Plugin.strict) local strict = require(Plugin.strict)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
local ServeSession = require(Plugin.ServeSession) local ServeSession = require(Plugin.ServeSession)
@@ -14,7 +19,6 @@ local ApiContext = require(Plugin.ApiContext)
local preloadAssets = require(Plugin.preloadAssets) local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer) local soundPlayer = require(Plugin.soundPlayer)
local Theme = require(script.Theme) local Theme = require(script.Theme)
local PluginSettings = require(script.PluginSettings)
local Page = require(script.Page) local Page = require(script.Page)
local Notifications = require(script.Notifications) local Notifications = require(script.Notifications)
@@ -42,6 +46,10 @@ function App:init()
self.host, self.setHost = Roact.createBinding("") self.host, self.setHost = Roact.createBinding("")
self.port, self.setPort = Roact.createBinding("") self.port, self.setPort = Roact.createBinding("")
self.patchInfo, self.setPatchInfo = Roact.createBinding({
changes = 0,
timestamp = os.time(),
})
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
@@ -52,7 +60,7 @@ function App:init()
end end
function App:addNotification(text: string, timeout: number?) function App:addNotification(text: string, timeout: number?)
if not self.props.settings:get("showNotifications") then if not Settings:get("showNotifications") then
return return
end end
@@ -87,12 +95,70 @@ function App:getHostAndPort()
return host, port return host, port
end end
function App:claimSyncLock()
if #Players:GetPlayers() == 0 then
Log.trace("Skipping sync lock because this isn't in Team Create")
return true
end
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
if not lock then
lock = Instance.new("ObjectValue")
lock.Name = "__Rojo_SessionLock"
lock.Archivable = false
lock.Value = Players.LocalPlayer
lock.Parent = ServerStorage
Log.trace("Created and claimed sync lock")
return true
end
if lock.Value and lock.Value ~= Players.LocalPlayer and lock.Value.Parent then
Log.trace("Found existing sync lock owned by {}", lock.Value)
return false, lock.Value
end
lock.Value = Players.LocalPlayer
Log.trace("Claimed existing sync lock")
return true
end
function App:releaseSyncLock()
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
if not lock then
Log.trace("No sync lock found, assumed released")
return
end
if lock.Value == Players.LocalPlayer then
lock.Value = nil
Log.trace("Released sync lock")
return
end
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
end
function App:startSession() function App:startSession()
local claimedLock, priorOwner = self:claimSyncLock()
if not claimedLock then
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
Log.warn(msg)
self:addNotification(msg, 10)
self:setState({
appStatus = AppStatus.Error,
errorMessage = msg,
toolbarIcon = Assets.Images.PluginButtonWarning,
})
return
end
local host, port = self:getHostAndPort() local host, port = self:getHostAndPort()
local sessionOptions = { local sessionOptions = {
openScriptsExternally = self.props.settings:get("openScriptsExternally"), openScriptsExternally = Settings:get("openScriptsExternally"),
twoWaySync = self.props.settings:get("twoWaySync"), twoWaySync = Settings:get("twoWaySync"),
} }
local baseUrl = ("http://%s:%s"):format(host, port) local baseUrl = ("http://%s:%s"):format(host, port)
@@ -104,6 +170,34 @@ function App:startSession()
twoWaySync = sessionOptions.twoWaySync, twoWaySync = sessionOptions.twoWaySync,
}) })
serveSession:onPatchApplied(function(patch, unapplied)
local now = os.time()
local changes = 0
for _, set in patch do
for _ in set do
changes += 1
end
end
for _, set in unapplied do
for _ in set do
changes -= 1
end
end
if changes == 0 then return end
local old = self.patchInfo:getValue()
if now - old.timestamp < 2 then
changes += old.changes
end
self.setPatchInfo({
changes = changes,
timestamp = now,
})
end)
serveSession:onStatusChanged(function(status, details) serveSession:onStatusChanged(function(status, details)
if status == ServeSession.Status.Connecting then if status == ServeSession.Status.Connecting then
self:setState({ self:setState({
@@ -122,6 +216,7 @@ function App:startSession()
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5) self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
elseif status == ServeSession.Status.Disconnected then elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil self.serveSession = nil
self:releaseSyncLock()
-- Details being present indicates that this -- Details being present indicates that this
-- disconnection was from an error. -- disconnection was from an error.
@@ -147,6 +242,16 @@ function App:startSession()
serveSession:start() serveSession:start()
self.serveSession = serveSession self.serveSession = serveSession
task.defer(function()
while self.serveSession == serveSession do
-- Trigger rerender to update timestamp text
local patchInfo = table.clone(self.patchInfo:getValue())
self.setPatchInfo(patchInfo)
local elapsed = os.time() - patchInfo.timestamp
task.wait(elapsed < 60 and 1 or elapsed/5)
end
end)
end end
function App:endSession() function App:endSession()
@@ -230,6 +335,7 @@ function App:render()
Connected = createPageElement(AppStatus.Connected, { Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName, projectName = self.state.projectName,
address = self.state.address, address = self.state.address,
patchInfo = self.patchInfo,
onDisconnect = function() onDisconnect = function()
self:endSession() self:endSession()
@@ -272,6 +378,12 @@ function App:render()
VerticalAlignment = Enum.VerticalAlignment.Bottom, VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5), Padding = UDim.new(0, 5),
}), }),
padding = e("UIPadding", {
PaddingTop = UDim.new(0, 5);
PaddingBottom = UDim.new(0, 5);
PaddingLeft = UDim.new(0, 5);
PaddingRight = UDim.new(0, 5);
}),
notifs = e(Notifications, { notifs = e(Notifications, {
soundPlayer = self.props.soundPlayer, soundPlayer = self.props.soundPlayer,
notifications = self.state.notifications, notifications = self.state.notifications,
@@ -345,15 +457,9 @@ function App:render()
end end
return function(props) return function(props)
return e(PluginSettings.StudioProvider, { local mergedProps = Dictionary.merge(props, {
plugin = props.plugin, soundPlayer = soundPlayer.new(Settings),
}, {
App = PluginSettings.with(function(settings)
local mergedProps = Dictionary.merge(props, {
settings = settings,
soundPlayer = soundPlayer.new(settings),
})
return e(App, mergedProps)
end),
}) })
return e(App, mergedProps)
end end

View File

@@ -5,7 +5,8 @@
of instances) and return the patch. of instances) and return the patch.
]] ]]
local Log = require(script.Parent.Parent.Parent.Log) local Packages = script.Parent.Parent.Parent.Packages
local Log = require(Packages.Log)
local PatchSet = require(script.Parent.Parent.PatchSet) local PatchSet = require(script.Parent.Parent.PatchSet)

View File

@@ -1,5 +1,6 @@
local Log = require(script.Parent.Parent.Parent.Log) local Packages = script.Parent.Parent.Parent.Packages
local RbxDom = require(script.Parent.Parent.Parent.RbxDom) local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom)
local encodeProperty = require(script.Parent.encodeProperty) local encodeProperty = require(script.Parent.encodeProperty)

View File

@@ -1,5 +1,6 @@
local Log = require(script.Parent.Parent.Parent.Log) local Packages = script.Parent.Parent.Parent.Packages
local RbxDom = require(script.Parent.Parent.Parent.RbxDom) local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom)
return function(instance, propertyName, propertyDescriptor) return function(instance, propertyName, propertyDescriptor)
local readSuccess, readResult = propertyDescriptor:read(instance) local readSuccess, readResult = propertyDescriptor:read(instance)

View File

@@ -1,6 +1,7 @@
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local Log = require(script.Parent.Parent.Log) local Packages = script.Parent.Parent.Packages
local Log = require(Packages.Log)
--[[ --[[
A bidirectional map between instance IDs and Roblox instances. It lets us A bidirectional map between instance IDs and Roblox instances. It lets us

View File

@@ -3,7 +3,8 @@
patch returned from the API. patch returned from the API.
]] ]]
local t = require(script.Parent.Parent.t) local Packages = script.Parent.Parent.Packages
local t = require(Packages.t)
local Types = require(script.Parent.Types) local Types = require(script.Parent.Types)
@@ -181,4 +182,4 @@ function PatchSet.humanSummary(instanceMap, patchSet)
return table.concat(statements, "\n") return table.concat(statements, "\n")
end end
return PatchSet return PatchSet

View File

@@ -2,7 +2,8 @@
Defines the errors that can be returned by the reconciler. Defines the errors that can be returned by the reconciler.
]] ]]
local Fmt = require(script.Parent.Parent.Parent.Fmt) local Packages = script.Parent.Parent.Parent.Packages
local Fmt = require(Packages.Fmt)
local Error = {} local Error = {}
@@ -34,4 +35,4 @@ function Error:__tostring()
return Fmt.fmt("Error({}): {:#?}", self.kind, self.details) return Fmt.fmt("Error({}): {:#?}", self.kind, self.details)
end end
return Error return Error

View File

@@ -5,7 +5,8 @@
Patches can come from the server or be generated by the client. Patches can come from the server or be generated by the client.
]] ]]
local Log = require(script.Parent.Parent.Parent.Log) local Packages = script.Parent.Parent.Parent.Packages
local Log = require(Packages.Log)
local PatchSet = require(script.Parent.Parent.PatchSet) local PatchSet = require(script.Parent.Parent.PatchSet)
local Types = require(script.Parent.Parent.Types) local Types = require(script.Parent.Parent.Types)

View File

@@ -3,7 +3,8 @@
usable by Rojo's reconciler, potentially using RbxDom. usable by Rojo's reconciler, potentially using RbxDom.
]] ]]
local RbxDom = require(script.Parent.Parent.Parent.RbxDom) local Packages = script.Parent.Parent.Parent.Packages
local RbxDom = require(Packages.RbxDom)
local Error = require(script.Parent.Error) local Error = require(script.Parent.Error)
local function decodeValue(encodedValue, instanceMap) local function decodeValue(encodedValue, instanceMap)
@@ -38,4 +39,4 @@ local function decodeValue(encodedValue, instanceMap)
return true, decodedValue return true, decodedValue
end end
return decodeValue return decodeValue

View File

@@ -3,7 +3,9 @@
patch that can be later applied. patch that can be later applied.
]] ]]
local Log = require(script.Parent.Parent.Parent.Log) local Packages = script.Parent.Parent.Parent.Packages
local Log = require(Packages.Log)
local invariant = require(script.Parent.Parent.invariant) local invariant = require(script.Parent.Parent.invariant)
local getProperty = require(script.Parent.getProperty) local getProperty = require(script.Parent.getProperty)
local Error = require(script.Parent.Error) local Error = require(script.Parent.Error)
@@ -113,6 +115,16 @@ local function diff(instanceMap, virtualInstances, rootId)
local childId = instanceMap.fromInstances[childInstance] local childId = instanceMap.fromInstances[childInstance]
if childId == nil then if childId == nil then
-- pcall to avoid security permission errors
local success, skip = pcall(function()
-- We don't remove instances that aren't going to be saved anyway,
-- such as the Rojo session lock value.
return childInstance.Archivable == false
end)
if success and skip then
continue
end
-- This is an existing instance not present in the virtual DOM. -- This is an existing instance not present in the virtual DOM.
-- We can mark it for deletion unless the user has asked us not -- We can mark it for deletion unless the user has asked us not
-- to delete unknown stuff. -- to delete unknown stuff.
@@ -152,4 +164,4 @@ local function diff(instanceMap, virtualInstances, rootId)
return true, patch return true, patch
end end
return diff return diff

View File

@@ -1,5 +1,6 @@
return function() return function()
local Log = require(script.Parent.Parent.Parent.Log) local Packages = script.Parent.Parent.Parent.Packages
local Log = require(Packages.Log)
local InstanceMap = require(script.Parent.Parent.InstanceMap) local InstanceMap = require(script.Parent.Parent.InstanceMap)
local PatchSet = require(script.Parent.Parent.PatchSet) local PatchSet = require(script.Parent.Parent.PatchSet)
@@ -286,4 +287,4 @@ return function()
expect(size(patch.added)).to.equal(1) expect(size(patch.added)).to.equal(1)
expect(patch.added["CHILD"]).to.equal(virtualInstances["CHILD"]) expect(patch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
end) end)
end end

View File

@@ -2,7 +2,8 @@
Attempts to read a property from the given instance. Attempts to read a property from the given instance.
]] ]]
local RbxDom = require(script.Parent.Parent.Parent.RbxDom) local Packages = script.Parent.Parent.Parent.Packages
local RbxDom = require(Packages.RbxDom)
local Error = require(script.Parent.Error) local Error = require(script.Parent.Error)
local function getProperty(instance, propertyName) local function getProperty(instance, propertyName)
@@ -56,4 +57,4 @@ local function getProperty(instance, propertyName)
return true, valueOrErr return true, valueOrErr
end end
return getProperty return getProperty

View File

@@ -2,8 +2,9 @@
Attempts to set a property on the given instance. Attempts to set a property on the given instance.
]] ]]
local RbxDom = require(script.Parent.Parent.Parent.RbxDom) local Packages = script.Parent.Parent.Parent.Packages
local Log = require(script.Parent.Parent.Parent.Log) local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom)
local Error = require(script.Parent.Error) local Error = require(script.Parent.Error)
local function setProperty(instance, propertyName, value) local function setProperty(instance, propertyName, value)
@@ -45,4 +46,4 @@ local function setProperty(instance, propertyName, value)
return true return true
end end
return setProperty return setProperty

View File

@@ -1,9 +1,10 @@
local StudioService = game:GetService("StudioService") local StudioService = game:GetService("StudioService")
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local Log = require(script.Parent.Parent.Log) local Packages = script.Parent.Parent.Packages
local Fmt = require(script.Parent.Parent.Fmt) local Log = require(Packages.Log)
local t = require(script.Parent.Parent.t) local Fmt = require(Packages.Fmt)
local t = require(Packages.t)
local ChangeBatcher = require(script.Parent.ChangeBatcher) local ChangeBatcher = require(script.Parent.ChangeBatcher)
local InstanceMap = require(script.Parent.InstanceMap) local InstanceMap = require(script.Parent.InstanceMap)
@@ -94,6 +95,7 @@ function ServeSession.new(options)
__instanceMap = instanceMap, __instanceMap = instanceMap,
__changeBatcher = changeBatcher, __changeBatcher = changeBatcher,
__statusChangedCallback = nil, __statusChangedCallback = nil,
__patchAppliedCallback = nil,
__connections = connections, __connections = connections,
} }
@@ -121,6 +123,10 @@ function ServeSession:onStatusChanged(callback)
self.__statusChangedCallback = callback self.__statusChangedCallback = callback
end end
function ServeSession:onPatchApplied(callback)
self.__patchAppliedCallback = callback
end
function ServeSession:start() function ServeSession:start()
self:__setStatus(Status.Connecting) self:__setStatus(Status.Connecting)
@@ -137,7 +143,9 @@ function ServeSession:start()
end) end)
end) end)
:catch(function(err) :catch(function(err)
self:__stopInternal(err) if self.__status ~= Status.Disconnected then
self:__stopInternal(err)
end
end) end)
end end
@@ -231,6 +239,10 @@ function ServeSession:__initialSync(rootInstanceId)
Log.warn("Could not apply all changes requested by the Rojo server:\n{}", Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)) PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
end end
if self.__patchAppliedCallback then
pcall(self.__patchAppliedCallback, catchUpPatch, unappliedPatch)
end
end) end)
end end
@@ -244,6 +256,10 @@ function ServeSession:__mainSyncLoop()
Log.warn("Could not apply all changes requested by the Rojo server:\n{}", Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)) PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
end end
if self.__patchAppliedCallback then
pcall(self.__patchAppliedCallback, message, unappliedPatch)
end
end end
if self.__status ~= Status.Disconnected then if self.__status ~= Status.Disconnected then

79
plugin/src/Settings.lua Normal file
View File

@@ -0,0 +1,79 @@
--[[
Persistent plugin settings.
]]
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Log = require(Packages.Log)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
showNotifications = true,
playSounds = true,
}
local Settings = {}
Settings._values = table.clone(defaultSettings)
Settings._updateListeners = {}
if plugin then
for name, defaultValue in pairs(Settings._values) do
local savedValue = plugin:GetSetting("Rojo_" .. name)
if savedValue == nil then
-- plugin:SetSetting hits disc instead of memory, so it can be slow. Spawn so we don't hang.
task.spawn(plugin.SetSetting, plugin, "Rojo_" .. name, defaultValue)
Settings._values[name] = defaultValue
else
Settings._values[name] = savedValue
end
end
Log.trace("Loaded settings from plugin store")
end
function Settings:get(name)
if defaultSettings[name] == nil then
error("Invalid setings name " .. tostring(name), 2)
end
return self._values[name]
end
function Settings:set(name, value)
self._values[name] = value
if plugin then
-- plugin:SetSetting hits disc instead of memory, so it can be slow. Spawn so we don't hang.
task.spawn(plugin.SetSetting, plugin, "Rojo_" .. name, value)
end
if self._updateListeners[name] then
for callback in pairs(self._updateListeners[name]) do
task.spawn(callback, value)
end
end
Log.trace(string.format("Set setting '%s' to '%s'", name, tostring(value)))
end
function Settings:onChanged(name, callback)
local listeners = self._updateListeners[name]
if listeners == nil then
listeners = {}
self._updateListeners[name] = listeners
end
listeners[callback] = true
Log.trace(string.format("Added listener for setting '%s' changes", name))
return function()
listeners[callback] = nil
Log.trace(string.format("Removed listener for setting '%s' changes", name))
end
end
return Settings

View File

@@ -1,5 +1,5 @@
local t = require(script.Parent.Parent.t) local Packages = script.Parent.Parent.Packages
local t = require(Packages.t)
local DevSettings = require(script.Parent.DevSettings) local DevSettings = require(script.Parent.DevSettings)
local strict = require(script.Parent.strict) local strict = require(script.Parent.strict)

View File

@@ -2,19 +2,20 @@ if not plugin then
return return
end end
local Log = require(script.Parent.Log) local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Log = require(Packages.Log)
local Roact = require(Packages.Roact)
local DevSettings = require(script.DevSettings) local DevSettings = require(script.DevSettings)
local Config = require(script.Config)
local App = require(script.App)
Log.setLogLevelThunk(function() Log.setLogLevelThunk(function()
return DevSettings:getLogLevel() return DevSettings:getLogLevel()
end) end)
local Roact = require(script.Parent.Roact)
local Config = require(script.Config)
local App = require(script.App)
local app = Roact.createElement(App, { local app = Roact.createElement(App, {
plugin = plugin plugin = plugin
}) })

View File

@@ -1,4 +1,6 @@
local Fmt = require(script.Parent.Parent.Fmt)
local Packages = script.Parent.Parent.Packages
local Fmt = require(Packages.Fmt)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
@@ -26,4 +28,4 @@ else
end end
end end
return invariant return invariant

View File

@@ -1,6 +1,7 @@
local ContentProvider = game:GetService("ContentProvider") local ContentProvider = game:GetService("ContentProvider")
local Log = require(script.Parent.Parent.Log) local Packages = script.Parent.Parent.Packages
local Log = require(Packages.Log)
local Assets = require(script.Parent.Assets) local Assets = require(script.Parent.Assets)
@@ -29,4 +30,4 @@ local function preloadAssets()
end)() end)()
end end
return preloadAssets return preloadAssets

View File

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

View File

@@ -1,13 +1,16 @@
-- Sounds only play in Edit mode when parented to a plugin widget, for some reason -- Sounds only play in Edit mode when parented to a plugin widget, for some reason
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin") local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
local widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new( local widget = nil
Enum.InitialDockState.Float, if plugin then
false, true, widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
10, 10, Enum.InitialDockState.Float,
10, 10 false, true,
)) 10, 10,
widget.Name = "Rojo_soundPlayer" 10, 10
widget.Title = "Rojo Sound Player" ))
widget.Name = "Rojo_soundPlayer"
widget.Title = "Rojo Sound Player"
end
local SoundPlayer = {} local SoundPlayer = {}
SoundPlayer.__index = SoundPlayer SoundPlayer.__index = SoundPlayer

View File

@@ -8,8 +8,8 @@
"$path": "default.project.json" "$path": "default.project.json"
}, },
"TestEZ": { "Packages": {
"$path": "modules/testez" "$path": "DevPackages"
} }
}, },

33
plugin/wally.lock Normal file
View File

@@ -0,0 +1,33 @@
# This file is automatically @generated by Wally.
# It is not intended for manual editing.
registry = "test"
[[package]]
name = "evaera/promise"
version = "4.0.0"
dependencies = []
[[package]]
name = "osyrisrblx/t"
version = "3.0.0"
dependencies = []
[[package]]
name = "reselim/flipper"
version = "2.0.0"
dependencies = []
[[package]]
name = "roblox/roact"
version = "1.4.4"
dependencies = []
[[package]]
name = "roblox/testez"
version = "0.4.1"
dependencies = []
[[package]]
name = "rojo-rbx/rojo"
version = "7.2.1"
dependencies = [["Flipper", "reselim/flipper@2.0.0"], ["Promise", "evaera/promise@4.0.0"], ["Roact", "roblox/roact@1.4.4"], ["t", "osyrisrblx/t@3.0.0"]]

17
plugin/wally.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "rojo-rbx/rojo"
description = "Rojo enables Roblox developers to use professional-grade software engineering tools"
version = "7.2.1"
license = "MPL-2.0"
authors = ["LPGhatguy (https://lpg.space/)"]
registry = "https://github.com/upliftgames/wally-index"
realm = "shared"
[dependencies]
Flipper = "reselim/flipper@2.0.0"
Promise = "evaera/promise@4.0.0"
Roact = "roblox/roact@1.4.4"
t = "osyrisrblx/t@3.0.0"
[dev-dependencies]
TestEZ = "roblox/testez@0.4.1"

View File

@@ -1,6 +1,5 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
assertion_line: 99
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">
@@ -11,12 +10,24 @@ expression: contents
<Item class="Folder" referent="1"> <Item class="Folder" referent="1">
<Properties> <Properties>
<string name="Name">Explicit</string> <string name="Name">Explicit</string>
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString> <BinaryString name="AttributesSerialize">DQAAAAQAAABCb29sAwEKAAAAQnJpY2tDb2xvcg4BAAAABgAAAENvbG9yMw8AAAAAAAAAAAAAAAANAAAAQ29sb3JTZXF1ZW5jZRkCAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAAAAAACAPwAAgD8AAIA/AACAPwcAAABGbG9hdDMyBQAAAAAHAAAARmxvYXQ2NAYAAAAAAAAAAAsAAABOdW1iZXJSYW5nZRsAAAAAAAAAAA4AAABOdW1iZXJTZXF1ZW5jZRcCAAAAAAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAABAAAAFJlY3QcAAAAAAAAAAAAAAAAAAAAAAQAAABVRGltCQAAAAAAAAAABQAAAFVEaW0yCgAAAAAAAAAAAAAAAAAAAAAHAAAAVmVjdG9yMhAAAAAAAAAAAAcAAABWZWN0b3IzEQAAAAAAAAAAAAAAAA==</BinaryString>
</Properties> </Properties>
</Item> </Item>
<Item class="Folder" referent="2"> <Item class="Folder" referent="2">
<Properties> <Properties>
<string name="Name">ImplicitAttributes</string> <string name="Name">Implicit</string>
<BinaryString name="AttributesSerialize">AwAAAAQAAABCb29sAwEGAAAATnVtYmVyBgAAAAAAAOA/BgAAAFN0cmluZwIEAAAAVGVzdA==</BinaryString>
</Properties>
</Item>
<Item class="Folder" referent="3">
<Properties>
<string name="Name">LegacyExplicit</string>
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString>
</Properties>
</Item>
<Item class="Folder" referent="4">
<Properties>
<string name="Name">LegacyImplicit</string>
<BinaryString name="AttributesSerialize">AgAAAAMAAABIZXkCBwAAAEdyYW5kbWEGAAAAVmVjdG9yEQAAgEAAAKBAAADAQA==</BinaryString> <BinaryString name="AttributesSerialize">AgAAAAMAAABIZXkCBwAAAEdyYW5kbWEGAAAAVmVjdG9yEQAAgEAAAKBAAADAQA==</BinaryString>
</Properties> </Properties>
</Item> </Item>

View File

@@ -0,0 +1,19 @@
---
source: tests/tests/build.rs
assertion_line: 100
expression: contents
---
<roblox version="4">
<Item class="LocalizationTable" referent="0">
<Properties>
<string name="Name">init_csv_with_children</string>
<string name="Contents">[{"key":"init.csv","values":{}}]</string>
</Properties>
<Item class="LocalizationTable" referent="1">
<Properties>
<string name="Name">other</string>
<string name="Contents">[{"key":"other.csv","values":{}}]</string>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -3,7 +3,85 @@
"tree": { "tree": {
"$className": "Folder", "$className": "Folder",
"Implicit": {
"$className": "Folder",
"$attributes": {
"Bool": true,
"Number": 0.5,
"String": "Test"
}
},
"Explicit": { "Explicit": {
"$className": "Folder",
"$attributes": {
"Bool": {
"Bool": true
},
"Float32": {
"Float32": 0
},
"Float64": {
"Float64": 0
},
"UDim": {
"UDim": [0, 0]
},
"UDim2": {
"UDim2": [[0, 0], [0, 0]]
},
"BrickColor": {
"BrickColor": 1
},
"Color3": {
"Color3": [0, 0, 0]
},
"Vector2": {
"Vector2": [0, 0]
},
"Vector3": {
"Vector3": [0, 0, 0]
},
"NumberSequence": {
"NumberSequence": {
"keypoints": [
{
"time": 0,
"value": 0,
"envelope": 0
},
{
"time": 1,
"value": 0,
"envelope": 0
}
]
}
},
"ColorSequence": {
"ColorSequence": {
"keypoints": [
{
"time": 0,
"color": [1, 1, 1]
},
{
"time": 1,
"color": [1, 1, 1]
}
]
}
},
"NumberRange": {
"NumberRange": [0, 0]
},
"Rect": {
"Rect": [[0, 0], [0, 0]]
}
}
},
"LegacyExplicit": {
"$className": "Folder", "$className": "Folder",
"$properties": { "$properties": {
"Attributes": { "Attributes": {
@@ -19,7 +97,7 @@
} }
}, },
"ImplicitAttributes": { "LegacyImplicit": {
"$className": "Folder", "$className": "Folder",
"$properties": { "$properties": {
"Attributes": { "Attributes": {

View File

@@ -0,0 +1,6 @@
{
"name": "init_csv_with_children",
"tree": {
"$path": "src"
}
}

View File

@@ -0,0 +1,2 @@
Key
init.csv
1 Key
2 init.csv

View File

@@ -0,0 +1,2 @@
Key
other.csv
1 Key
2 other.csv

View File

@@ -154,7 +154,9 @@ impl JobThreadContext {
for id in affected_ids { for id in affected_ids {
if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) { if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) {
applied_patches.push(patch); if !patch.is_empty() {
applied_patches.push(patch);
}
} }
} }
} }
@@ -253,7 +255,9 @@ impl JobThreadContext {
apply_patch_set(&mut tree, patch_set) apply_patch_set(&mut tree, patch_set)
}; };
self.message_queue.push_messages(&[applied_patch]); if !applied_patch.is_empty() {
self.message_queue.push_messages(&[applied_patch]);
}
} }
} }

View File

@@ -5,6 +5,8 @@ use clap::Parser;
use crate::project::Project; use crate::project::Project;
use super::resolve_path;
/// Reformat a Rojo project using the standard JSON formatting rules. /// Reformat a Rojo project using the standard JSON formatting rules.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct FmtProjectCommand { pub struct FmtProjectCommand {
@@ -15,7 +17,8 @@ pub struct FmtProjectCommand {
impl FmtProjectCommand { impl FmtProjectCommand {
pub fn run(self) -> anyhow::Result<()> { pub fn run(self) -> anyhow::Result<()> {
let project = Project::load_fuzzy(&self.project)? let base_path = resolve_path(&self.project);
let project = Project::load_fuzzy(&base_path)?
.context("A project file is required to run 'rojo fmt-project'")?; .context("A project file is required to run 'rojo fmt-project'")?;
let serialized = serde_json::to_string_pretty(&project) let serialized = serde_json::to_string_pretty(&project)

View File

@@ -1,5 +1,6 @@
use std::{ use std::{
io::{BufWriter, Write}, io::{BufWriter, Write},
mem::forget,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -8,6 +9,7 @@ use fs_err::File;
use memofs::Vfs; use memofs::Vfs;
use rbx_dom_weak::types::Ref; use rbx_dom_weak::types::Ref;
use serde::Serialize; use serde::Serialize;
use tokio::runtime::Runtime;
use crate::{ use crate::{
serve_session::ServeSession, serve_session::ServeSession,
@@ -50,6 +52,10 @@ pub struct SourcemapCommand {
/// If non-script files should be included or not. Defaults to false. /// If non-script files should be included or not. Defaults to false.
#[clap(long)] #[clap(long)]
pub include_non_scripts: bool, pub include_non_scripts: bool,
/// Whether to automatically recreate a snapshot when any input files change.
#[clap(long)]
pub watch: bool,
} }
impl SourcemapCommand { impl SourcemapCommand {
@@ -58,9 +64,10 @@ impl SourcemapCommand {
log::trace!("Constructing in-memory filesystem"); log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default(); let vfs = Vfs::new_default();
vfs.set_watch_enabled(self.watch);
let session = ServeSession::new(vfs, &project_path)?; let session = ServeSession::new(vfs, &project_path)?;
let tree = session.tree(); let mut cursor = session.message_queue().cursor();
let filter = if self.include_non_scripts { let filter = if self.include_non_scripts {
filter_nothing filter_nothing
@@ -68,19 +75,24 @@ impl SourcemapCommand {
filter_non_scripts filter_non_scripts
}; };
let root_node = recurse_create_node(&tree, tree.get_root_id(), session.root_dir(), filter); write_sourcemap(&session, self.output.as_deref(), filter)?;
if let Some(output_path) = self.output { if self.watch {
let mut file = BufWriter::new(File::create(&output_path)?); let rt = Runtime::new().unwrap();
serde_json::to_writer(&mut file, &root_node)?;
file.flush()?;
println!("Created sourcemap at {}", output_path.display()); loop {
} else { let receiver = session.message_queue().subscribe(cursor);
let output = serde_json::to_string(&root_node)?; let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
println!("{}", output); cursor = new_cursor;
write_sourcemap(&session, self.output.as_deref(), filter)?;
}
} }
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
// and we're about to exit anyways.
forget(session);
Ok(()) Ok(())
} }
} }
@@ -90,10 +102,10 @@ fn filter_nothing(_instance: &InstanceWithMeta) -> bool {
} }
fn filter_non_scripts(instance: &InstanceWithMeta) -> bool { fn filter_non_scripts(instance: &InstanceWithMeta) -> bool {
match instance.class_name() { matches!(
"Script" | "LocalScript" | "ModuleScript" => true, instance.class_name(),
_ => false, "Script" | "LocalScript" | "ModuleScript"
} )
} }
fn recurse_create_node( fn recurse_create_node(
@@ -106,7 +118,7 @@ fn recurse_create_node(
let mut children = Vec::new(); let mut children = Vec::new();
for &child_id in instance.children() { for &child_id in instance.children() {
if let Some(child_node) = recurse_create_node(tree, child_id, &project_dir, filter) { if let Some(child_node) = recurse_create_node(tree, child_id, project_dir, filter) {
children.push(child_node); children.push(child_node);
} }
} }
@@ -134,3 +146,26 @@ fn recurse_create_node(
children, children,
}) })
} }
fn write_sourcemap(
session: &ServeSession,
output: Option<&Path>,
filter: fn(&InstanceWithMeta) -> bool,
) -> anyhow::Result<()> {
let tree = session.tree();
let root_node = recurse_create_node(&tree, tree.get_root_id(), session.root_dir(), filter);
if let Some(output_path) = output {
let mut file = BufWriter::new(File::create(&output_path)?);
serde_json::to_writer(&mut file, &root_node)?;
file.flush()?;
println!("Created sourcemap at {}", output_path.display());
} else {
let output = serde_json::to_string(&root_node)?;
println!("{}", output);
}
Ok(())
}

View File

@@ -229,6 +229,13 @@ pub struct ProjectNode {
)] )]
pub properties: HashMap<String, UnresolvedValue>, pub properties: HashMap<String, UnresolvedValue>,
#[serde(
rename = "$attributes",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub attributes: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox /// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered /// Studio during live sync. `$ignoreUnknownInstances` should be considered
/// a large hammer and used with care. /// a large hammer and used with care.

View File

@@ -1,6 +1,6 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use anyhow::format_err; use anyhow::{bail, format_err};
use rbx_dom_weak::types::{ use rbx_dom_weak::types::{
Attributes, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Attributes, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2,
Vector3, Vector3,
@@ -28,6 +28,13 @@ impl UnresolvedValue {
UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name), UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name),
} }
} }
pub fn resolve_unambiguous(self) -> anyhow::Result<Variant> {
match self {
UnresolvedValue::FullyQualified(full) => Ok(full),
UnresolvedValue::Ambiguous(partial) => partial.resolve_unambiguous(),
}
}
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -148,6 +155,16 @@ impl AmbiguousValue {
} }
} }
pub fn resolve_unambiguous(self) -> anyhow::Result<Variant> {
match self {
AmbiguousValue::Bool(value) => Ok(value.into()),
AmbiguousValue::Number(value) => Ok(value.into()),
AmbiguousValue::String(value) => Ok(value.into()),
other => bail!("Cannot unambiguously resolve the value {other:?}"),
}
}
fn describe(&self) -> &'static str { fn describe(&self) -> &'static str {
match self { match self {
AmbiguousValue::Bool(_) => "a bool", AmbiguousValue::Bool(_) => "a bool",
@@ -218,12 +235,20 @@ mod test {
unresolved.resolve(class, prop).unwrap() unresolved.resolve(class, prop).unwrap()
} }
fn resolve_unambiguous(json_value: &str) -> Variant {
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve_unambiguous().unwrap()
}
#[test] #[test]
fn bools() { fn bools() {
assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false)); assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false));
// Script.Disabled is inherited from BaseScript // Script.Disabled is inherited from BaseScript
assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true)); assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true));
assert_eq!(resolve_unambiguous("false"), Variant::Bool(false));
assert_eq!(resolve_unambiguous("true"), Variant::Bool(true));
} }
#[test] #[test]
@@ -247,6 +272,11 @@ mod test {
// resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""), // resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""),
// Variant::BinaryString(b"a\0b\0c".to_vec().into()), // Variant::BinaryString(b"a\0b\0c".to_vec().into()),
// ); // );
assert_eq!(
resolve_unambiguous("\"Hello world!\""),
Variant::String("Hello world!".into()),
);
} }
#[test] #[test]
@@ -257,12 +287,14 @@ mod test {
); );
assert_eq!( assert_eq!(
resolve("Folder", "SourceAssetId", "532413"), resolve("IntValue", "Value", "532413"),
Variant::Int64(532413), Variant::Int64(532413),
); );
assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0)); assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0));
assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0)); assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0));
assert_eq!(resolve_unambiguous("12.5"), Variant::Float64(12.5));
} }
#[test] #[test]

View File

@@ -19,7 +19,7 @@ pub struct PatchSet {
pub updated_instances: Vec<PatchUpdate>, pub updated_instances: Vec<PatchUpdate>,
} }
impl<'a> PatchSet { impl PatchSet {
pub fn new() -> Self { pub fn new() -> Self {
PatchSet { PatchSet {
removed_instances: Vec::new(), removed_instances: Vec::new(),
@@ -76,6 +76,10 @@ impl AppliedPatchSet {
updated: Vec::new(), updated: Vec::new(),
} }
} }
pub fn is_empty(&self) -> bool {
self.removed.is_empty() && self.added.is_empty() && self.updated.is_empty()
}
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -7,7 +7,11 @@ use serde::Serialize;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{meta_file::AdjacentMetadata, util::PathExt}; use super::{
dir::{dir_meta, snapshot_dir_no_meta},
meta_file::AdjacentMetadata,
util::PathExt,
};
pub fn snapshot_csv( pub fn snapshot_csv(
_context: &InstanceContext, _context: &InstanceContext,
@@ -46,6 +50,43 @@ pub fn snapshot_csv(
Ok(Some(snapshot)) Ok(Some(snapshot))
} }
/// Attempts to snapshot an 'init' csv contained inside of a folder with
/// the given name.
///
/// csv named `init.csv`
/// their parents, which acts similarly to `__init__.py` from the Python world.
pub fn snapshot_csv_init(
context: &InstanceContext,
vfs: &Vfs,
init_path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let folder_path = init_path.parent().unwrap();
let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
if dir_snapshot.class_name != "Folder" {
anyhow::bail!(
"init.csv can only be used if the instance produced by \
the containing directory would be a Folder.\n\
\n\
The directory {} turned into an instance of class {}.",
folder_path.display(),
dir_snapshot.class_name
);
}
let mut init_snapshot = snapshot_csv(context, vfs, init_path)?.unwrap();
init_snapshot.name = dir_snapshot.name;
init_snapshot.children = dir_snapshot.children;
init_snapshot.metadata = dir_snapshot.metadata;
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
meta.apply_all(&mut init_snapshot)?;
}
Ok(Some(init_snapshot))
}
/// Struct that holds any valid row from a Roblox CSV translation table. /// Struct that holds any valid row from a Roblox CSV translation table.
/// ///
/// We manually deserialize into this table from CSV, but let serde_json handle /// We manually deserialize into this table from CSV, but let serde_json handle

View File

@@ -87,6 +87,7 @@ pub fn snapshot_dir_no_meta(
path.join("init.server.luau"), path.join("init.server.luau"),
path.join("init.client.lua"), path.join("init.client.lua"),
path.join("init.client.luau"), path.join("init.client.luau"),
path.join("init.csv"),
]; ];
let snapshot = InstanceSnapshot::new() let snapshot = InstanceSnapshot::new()

View File

@@ -2,6 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path, str};
use anyhow::Context; use anyhow::Context;
use memofs::Vfs; use memofs::Vfs;
use rbx_dom_weak::types::Attributes;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
@@ -78,6 +79,9 @@ struct JsonModel {
skip_serializing_if = "HashMap::is_empty" skip_serializing_if = "HashMap::is_empty"
)] )]
properties: HashMap<String, UnresolvedValue>, properties: HashMap<String, UnresolvedValue>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
attributes: HashMap<String, UnresolvedValue>,
} }
impl JsonModel { impl JsonModel {
@@ -96,6 +100,17 @@ impl JsonModel {
properties.insert(key, value); properties.insert(key, value);
} }
if !self.attributes.is_empty() {
let mut attributes = Attributes::new();
for (key, unresolved) in self.attributes {
let value = unresolved.resolve_unambiguous()?;
attributes.insert(key, value);
}
properties.insert("Attributes".into(), attributes.into());
}
Ok(InstanceSnapshot { Ok(InstanceSnapshot {
snapshot_id: None, snapshot_id: None,
metadata: Default::default(), metadata: Default::default(),

View File

@@ -1,6 +1,7 @@
use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use anyhow::{format_err, Context}; use anyhow::{format_err, Context};
use rbx_dom_weak::types::Attributes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot}; use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot};
@@ -78,6 +79,9 @@ pub struct DirectoryMetadata {
#[serde(default, skip_serializing_if = "HashMap::is_empty")] #[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedValue>, pub properties: HashMap<String, UnresolvedValue>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub attributes: HashMap<String, UnresolvedValue>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>, pub class_name: Option<String>,
@@ -139,6 +143,19 @@ impl DirectoryMetadata {
snapshot.properties.insert(key, value); snapshot.properties.insert(key, value);
} }
if !self.attributes.is_empty() {
let mut attributes = Attributes::new();
for (key, unresolved) in self.attributes.drain() {
let value = unresolved.resolve_unambiguous()?;
attributes.insert(key, value);
}
snapshot
.properties
.insert("Attributes".into(), attributes.into());
}
Ok(()) Ok(())
} }
} }

View File

@@ -24,7 +24,7 @@ use memofs::{IoResultExt, Vfs};
use crate::snapshot::{InstanceContext, InstanceSnapshot}; use crate::snapshot::{InstanceContext, InstanceSnapshot};
use self::{ use self::{
csv::snapshot_csv, csv::{snapshot_csv, snapshot_csv_init},
dir::snapshot_dir, dir::snapshot_dir,
json::snapshot_json, json::snapshot_json,
json_model::snapshot_json_model, json_model::snapshot_json_model,
@@ -87,12 +87,19 @@ pub fn snapshot_from_vfs(
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
} }
let init_path = path.join("init.csv");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_csv_init(context, vfs, &init_path);
}
snapshot_dir(context, vfs, path) snapshot_dir(context, vfs, path)
} else { } else {
let script_name = path let script_name = path
.file_name_trim_end(".lua") .file_name_trim_end(".lua")
.or_else(|_| path.file_name_trim_end(".luau")); .or_else(|_| path.file_name_trim_end(".luau"));
let csv_name = path.file_name_trim_end(".csv");
if let Ok(name) = script_name { if let Ok(name) = script_name {
match name { match name {
// init scripts are handled elsewhere and should not turn into // init scripts are handled elsewhere and should not turn into
@@ -110,8 +117,14 @@ pub fn snapshot_from_vfs(
return Ok(None); return Ok(None);
} else if path.file_name_ends_with(".json") { } else if path.file_name_ends_with(".json") {
return snapshot_json(context, vfs, path); return snapshot_json(context, vfs, path);
} else if path.file_name_ends_with(".csv") { } else if let Ok(name) = csv_name {
return snapshot_csv(context, vfs, path); match name {
// init csv are handled elsewhere and should not turn into
// their own children.
"init" => return Ok(None),
_ => return snapshot_csv(context, vfs, path),
}
} else if path.file_name_ends_with(".txt") { } else if path.file_name_ends_with(".txt") {
return snapshot_txt(context, vfs, path); return snapshot_txt(context, vfs, path);
} else if path.file_name_ends_with(".rbxmx") { } else if path.file_name_ends_with(".rbxmx") {

View File

@@ -2,6 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use memofs::Vfs; use memofs::Vfs;
use rbx_dom_weak::types::Attributes;
use rbx_reflection::ClassTag; use rbx_reflection::ClassTag;
use crate::{ use crate::{
@@ -231,6 +232,23 @@ pub fn snapshot_project_node(
properties.insert(key.clone(), value); properties.insert(key.clone(), value);
} }
if !node.attributes.is_empty() {
let mut attributes = Attributes::new();
for (key, unresolved) in &node.attributes {
let value = unresolved.clone().resolve_unambiguous().with_context(|| {
format!(
"Unresolvable attribute in project at path {}",
project_path.display()
)
})?;
attributes.insert(key.clone(), value);
}
properties.insert("Attributes".into(), attributes.into());
}
// If the user specified $ignoreUnknownInstances, overwrite the existing // If the user specified $ignoreUnknownInstances, overwrite the existing
// value. // value.
// //

View File

@@ -1,5 +1,6 @@
--- ---
source: src/snapshot_middleware/dir.rs source: src/snapshot_middleware/dir.rs
assertion_line: 127
expression: instance_snapshot expression: instance_snapshot
--- ---
snapshot_id: ~ snapshot_id: ~
@@ -16,6 +17,7 @@ metadata:
- /foo/init.server.luau - /foo/init.server.luau
- /foo/init.client.lua - /foo/init.client.lua
- /foo/init.client.luau - /foo/init.client.luau
- /foo/init.csv
context: {} context: {}
name: foo name: foo
class_name: Folder class_name: Folder

View File

@@ -1,5 +1,6 @@
--- ---
source: src/snapshot_middleware/dir.rs source: src/snapshot_middleware/dir.rs
assertion_line: 148
expression: instance_snapshot expression: instance_snapshot
--- ---
snapshot_id: ~ snapshot_id: ~
@@ -16,6 +17,7 @@ metadata:
- /foo/init.server.luau - /foo/init.server.luau
- /foo/init.client.lua - /foo/init.client.lua
- /foo/init.client.luau - /foo/init.client.luau
- /foo/init.csv
context: {} context: {}
name: foo name: foo
class_name: Folder class_name: Folder
@@ -35,6 +37,7 @@ children:
- /foo/Child/init.server.luau - /foo/Child/init.server.luau
- /foo/Child/init.client.lua - /foo/Child/init.client.lua
- /foo/Child/init.client.luau - /foo/Child/init.client.luau
- /foo/Child/init.csv
context: {} context: {}
name: Child name: Child
class_name: Folder class_name: Folder

View File

@@ -21,6 +21,7 @@ macro_rules! gen_build_tests {
} }
gen_build_tests! { gen_build_tests! {
init_csv_with_children,
attributes, attributes,
client_in_folder, client_in_folder,
client_init, client_init,